为什么 Webpack 5 包含我未使用的 TypeScript 枚举导出,即使启用了 Tree Shaking 也是如此?

lbr*_*ile 5 enums typescript webpack tree-shaking

我的 TypeScript 枚举是这样定义的,如以下文件所示

\n
export enum HueColors {\n  "red"    = "hsl(0, 100%, 50%)",\n  "orange" = "hsl(30, 100%, 50%)",\n  // ...\n  "pink"   = "hsl(330, 100%, 50%)",\n}\n\nexport enum RGBExtended { /* ... */ }\nexport enum WebSafe { /* ... */ }\n
Run Code Online (Sandbox Code Playgroud)\n

设置/配置

\n
export enum HueColors {\n  "red"    = "hsl(0, 100%, 50%)",\n  "orange" = "hsl(30, 100%, 50%)",\n  // ...\n  "pink"   = "hsl(330, 100%, 50%)",\n}\n\nexport enum RGBExtended { /* ... */ }\nexport enum WebSafe { /* ... */ }\n
Run Code Online (Sandbox Code Playgroud)\n
// package.json\n{\n  ...\n  "main": "./index.js",\n  "types": "./index.d.ts",\n  "files": [\n    "**/*.{js,ts, map}"\n  ],\n  "sideEffects": false,\n  "scripts": {\n    ...\n    "build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.config.js",\n    "build": "cross-env NODE_ENV=production webpack --config config/webpack.config.js",\n    ...\n  },\n  "babel": {\n    "extends": "./config/.babelrc.json"\n  },\n  ...\n  "devDependencies": {\n    "@babel/core": "^7.14.8",\n    "@babel/preset-env": "^7.14.8",\n    "@types/jest": "^26.0.24",\n    "@types/node": "^16.4.0",\n    "@typescript-eslint/eslint-plugin": "^4.28.4",\n    "@typescript-eslint/parser": "^4.28.4",\n    "copy-webpack-plugin": "^9.0.1",\n    "cross-env": "^7.0.3",\n    "eslint": "^7.31.0",\n    "eslint-plugin-jest": "^24.4.0",\n    "jest": "^27.0.6",\n    "prettier": "^2.3.2",\n    "terser-webpack-plugin": "^5.1.4",\n    "ts-jest": "^27.0.4",\n    "ts-loader": "^9.2.4",\n    "ts-node": "^10.1.0",\n    "typedoc": "^0.21.4",\n    "typescript": "^4.3.5",\n    "webpack": "^5.46.0",\n    "webpack-cli": "^4.7.2"\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
// config/.babelrc.json\n{\n  "presets": [\n    [\n      "@babel/preset-env",\n      {\n        "targets": {\n          "node": "current"\n        },\n        "modules": false\n      }\n    ]\n  ]\n}\n
Run Code Online (Sandbox Code Playgroud)\n
// config/tsconfig.json\n{\n  "compilerOptions": {\n    "target": "ES6",\n    "module": "ES6",\n    "lib": ["DOM", "DOM.Iterable", "ES2017"],\n    "moduleResolution": "node", \n    "outDir": "../dist", \n    "noEmit": false,\n    "declaration": true, \n    "strict": true,\n    "esModuleInterop": true,\n    "allowSyntheticDefaultImports": true,\n    "removeComments": false,\n    "forceConsistentCasingInFileNames": true,\n    "noFallthroughCasesInSwitch": true\n  },\n  "include": ["../src"],\n  "exclude": ["../node_modules", "../tests", "../coverage", "../src/debug.ts"]\n}\n
Run Code Online (Sandbox Code Playgroud)\n

开发构建输出

\n

我在控制台中看到以下内容:

\n
// config/webpack.config.js\n\n/* eslint-disable @typescript-eslint/no-var-requires */\nconst CopyPlugin = require("copy-webpack-plugin");\n\nconst path = require("path");\n\nconst basePath = path.resolve(__dirname, "../");\n\nmodule.exports = {\n  entry: path.join(basePath, "src", "index.ts"),\n  mode: process.env.NODE_ENV,\n  devtool: process.env.NODE_ENV === "production" ? "source-map" : false,\n  module: {\n    rules: [\n      {\n        test: /\\.ts$/,\n        loader: "ts-loader",\n        options: {\n          configFile: path.join(__dirname, "tsconfig.json")\n        },\n        exclude: /node_modules/\n      }\n    ]\n  },\n  plugins: [\n    new CopyPlugin({\n      patterns: [\n        ... // not important for question\n      ]\n    })\n  ],\n  optimization: {\n    minimize: process.env.NODE_ENV === "production",\n    minimizer: [\n      (compiler) => {\n        const TerserPlugin = require("terser-webpack-plugin");\n        new TerserPlugin({\n          terserOptions: {\n            ecma: 5,\n            mangle: true,\n            module: false\n          }\n        }).apply(compiler);\n      }\n    ],\n    usedExports: true,\n    sideEffects: true,\n    innerGraph: true\n  },\n  stats: {\n    usedExports: true,\n    providedExports: true,\n    env: true\n  },\n  resolve: {\n    extensions: [".ts"]\n  },\n  output: {\n    filename: "index.js",\n    path: path.join(basePath, "dist"),\n    library: "colormaster",\n    libraryTarget: "umd",\n    globalObject: "this",\n    clean: true\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n

我在生成的 dist 文件夹输出中看到以下内容:

\n
...\n./src/enums/colors.ts 17.6 KiB [built] [code generated]\n    [exports: HueColors, RGBExtended, WebSafe]\n    [only some exports used: HueColors] // \xe2\x86\x90 indicates that tree shaking should occur in production build\nwebpack 5.46.0 compiled successfully in 2368 ms\n
Run Code Online (Sandbox Code Playgroud)\n

生产构建输出

\n

但是,在生产构建输出中,我看到以下内容:\n生产构建输出

\n

其中显然仍然包括未使用的出口。

\n

可以采取什么措施来规避这个问题?

\n

解决方案

\n

感谢 @Jeff Bowman\ 的广泛回应,我们能够推断出根本原因是 TypeScript 编译enum成 IIFE。

\n

只需用(记录实用程序)替换enum变量const即可解决问题,并且 Tree Shaking 在生产包中可见。

\n

Jef*_*ica 5

colors.ts这是因为 Terser 无法推断enum中的副作用,因此 Terser 保留所有三个定义,即使它只导出其中之一。

如果这些不是转译的 TypeScript 枚举,我建议简化声明,最好是标记每个函数/*#__PURE__*/并使其返回其预期值。然而,由于它们TypeScript 枚举,您可能需要将它们转换为对象文字as const,这对于 Terser 来说当然更容易推理,并且可能足以满足您的需求。


如果我正确地阅读了您的输出,那么您尝试删除的数组同时存在于开发和运行时构建中;你用“...”省略了它们,但它们就在那里。

根据您的情况,package.json您正在使用Webpack 的tree-shaking 功能集sideEffects和。正确断言除了导出之外您没有更改任何内容,因此如果您的项目不使用其导出,Webpack 可以安全地跳过整个模块。然而,可能并不像您希望的那么聪明:usedExportssideEffectsusedExports

usedExports依靠terser来检测语句中的副作用。在 JavaScript 中这是一项艰巨的任务,并且不如简单的sideEffects标志有效。它也不能跳过子树/依赖项,因为规范表示需要评估副作用。

看起来,对于开发和生产来说,Webpack 都足够聪明,可以检测到您的 HueColors 是您使用的唯一导出,但 Terser 不够聪明,无法确定每个自初始化 IIFE 都不会产生影响其他 IIFE 的副作用。从技术上讲,作为一个人,我也无法推理:即使您的函数没有使用联赋值或修改同名隐藏变量,其他一些代码也可能以奇怪的方式更改了对象或数组原型IIFE 中的封闭范围。


通过浏览器内的 terser 副本,我已经能够重现您的问题。

首先,切换到 const 对象文字将是完全有效的:

const foo = {foo: "foo"};
const bar = {bar: "bar"};
const baz = {baz: "baz"};

window.alert(foo);

// output: window.alert({foo:"foo"})
// correctly minifed
Run Code Online (Sandbox Code Playgroud)

在您的格式中,相同的定义表现出您试图避免的行为:

var foo;
(function(x) {
  x.foo = "foo";
})(foo || (foo = {}));
var bar;
(function(x) {
  x.bar = "bar";
})(bar || (bar = {}));
var baz;
(function(x) {
  x.baz = "baz";
})(baz || (baz = {}));

window.alert(foo);

// output: o,n,a;(o||(o={})).foo="foo",function(o){o.bar="bar"}(n||(n={})),function(o){o.baz="baz"}(a||(a={})),window.alert(o)
// incorrectly minified; foo, bar, and baz all survive
Run Code Online (Sandbox Code Playgroud)

仅仅避免内联定义是不够的,尽管这是一个好的开始:

var foo = {};
(function(x) {
  x.foo = "foo";
})(foo);
var bar = {};
(function(x) {
  x.bar = "bar";
})(bar);
var baz = {};
(function(x) {
  x.baz = "baz";
})(baz);

window.alert(foo);

// output: o={};o.foo="foo";!function(o){o.bar="bar"}({});!function(o){o.baz="baz"}({}),window.alert(o)
// incorrectly minified: definitions remain, but
//     ! shows how terser is just looking for side effects
Run Code Online (Sandbox Code Playgroud)

如果您让每个函数返回一个值,并使用webpack 文档terser 文档中的 as标记函数,就足够。这对您的枚举没有帮助,但确实表明如何调整输出以满足 Terser 的要求。/*#__PURE__*/

var foo = /*#__PURE__*/ (function() {
  var x = {};
  x.foo = "foo";
  return x;
})();
var bar = /*#__PURE__*/ (function() {
  var x = {};
  x.bar = "bar";
  return x;
})();
var baz = /*#__PURE__*/ (function() {
  var x = {};
  x.baz = "baz";
  return x;
})();

window.alert(foo);

// output: let o=function(){var o={foo:"foo"};return o}();window.alert(o)
// correctly minified, though longer than the literal example
Run Code Online (Sandbox Code Playgroud)