如何为库设置 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: 设置不一定足够,甚至可能不理想。请参阅以下部分:

副作用 导入的非 ES6 库 如果使用 babel,babel 设置可能会冲突

副作用

webpack 2 版本内置了对 ES2015 模块(别名 Harmony 模块)的支持以及未使用的模块导出检测。新的 webpack 4 版本扩展了此功能,通过“sideEffects”package.json 属性向编译器提供提示,以指示项目中的哪些文件是“纯”的,因此如果未使用,可以安全地修剪。

实际上,sideEffects: 的默认值似乎是false,所以除非你的 ES6 代码/模块有某些副作用,否则你不必担心不应该进行 tree-shaking。事实上,在*** package.json 中设置 sideEffects:false 对于在您的项目中启用 tree-shaking 至关重要,如下所示。

(在不同的项目中取决于lodashsideEffects 并不重要。这可能是因为库目录结构和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.jsons:在库中和在消费项目中。哪个更重要? 库的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 类型?

如何为 TypeScript React 应用程序设置键/值默认状态