原理
webpack的运行过程大致可以分为以下几个步骤:webpack的运行过程实际上就是等待上一个钩子结束调用下一个钩子的过程
- 初始化:webpack接收命令行参数或配置文件,创建一个Compiler对象
- Compiler对象提供了编译器钩子,在Compiler.compile方法中会依次调用这些钩子
- 然后注册插件,即往钩子里面注入回调函数
- 然后初始化Compiler变量和选项
- 然后
compiler.run
调用开始运行Compiler对象,调用 hooks.beforeRun.callAsync、hooks.run - 然后调用Compiler对象的 compile 方法
- 该方法先通过 newCompilationParams 创建内置的模块工厂 NormalModuleFactory和ContextModuleFactory
- NormalModuleFactory 在 creat 方法中依次调用了 hooks.beforeResolve、hooks.factorize
- hooks.factorize 被调用时会调用 hooks.resolve、hooks.afterResolve、hooks.createModule、hooks.module
- hooks.resolve被调用时会通过this.getResolver获取不同类型的模块解析器
- 然后按顺序依次调用钩子 hooks.beforeCompile、hooks.compile
- 然后使用 newCompilationParams 创建的模块工厂作为参数,创建一个Compilation对象,Compilation.params就是这两个模块工厂
- 创建Compilation对象时会调用 hooks.thisCompilation、hooks.compilation
- hooks.compilation 被调用时会执行 ImportPlugin 插件中的回调,这个回调会向 hooks.parser 注入回调,这个回调会执行 ImportParserPlugin
- ImportParserPlugin 插件会在 hooks.importCall 注入回调,用以处理 import 导入,用以获取当前模块中的所有依赖信息
- 接着再依次调用hooks.make、hooks.finishMak、hooks.afterCompile
- 该方法先通过 newCompilationParams 创建内置的模块工厂 NormalModuleFactory和ContextModuleFactory
- 钩子是由webpack核心代码提供,可以分为编译器(compiler)钩子,解析器(parser)钩子和编译(compilation)钩子,分别对应不同的阶段和对象
- webpack插件除了注册钩子回调函数,还可以访问webpack的API,修改配置选项,操作编译后的文件,生成额外的输出等
- 编译:Compiler对象开始编译,创建一个Compilation对象,表示一次编译过程,包含了所有的模块和 chunk 信息
- 在 hooks.make 钩子中会调用内部插件 EnrtyPlugin 注册的方法,从配置中找到入口模块,开始递归编译,然后使用 compilation 对象的 addEntry 方法来创建入口模块。
- 在编译过程中调用模块工厂和模块解析器来创建模块实例
- 模块工厂主要是根据模块的类型和配置,创建对应的模块对象,并调用相应的 loader 和 parser 来处理模块的内容和依赖
- 首先它会根据不同的模块类型使用不同的模块工厂,例如NormalModuleFactory、ContextModuleFactory。
- 模块工厂是根据模块的请求路径来判断模块类型的。
- NormalModuleFactory 会根据模块的路径和规则,匹配合适的 loader 来转换模块的源码,然后调用 acorn 这个 JS 解析器,将源码转换为 AST,从中提取出模块的依赖
- ContextModuleFactory 会根据上下文路径和正则表达式,动态地生成一个包含多个模块的 ContextModule 对象,并为每个模块创建一个 DelegatedModule 对象,用于代理模块的请求
- AsyncModuleRuntimeModule 会根据异步加载的语句,生成一个包含异步请求和回调函数的 RuntimeModule 对象,并注入到编译后的代码中
- 然后使用 loader 把模块编译为 js,在调用 acorn 把 js 编译为 ast
- 然后通过模块解析器解析 ast 中的导入,构建依赖树
- webpack 在编译器 (compiler) 类中提供了三种类型的内置模块解析器:normal, context 和 loader
- 首先它会根据不同的模块类型使用不同的模块工厂,例如NormalModuleFactory、ContextModuleFactory。
- 模块实例是webpack内部用来表示一个模块的对象,它包含了模块的信息,如请求路径,依赖,导出,解析器,生成器等。
- webpack会通过模块工厂将每个依赖转换为一个模块实例,并将它们放入一个模块图中,以便后续的优化和打包
- 在 hooks.make 钩子中会调用内部插件 EnrtyPlugin 注册的方法,从配置中找到入口模块,开始递归编译,然后使用 compilation 对象的 addEntry 方法来创建入口模块。
- 优化:Compilation(编译)对象对模块进行优化,例如删除未引用的模块,合并相同的模块,拆分大的模块等。
- 生成:Compilation对象根据模块和依赖关系生成输出文件,例如JavaScript文件、CSS文件、HTML文件等。
- 发射:Compilation对象将输出文件写入到指定的目录或内存中,完成一次编译过程。
https://www.webpackjs.com/concepts/
概念
- 可以通过在 webpack 配置中配置 entry 属性,来指定一个入口起点(或多个入口起点)
- loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)
- loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。
- 注释:在js中引入 css 看起来是件很奇怪的事,从标签来看,应该是在 html 中引入 style 标签
- 注释:webpack 这种处理方式,实际是以 js 为主导的实现方式。在代码编写时把引入关系都写入 js 文件,然后再编译时根据类型和加载优先级对引入关系进行处理
- loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务
- 通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
- Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)
- 注释:webpack 提供模块加载代码
- webpack 的 import() 和 require.ensure() 需要 Promise
- 如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill
入口起点
- 在 webpack 配置中有多种方式定义 entry 属性
- 注释:包括 字符串、数组、value为字符串路径的对象、value为对象的对象
- 也可以将一个文件路径数组传递给 entry 属性,这将创建一个所谓的 "multi-main entry"。
- 在你想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用。
- 注释:多个入口是为多页面应用准备的,每个入口可以作为一个页面对应的js文件
- 对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。
- “webpack 配置的可扩展” 是指,这些配置可以重复使用,并且可以与其他配置组合使用。
- 这是一种流行的技术,用于将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。
- 然后使用专门的工具(如 webpack-merge)将它们合并起来。
- 当你通过插件生成入口时,你可以传递空对象 {} 给 entry。
- 用于描述入口的对象。你可以使用如下属性:
- 注释:入口不仅可以配置为一个对象,每个入口还可以是一个对象
- dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载。
- dependOn 不能是循环引用的
- 注释:默认情况下,每个入口 chunk 会保存全部使用到的模块,可以使用 dependOn 可以定义在多个 chunk 之间共享的模块。
- filename: 指定要输出的文件名称。
- import: 启动时需加载的模块。(疑问)
- library: 指定 library 选项,为当前 entry 构建一个 library
- 注释:library 一个供别人使用的模块
- runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。
- runtime 和 dependOn 不应在同一个入口上同时使用
- 确保 runtime 不能指向已存在的入口名称
- 注释:可以使一个入口有个独立的 runtime
- publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。
module.exports = {
entry: {
main: './src/app.js',
vendor: './src/vendor.js',
},
output: {
filename: '[name].[contenthash].bundle.js',
},
};
- 这是告诉 webpack 我们想要配置 2 个单独的入口点
- 这样你就可以在 vendor.js 中存入未做修改的必要 library 或文件(例如 Bootstrap, jQuery, 图片等),然后将它们打包在一起成为单独的 chunk。
- 在 webpack < 4 的版本中,通常将 vendor 作为一个单独的入口起点添加到 entry 选项中,以将其编译为一个单独的文件(与 CommonsChunkPlugin 结合使用)
- 而在 webpack 4 中不鼓励这样做。而是使用 optimization.splitChunks 选项,将 vendor 和 app(应用程序) 模块分开,并为其创建一个单独的文件。
- 多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益
- 注释:多入口是同一个构建,能够对使用到的公共模块进行处理。
输出(output)
- 即使可以存在多个 entry 起点,但只能指定一个 output 配置
- 如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions) 来确保每个文件具有唯一的名称
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js',
},
output: {
filename: '[name].js',
path: __dirname + '/dist',
},
};
- 以下是对资源使用 CDN 和 hash 的复杂示例
- 注释:CDN就是采用更多的缓存服务器(CDN边缘节点),布放在用户访问相对集中的地区或网络中
module.exports = {
//...
output: {
path: '/home/proj/cdn/assets/[fullhash]',
publicPath: 'https://cdn.example.com/assets/[fullhash]/',
},
};
- 如果在编译时,不知道最终输出文件的 publicPath 是什么地址,则可以将其留空,并且在运行时通过入口起点文件中的 webpack_public_path 动态设置
- 注释:webpack_public_path 是一个变量,可以在代码中使用,也可以在代码中赋值,通过该变量可以使资源得引用路径动态生成
- 注释:在 css 中使用的话,style-loader 会将 url() 转换为 js 的 require() 方法来解析资源路径,然后生成正确的 css 插入 html 中
- 注释:在 css 可以直接使用 require() 它由 webpack 的 file-loader 提供
background-image: url(require('./image.png'));
loader
- 在你的应用程序中,有两种使用 loader 的方式:
- 配置方式(推荐):在 webpack.config.js 文件中指定 loader。
- 内联方式:在每个 import 语句中显式指定 loader。
- module.rules 允许你在 webpack 配置中指定多个 loader。
- loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。
- 最后,链中的最后一个 loader,返回 webpack 所期望的 JavaScript
- 通过为内联 import 语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader
- 使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader)
- 使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)
- 使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
- 选项可以传递查询参数,例如 ?key=value&foo=bar,或者一个 JSON 对象,例如 ?
- 注释:最后应该是需要以!结尾
import Styles from '!!style-loader!css-loader?modules!./styles.css';
- loader 可以是同步的,也可以是异步的
- loader 可以通过 options 对象配置
- 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块
- 注释:支持本地调试 loader
- loader 能够产生额外的任意文件
- loader 遵循标准 模块解析 规则。多数情况下,loader 将从 模块路径 加载(通常是从 npm install, node_modules 进行加载)
- 我们预期 loader 模块导出为一个函数,并且编写为 Node.js 兼容的 JavaScript。
plugin
- Webpack 自身也是构建于你在 webpack 配置中用到的 相同的插件系统 之上
- 插件目的在于解决 loader 无法实现的其他事。
- webpack 插件是一个具有 apply 方法的 JavaScript 对象。
- apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
- 注释:compiler 编译器对象,作为 apply 方法的第一个参数
- compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中重复使用。
- 注释:hooks 钩子
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建正在启动!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
- 在使用 Node API 时,还可以通过配置中的 plugins 属性传入插件
const webpack = require('webpack'); // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');
let compiler = webpack(configuration);
// 把 ProgressPlugin 插件添加到编译器中
new webpack.ProgressPlugin().apply(compiler);
compiler.run(function (err, stats) {
// ...
});
- 以上看到的示例和 webpack 运行时(runtime)本身 极其类似。webpack 源码 中隐藏有大量使用示例,你可以将其应用在自己的配置和脚本中
配置(Configuration)
- 由于 webpack 遵循 CommonJS 模块规范,因此,你可以在配置中使用
- 通过 require(...) 引入其他文件
- 通过 require(...) 使用 npm 下载的工具函数
- 使用 JavaScript 控制流表达式,例如 ?: 操作符
- 注释:3元运算符
- 对 value 使用常量或变量赋值
- 编写并执行函数,生成部分配置
- 除了可以将单个配置导出为 object,function 或 Promise 以外,还可以将其导出为多个配置
- 注释:多个配置,支持多个项目同时打包,而不仅仅是多个入口
模块(Modules)
- 与 Node.js 模块相比,webpack 模块 能以各种方式表达它们的依赖关系。
- ES2015 import 语句
- CommonJS require() 语句
- AMD define 和 require 语句
- css/sass/less 文件中的 @import 语句。
- stylesheet url(...) 或者 HTML
<img src=...>
文件中的图片链接。
- Webpack 天生支持如下模块类型
- ECMAScript 模块
- CommonJS 模块
- AMD 模块
- Assets
- 注释:资源模块,例如字体等
- WebAssembly 模块
模块解析(Module Resolution)
- resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。
- 注释:绝对路径
import '/home/me/file';
- 注释:绝对路径
- 模块路径
import 'module';
- 在 resolve.modules 中指定的所有目录中检索模块。 你可以通过配置别名的方式来替换初始模块路径,具体请参照 resolve.alias 配置选项。
- 如果 依赖 中包含 package.json 文件,那么在 resolve.exportsFields 配置选项中指定的字段会被依次查找,package.json 中的第一个字段会根据 package 导出指南确定 package 中可用的 export。
- 疑问:package.json 中的 export 用于为包内的模块提供别名,exportsFields 是指定当默认模块不存在时需要加载的别名?
- 一旦根据上述规则解析路径后,resolver 将会检查路径是指向文件还是文件夹。
- 如果路径指向文件:
- 如果文件具有扩展名,则直接将文件打包。
- 将使用 resolve.extensions 选项作为文件扩展名来解析,此选项会告诉解析器在解析中能够接受那些扩展名(例如 .js,.jsx)
- 如果路径指向一个文件夹,则进行如下步骤寻找具有正确扩展名的文件:
- 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。
- 注释:resolve.mainFields 和 resolve.exportsFields 分别对应 npm 规范中的 browser、module等 和 exports 两类字段
- 注释:exports 用来定义模块内部引用的别名
- 注释:browser、module等 用来定义模块整体引用的目标文件
- 注释:jsnext:main字段:指定ES6模块规范的入口文件。
- 注释:main字段:指定CommonJS规范的入口文件。
- 如果不存在 package.json 文件或 resolve.mainFields 没有返回有效路径,则会根据 resolve.mainFiles 配置选项中指定的文件名顺序查找,看是否能在 import/require 的目录下匹配到一个存在的文件名。
- 然后使用 resolve.extensions 选项,以类似的方式解析文件扩展名。
- 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。
- Webpack 会根据构建目标,为这些选项提供合理的默认配置
- 如果路径指向文件:
- loader 的解析规则也遵循特定的规范。但是 resolveLoader 配置项可以为 loader 设置独立的解析规则。
- 注释:loader 包的路径解析规则
- 每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的多个并行或串行请求。
- 在 watch 模式 下,只有修改过的文件会被从缓存中移出。
- 如果关闭 watch 模式,则会在每次编译前清理缓存。
Module Federation 模块联合
- 注释:允许在多个 webpack 编译产物之间互相加载模块
- 该项目供其他项目使用的导出模块:通过 ModuleFederationPlugin 的 exposes 配置项
- 该项目引入其他项目的导入模块:通过 ModuleFederationPlugin 的 remotes 配置项
- 共同拥有的依赖:通过 ModuleFederationPlugin 的 shared 配置项
- shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。
- remotes 将会首先依赖来自 host 的依赖,如果 host 没有依赖,它将会下载自己的依赖。
- shareScope 当前共享依赖的作用域名称,默认为 default
- 共享依赖在打包过程中是否被分离为 async chunk
- 页面、应用本质上和模块是相同的
- ModuleFederationPlugin 的 library 决定了引入的 remote 提供的内容将以何种方式暴露给 host。
- 例如:微前端需要做 js 隔离,就是通过作用域原理隔离了每个应用的上下文环境。
- 本地模块即为普通模块,是当前构建的一部分。远程模块不属于当前构建,并在运行时从所谓的容器加载。
- 注释:需要远程加载的 chunk 并不一定是远程模块
- 当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk 的加载操作中
- 注释:远程模块和本地模块的加载时机一致,都是在入口的下一个 chunk 加载操作中执行
- chunk 的加载操作通常是通过调用 import() 实现的,但也支持像 require.ensure 或 require([...]) 之类的旧语法。
- 注释:在项目中引用远程模块时,需要先配置当前项目的 ModuleFederationPlugin 插件,通过 remotes 定义其他打包 exposes 出的模块,然后就可以像引用普通模块一样
- 容器是由容器入口创建的,该入口暴露了对特定模块的异步访问。暴露的访问分为两个步骤:
- 1.加载模块(异步的)
- 2.执行模块(同步的)
- 注释:在 ModuleFederationPlugin 插件 exposes 配置的模块将被打包为一个容器,这个容器包括加载和执行两部分。
- 步骤 1 将在 chunk 加载期间完成。步骤 2 将在与其他(本地和远程)的模块交错执行期间完成。这样一来,执行顺序不受模块从本地转换为远程或从远程转为本地的影响。
- 注释:远程模块的加载及执行 和 本地模块中的异步 chunk 是一样的。
- 容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。
- 共享模块是指既可重写的又可作为向嵌套容器提供重写的模块。它们通常指向每个构建中的相同模块,例如相同的库。
- 注释:插件中通过 shared 设置,共同拥有的依赖
- packageName 选项允许通过设置包名来查找所需的版本。
- 默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将 requiredVersion 设置为 false 。
- 注释:获取依赖时是否需要检查版本
- ContainerPlugin 该插件使用指定的公开模块来创建一个额外的容器入口。
- 注释:创建容器
- ContainerReferencePlugin
- 该插件将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。
- 注释:为容器管理两个项目的公共依赖
- 它还会调用这些容器的 override API 来为它们提供重载。本地的重载(当构建也是一个容器时,通过 webpack_override 或 override API)和指定的重载被提供给所有引用的容器。
- 注释:重载就是依赖,远程模块在运行时的依赖是通过容器的 override API 来获取的,可以是本地的也可以是远程的。这些依赖可以重复被所有的容器引用,本地缺少的依赖会从当前远程模块所在构建中获取远程 chunk。
- 该插件将特定的引用添加到作为外部资源(externals)的容器中,并允许从这些容器中导入远程模块。
- ModuleFederationPlugin 组合了 ContainerPlugin 和 ContainerReferencePlugin
- 模块联合目标
- 它既可以暴露,又可以使用 webpack 支持的任何模块类型
- 代码块加载应该并行加载所需的所有内容(web:到服务器的单次往返)
- 从使用者到容器的控制
- 重写模块是一种单向操作
- 疑问:重写是什么意思?
- 同级容器不能重写彼此的模块
- 重写模块是一种单向操作
- 概念适用于独立于环境
- 可用于 web、Node.js 等
- 共享中的相对和绝对请求
- 会一直提供,即使不使用
- 疑问:共享中的相对和绝对请求是指什么?
- 会将相对路径解析到 config.context
- 注释:在共享模块中使用相对路径加载的,能够被正确解析。即使该模块作为远程模块被其他容器使用
- 默认不会使用 requiredVersion
- 会一直提供,即使不使用
- 共享中的模块请求
- 注释:应该是指远程模块中的共享依赖
- 只在使用时提供
- 会匹配构建中所有使用的相等模块请求
- 疑问:在加载时支持动态 import()?ModuleFederationPlugin 的 shared 共享依赖配置也允许配置为模糊路径?
- 将提供所有匹配模块
- 将从图中这个位置的 package.json 提取 requiredVersion(疑问)
- 当你有嵌套的 node_modules 时,可以提供和使用多个不同的版本
- 共享中尾部带有 / 的模块请求将匹配所有具有这个前缀的模块请求
- 动态远程容器
- 该容器接口支持 get 和 init 方法。
- init 是一个兼容 async 的方法,调用时,只含有一个参数:共享作用域对象(疑问)
- 此对象在远程容器中用作共享作用域,并由 host 提供的模块填充
- 可以利用它在运行时动态地将远程容器连接到 host 容器
- 注释:这里应该是指插件 ModuleFederationPlugin 对于容器的实现方法
- 该容器接口支持 get 和 init 方法。
- 容器尝试提供共享模块,但是如果共享模块已经被使用,则会发出警告,并忽略所提供的共享模块。
- 注释:一个共享模块被反复使用时不会反复加载,他们是同一个实例
- 注释:共享模块是指两个项目共享的依赖
- 你可以通过动态加载的方式,提供一个共享模块的不同版本,从而实现 A/B 测试。
- 注释:A/B 测试是一种将网页或应用程序的两个版本相互比较以确定哪个版本的性能更好的方法
- 注释:不通过插件的 remotes 配置项,手动实现远程模块加载。例如
- 一般来说,remote 是使用 URL 配置的,示例如下
- remote 用于配置需要加载的其他项目中的模块
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
}),
],
};
- 但是你也可以向 remote 传递一个 promise,其会在运行时被调用。
- 你应该用任何符合上面描述的 get/init 接口的模块来调用这个 promise。
- 例如,如果你想传递你应该使用哪个版本的联邦模块,你可以通过一个查询参数做以下事情:
- 注释:插件中的 remote 用来配置对远程模块的引用,加载后 promise 返回的应该是一个容器对象,即具有get和init方法的对象
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: `promise new Promise(resolve => {
const urlParams = new URLSearchParams(window.location.search)
const version = urlParams.get('app1VersionParam')
// This part depends on how you plan on hosting and versioning your federated modules
// 这部分取决于您计划如何托管和版本化联合模块
const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// 注入的脚本已加载并在窗口中可用
// we can now resolve this Promise
const proxy = {
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
// ...
}),
],
};
- 可以允许 host 在运行时通过公开远程模块的方法来设置远程模块的 publicPath
- 你在 https://my-host.com/app/* 上有一个 host 应用,并且在 https://foo-app.com 上有一个子应用。子应用程序也挂载在 host 域上, 因此, https://foo-app.com 可以通过 https://my-host.com/app/foo-app 访问,并且 https://my-host.com/app/foo-app/* 可以通过代理重定向到 https://foo-app.com/*。
- 注释:以下是 remote 中暴露 publicPath 供使用者修改的方法,实际上就是把修改 webpack_public_path 的方法作为一个模块供外部使用
- webpack.config.js (remote)
- 注释:导出的模块
module.exports = {
entry: {
remote: './public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // 该名称必须与入口名称相匹配
exposes: ['./public-path'],
// ...
}),
],
};
- public-path.js (remote)
export function set(value) {
__webpack_public_path__ = value;
}
- src/index.js (host)
const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');
//bootstrap app e.g. import('./bootstrap.js')
- 你可以在模块联邦的高级 API 中将依赖设置为即时依赖,此 API 不会将模块放在异步 chunk 中,而是同步地提供它们。
- 这使得我们在初始块中可以直接使用这些共享模块。
- 但是要注意,由于所有提供的和降级模块是要异步下载的,因此,建议只在应用程序的某个地方提供它,例如 shell。
- 我们强烈建议使用异步边界(asynchronous boundary)。它将把初始化代码分割成更大的块,以避免任何额外的开销,以提高总体性能。
- 注释:通过 shared 配置的共同依赖如果在入口文件就使用到,这样可以把这些共同依赖打包为一个块,而不是拆分成独立的好几块?
- 可以把这部分引用迁移到独立的文件中,然后 import 到入口文件,
- 也可以在 shared 中配置改模块的 eager 为 true
- index.js
import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));
- bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
- webpack.config.js
// ...
new ModuleFederationPlugin({
shared: {
...deps,
react: {
eager: true,
},
},
});
- 如果你想从不同的 remote 中加载多个模块,建议为你的远程构建设置 output.uniqueName 以避免多个 webpack 运行时之间的冲突。
- 注释:当这些 remote 提供的模块使用同一个全局变量名作为模块名时,就会造成彼此冲突
target 目标
- webpack 的 target 属性,不要和 output.libraryTarget 属性混淆。
- 注释:libraryTarget 打包 blund 的输出方式,例如:采用全局变量输出时,会把打包结果作为一个对象赋值给一个全局变量
- target 设置为 node,webpack 将在类 Node.js 环境编译代码。(使用 Node.js 的 require 加载 chunk,而不加载任何内置模块,如 fs 或 path)。
module.exports = {
target: 'node',
};
- 虽然 webpack 不支持 向 target 属性传入多个字符串,但是可以通过设置两个独立配置,来构建对 library 进行同构:
const path = require('path');
const serverConfig = {
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.node.js',
},
//…
};
const clientConfig = {
target: 'web', // <=== 默认为 'web',可省略
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.js',
},
//…
};
module.exports = [serverConfig, clientConfig];
manifest 显示
- 注释:模块和chunk的影射关系
- runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行过程中,webpack 用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑。包括:已经加载到浏览器中的连接模块逻辑,以及尚未加载模块的延迟加载逻辑。
- 一旦你的应用在浏览器中以 index.html 文件的形式被打开,一些 bundle 和应用需要的各种资源都需要用某种方式被加载与链接起来。在经过打包、压缩、为延迟加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件结构都已经不再存在。所以 webpack 如何管理所有所需模块之间的交互呢?这就是 manifest 数据用途的由来
- 当 compiler 开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "manifest",当完成打包并发送到浏览器时,runtime 会通过 manifest 来解析和加载模块。
- 无论你选择哪种 模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够检索这些标识符,找出每个标识符背后对应的模块。
- 通过使用内容散列(content hash)作为 bundle 文件的名称,这样在文件内容修改时,会计算出新的 hash,浏览器会使用新的名称加载文件,从而使缓存无效。一旦你开始这样做,你会立即注意到一些有趣的行为。即使某些内容明显没有修改,某些 hash 还是会改变。这是因为,注入的 runtime 和 manifest 在每次构建后都会发生变化。
模块热替换
- 主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 疑问:当前模块变动,模块内的数据不变。这个是如何实现的?
- 注释:vue中修改template能够保留数据,但是修改js代码就会导致该组件重新执行。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 在应用程序中
- 注释:应用程序是指服务端,每次服务端更新后都会通过 ws://localhost:8081/sockjs-node/672/qhzofehc/websocket 接口推送两条消息要求客户端检查更新。等待异步下载后再推送两条消息要求应用更新。
- 应用程序要求 HMR runtime 检查更新
- HMR runtime 异步地下载更新,然后通知应用程序
- 应用程序要求 HMR runtime 应用更新
- HMR runtime 同步地应用更新
- 在 compiler 中
- 除了普通资源,compiler 需要发出 "update",将之前的版本更新到新的版本。"update" 由两部分组成:
- 更新后的 manifest (JSON)
- manifest 包括新的 compilation hash 和所有的 updated chunk 列表
- 注释:hash 并不是必须的
- 一个或多个 updated chunk (JavaScript)
- 每个 chunk 都包含着全部更新模块的最新代码(或一个 flag 用于表明此模块需要被移除)。
- 更新后的 manifest (JSON)
- compiler 会确保在这些构建之间的模块 ID 和 chunk ID 保持一致。
- 注释:id保持一致,才能根据id知道需要替换得 chunk 是哪些。
- 通常将这些 ID 存储在内存中(例如,使用 webpack-dev-server 时),但是也可能会将它们存储在一个 JSON 文件中。
- 除了普通资源,compiler 需要发出 "update",将之前的版本更新到新的版本。"update" 由两部分组成:
- 在模块中
- HMR 是可选功能,只会影响包含 HMR 代码的模块。
- 举个例子,通过 style-loader 为 style 追加补丁。为了运行追加补丁,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。
- 注释:vue-loader 应该也对 HMR 进行了优化,template、script 会分开更更新
- 然而在多数情况下,不需要在每个模块中强行写入 HMR 代码。
- 如果一个模块没有 HMR 处理函数,更新就会冒泡(bubble up)。
- 这意味着某个单独处理函数能够更新整个模块树。如果在模块树的一个单独模块被更新,那么整组依赖模块都会被重新加载。
- 注释:如果一个模块对应的loader没有对热更新进行处理,那么整个chunk会被整体替换
- HMR 是可选功能,只会影响包含 HMR 代码的模块。
- 在 runtime 中
- 对于模块系统运行时(module system runtime),会发出额外代码,来跟踪模块 parents 和 children 关系。在管理方面,runtime 支持两个方法 check 和 apply。
- 注释:编译时会生成模块和chunk的关系图
- 注释:这里的父子关系是指模块之间的关系,在正常运行时只需要编译关系便能加载
- 疑问:跟踪额外的模块父子关系是为了什么?为了便于热更新进行替换吗?
- check 方法,发送一个 HTTP 请求来更新 manifest。如果请求失败,说明没有可用更新。
- 如果请求成功,会将 updated chunk 列表与当前的 loaded chunk 列表进行比较。
- 每个 loaded chunk 都会下载相应的 updated chunk。当所有更新 chunk 完成下载,runtime 就会切换到 ready 状态。
- 注释:在控制台表现为客户端从 websocket 收到更新要求后发起 http://czb.mtds2ver.oppein.com:8081/1512dd855b7a8e2a40f4.hot-update.json 来获取需要加载的文件,然后加载对应的文件
- apply 方法,将所有 updated module 标记为无效。
- 对于每个无效 module,都需要在模块中有一个 update handler,或者在此模块的父级模块中有 update handler。
- 注释:handler 处理器
- 否则,会进行无效标记冒泡,并且父级也会被标记为无效。继续每个冒泡,直到到达应用程序入口起点,或者到达带有 update handler 的 module(以最先到达为准,冒泡停止)。
- 所有无效 module 都会被(通过 dispose handler)处理和解除加载。
- 然后更新当前 hash,并且调用所有 accept handler。runtime 切换回 idle 状态,一切照常继续。
- 对于每个无效 module,都需要在模块中有一个 update handler,或者在此模块的父级模块中有 update handler。
- 对于模块系统运行时(module system runtime),会发出额外代码,来跟踪模块 parents 和 children 关系。在管理方面,runtime 支持两个方法 check 和 apply。
揭示内部原理
- 一个 chunk 组中可能有多个 chunk。例如,SplitChunksPlugin 会将一个 chunk 拆分为一个或多个 chunk。
- chunk 有两种形式:
- initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
- non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports) 或者 SplitChunksPlugin 时。
- 默认情况下,这些 non-initial chunk 没有名称,因此会使用唯一 ID 来替代名称。
- 在使用动态导入时,我们可以通过使用 magic comment(魔术注释) 来显式指定 chunk 名称
import(
/* webpackChunkName: "app" */
'./app.jsx'
).then((App) => {
ReactDOM.render(<App />, root);
});
- 输出文件的名称会受配置中的两个字段的影响:
- output.filename - 用于 initial chunk 文件
- output.chunkFilename - 用于 non-initial chunk 文件
- 在某些情况下,使用 initial 和 non-initial 的 chunk 时,可以使用 output.filename
- 这些字段中会有一些 占位符。常用的占位符如下:
- [id] - chunk id(例如 [id].js -> 485.js)
- [name] - chunk name(例如 [name].js -> app.js)。如果 chunk 没有名称,则会使用其 id 作为名称
- [contenthash] - 输出文件内容的 md4-hash(例如 [contenthash].js -> 4ea6ff1de66c537eb9b2.js)
————————————————————————————————————————————————————————
指南
起步
- 除了 import 和 export,webpack 还能够很好地支持多种其他模块语法,更多信息请查看 模块 API。
- webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack loader 系统 中使用了一个像是 Babel 的 transpiler(转译器)。
- 可以通过在 npm run build 命令与参数之间添加两个连接符的方式向 webpack 传递自定义参数,例如:npm run build -- --color。
管理资源
- 模块 loader 可以链式调用。
- 最后,webpack 期望链中的最后的 loader 返回 JavaScript。
- 这使你可以在依赖于此样式的 js 文件中 import './style.css'。现在,在此模块执行过程中,含有 CSS 字符串的 <style> 标签,将被插入到 html 文件的 <head> 中。
- 不要查看页面源代码,它不会显示结果,因为 <style> 标签是由 JavaScript 动态创建的
- 现在,在 import MyImage from './my-image.png' 时,此图像将被处理并添加到 output 目录,并且 MyImage 变量将包含该图像在处理后的最终 url。
- 使用 css-loader 时,如前所示,会使用类似过程处理你的 CSS 中的 url('./my-image.png')。loader 会识别这是一个本地文件,并将 './my-image.png' 路径,替换为 output 目录中图像的最终路径
- 而 html-loader 以相同的方式处理 <img src="./my-image.png" />
- 注释:不同文件中的资源是由不同的loader处理的
- 在 webpack 5 中,可以使用内置的 Asset Modules,我们可以轻松地将这些内容混入我们的系统中
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
};
- 使用 Asset Modules 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,也包括字体。
- 注释:字体在 css 中使用,应该是css-loader对css中的url进行了处理,这些处理的依据是来源于内置asset/resource对于资源路径的改动
- 类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from './data.json' 默认将正常运行。
- 注释:JSON被解析为 javascript 对象
- 要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。
- 注释:CSV 逗号分隔值文件格式。TSV 制表符分隔值文件格式
- 通过使用 自定义 parser 替代特定的 webpack loader,可以将任何 toml、yaml 或 json5 文件作为 JSON 模块导入。
const path = require('path');
const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
{
test: /\.toml$/i,
type: 'json',
parser: {
parse: toml.parse,
},
},
{
test: /\.yaml$/i,
type: 'json',
parser: {
parse: yaml.parse,
},
},
{
test: /\.json5$/i,
type: 'json',
parser: {
parse: json5.parse,
},
},
],
},
};
管理输出
- 注释,多个入口会生成相应的多个出口 bundle
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ index: './src/index.js',
+ print: './src/print.js',
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
- HtmlWebpackPlugin 创建了一个全新的文件,所有的 bundle 会自动添加到 html 中。
- 在每次构建前清理 /dist 文件夹,这样只会生成用到的文件。让我们使用 output.clean 配置项实现这个需求
- 注释:会清理output.path指向的文件夹
- 通过 WebpackManifestPlugin 插件,可以将 manifest 数据提取为一个 json 文件以供使用。
开发环境
- 将 mode 设置为 'development'
- 为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。
- 使用 inline-source-map 选项,这有助于解释说明示例意图
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
+ devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
- webpack 提供几种可选方式,帮助你在代码发生变化后自动编译代码:
- webpack's Watch Mode
- 如果其中一个文件被更新,代码将被重新编译,所以你不必再去手动运行整个构建
- 在命令行中运行 npm run watch
- webpack-dev-server
- webpack-dev-middleware
- webpack's Watch Mode
- webpack-dev-server 示例
- 为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能
- 将 dist 目录下的文件 serve 到 localhost:8080 下
- 因为在这个示例中单个 HTML 页面有多个入口,所以添加了 optimization.runtimeChunk: 'single' 配置。
- 注释:optimization.runtimeChunk: 'single' 是把运行时拆分成单独得 chunk,单个 HTML 页面有多个入口可以避免运行时代码重复加载。
- webpack-dev-server 会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问
- webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。
- 如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 devMiddleware.publicPath 选项进行修改
- 注释:output.publicPath是指打包后资源得公共路径,devMiddleware.publicPath则是本地服务得公共路径,当访问某个资源时,需要通过 devMiddleware.publicPath + output.publicPath 来访问
- 疑问:那 devServer.static 的设置有什么意思?读取资源吗?
- 如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的 devMiddleware.publicPath 选项进行修改
"start": "webpack serve --open"
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
+ devServer: {
+ static: './dist',
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
+ optimization: {
+ runtimeChunk: 'single',
+ },
};
- webpack-dev-middleware
- 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server
- webpack-dev-server 在内部使用了它
- 注释:虽然是webpack插件,但是在服务器框架中应用,后面第二个代码类似于 webpack-dev-server 得实现
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
+ publicPath: '/',
},
};
// server.js
// `"server": "node server.js"`
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
代码分离
- 代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
- 常用的代码分离方法有三种
- 入口起点:使用 entry 配置手动地分离代码
- 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
- 注释:
- Entry dependencies 入口配置实现代码分离,SplitChunksPlugin 使用插件或 optimization.splitChunks 实现
- 最初,chunks(以及内部导入的模块)是通过内部 webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。
- 从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks。
- 注释:
- 动态导入:通过模块的内联函数调用来分离代码。
- 入口起点
- 这种方式存在一些隐患
- 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。
const path = require('path');
module.exports = {
- entry: './src/index.js',
mode: 'development',
+ entry: {
+ index: './src/index.js',
+ another: './src/another-module.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
- 配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块
- 除了生成 shared.bundle.js,index.bundle.js 和 another.bundle.js 之外,还生成了一个 runtime.bundle.js 文件
const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
- 如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: 'single',否则还会遇到这里所述的麻烦
- 注释:runtimeChunk:true 是把 运行时独立为 chunk,多个入口时如果不设置,会把运行时打包到所有入口生成的blund中
- SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
- mini-css-extract-plugin: 用于将 CSS 从主应用程序中分离
- 动态代码拆分时,webpack 提供了两个类似的技术
- 使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入
- 使用 webpack 特定的 require.ensure
- Webpack v4.6.0+ 增加了对预获取和预加载的支持
- 在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- 会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部
- preload(预加载):当前导航下可能需要资源
- 在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload"> 请求 charting-library-chunk
- 假定 page-chunk 体积比 charting-library-chunk 更小,也更快地被加载完成,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。这将会使得加载时间能够更短一点,因为只进行单次往返,而不是两次往返。
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');
import(/* webpackPreload: true */ 'ChartingLibrary');
- 如果在 webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。
- 注释:例如:prefetch 会先创建 script 标签来预获取,但是这种获取不会触发 import() 的 catch 方法,而是会触发 timeout 事件
- Webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中
- 疑问:当已存在且加载失败的 script 添加 onerror 属性时会,事件并不会被触发
- 疑问:当通过 document.append 添加 script 标签时,该资源会被加载,重复添加的话资源会被重复加载
- 一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。 官方分析工具 是一个不错的开始。
- 注释:官方分析工具 Webpack Analysis
- 还有一些其他社区支持的可选项:
- webpack-chart: webpack stats 可交互饼图。
- webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
- webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。
- webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
- bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。
缓存
- 可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的名称
- 注释:substitutions 占位符
- [contenthash] substitution 将根据资源内容创建出唯一 hash
- webpack 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest
- 注释:所以入口 chunk 只要重新打包 hash 就会改变
- optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk
- 将其设置为 single 来为所有 chunk 创建一个 runtime bundle
- 注释:该 chunk 包含了 manifest
- 将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法
- 可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
runtimeChunk: 'single',
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ chunks: 'all',
+ },
+ },
+ },
},
};
- 每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变
- 注释:module.id 会导致 hash 也改变
- 将 optimization.moduleIds 设置为 'deterministic'
- 不论是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致
- 注释:不是为了依赖改变时hash不变,而是当依赖稳定不变时,避免打包顺序引起得hash变化
创建 library
- 通过 output.library 配置项暴露从入口导出的内容
- 将入口起点公开为 webpackNumbers,这样用户就可以通过 script 标签使用它
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
+ library: "webpackNumbers",
},
};
<script src="https://example.org/webpack-numbers.js"></script>
<script>
window.webpackNumbers.wordToNum('Five');
</script>
- 更新 output.library 配置项,将其 type 设置为 'umd'
- 注释:可以兼容 CommonJS、AMD 和 window 全局变量
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
- library: 'webpackNumbers',
+ library: {
+ name: 'webpackNumbers',
+ type: 'umd',
+ },
},
};
- 指定一个入口就已经足够了。尽管 multi-part 库 是有可能的,但通过作为单个入口点的索引脚本暴露部分导出会更简单。不推荐使用一个 array 作为库的 entry
- 把 lodash 当作 peerDependency
- 可以使用 externals 配置来完成
- 注释:peerDependency时package.json的属性,标识宿主项目需要提供这个库
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: {
name: "webpackNumbers",
type: "umd"
},
},
+ externals: {
+ lodash: {
+ commonjs: 'lodash',
+ commonjs2: 'lodash',
+ amd: 'lodash',
+ root: '_',
+ },
+ },
};
- 对于想要实现从一个依赖中调用多个文件的那些 library
- 注释:一个 library 提供多个导出,或者用户直接访问 library 某个文件
- 无法通过在 externals 中指定整个 library 的方式,将它们从 bundle 中排除。而是需要逐个或者使用一个正则表达式,来排除它们。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// 匹配以 "library/" 开始的所有依赖
/^library\/.+$/,
],
};
- 需要将生成 bundle 的文件路径,添加到 package.json 中
- main 是参照 package.json 标准,而 module 是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。
- module 属性应指向一个使用 ES2015 模块语法的脚本,但不包括浏览器或 Node.js 尚不支持的其他语法特性。
- 这使得 webpack 本身就可以解析模块语法,如果用户只用到 library 的某些部分,则允许通过 tree shaking 打包更轻量的包。
- 为了暴露和 library 关联着的样式表,你应该使用 MiniCssExtractPlugin。然后,用户可以像使用其他样式表一样使用和加载这些样式表。
环境变量
- 想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量
- webpack 命令行 环境配置 的 --env 参数,可以允许你传入任意数量的环境变量。而在 webpack.config.js 中可以访问到这些环境变量。
npx webpack --env goal=local --env production --progress
- 通常,module.exports 指向配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数
const path = require('path');
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log('Goal: ', env.goal); // 'local'
console.log('Production: ', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
- webpack CLI 提供一些你可以在 webpack 配置中访问的内建环境变量。
构建性能
1.通用环境
- 通过使用 include 字段,仅将 loader 应用在实际需要将其转换的模块
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
- 以下步骤可以提高解析速度:
- 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
- 注释:文档中有对 resolve 下属性的解析
- 如果你不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false
- 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false。
- 注释:resolve 配置模块如何解析
- 疑问:这个设置用来干嘛的?不缓存解析到的文件?
- 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
- 使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。
- 减少编译结果的整体大小,以提高构建性能。尽量保持 chunk 体积小。
- 在多页面应用程序中使用 SplitChunksPlugin ,并开启 async 模式。
- 疑问:async 是指可以同时编译多个 chunk 吗
- 移除未引用代码
- 只编译你当前正在开发的那些代码
- 疑问:热编译热更新吗?
- 在多页面应用程序中使用 SplitChunksPlugin ,并开启 async 模式。
- thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool
- 注释:thread 线程,
- 不要使用太多的 worker,因为 Node.js 的 runtime 和 loader 都有启动开销。
- 最小化 worker 和 main process(主进程) 之间的模块传输。进程间通讯(IPC, inter process communication)是非常消耗资源的。
- 持久化缓存
- 在 webpack 配置中使用 cache 选项
- 注释:缓存生成的 webpack 模块和 chunk,来改善构建速度
- 使用 package.json 中的 "postinstall" 清除缓存目录
- 注释:postinstall 是一个包,可以通过在 package.json 中配置来实现将某个文件从npm包复制到用户的本地目录
- 我们支持 yarn PnP v3 yarn 2 berry,来进行持久缓存
- 注释:yarn PnP v3 是一种 nodejs 包的管理规范,通过生成 .pnp.cjs 文件来声明当前项目需要依赖哪些包、这些包的实际位置 以及 这些包内部的依赖关系
- 在 webpack 配置中使用 cache 选项
- 将 ProgressPlugin 从 webpack 中删除,可以缩短构建时间。请注意,ProgressPlugin 可能不会为快速构建提供太多价值,因此,请权衡利弊再使用
- 注释:ProgressPlugin 用于自定义构建过程中日志如何报告
2.开发环境
- 使用 webpack 的 watch mode(监听模式)。而不使用其他工具来 watch 文件和调用 webpack 。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效
- 疑问:是记录文件最后的修改时间吗?
- 在某些配置环境中,watch mode 会回退到 poll mode(轮询模式)。监听许多文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll 来增加轮询的间隔时间。
- 下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能
- webpack-dev-server
- webpack-hot-middleware
- webpack-dev-middleware
- webpack 4 默认使用 stats.toJson() 输出大量数据。除非在增量步骤中做必要的统计,否则请避免获取 stats 对象的部分内容。
- webpack-dev-server 在 v3.1.3 以后的版本,包含一个重要的性能修复,即最小化每个增量构建步骤中,从 stats 对象获取的数据量。
- 注释:stats 是 webpack 编译器回调函数的一个参数。
- 不同的 devtool 设置,会导致性能差异
- 注释:devtool 用来设置 source map
- "eval" 具有最好的性能,但并不能帮助你转译代码。
- 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
- 使用 eval-source-map 变体配置进行增量编译
- 在大多数情况下,最佳选择是 eval-cheap-module-source-map
- 通常在开发环境下,应该排除以下这些工具:
- TerserPlugin
- 使用 TerserPlugin 来 minify(压缩) 和 mangle(混淆破坏) 代码
- [fullhash]/[chunkhash]/[contenthash]
- AggressiveSplittingPlugin
- 注释:可以将 bundle 拆分成更小的 chunk,直到各个 chunk 的大小达到 option 设置的 maxSize
- AggressiveMergingPlugin
- 注释:在工程文件中,经常会在不同文件中引用同一个module,使用该配置可以防止重复打包,减小最终文件的大小
- 疑问:重复打包,会导致不是同一个module实例吗
- ModuleConcatenationPlugin
- 注释:webpack 之前采用闭包包裹每一个模块,使用该插件可以将模块只包裹在一个闭包中,提高了浏览器运行速度
- TerserPlugin
- Webpack 只会在文件系统中输出已经更新的 chunk。
- 某些配置选项(HMR, output.chunkFilename 的 [name]/[chunkhash]/[contenthash],[fullhash])来说,除了对已经更新的 chunk 无效之外,对于 entry chunk 也不会生效。
- 确保在生成 entry chunk 时,尽量减少其体积以提高性能。
- 下面的配置为运行时代码创建了一个额外的 chunk,所以它的生成代价较低
module.exports = {
// ...
optimization: {
runtimeChunk: true,
},
};
- Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能
- 这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:
module.exports = {
// ...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false, // 把app中node_module中的模块和拆封成独立chunk
},
};
- Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:
- 疑问:什么是路径信息?
module.exports = {
// ...
output: {
pathinfo: false,
},
};
- TypeScript loader 可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。
- 使用此选项,会关闭类型检查。
- 如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度
module.exports = {
// ...
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
};
3.生产环境
- Babel 最小化项目中的 preset/plugin 数量
- 注释:preset 预设可以作为 Babel 插件和配置 选项 的共享集
- @babel/preset-env 用于编译 ES2015+ 语法
- @babel/preset-typescript 用于 TypeScript
- @babel/preset-react 用于 React
- @babel/preset-flow 用于 Flow
- 注释:preset 预设可以作为 Babel 插件和配置 选项 的共享集
- TypeScript 在单独的进程中使用
- fork-ts-checker-webpack-plugin 进行类型检查。
- 配置 loader 跳过类型检查。
- 使用 ts-loader 时,设置 happyPackMode: true / transpileOnly: true。
- Sass node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader 时,需要设置 workerParallelJobs: 2
- 注释:thread-loader 本文有提及
内容安全策略
- Webpack 能够为其加载的所有脚本添加 nonce
- 注释:nonce 是一串唯一值字符串,它配合内容安全策略使用,内容安全策略会告诉浏览器哪些 nonce 允许被使用。
- 要启用此功能,需要在引入的入口脚本中设置一个 webpack_nonce 变量
- 应该为每个唯一的页面视图生成和提供一个唯一的基于 hash 的 nonce,这就是为什么 webpack_nonce 要在入口文件中指定,而不是在配置中指定的原因。
- 注意,webpack_nonce 应该是一个 base64 编码的字符串
__webpack_nonce__ = 'c29tZSBjb29sIHN0cmluZyB3aWxsIHBvcCB1cCAxMjM=';
- webpack 还能够使用 Trusted Types 来加载动态构建的脚本,遵守 CSP require-trusted-types-for 指令的限制。可查看 output.trustedTypes 配置项。
开发 - Vagrant(略)
- 如果你在开发一个更加高级的项目,并且使用 Vagrant 来实现在虚拟机(Virtual Machine)上运行你的开发环境,那你可能会需要在虚拟机中运行 webpack。
- 注释:Vagrant 应该是用来创建一套开发环境的工具,可以方便的在虚拟机中部署这个开发环境。有点像 Docker
依赖管理
- 如果你的 request 含有表达式(expressions),就会创建一个上下文(context),因为在编译时(compile time)并不清楚 具体 导入哪个模块。
- 当台下的 require() 调用被评估解析:
require('./template/' + name + '.ejs');
- webpack 解析 require() 调用,然后提取出如下一些信息:
Directory: ./template
Regular expression: /^.*\.ejs$/
- 会生成一个 context module(上下文模块)。它包含 目录下的所有模块 的引用,如果一个 request 符合正则表达式,就能 require 进来。
- 该 context module 包含一个 map(映射)对象,会把 requests 翻译成对应的模块 id。
- 此 context module 还包含一些访问这个 map 对象的 runtime 逻辑。
- 这意味着 webpack 能够支持动态地 require,但会导致所有可能用到的模块都包含在 bundle 中。
- 注释:这个 request 属于异步加载,应该会分别打包。
- 疑问:是所有的模块 context 都打包到一个独立的 bundle 中吗?
{
"./table.ejs": 42,
"./table-row.ejs": 43,
"./directory/another.ejs": 44
}
- 还可以通过 require.context() 函数来创建自己的 context
- 注释:应该就是 webpack 对于上下文得实现。
- 可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
- 传递给 require.context 的参数必须是字面量
require.context('../', true, /\.stories\.js$/);
// (创建出)一个 context,其中所有文件都来自父文件夹及其所有子级文件夹,request 以 `.stories.js` 结尾。
- 一个 context module 会导出一个(require)函数,此函数可以接收一个参数:request。
- 注释:require.context 运行后会返回一个函数对象
- 此导出函数有三个属性:resolve, keys, id
- resolve 是一个函数,它返回 request 被解析后得到的模块 id。
- keys 也是一个函数,它返回一个数组,由所有可能被此 context module 处理的请求(译者注:参考下面第二段代码中的 key)组成。
- id 是 context module 的模块 id. 它可能在你使用 module.hot.accept 时会用到。
const cache = {};
function importAll(r) {
r.keys().forEach((key) => (cache[key] = r(key)));
}
importAll(require.context('../components/', true, /\.js$/));
// 在构建时(build-time),所有被 require 的模块都会被填充到 cache 对象中。
安装
- 如果你使用 webpack v4+ 版本,并且想要在命令行中调用 webpack,你还需要安装 CLI。
npm install --save-dev webpack-cli
- 想要运行本地安装的 webpack,你可以通过 node_modules/.bin/webpack 来访问它的二进制版本。
- 注释:npm为script字段中的脚本路径都加上了node_moudles/.bin前缀
模块热替换
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ hot: true,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
- 可以为 HMR 提供入口
- 注释:添加的 hot 和 client 应该也是把客户端需要的热更新代码打进客户端,已实现客户端执行热更新
- 注释:这应该是上例中 hot: true 的内部实现方式
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require("webpack");
module.exports = {
entry: {
app: './src/index.js',
+ // Runtime code for hot module replacement 热模块更换的运行时代码
+ hot: 'webpack/hot/dev-server.js',
+ // Dev server client for web socket transport, hot and live reload logic 开发用于web套接字传输、热加载和实时加载逻辑的服务器客户端
// client 客户
+ client: 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
},
devtool: 'inline-source-map',
devServer: {
static: './dist',
+ // Dev server client for web socket transport, hot and live reload logic 开发用于web套接字传输、热加载和实时加载逻辑的服务器客户端
+ hot: false,
+ client: false,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
+ // Plugin for hot module replacement 热模块更换插件
+ new webpack.HotModuleReplacementPlugin(),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
- 注释:可以在运行时直接访问module对象
- 注释:HMR 插件提供了对文件修改的监听,还提供了监听到改变后触发的事件,供loader对变更进行处理。这里只是loader对处理文件的热更新范例
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
- 在 Node.js API 中使用 webpack dev server 时,不要将 dev server 选项放在 webpack 配置对象中。而是在创建时, 将其作为第二个参数传递。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const webpackDevServer = require('webpack-dev-server');
const config = {
mode: 'development',
entry: [
// Runtime code for hot module replacement
'webpack/hot/dev-server.js',
// Dev server client for web socket transport, hot and live reload logic
'webpack-dev-server/client/index.js?hot=true&live-reload=true',
// Your entry
'./src/index.js',
],
devtool: 'inline-source-map',
plugins: [
// Plugin for hot module replacement
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
const compiler = webpack(config);
// `hot` and `client` options are disabled because we added them manually
const server = new webpackDevServer({ hot: false, client: false }, compiler);
(async () => {
await server.start();
console.log('dev server is running');
})();
- 如果你正在使用 webpack-dev-middleware,可以通过 webpack-hot-middleware 依赖包,在自定义 dev server 中启用 HMR
- 注释:webpackDevServer 用 HotModuleReplacementPlugin 实现热更新
- 社区还提供许多其他 loader 和示例,可以使 HMR 与各种框架和库平滑地进行交互
- React Hot Loader: 实时调整 react 组件。
- Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
- Elm Hot webpack Loader: 支持 Elm 编程语言的 HMR。
- 注释:Elm 一种变成语言,可以用来写前端
- Angular HMR: 没有必要使用 loader!直接修改 NgModule 主文件就够了,它可以完全控制 HMR API。
- 注释:NgModule 是 @angular/cli 打包的最小单位。打包的时候,@angular/cli 会检查所有 @NgModule 和路由配置,如果你配置了异步模块,cli 会自动把模块切分成独立的 chunk(块)。
- Svelte Loader: 此 loader 开箱即用地支持 Svelte 组件的热更新。
Tree Shaking
- tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。
- 新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
- 注释:只要我们确定当前包里的模块不包含副作用,然后将发布到 npm 里的包标注为 sideEffects: false ,我们就能为使用方提供更好的打包体验。原理是 webpack 能将标记为 side-effects-free 的包由 import {a} from xx 转换为 import {a} from 'xx/a',从而自动修剪掉不必要的 import
- 需要将 mode 配置设置成development,以确定 bundle 不会被压缩
- 注释:这里启用的是 usedExports,在 development 模式下会标记需要修剪得代码,但不会进行真得修剪
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ mode: 'development',
+ optimization: {
+ usedExports: true,
+ },
};
- 如果你的代码确实有一些副作用,可以改为提供一个数组:
- 此数组支持简单的 glob 模式匹配相关文件。其内部使用了 glob-to-regexp(支持:*,**,{a,b},[a-z])
- 注释:用以告诉webpack,那些文件是有副作用的,不能删除
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
- 还可以在 module.rules 配置选项 中设置 "sideEffects"
- sideEffects 和 usedExports(更多被认为是 tree shaking)是两种不同的优化方式
- sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。
- 注释:通过 sideEffects 排除的文件,其内的所有关联模块都会被排除,即使满足 sideEffects 匹配的
- usedExports 依赖于 terser 去检测语句中的副作用。它是一个 JavaScript 任务而且没有像 sideEffects 一样简单直接。而且它不能跳转子树/依赖由于细则中说副作用需要被评估。
- 注释:terser原作用是用来压缩和混淆代码的
- 尽管导出函数能运作如常,但 React 框架的高阶函数(HOC)在这种情况下是会出问题的。
- 我们可以通过 /#PURE/ 注释来帮忙 terser。它给一个语句标记为没有副作用。就这样一个简单的改变就能够使下面的代码被 tree-shake
var Button$1 = /*#__PURE__*/ withAppProvider()(Button);
- 是可以告诉 webpack 一个函数调用是无副作用的,只要通过 /#PURE/ 注释。它可以被放到函数调用之前,用来标记它们是无副作用的(pure)。
/*#__PURE__*/ double(55);
- 注释:一个文件如果只包含重新导出,那么这个文件将会被树摇掉。例如:
export {default as XXX}
- sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。
- 通过 import 和 export 语法,我们已经找出需要删除的“未引用代码(dead code)”,然而,不仅仅是要找出,还要在 bundle 中删除它们。为此,我们需要将 mode 配置选项设置为 production。
- 注释:设置成 development 为了加快打包速度,是不会进行删除的
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
- mode: 'development',
- optimization: {
- usedExports: true,
- }
+ mode: 'production',
};
- 可以在命令行接口中使用 --optimize-minimize 标记,来启用 TerserPlugin
- 在使用 tree shaking 时必须有 ModuleConcatenationPlugin 的支持,您可以通过设置配置项 mode: "production" 以启用它。如果您没有如此做,请记得手动引入 ModuleConcatenationPlugin。
生产环境
- 许多 library 通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。
- NODE_ENV 是一个由 Node.js 暴露给执行脚本的系统环境变量。通常用于决定在开发环境与生产环境(dev-vs-prod)下,server tools(服务期工具)、build scripts(构建脚本) 和 client-side libraries(客户端库) 的行为。
- 从 webpack v4 开始, 指定 mode 会自动地配置 DefinePlugin
- 注释:DefinePlugin 允许在 编译时 将你代码中的变量替换为其他值或表达式。
- 注释:使用了该插件后会根据 mode 将参与打包得代码中得 process.env.NODE_ENV 替换为 mode 得值
- 然而,与预期相反,在构建脚本 webpack.config.js 中 process.env.NODE_ENV 并没有被设置为 "production"
- 注释:这个 webpack.config.js 种 mode 被设置为 production
- 注释:DefinePlugin 插件只影响被打包的文件,在配置中无效
- 压缩
- ClosureWebpackPlugin
- 如果决定尝试一些其他压缩插件,确保新插件也会按照 tree shake 指南中所陈述的具有删除未引用代码(dead code)的能力,并将它作为 optimization.minimizer。
- 源码映射 将在 生产环境 中使用 source-map 选项,而不是我们在 开发环境 中用到的 inline-source-map
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
+ devtool: 'source-map',
});
- 上述许多选项都可以通过命令行参数进行设置。例如,optimize-minimize 可以使用 --optimization-minimize 进行设置,mode 可以使用 --mode 进行设置。
- 运行 npx webpack --help=verbose 可以查看所有关于 CLI 的可用参数。
懒加载
- 注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。
ECMAScript 模块
- Node.js 通过设置 package.json 中的属性来显式设置文件模块类型。
- 在 package.json 中设置 "type": "module" 会强制 package.json 下的所有文件使用 ECMAScript 模块。 设置 "type": "commonjs" 将会强制使用 CommonJS 模块。
- 除此之外,文件还可以通过使用 .mjs 或 .cjs 扩展名来设置模块类型。 .mjs 将它们强制置为 ESM,.cjs 将它们强制置为 CommonJs。
- 在使用 text/javascript 或 application/javascript mime type 的 DataURI 中,也将使用 ESM。
- javascript 文本可以转化为 DataURI 模式嵌入到 html 中,它依然支持 ESM 模式。
- 注释:使用 data URI scheme 获取资料可以写成
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="">
- Data URI scheme 的语法
data:①[<mime type>]②[;charset=<charset>]③[;<encoding>]④,<encoded data>⑤
- 【1】第①部分data: 协议头,它标识这个内容为一个 data URI 资源。
- 【2】第②部分MIME 类型(可选项):浏览器通常使用MIME类型(而不是文件扩展名)来确定如何处理文档;
- MIME类型对大小写不敏感,但是传统写法都是小写。
- 【3】第③部分 ;charset=
: 源文本的字符集编码方式,默认编码是 charset=US-ASCII, 即数据部分的每个字符都会自动编码为 %xx - 【4】第④部分 [;
] : 数据编码方式(默认US-ASCII,BASE64两种) - 【5】第⑤部分 ,
: 编码后的数据 - 可以直接在浏览器的地址栏中输入进行访问
- Data URI scheme 的语法
- 导入模块在 ESM 中更为严格,导入相对路径的模块必须包含文件名和文件扩展名(例如 *.js 或者 *.mjs),除非你设置了 fullySpecified=false。
- non-ESM 仅能导入 default 导出的模块,不支持命名导出的模块
- 注释:non-ESM 没有设置 __esModule: true 的 CommonJS 或者 AMD 模块
- CommonJs 语法不可用: require, module, exports, __filename, __dirname.
- HMR 使用 import.meta.webpackHot 代替 module.hot
Shimming 预置依赖
- 一些 third party(第三方库) 可能会引用一些全局依赖(例如 jQuery 中的 $)。因此这些 library 也可能会创建一些需要导出的全局变量。这些 "broken modules(不符合规范的模块)" 就是 shimming(预置依赖) 发挥作用的地方。
- shim 另外一个极其有用的使用场景就是:当你希望 polyfill 扩展浏览器能力,来支持到更多用户时。在这种情况下,你可能只是想要将这些 polyfills 提供给需要修补(patch)的浏览器(也就是实现按需加载)。
- 注释:以下代码来自于同一章节得稍后部分,先将所有 polyfill 拆分为独立入口,为按需引入做准备。然后在 html 通过判断属性来按需引入。感觉这个应该有现成的插件
const path = require('path');
const webpack = require('webpack');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ polyfills: './src/polyfills',
+ index: './src/index.js',
+ },
output: {
- filename: 'main.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
{
test: require.resolve('./src/globals.js'),
// 这里和后文中的同一用例不一致,存在疑问
use:
'exports-loader?type=commonjs&exports[]=file&exports[]=multiple|helpers.parse|parse',
},
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting Started</title>
+ <script>
+ const modernBrowser = 'fetch' in window && 'assign' in Object;
+ if (!modernBrowser) {
+ const scriptElement = document.createElement('script');
+ scriptElement.async = false;
+ scriptElement.src = '/polyfills.bundle.js';
+ document.head.appendChild(scriptElement);
+ }
+ </script>
</head>
<body>
- <script src="main.js"></script>
+ <script src="index.bundle.js"></script>
</body>
</html>
- 应用程序中的模块依赖,改为一个全局变量依赖。要实现这些,我们需要使用 ProvidePlugin 插件。
- 能够在 webpack 编译的每个模块中,通过访问一个变量来获取一个 package。
- 如果 webpack 看到模块中用到这个变量,它将在最终 bundle 中引入给定的 package。
- 注释:是一个库以全局变量的形式注入
const path = require('path');
+const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ _: 'lodash',
+ }),
+ ],
};
- 可以使用 ProvidePlugin 暴露出某个模块中单个导出,通过配置一个“数组路径”(例如 [module, child, ...children?])实现此功能。
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new webpack.ProvidePlugin({
- _: 'lodash',
+ join: ['lodash', 'join'],
}),
],
};
- 一些遗留模块依赖的 this 指向的是 window 对象
- 注释:对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined
- 可以通过使用 imports-loader 覆盖 this 指向
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
+ rules: [
+ {
+ test: require.resolve('./src/index.js'),
+ use: 'imports-loader?wrapper=window',
+ },
+ ],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
- 可以使用 exports-loader,将一个全局变量作为一个普通的模块来导出
- 例如,为了将 file 导出为 file 以及将 helpers.parse 导出为 parse,做如下调整:
- 使用
const { file, parse } = require('./globals.js');
,可以保证一切将顺利运行
- 使用
// 目标代码为
const file = 'blah.txt';
const helpers = {
test: function () {
console.log('test something');
},
parse: function () {
console.log('parse something');
},
};
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
+ {
+ test: require.resolve('./src/globals.js'),
+ use:
+ 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
+ },
],
},
plugins: [
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
],
};
- 社区中存在许多误解,即现代浏览器“不需要”polyfill,或者 polyfill/shim 仅用于添加缺失功能 - 实际上,它们通常用于修复损坏实现(repair broken implementation)
- 因此,最佳实践仍然是,不加选择地和同步地加载所有 polyfill/shim,尽管这会导致额外的 bundle 体积成本。
- babel-preset-env package 通过 browserslist 来转译那些你浏览器中不支持的特性。
- 注释:babel-preset-env 是 babel 的一个预设,可以在编译时用来为目标浏览器添加 polyfill
- 像 process 这种 Node 内置模块,能直接根据配置文件进行正确的 polyfill,而不需要任何特定的 loader 或者 plugin。
- 疑问:process 在 node 使用时是不需要 polyfill 的。
- 如果这些遗留模块没有 AMD/CommonJS 版本,但你也想将他们加入 dist 文件,则可以使用 noParse 来标识出这个模块。
- 注释:noParse 一个 webpack 配置项,支持正则和函数,用以声明哪些库不需要被解析和转换
- 这样就能使 webpack 将引入这些模块,但是不进行转化(parse),以及不解析(resolve) require() 和 import 语句。这种用法还会提高构建性能。
- 任何需要 AST 的功能(例如 ProvidePlugin)都不起作用。
- 一些模块支持多种 模块格式,例如一个混合有 AMD、CommonJS 和 legacy(遗留) 的模块。在大多数这样的模块中,会首先检查 define,然后使用一些怪异代码导出一些属性。
- 注释:例如 JQ,它在一个入口文件同时支持 AMD、CommonJS 和 全局变量,这种支持的实现,其实是一个自运行函数,通过检查 define 来判断是否 AMD 模块方式,否则采用其他导出方式导出。
- 注释:在 AMD 中导出模块实际是调用
define(["jquery"], factory);
,而 COMMONJS 的导出方式是factory(require("jquery"));
- 可以通过 imports-loader 设置
additionalCode=var%20define%20=%20false;
来强制 CommonJS 路径。
TypeScript
- 现在让我们改变 lodash 在 ./index.ts 文件中的引入, 因为在 lodash 的定义中没有默认(default)的导出。
- import _ from 'lodash';
+ import * as _ from 'lodash';
- 如果想在 TypeScript 中保留如import _ from 'lodash';的语法被让它作为一种默认的导入方式,需要在文件 tsconfig.json 中设置 "allowSyntheticDefaultImports" : true 和 "esModuleInterop" : true 。
- Note that if you're already using babel-loader to transpile your code, you can use @babel/preset-typescript and let Babel handle both your JavaScript and TypeScript files instead of using an additional loader. Keep in mind that, contrary to ts-loader, the underlying @babel/plugin-transform-typescript plugin does not perform any type checking.
- 注释:可以使用 @babel/preset-typescript 来代替 ts-loader 转换代码,但是它不会执行 ts 检查
- 想要启用 source map,我们必须配置 TypeScript,以将内联的 source map 输出到编译后的 JavaScript 文件中。
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
}
}
- 可以在 TypeScript 代码中使用 webpack 特定的特性,比如 import.meta.webpack。并且 webpack 也会为它们提供类型支持,只需要添加一个 TypeScript reference 声明:
/// <reference types="webpack/module" />
console.log(import.meta.webpack); // 没有上面的声明的话,TypeScript 会抛出一个错误
- 想要在 TypeScript 中使用非代码资源(non-code asset),我们需要告诉 TypeScript 推断导入资源的类型。在项目里创建一个 custom.d.ts 文件,这个文件用来表示项目中 TypeScript 的自定义类型声明。
declare module '*.svg' {
const content: any;
export default content;
}
Web Workers
- 从 webpack 5 开始,你可以使用 Web Workers 代替 worker-loader。
- 注释:worker-loader 能够把一个 js 模块包装为一个类,在代码中引入这个模块时就等同于引入了一个类,通过 new 这个类可以建立一个运行这个js代码得 Web Workers 对象
- 选择这种语法是为了实现不使用 bundler 就可以运行代码,它也可以在浏览器中的原生 ECMAScript 模块中使用。
- 注释:下文中得语法更贴近于 ECMAScript 规范,使你编写得代码可以直接在浏览器模块中运行,import.meta.url 是 ECMAScript 模块中的一个特殊属性,用于获取当前模块的 URL 地址。
new Worker(new URL('./worker.js', import.meta.url));
- Node.js(>= 12.17.0) 也支持类似的语法:
- 这仅在 ESM 中可用。但不可用于 ComonnJS,无论 webpack 还是 Node.js 均是如此。
import { Worker } from 'worker_threads';
new Worker(new URL('./worker.js', import.meta.url));
渐进式网络应用程序
- 使用名为 Service Workers 的 web 技术来实现的
- 注释:Service Worker 是在 Web Worker 的基础上扩展而来,可以拦截和处理网络请求,从而实现离线缓存、推送通知、消息推送等功能。
- 名为 Workbox 的 Google 项目来实现此目的,该项目提供的工具可帮助我们更简单地为 web app 提供离线支持
- 添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件
- 注释:workbox-webpack-plugin 是一个 Webpack 插件,用于集成 Workbox 库。可以在 Webpack 构建过程中自动地生成 Service Worker,并将其注入到 HTML 文件中。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js',
},
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management',
+ title: 'Progressive Web Application',
}),
+ new WorkboxPlugin.GenerateSW({
+ // 这些选项帮助快速启用 ServiceWorkers
+ // 不允许遗留任何“旧的” ServiceWorkers
+ clientsClaim: true,
+ skipWaiting: true,
+ }),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
import _ from 'lodash';
import printMe from './print.js';
+ if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker.register('/service-worker.js').then(registration => {
+ console.log('SW registered: ', registration);
+ }).catch(registrationError => {
+ console.log('SW registration failed: ', registrationError);
+ });
+ });
+ }
公共路径
- 在开发环境中,我们通常有一个 assets/ 文件夹,它与索引页面位于同一级别。这没太大问题,但是,如果我们将所有静态资源托管至 CDN,然后想在生产环境中使用呢?
- 可以直接使用一个有着悠久历史的 environment variable(环境变量)
- 注释:DefinePlugin用来将代码中得变量替换为具体值
import webpack from 'webpack';
// 尝试使用环境变量,否则使用根路径
const ASSET_PATH = process.env.ASSET_PATH || '/';
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// 这可以帮助我们在代码中安全地使用环境变量
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
- 如果在 entry 文件中使用 ES2015 module import,则会在 import 之后进行 webpack_public_path 赋值。在这种情况下,你必须将 public path 赋值移至一个专用模块中,然后将它的 import 语句放置到 entry.js 最上面:
- 注释:ES2015 module import 在 import 时便会运行内部的代码,这个时候为了保证 webpack_public_path 赋值早于所有模块,就需要把赋值行为移至一个专用模块中,然后在所有模块之前 import
- webpack 会自动根据 import.meta.url、document.currentScript、script.src 或者 self.location 变量设置 publicPath。你需要做的是将 output.publicPath 设为 'auto'
- 注释:document.currentScript 是一个只读属性,返回正在执行的 <script> 元素。
- 注释:self.location:Window 对象可以通过 self 或 window 访问。
module.exports = {
output: {
publicPath: 'auto',
},
};
集成
- 通常 webpack 用户使用 npm scripts 来作为任务执行工具。这是比较好的开始。然而跨平台支持可能是个问题
- 注释:因为本质上 npm scripts 是执行一串字符串,在不同平台命令行语法和文件路径表示会有不同,所以会存在跨平台支持问题
- 注释:可以使用跨平台的命令行工具,例如Node.js的跨平台命令行工具"cross-env"
- 对于那些使用 Grunt 的人,我们推荐使用 grunt-webpack package
- 注释:Grunt 是一种基于 JavaScript 的前端开发工具,用于自动化构建、测试和部署 Web 应用程序
- 使用 grunt-webpack 你可以将 webpack 或 webpack-dev-server 作为一项任务(task)执行
- 注释:"grunt-webpack" 是一种前端自动化工具,它基于 Grunt 任务运行器和 Webpack 模块打包器。
- 注释:它的主要作用是将 Webpack 和 Grunt 整合在一起,使得在 Grunt 中可以方便地使用 Webpack 来打包 JavaScript 和其他前端资源。
- 访问 grunt template tags 中的统计信息
// npm install --save-dev grunt-webpack webpack
// Gruntfile.js
const webpackConfig = require('./webpack.config.js');
module.exports = function (grunt) {
grunt.initConfig({
webpack: {
options: {
stats: !process.env.NODE_ENV || process.env.NODE_ENV === 'development',
},
prod: webpackConfig,
dev: Object.assign({ watch: true }, webpackConfig),
},
});
grunt.loadNpmTasks('grunt-webpack');
};
- 在 webpack-stream package(也称作 gulp-webpack) 的帮助下,可以相当直接地将 Gulp 与 webpack 集成
- 在这种情况下,不需要单独安装 webpack,因为它是 webpack-stream 直接依赖
// gulpfile.js
const gulp = require('gulp');
const webpack = require('webpack-stream');
gulp.task('default', function () {
return gulp
.src('src/entry.js')
.pipe(
webpack({
// Any configuration options...
})
)
.pipe(gulp.dest('dist/'));
});
- mocha-webpack 可以将 Mocha 与 webpack 完全集成。
- 注释:Mocha 是一个 JavaScript 测试框架,用于编写和运行单元测试、集成测试和端到端测试等不同类型的测试。
- 注释:mocha-webpack 用来把目标文件先通过 webpack 进行打包,然后再用 mocha 进行测试
- 基本上 mocha-webpack 只是一个简单封装,提供与 Mocha 几乎相同的 CLI,并提供各种 webpack 功能
npm install --save-dev webpack mocha mocha-webpack
mocha-webpack 'test/**/*.js'
- karma-webpack package 允许你使用 webpack 预处理 Karma 中的文件。
- 注释:Karma是一个基于Node.js的JavaScript测试运行器,它可以自动化运行测试用例并在不同浏览器或平台上进行测试,以确保JavaScript代码的跨浏览器兼容性。
// karma.conf.js
module.exports = function (config) {
config.set({
frameworks: ['webpack'],
files: [
{ pattern: 'test/*_test.js', watched: false },
{ pattern: 'test/**/*_test.js', watched: false },
],
preprocessors: {
'test/*_test.js': ['webpack'],
'test/**/*_test.js': ['webpack'],
},
webpack: {
// Any custom webpack configuration...
},
plugins: ['karma-webpack'],
});
};
资源模块
- 资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。
- 在 webpack 5 之前,通常使用
- raw-loader 将文件导入为字符串
- url-loader 将文件作为 data URI 内联到 bundle 中
- file-loader 将文件发送到输出目录
- 资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
- asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
- asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
- asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
- asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
- 当在 webpack 5 中使用旧的 assets loader(如 file-loader/url-loader/raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。
- 注释:默认情况下,webpack 5 会使用内置的 Asset Modules 处理资源文件,所以当用户又配置了 loader 时就会被处理两次。
- 注释:
type: 'javascript/auto'
告诉 webpack 这类文件按照 javascrip 模块处理
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
}
},
],
+ type: 'javascript/auto'
},
]
},
}
- 如需从 asset loader 中排除来自新 URL 处理的 asset,请添加
dependency: { not: ['url'] }
到 loader 配置中。- 注释:
dependency: { not: ['url'] }
用来告诉该 rule 只处理本地模块,不处理那些通过 url 加载来的资源
- 注释:
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
+ dependency: { not: ['url'] },
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
],
}
}
- 可以为它们自定义 outputPath 和 publicPath 属性
- 注释:每个 rule 都可以单独定义 outputPath 和 publicPath
- 默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录
- 可以通过在 webpack 配置中设置 output.assetModuleFilename 来修改此模板字符串
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
+ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
};
- 另一种自定义输出文件名的方式是,将某些资源发送到指定目录
- Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
- }
+ },
+ {
+ test: /\.html/,
+ type: 'asset/resource',
+ generator: {
+ filename: 'static/[hash][ext][query]'
+ }
+ }
]
},
};
- webpack 输出的 data URI,默认是呈现为使用 Base64 算法编码的文件内容
- 如果要使用自定义编码算法,则可以指定一个自定义函数来编码文件内容
- 注释:dataUrl还可以传如对象
- 注释:必须为每个模块执行且并须返回一个数据链接(data URI)字符串
const path = require('path');
+ const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
+ generator: {
+ dataUrl: content => {
+ content = content.toString();
+ return svgToMiniDataURI(content);
+ }
+ }
}
]
},
};
- 当使用 new URL('./path/to/asset', import.meta.url),webpack 也会创建资源模块
- 注释:new URL() 方法可以校验 URL 的合法性,如果传入的字符串不符合 URL 的格式
- 注释:new URL() 对象可以避免一些 URL 攻击,例如打开一个恶意的链接
const logo = new URL('./logo.svg', import.meta.url);
- 根据你配置中 target 的不同,webpack 会将上述代码编译成不同结果
const logo = new URL('./logo.svg', import.meta.url);
- 注释:document.baseURI 是一个只读属性,用于获取当前文档的基础 URI(Uniform Resource Identifier),也就是当前文档所在的 URL 地址
- 注释:如果当前文档包含了 <base> 标签,则 document.baseURI 返回的是该标签中指定的 URL 地址
- 注释:<base> 是 HTML 中的一个元素,用于指定当前文档中所有相对路径的基础 URL 地址。
// target: web
new URL(
__webpack_public_path__ + 'logo.svg',
document.baseURI || self.location.href
);
// target: webworker
new URL(__webpack_public_path__ + 'logo.svg', self.location);
// target: node, node-webkit, nwjs, electron-main, electron-renderer, electron-preload, async-node
new URL(
__webpack_public_path__ + 'logo.svg',
require('url').pathToFileUrl(__filename)
);
- 自 webpack 5.38.0 起,Data URLs 也支持在 new URL() 中使用了
- 注释:new URL() 自身是支持得,只是旧版 webpack 转换不支持
- webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型
- 注释:当 rule 得 type: 'asset' 时
- 可以通过在 webpack 配置的 module rule 层级中,设置 Rule.parser.dataUrlCondition.maxSize 选项来修改此条件
- 还可以 指定一个函数 来决定是否 inline 模块。
- 注释:当提供函数时,返回 true 值时告知 webpack 将模块作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中
- 注释:dataUrlCondition允许传如一个函数
dataUrlCondition: (source, { filename, module }) => {
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
+ parser: {
+ dataUrlCondition: {
+ maxSize: 4 * 1024 // 4kb
+ }
+ }
}
]
},
};
- 在 asset 模块和 webpack 5 之前,可以使用内联语法与上述传统的 loader 结合使用
- 现在建议去掉所有的内联 loader 的语法,使用资源查询条件来模仿内联语法的功能。
- import myModule from 'raw-loader!my-module';
+ import myModule from 'my-module?raw';
module: {
rules: [
{
test: /\.m?js$/,
resourceQuery: { not: [/raw/] },
use: [ ... ]
},
// ...
{
resourceQuery: /raw/,
type: 'asset/source',
}
]
},
- 使用 oneOf 的规则列表。此处只应用第一个匹配规则
module: {
rules: [
// ...
{ oneOf: [
{
resourceQuery: /raw/,
type: 'asset/source',
},
{
test: /\.m?js$/,
use: [ ... ]
},
] }
]
},
entry 高级用法
- 在不使用 import 样式文件的应用程序中(预单页应用程序或其他原因),使用一个值数组结构的 entry,并且在其中传入不同类型的文件,可以实现将 CSS 和 JavaScript(和其他)文件分离在不同的 bundle
- 注释:css 可以作为入口文件
- 我们将在 production(生产) 模式中使用 MiniCssExtractPlugin 作为 CSS 的一个最佳实践。
- 注释:style-loader 会把 css 打包为动态生成 <style> 的 js 代码, 然后打包到 blund 中
- 注释:MiniCssExtractPlugin 能够把这些 css 拆分成独立的样式
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: process.env.NODE_ENV,
entry: {
home: ['./home.js', './home.scss'],
account: ['./account.js', './account.scss'],
},
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.scss$/,
use: [
// fallback to style-loader in development
process.env.NODE_ENV !== 'production'
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
};
Package exports
General syntax 常规语法
- In general the exports field should contain an object where each properties specifies a sub path of the module request. For the examples above the following properties could be used: "." for import "package" and "./sub/path" for import "package/sub/path". Properties ending with a / will forward a request with this prefix to the old file system lookup algorithm. For properties ending with *, * may take any value and any * in the property value is replaced with the taken value.
- 注释:/ 结尾的,之前的部分按照 exports 进行匹配,之后的按照传统方式进行解析,例如:
package/prefix/some/file.js
会加载package/directory/some/file.js
- 注释:* 结尾的,之前的部分按照 exports 进行匹配,之后的部分在目标范围内搜索,是否有满足的。例如:
package/other-prefix/deep/file.js
能够匹配package/yet-another/deep/file/deep/file.js
- 注释:/ 结尾的,之前的部分按照 exports 进行匹配,之后的按照传统方式进行解析,例如:
{
"exports": {
".": "./main.js",
"./sub/path": "./secondary.js",
"./prefix/": "./directory/",
"./prefix/deep/": "./other-directory/",
"./other-prefix/*": "./yet-another/*/*.js"
}
}
Alternatives 选择语法
- Instead of providing a single result, the package author may provide a list of results. In such a scenario this list is tried in order and the first valid result will be used.
- 注释:value可以是一个数组,用来提供多个匹配,直到遇到第一个可以满足的返回
{
"exports": {
"./things/": ["./good-things/", "./bad-things/"]
}
}
Conditional syntax 条件语法
- Instead of providing results directly in the exports field, the package author may let the module system choose one based on conditions about the environment.
- In this case an object mapping conditions to results should be used. Conditions are tried in object order. Conditions that contain invalid results are skipped. Conditions might be nested to create a logical AND. The last condition in the object might be the special "default" condition, which is always matched.
- 注释:exports 中的value还可以是对象,来实现层层嵌套,并且还可以通过 default 设置一个默认值,当其他都不匹配时使用
- 注释:当 import 'packageName' 时获取的是"./drive-carefully.js"
{
"exports": {
".": {
"red": "./stop.js",
"yellow": "./stop.js",
"green": {
"free": "./drive.js",
"default": "./wait.js"
},
"default": "./drive-carefully.js"
}
}
}
Abbreviation 缩写
- When only a single entry (".") into the package should be supported the { ".": ... } object nesting can be omitted: 当只包含单个导出时,可以省略
{
"exports": "./index.mjs"
}
{
"exports": {
"red": "./stop.js",
"green": "./drive.js"
}
}
Notes about ordering 注意事项
- In an object where each key is a condition, order of properties is significant. Conditions are handled in the order they are specified.
- 注释:在每个key都是具体值时,顺序很重要。
- In an object where each key is a subpath, order of properties (subpaths) is not significant. More specific paths are preferred over less specific ones.
- order will always be: ./a/b/c > ./a/b/ > ./a/
- 注释:当每个键都是带/的子路径匹配时,exports对象中的顺序就比较不重要,它永远是匹配满足率最高的那个。
- exports field is preferred over other package entry fields like main, module, browser or custom ones.
- exports优先于main, module, browser
Support 支持情况
- 备注:exports 中允许使用的属性
- esinstall 是 Snowpack 的一部分,它可以将任何 npm 包转换为可在浏览器中运行的 ES 模块。Snowpack 和 vite 类似
- wmr 类似于 vite,由 Preact 作者开发,Preact 是 React 的轻量级替代方案,具有相同的 API,适用于移动端
- "." property
- normal property 怀疑是字符串属性
- property ending with /
- deprecated in Node.js, * should be preferred. 在 node.js 中以 / 结尾的语法已经弃用,优先选择使用 * 语法
- esinstall "./" is intentionally ignored as key. esinstall中忽略 key 名为 ./ 的键
- The property value is ignored and property key is used as target. Effectively only allowing mappings with key and value are identical.在 wmr 中以 / 结尾的语法被忽略,会把属性键直接作为路径去查找模块
- property ending with *
- Alternatives 选择器,即 exports 中的 value 是数组时
- Fallback to alternative sibling parent conditions is handling incorrectly. 在 value 为嵌套条件数组中,wmr 当子级无法满足时,无法回退到父级,去查找下一个满足的父级条件。
- Abbreviation only path 当 value 是数组时,数组内只允许使用路径?
- Abbreviation only conditions 当 value 是数组时,数组内只允许使用条件?
- Conditional syntax 条件语法
- Nested conditional syntax 嵌套条件
- For the require condition object order is handled incorrectly. This is intentionally as wmr doesn't differ between referencing syntax. 条件语法允许使用 require\import\node\browser 这是保留语法,但是在 wmr 中使用 require 时是不会加载对应的语法的文件的,依然会加载 import 指向的文件,因为 wmr 不区分导入语法
- Conditions Order 条件匹配满足率最高的那个
- "default" condition 默认条件
- Path Order 路径匹配?
- Error when not mapped 为映射时抛错
- When using "exports": "./file.js" abbreviation, any request e. g. package/not-existing will resolve to that. When not using the abbreviation, direct file access e. g. package/file.js will not lead to an error. 当 exports 使用缩写时,即 value 为字符串时,在 wmr 中会把所有导入都指向这个文件,不适用缩写的话就不会有这个问题
- Error when mixing conditions and paths 混合条件出错时?
Conditions 条件语法
- 备注:嵌套条件语法支持程度
- One of these conditions is set depending on the syntax used to reference the module: 在嵌套条件中,根据引用的来源,支持以下语法
- import 优先级高于 require
- 备注:以下语法会使用该指向
- HTML <script type="module"> in HTML
- HTML <link rel="preload/prefetch"> in HTML
- JS new Worker(..., { type: "module" })
- WASM import section,WASM 是 WebAssembly 的缩写
- ESM HMR (webpack) import.hot.accept/decline([...])
- JS Worklet.addModule Worklet 是 Web Workers 的轻量级版本,它可以让开发者访问渲染流程的低层部分。你可以用 Worklet 来运行 JavaScript 和 WebAssembly 代码,实现高性能的图形渲染或音频处理。
- Using javascript as entrypoint 在入口文件中使用的js,例如 webpack 的 entry 属性配置时
- require
- 备注:以下语法会使用该指向
- CommonJs require(...)
- AMD define() 用来定义一个模块,它接受三个参数:模块名,依赖数组,工厂函数2。
- AMD require([...]) 用来加载模块
- CommonJs require.resolve() 用来获取一个模块的完整文件名,但不加载该模块。它接受一个参数:模块标识符。
- CommonJs (webpack) require.ensure([...]) 它可以显示的声明将一些模块打包成独立的chunk文件,然后在需要的时候动态加载
- CommonJs (webpack) require.context() 用来导入一个文件中的指定文件
- CommonJs HMR (webpack) module.hot.accept/decline([...])
- HTML <script src="...">
- style Request is issued from a stylesheet reference. 从样式表中引入时
- CSS @import
- HTML <link rel="stylesheet">
- sass Request is issued from a sass stylesheet reference.
- asset Request is issued from a asset reference. 从资源中引用时
- CSS url()
- ESM new URL(..., import.meta.url)
- HTML <img src="...">
- script Request is issued from a normal script tag without module system. 从没有模块系统的普通脚本发起时
- HTML <script src="...">
- import 优先级高于 require
- These conditions might also be set additionally: 在嵌套条件中,其他语法
- module All module syntax that allows to reference javascript supports ESM.(only combined with import or require) 使用 Import 或 require 加载模块时指向的文件
- esmodules Always set by supported tools. 使用支持的工具进行设置?
- types Request is issued from typescript that is interested in type declarations.加载对应的.d.ts文件的路径
- The following conditions are set for various optimizations: 根据打包环境可以使用以下两类,只有webpack支持
- production
- development
- 备注:可以搭配 default 来设置其他环境
- The following conditions are set depending on the target environment: 根据目标环境设置以下条件
- 备注:目标环境条件也是用在exports中的条件语法中的,有工具限制
- 备注:目标环境条件使用时要考虑代码兼容目标环境的哪个版本
- browser
- Compatible with current Spec and stage 4 proposals at time of publishing the package. Polyfilling resp. transpiling must be handled on consumer side.兼容性参考 stage 4 填,需要在用户端处理 polyfill 或 transpile(转义)。
- electron 桌面应用开发环境
- 备注:electron可以在主进程中使用node环境,在渲染进程中使用browser环境。
- worker
- worklet Worklet框架是一种轻量级的Web Workers,它可以让开发者访问渲染流程的底层部分
- node
- wmr This is set for browser target environment.疑问:这是针对browser目标环境设置的?
- See engines field for compatibility 参考 engines 字段来确定代码的兼容性
- deno Deno类似与 Node.js
- react-native
- The following conditions are set depending on which tool preprocesses the source code.
- 备注:以下条件语法表明这个代码被哪个预处理器处理过,目前只支持一个
- webpack
- The following tools support custom conditions: 以下工具支持自定义条件
Common patterns 常见模式
- All patterns are explained with a single "." entry into the package, but they can be extended from multiple entries too, by repeating the pattern for each entry.所有的模式都用一个"."条目来解释包,但它们也可以通过重复每个条目的模式来扩展。
- 注释:当exports是一个对象时,可以存在多个由"."开头的 key
- Packages are rotting.包正在被废弃
- exports should be written to use fallbacks for unknown future cases. default condition can be used for that. exports 应该被写成能够使用回退方案来处理未知的未来情况。default 条件可以用于此目的。
- Not all conditions are supported by every tool 并非所有条件都被每个工具支持
- Fallbacks should be used to handled these cases. 应该使用回退方案来处理这些情况
- We assume the following fallback make sense in general 我们假设以下回退方式通常是有意义的
- ESM > CommonJs
- Production > Development
- Browser > node.js
- For a command line tool a browser-like future and fallback doesn't make a lot of sense, and in this case node.js-like environments and fallbacks should be used instead. 对于一个命令行工具,一个类似浏览器的未来和回退并没有太大的意义,在这种情况下,应该使用类似node.js的环境和回退
Providing CommonJs and ESM version (stateless) 提供 CommonJs and ESM 版本
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
- Most tools get the ESM version. Node.js is an exception here. It gets a CommonJs version when using require(). This will lead to two instances of these package when referencing it with require() and import, but that doesn't hurt as the package doesn't have state.大多数工具都有ESM版本。Node.js是一个例外。当使用require()时,它会获得CommonJs版本。当使用require()和import引用包时,这将导致这些包的两个实例,但这并不有害,因为包没有状态。
- 备注:包有没有状态和包的构成有关,并不是所有包都没有状态的,如果是有状态的包的话就会导致两个实例造成的状态问题
- The module condition is used as optimization when preprocessing node-targeted code with a tool that supports ESM for require() (like a bundler, when bundling for Node.js). For such a tool the exception is skipped. This is technically optional, but bundlers would include the package source code twice otherwise.当使用支持ESM for require()的工具(如绑定node.js时的绑定器)预处理节点目标代码时,模块条件用作优化。对于这样的工具,将跳过异常。这在技术上是可选的,否则 bundler 会将包源代码包含两次。
- You can also use the stateless pattern if you are able to isolate your package state in JSON files. JSON is consumable from CommonJs and ESM without polluting the graph with the other module system. JSON 文件可以同时在 CommonJs and ESM 中使用,可以用来解决打包两次的状态问题
- Note that here stateless also means class instances are not tested with instanceof as there can be two different classes because of the double module instantiation.也可以使用无状态模式,但是还是会存在两个class,通过 instanceof 检查类继承时依然会有问题。
- For Node.js we always use the CommonJs version and expose named exports in the ESM with a ESM wrapper. 在 node.js 中可以采用以下方式管理公共状态,
// package.json
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"import": "./wrapper.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
// wrapper.js
import cjs from './index.cjs';
export const A = cjs.A;
export const B = cjs.B;
- Providing "type": "commonjs" helps to statically detect CommonJs files. 提供“type”:“commonjs”有助于静态检测commonjs文件。
- 备注:types是package.json中用来声明.d.ts文件在哪取得
{
"type": "commonjs",
"exports": "./index.js"
}
- Note that despite using "type": "module" and .js for dist-bundle.js this file is not in ESM format. It should use globals to allow direct consumption as script tag. 虽然 type 指向了 module,但是 script 实际是非 esm 得普通脚本,即通过全局变量使用脚本赋值得方式
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
- Node.js allows to detection production/development mode at runtime via process.env.NODE_ENV, so we use that as fallback in Node.js. node.js允许通过 process.env.NODE_ENV 在运行时选择性得加载 开发 或 生产的版本
// package.json
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"production": "./index-optimized.js",
"node": "./wrapper-process-env.cjs",
"default": "./index-optimized.js"
}
}
// wrapper-process-env.cjs
if (process.env.NODE_ENV !== 'development') {
module.exports = require('./index-optimized.cjs');
} else {
module.exports = require('./index-with-devtools.cjs');
}
————————————————————————————————————————————————————————
API
命令行接口(CLI)
- 如果你想使用 npx 来运行 webpack,请确保你已经安装了 webpack-cli
- 备注:vue/cli只安装了Node接口?
- webpack-cli 提供了许多命令来使 webpack 的工作变得更简单。默认情况下,webpack 提供了以下命令
- configtest 验证webpack配置
npx webpack configtest ./webpack.config.js
- info 输出有关系统的信息,包括操作系统、硬件、node环境、包管理工具、浏览器等
npx webpack info --output json --addition-package postcss
- -a, --additional-package 在输出信息中添加额外的包。
- -o, --output 获取指定格式的输出。可以使用 markdown
- init 初始化新的webpack项目。跟vue/cli一样会提供一堆选项给你,并创建一个开发环境。
- 生成配置的位置。默认为 process.cwd()
- 备注:process.cwd()为当前目录
npx webpack init ./my-app --force --template=default
- -t, --template 要生成的模板名称
- -f, --force 不输入配置项就生成一个项目。该配置启用时所有命令行配置项将使用默认值。
- 完整文档
- loader 初始化一个 loader 开发项目
npx webpack loader ./my-loader --template=default
- plugin
- serve 所有参数
- watch
npx webpack watch --mode development
- configtest 验证webpack配置
Flags
- --config-name 用于在有多个配置文件的情况下,选择要使用的配置文件
module.exports = [
{
name: 'dev', // npx webpack --config-name dev
mode: 'development',
// other dev config
},
{
name: 'prod',
mode: 'production',
// other prod config
}
];
- --name 如果你有一个名为 webpack.dev.js 的配置文件,你可以使用 webpack --name dev 来运行它
- --color 启用控制台颜色
- --merge, -m
webpack --config-name dev --merge webpack.common.js webpack.dev.js
- 备注:--config-name 是合并后新 config 的名字
- --env 当它是一个函数时,传递给配置的环境变量
- --progress 在构建过程中打印编译进度
- --output-path, -o webpack 生成文件的输出位置,例如 ./dist
- --target, -t 设置要构建的 target
- --watch, -w 监听文件变化
- --watch-options-stdin stdin stream 结束时,停止监听
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具