如何为库设置 TypeScript 编译器,以便 Webpack 在依赖项目中切断未使用的模块?
Posted
技术标签:
【中文标题】如何为库设置 TypeScript 编译器,以便 Webpack 在依赖项目中切断未使用的模块?【英文标题】:How to setup the TypeScript compiler for the library so that the unused modules will be cut off by Webpack in the dependents projects? 【发布时间】:2021-09-21 05:19:50 【问题描述】:主题库初步说明
很抱歉让您阅读本文占用了您的时间。我写它是为了回答诸如“你在做什么?”之类的问题。和“你为什么这样做?”。
library 由大量辅助函数和类组成。在这方面它类似于 lodash (check the structure of lodash),但与 lodash 不同的是,源代码由multilevel directories 组织。这对开发人员来说很舒服,但对用户来说可能不舒服:要将所需的功能导入项目,用户必须知道它在哪里,例如。 g.:
import
computeFirstItemNumberForSpecificPaginationPage
from "@yamato-daiwa/es-extensions/Number/Pagination";
为了解决这个问题,大部分函数都被导入到index.ts
并从那里再次导出。现在用户可以获得所需的功能:
import
computeFirstItemNumberForSpecificPaginationPage
from "@yamato-daiwa/es-extensions";
请注意,index.ts
中的所有函数(将由 TypeScript 编译为index.js
)都适用于 BrowserJS 和 NodeJS。 BrowserJS 的功能在 BrowserJS.ts
中,特别是 NodeJS 的功能在 NodeJS.ts
中(目前几乎是空的,但重新导出的方法是相同的)。
另外,在解决这个问题之前,我将编译后的 javascript 包含到库存储库 (Distributable
directory)。
问题
从现在开始,@yamato-daiwa/es-extensions 就是库,任何依赖它的项目都是消费项目。
我预计消费项目的所有未使用的函数/类都将被Webpack optimizations 切断。例如,在以下情况下,我预计 isUndefined
函数只会留在 Webpack 包中:
import isUndefined from "@yamato-daiwa/es-extensions"
const test: string | undefined = "ALPHA";
console.log(isUndefined(test));
但实际上,Webpack 从库的index.js
中留下了所有内容。我美化了 Webpack 构建的缩小版 JavaScript;就像:
(() =>
"use strict";
var e =
5272: (e, t) =>
Object.defineProperty(t, "__esModule",
value: !0
), t.default = function(e, t)
for (const [a, n] of e.entries())
if (t(n)) return a;
return null
,
7684: (e, t) =>
Object.defineProperty(t, "__esModule",
value: !0
), t.default = function(e, t)
const a = [];
return e.forEach(((e, n) =>
t(e) && a.push(n)
)), a
,
// ...
我想每个人都明白这是不可接受的,尤其是对于每千字节计数的浏览器应用程序。
如何解决这个问题?理想的解决方案(如果存在)不会触及源文件组织,只需更改 TypeScript 配置即可。
复制
我创建了one more repository (repro),您可以在其中尝试上述示例。
实验流程
-
通过 VCS 获取此存储库
一如既往地安装依赖项(
npm i
命令)。
检查src/index.ts
。它从库中导入 isUndefined
函数并使用它。
运行npm run ProductionBuild
使用beautifier.io之类的工具美化输出index.js
。您会看到整个库已被捆绑,而您希望只捆绑inUndefined
。
关于原因的沉思
第一个候选原因是使用 reexportint 模式,正好是 Source/index.ts、Source/BrowserJS.ts 和 Source/NodeJS。编译后的index.js
看起来像:
const isStringifiedNonNegativeIntegerOfRegularNotation_1 = require("./Numbers/isStringifiedNonNegativeIntegerOfRegularNotation");
exports.isStringifiedNonNegativeIntegerOfRegularNotation = isStringifiedNonNegativeIntegerOfRegularNotation_1.default;
const separateEach3DigitsGroupWithComma_1 = require("./Numbers/separateEach3DigitsGroupWithComma");
exports.separateEach3DigitsGroupWithComma = separateEach3DigitsGroupWithComma_1.default;
(Check full file)
如果从它的单个模块(如import isUndefined from "@yamato-daiwa/es-extensions/TypeGuards/isUndefined"
而不是import isUndefined from "@yamato-daiwa/es-extensions"
)导入每个函数,则不会输出冗余代码。但正如我已经说过的,这种解决方案是不可接受的,因为图书馆用户必须知道isUndefined
和其他功能的组织位置。
另一个原因可能是输出模块类型。目前是CommonJS
。这里是
图书馆的tsconfig.json
:
"compilerOptions":
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"removeComments": true,
"outDir": "Distributable/",
"declaration": true
,
"include": [ "Source/**/*" ]
根据假设,根据特定的模块类型,Webpack 可以将代码捆绑到单体结构中,在这种结构中,即使某些模块没有被使用,也无法分解和过滤掉。
现在所有这些(AMD、UMD、CommonJS)慢慢成为历史的一部分, 但我们仍然可以在旧脚本中找到它们。
???? javascript.info
顺便说一句,消费项目中的 TypeScript 配置也可能会影响(包括在 repro 中)。目前是:
"compilerOptions":
"target": "ES2020",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"baseUrl": "./",
"paths":
"@SourceFilesRoot/*": ["./src/*"]
【问题讨论】:
如果您的 typescript 输出是 ES6,那么 webpack 将能够进行 tree shaking。但请注意,如果您包含的某些包被导入为 es5,那么它们将不会受到树抖动的影响。不确定这是否是您要问的问题,这就是我发表评论的原因。 @CraigHicks,感谢您的评论。你的意思是我需要将构建我的库的 TypeScript 编译器设置为"target": "ES2015"
或更高版本?或者"module"
必须是ES**
,而不是CommonJS
?
兄弟!你为什么要重新发明***???已经有一个 Babel 插件可以做到这一点npmjs.com/package/babel-plugin-import
“tree-shaking”比“cut-off”更好用。 SO问题甚至还有一个“摇树”标签。您可以将其添加到此问题中。
我也去看看github.com/TokugawaTakeshi/yamato_daiwa-es_extensions/tree/…
【参考方案1】:
我相信您至少需要设置module
以便输出ES6 或更高版本。可能的值包括
"es6"
"es2020"
"esnext"
webpack documentation of tree-shaking 说
Tree Shaking 是 JavaScript 上下文中常用的一个术语,用于消除死代码。它依赖于 ES2015 模块语法的静态结构,即导入和导出。名称和概念已被 ES2015 模块捆绑器汇总推广。
*注意:moduleResolution:
可以保留为"node"
但是,正确的module:
设置不一定足够,甚至可能不理想。请参阅以下部分:
副作用
webpack 2 版本内置了对 ES2015 模块(别名 Harmony 模块)的支持以及未使用的模块导出检测。新的 webpack 4 版本扩展了此功能,通过“sideEffects”package.json 属性向编译器提供提示,以指示项目中的哪些文件是“纯”的,因此如果未使用,可以安全地修剪。
实际上,事实上,在*** sideEffects:
的默认值似乎是false
,所以除非你的 ES6 代码/模块有某些副作用,否则你不必担心不应该进行 tree-shaking。package.json
中设置 sideEffects:false
对于在您的项目中启用 tree-shaking 至关重要,如下所示。
(在不同的项目中取决于lodash
,sideEffects
并不重要。这可能是因为库目录结构和index.js
不同。
导入的库
例如,常规的loadash
不会被摇树,因为它不是 es6。
要在 lodash 中启用树抖动,您必须添加这些包并明确使用 es6 版本:
npm i lodash-es
npm i @types/lodash-es
并更改您的导入语句来自
import * as _ from "lodash"
到
import * as _ from "lodash-es"
请参阅此SE answer 进行讨论。
babel
babel doc on its own settings for "module" 说
模块
“AMD” | “嗯” | “系统” | “普通” | "cjs" | “汽车” | false,默认为“自动”。
启用将 ES 模块语法转换为另一种模块类型。注意,cjs 只是 commonjs 的别名。
将此设置为 false 将保留 ES 模块。仅当您打算将本机 ES 模块发送到浏览器时才使用此选项。如果你使用 Babel 打包器,默认模块:“auto”总是首选。 模块:“自动”
默认情况下,@babel/preset-env 使用调用者数据来确定是否应该转换 ES 模块和模块特性(例如 import())。通常调用者数据将在 bundler 插件中指定(例如 babel-loader、@rollup/plugin-babel),因此不建议自己传递调用者数据——传递的调用者可能会覆盖来自 bundler 插件和将来的调用者数据如果捆绑程序支持新的模块功能,您可能会得到次优结果。
所以我猜如果你真的需要 babel(而你 可能在 webpack4 中不需要它)那么你应该确保“caller”确实指定了“false”,所以 ES6 保持为“ES6” .在我成功缩小的设置中,我没有使用“babel”。
编辑:运行作者在 Gihub 上提供的实验编译,但根据标准输出诊断,与“CommonJS”相比,使用"module":"ESNext"
没有区别。难道@yamato-daiwa/
下的模块没有预编译成es6?
最佳解决方案
Issue-WebpackDoesNotCuttOfTheUnusedFunctionality
>package.json
"private": true,
"scripts":
"ProductionBuild": "webpack --mode production"
,
"sideEffects":false,
"devDependencies":
"ts-loader": "9.2.3",
"typescript": "4.3.2",
"webpack": "5.38.1",
"webpack-cli": "4.7.0",
"webpack-node-externals": "3.0.0",
"@yamato-daiwa/es-extensions": "file:../yamato_daiwa-es_extensions/Distributable/"
"sideEffects":false,
已添加。
tsconfig.json
"compilerOptions":
"target": "ES2020",
"strict": true,
"module": "es2020",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"baseUrl": "./",
"paths":
"@SourceFilesRoot/*": ["./src/*"]
模块改为"module": "es2020",
yamato_daiwa-es_extensions
tsconfig.json
"compilerOptions":
"target": "ES2020",
"module": "es2020",
"moduleResolution": "Node",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"removeComments": true,
"outDir": "Distributable/",
"declaration": true
,
"include": [ "Source/**/*" ],
模块改为"module": "es2020",
使用原始的index.ts
文件,不做任何更改
通过上述设置,我得到一个大小为 39 字节的输出 index.js
:
(()=>"use strict";console.log(!1))();
【讨论】:
感谢您的回答。抱歉,我不能将我的设置替换为您的设置。即使它会起作用,但在我们了解什么影响了树抖动之前,这个问题不会得到解决。也许是"module": "es2020"
设置?有时间我会检查的。
感谢您的回答。我需要一些时间来做一些实验。现在,我可以说我不使用 babel,并且目前我的库没有依赖项,所以很可能原因是模块类型。需要注意的是,有两个tsconfig.json
s:在库中和在消费项目中。哪个更重要?
库的tsconfig.json
设置需要将库转换为 es6+。除非是 es6+,否则 webpack 无法对库进行树摇动。所以“两者”都很重要,我相信正确的答案。我尝试只更改***目录 tsconfig.json,根据诊断输出,大小没有变化。
我使用"module": "ES2020"
设置创建了实现@yamato-daiwa/es-extensions@0.4.0-experimental,并将相同的设置添加到repro。很抱歉,输出包仍然被未使用的功能污染。让我重复一遍,目前我的库没有任何依赖关系,所以原因不是依赖关系。我可以请你再检查一次代码吗?找到原因后,我会尽快接受您的答复。
好消息!干得好!以上是关于如何为库设置 TypeScript 编译器,以便 Webpack 在依赖项目中切断未使用的模块?的主要内容,如果未能解决你的问题,请参考以下文章
如何为如下所示的 JavaScript 库编写 Typescript 定义文件?
如何为 TypeScript 配置 `*.graphql` 导入?
如何为属性创建 TypeScript @enumerable(false) 装饰器
如何为 subversion 存储库设置 svnserve <svn://> 协议?
如何为以下 contextapi 代码的 useReducer useContext 设置 Typescript 类型?