2025年6月6日 星期五 乙巳(蛇)年 三月初十 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > JavaScript

webpack进阶(二)

时间:02-19来源:作者:点击数:16
CDSY,CDSY.XYZ

五、代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

1、入口起点(entry point)

这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):

project

  • webpack-demo
  • |- package.json
  • |- webpack.config.js
  • |- /dist
  • |- /src
  • |- index.js
  • + |- another-module.js
  • |- /node_modules

another-module.js

  • import _ from 'lodash';
  • console.log(_.join(['Another', 'module', 'loaded!'], ' '));

webpack.config.js

  • 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'),
  • },
  • };

正如前面提到的,这种方式存在一些隐患:

  • 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用。在下一章节会移除重复的模块。

2、防止重复(prevent duplication)

(1) 入口依赖

配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块:

webpack.config.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',否则还会遇到这里所述的麻烦。

webpack.config.js

  • const path = require('path');
  • module.exports = {
  • mode: 'development',
  • entry: {
  • 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'),
  • },
  • + optimization: {
  • + runtimeChunk: 'single',
  • + },
  • };

构建结果如下:

  • ...
  • [webpack-cli] Compilation finished
  • asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
  • asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
  • asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
  • asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
  • Entrypoint index 1.77 KiB = index.bundle.js
  • Entrypoint another 1.65 KiB = another.bundle.js
  • Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
  • runtime modules 3.76 KiB 7 modules
  • cacheable modules 530 KiB
  • ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  • ./src/another-module.js 84 bytes [built] [code generated]
  • ./src/index.js 257 bytes [built] [code generated]
  • webpack 5.4.0 compiled successfully in 249 ms

由上可知,除了生成 shared.bundle.jsindex.bundle.js 和 another.bundle.js 之外,还生成了一个 runtime.bundle.js 文件。

尽管可以在 webpack 中允许每个页面使用多入口,应尽可能避免使用多入口:entry: { page: ['./analytics', './app'] }。如此,在使用 async 脚本标签时,会有更好的优化以及一致的执行顺序。

(2) SplitChunksPlugin

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash 模块去除:

webpack.config.js

  • 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',
  • + },
  • + },
  • };

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.js 和 another.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。执行 npm run build 查看效果:

  • ...
  • [webpack-cli] Compilation finished
  • asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
  • asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
  • asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
  • Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
  • Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
  • runtime modules 7.64 KiB 14 modules
  • cacheable modules 530 KiB
  • ./src/index.js 257 bytes [built] [code generated]
  • ./src/another-module.js 84 bytes [built] [code generated]
  • ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  • webpack 5.4.0 compiled successfully in 241 ms
(3) 其他分割工具

以下是由社区提供,一些对于代码分离很有帮助的 plugin 和 loader:

首先,需要安装 mini-css-extract-plugin:

  • npm install --save-dev mini-css-extract-plugin css-loader

推荐将 mini-css-extract-plugin 与 css-loader 结合使用。

接下来创建文件配置Webpack:

src/style.css

  • body {
  • background: green;
  • }

src/index.js

  • import _ from 'lodash'
  • +import './styles.css'
  • function component() {
  • const element = document.createElement('div')
  • element.innerHTML = _.join(['Hello', 'webpack'], ' ')
  • return element
  • }
  • document.body.appendChild(component())

webpack.config.js

  • const path = require('path')
  • +const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  • module.exports = {
  • mode: 'development',
  • entry: {
  • index: './src/index.js',
  • another: './src/another-module.js',
  • },
  • output: {
  • filename: '[name].bundle.js',
  • path: path.resolve(__dirname, 'dist'),
  • },
  • + module: {
  • + rules: [
  • + {
  • + test: /\.css$/i,
  • + use: [MiniCssExtractPlugin.loader, 'css-loader'],
  • + },
  • + ],
  • + },
  • + plugins: [
  • + new MiniCssExtractPlugin()
  • + ],
  • optimization: {
  • splitChunks: {
  • chunks: 'all',
  • },
  • },
  • }

执行npm run build结果如下:

  • assets by status 555 KiB [compared for emit]
  • asset vendors-node_modules_lodash_lodash_js.bundle.js 546 KiB [compared for emit] (id hint: vendors)
  • asset another.bundle.js 8.68 KiB [compared for emit] (name: another)
  • assets by status 9.26 KiB [emitted]
  • asset index.bundle.js 9.23 KiB [emitted] (name: index)
  • asset index.css 30 bytes [emitted] (name: index)
  • Entrypoint index 556 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 546 KiB index.css 30 bytes index.bundle.js 9.23 KiB
  • Entrypoint another 555 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 546 KiB another.bundle.js 8.68 KiB
  • runtime modules 7.64 KiB 14 modules
  • code generated modules 528 KiB (javascript) 29 bytes (css/mini-extract) [code generated]
  • modules by path ./src/*.js 316 bytes
  • ./src/index.js 232 bytes [built] [code generated]
  • ./src/another-module.js 84 bytes [built] [code generated]
  • modules by path ./src/*.css 50 bytes (javascript) 29 bytes (css/mini-extract)
  • ./src/style.css 50 bytes [built] [code generated]
  • css ./node_modules/css-loader/dist/cjs.js!./src/style.css 29 bytes [code generated]
  • ../../../node_modules/lodash/lodash.js 528 KiB [built] [code generated]
  • webpack 5.21.0 compiled successfully in 594 ms
  • ✨ Done in 1.44s.

如上所见,./src/style.css被单独的打包出来。

3、动态导入(dynamic import)

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。让我们先尝试使用第一种……

在我们开始之前,先从上述示例的配置中移除掉多余的 entry 和 optimization.splitChunks,因为接下来的演示中并不需要它们:

webpack.config.js

  • 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',
  • - },
  • - },
  • };

我们将更新我们的项目,移除现在未使用的文件:

project

  • webpack-demo
  • |- package.json
  • |- webpack.config.js
  • |- /dist
  • |- /src
  • |- index.js
  • - |- another-module.js
  • |- /node_modules

现在,我们不再使用 statically import(静态导入) lodash,而是通过 dynamic import(动态导入) 来分离出一个 chunk:

src/index.js

  • -import _ from 'lodash';
  • -
  • -function component() {
  • +function getComponent() {
  • const element = document.createElement('div');
  • - // Lodash, now imported by this script
  • - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • + return import('lodash')
  • + .then(({ default: _ }) => {
  • + const element = document.createElement('div');
  • +
  • + element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • - return element;
  • + return element;
  • + })
  • + .catch((error) => 'An error occurred while loading the component');
  • }
  • -document.body.appendChild(component());
  • +getComponent().then((component) => {
  • + document.body.appendChild(component);
  • +});

我们之所以需要 default,是因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象,更多有关背后原因的信息,请阅读 webpack 4: import() and CommonJs

让我们执行 webpack,查看 lodash 是否会分离到一个单独的 bundle:

  • ...
  • [webpack-cli] Compilation finished
  • asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
  • asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
  • runtime modules 7.37 KiB 11 modules
  • cacheable modules 530 KiB
  • ./src/index.js 434 bytes [built] [code generated]
  • ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  • webpack 5.4.0 compiled successfully in 268 ms

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。下面是如何通过 async 函数简化代码:

src/index.js

  • -function getComponent() {
  • +async function getComponent() {
  • const element = document.createElement('div');
  • + const { default: _ } = await import('lodash');
  • - return import('lodash')
  • - .then(({ default: _ }) => {
  • - const element = document.createElement('div');
  • + element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • -
  • - return element;
  • - })
  • - .catch((error) => 'An error occurred while loading the component');
  • + return element;
  • }
  • getComponent().then((component) => {
  • document.body.appendChild(component);
  • });

4、预获取/预加载模块(prefetch/preload module)

webpack v4.6.0+ 增加了对预获取和预加载的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

LoginButton.js

  • //...
  • import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

下面这个简单的 preload 示例中,有一个 Component,依赖于一个较大的 library,所以应该将其分离到一个独立的 chunk 中。

我们假想这里的图表组件 ChartComponent 组件需要依赖体积巨大的 ChartingLibrary 库。它会在渲染时显示一个 LoadingIndicator(加载进度条) 组件,然后立即按需导入 ChartingLibrary

ChartComponent.js

  • //...
  • import(/* webpackPreload: true */ 'ChartingLibrary');

在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload"> 请求 charting-library-chunk。假定 page-chunk 体积很小,很快就被加载好,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。启动仅需要很少的加载时间,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。

5、bundle 分析(bundle analysis)

一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。 官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:

  • webpack-chart: webpack stats 可交互饼图。
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
  • bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。

六、懒加载

懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

1、示例

我们在代码分离中的例子基础上,进一步做些调整来说明这个概念。那里的代码确实会在脚本运行的时候产生一个分离的代码块 lodash.bundle.js ,在技术概念上“懒加载”它。问题是加载这个包并不需要用户的交互 - 意思是每次加载页面的时候都会请求它。这样做并没有对我们有很多帮助,还会对性能产生负面影响。

我们试试不同的做法。我们增加一个交互,当用户点击按钮的时候用 console 打印一些文字。但是会等到第一次交互的时候再加载那个代码块(print.js)。为此,我们返回到代码分离的例子中,把 lodash 放到主代码块中,重新运行_代码分离_中的代码。

project

  • webpack-demo
  • |- package.json
  • |- webpack.config.js
  • |- /dist
  • |- /src
  • |- index.js
  • + |- print.js
  • |- /node_modules

src/print.js

  • console.log(
  • 'The print.js module has loaded! See the network tab in dev tools...'
  • );
  • export default () => {
  • console.log('Button Clicked: Here\'s "some text"!');
  • };

src/index.js

  • + import _ from 'lodash';
  • +
  • - async function getComponent() {
  • + function component() {
  • const element = document.createElement('div');
  • - const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
  • + const button = document.createElement('button');
  • + const br = document.createElement('br');
  • + button.innerHTML = 'Click me and look at the console!';
  • element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • + element.appendChild(br);
  • + element.appendChild(button);
  • +
  • + // Note that because a network request is involved, some indication
  • + // of loading would need to be shown in a production-level site/app.
  • + button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
  • + const print = module.default;
  • +
  • + print();
  • + });
  • return element;
  • }
  • - getComponent().then(component => {
  • - document.body.appendChild(component);
  • - });
  • + document.body.appendChild(component());

Warning

注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。

现在运行 webpack 来验证一下我们的懒加载功能:

  • ...
  • Asset Size Chunks Chunk Names
  • print.bundle.js 417 bytes 0 [emitted] print
  • index.bundle.js 548 kB 1 [emitted] [big] index
  • index.html 189 bytes [emitted]
  • ...

2、框架

许多框架和类库对于如何用它们自己的方式来实现(懒加载)都有自己的建议。这里有一些例子:

七、缓存

以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

1、输出文件的文件名(output filename)

我们可以通过替换 output.filename 中的 substitutions 设置,来定义输出文件的名称。webpack 提供了一种使用称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。其中,[contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化。

这里使用 起步 中的示例和 管理输出 中的 plugins 插件来作为项目基础,所以我们依然不必手动地维护 index.html 文件:

project

  • webpack-demo
  • |- package.json
  • |- webpack.config.js
  • |- /dist
  • |- /src
  • |- index.js
  • |- /node_modules

webpack.config.js

  • const path = require('path');
  • const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  • const HtmlWebpackPlugin = require('html-webpack-plugin');
  • module.exports = {
  • entry: './src/index.js',
  • plugins: [
  • // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
  • new CleanWebpackPlugin(),
  • new HtmlWebpackPlugin({
  • - title: 'Output Management',
  • + title: 'Caching',
  • }),
  • ],
  • output: {
  • - filename: 'bundle.js',
  • + filename: '[name].[contenthash].js',
  • path: path.resolve(__dirname, 'dist'),
  • },
  • };

使用此配置,然后运行我们的 build script npm run build,产生以下输出:

  • ...
  • Asset Size Chunks Chunk Names
  • main.7e2c49a622975ebd9b7e.js 544 kB 0 [emitted] [big] main
  • index.html 197 bytes [emitted]
  • ...

可以看到,bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此:

  • ...
  • Asset Size Chunks Chunk Names
  • main.205199ab45963f6a62ec.js 544 kB 0 [emitted] [big] main
  • index.html 197 bytes [emitted]
  • ...

这也是因为 webpack 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest。(boilerplate 指 webpack 运行时的引导代码)

2、提取引导模板(extracting boilerplate)

正如我们在 代码分离 中所学到的,SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle:

webpack.config.js

  • const path = require('path');
  • const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  • const HtmlWebpackPlugin = require('html-webpack-plugin');
  • module.exports = {
  • entry: './src/index.js',
  • plugins: [
  • // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
  • new CleanWebpackPlugin(),
  • new HtmlWebpackPlugin({
  • title: 'Caching',
  • }),
  • ],
  • output: {
  • filename: '[name].[contenthash].js',
  • path: path.resolve(__dirname, 'dist'),
  • },
  • + optimization: {
  • + runtimeChunk: 'single',
  • + },
  • };

再次构建,然后查看提取出来的 runtime bundle:

  • Hash: 82c9c385607b2150fab2
  • Version: webpack 4.12.0
  • Time: 3027ms
  • Asset Size Chunks Chunk Names
  • runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
  • main.e81de2cf758ada72f306.js 69.5 KiB 1 [emitted] main
  • index.html 275 bytes [emitted]
  • [1] (webpack)/buildin/module.js 497 bytes {1} [built]
  • [2] (webpack)/buildin/global.js 489 bytes {1} [built]
  • [3] ./src/index.js 309 bytes {1} [built]
  • + 1 hidden module

将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致。 这可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现。我们在 optimization.splitChunks 添加如下 cacheGroups 参数并构建:

webpack.config.js

  • const path = require('path');
  • const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  • const HtmlWebpackPlugin = require('html-webpack-plugin');
  • module.exports = {
  • entry: './src/index.js',
  • plugins: [
  • // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
  • new CleanWebpackPlugin(),
  • new HtmlWebpackPlugin({
  • title: 'Caching',
  • }),
  • ],
  • output: {
  • filename: '[name].[contenthash].js',
  • path: path.resolve(__dirname, 'dist'),
  • },
  • optimization: {
  • runtimeChunk: 'single',
  • + splitChunks: {
  • + cacheGroups: {
  • + vendor: {
  • + test: /[\\/]node_modules[\\/]/,
  • + name: 'vendors',
  • + chunks: 'all',
  • + },
  • + },
  • + },
  • },
  • };

再次构建,然后查看新的 vendor bundle:

  • ...
  • Asset Size Chunks Chunk Names
  • runtime.cc17ae2a94ec771e9221.js 1.42 KiB 0 [emitted] runtime
  • vendors.a42c3ca0d742766d7a28.js 69.4 KiB 1 [emitted] vendors
  • main.abf44fedb7d11d4312d7.js 240 bytes 2 [emitted] main
  • index.html 353 bytes [emitted]
  • ...

现在,我们可以看到 main 不再含有来自 node_modules 目录的 vendor 代码,并且体积减少到 240 bytes

3、模块标识符(module identifier)

在项目中再添加一个模块 print.js

project

  • webpack-demo
  • |- package.json
  • |- webpack.config.js
  • |- /dist
  • |- /src
  • |- index.js
  • + |- print.js
  • |- /node_modules

print.js

  • + export default function print(text) {
  • + console.log(text);
  • + };

src/index.js

  • import _ from 'lodash';
  • + import Print from './print';
  • function component() {
  • const element = document.createElement('div');
  • // lodash 是由当前 script 脚本 import 进来的
  • element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • + element.onclick = Print.bind(null, 'Hello webpack!');
  • return element;
  • }
  • document.body.appendChild(component());

再次运行构建,然后我们期望的是,只有 main bundle 的 hash 发生变化,然而……

  • ...
  • Asset Size Chunks Chunk Names
  • runtime.1400d5af64fc1b7b3a45.js 5.85 kB 0 [emitted] runtime
  • vendor.a7561fb0e9a071baadb9.js 541 kB 1 [emitted] [big] vendor
  • main.b746e3eb72875af2caa9.js 1.22 kB 2 [emitted] main
  • index.html 352 bytes [emitted]
  • ...

……我们可以看到这三个文件的 hash 都变化了。这是因为每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。因此,简要概括:

  • main bundle 会随着自身的新增内容的修改,而发生变化。
  • vendor bundle 会随着自身的 module.id 的变化,而发生变化。
  • manifest runtime 会因为现在包含一个新模块的引用,而发生变化。

第一个和最后一个都是符合预期的行为,vendor hash 发生变化是我们要修复的。我们将 optimization.moduleIds 设置为 'deterministic'

webpack.config.js

  • const path = require('path');
  • const { CleanWebpackPlugin } = require('clean-webpack-plugin');
  • const HtmlWebpackPlugin = require('html-webpack-plugin');
  • module.exports = {
  • entry: './src/index.js',
  • plugins: [
  • // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
  • new CleanWebpackPlugin(),
  • new HtmlWebpackPlugin({
  • title: 'Caching',
  • }),
  • ],
  • output: {
  • filename: '[name].[contenthash].js',
  • path: path.resolve(__dirname, 'dist'),
  • },
  • optimization: {
  • + moduleIds: 'deterministic',
  • runtimeChunk: 'single',
  • splitChunks: {
  • cacheGroups: {
  • vendor: {
  • test: /[\\/]node_modules[\\/]/,
  • name: 'vendors',
  • chunks: 'all',
  • },
  • },
  • },
  • },
  • };

现在,不论是否添加任何新的本地依赖,对于前后两次构建,vendor hash 都应该保持一致:

  • ...
  • Asset Size Chunks Chunk Names
  • main.216e852f60c8829c2289.js 340 bytes 0 [emitted] main
  • vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
  • runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
  • index.html 353 bytes [emitted]
  • Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.216e852f60c8829c2289.js
  • ...

然后,修改 src/index.js,临时移除额外的依赖:

src/index.js

  • import _ from 'lodash';
  • - import Print from './print';
  • + // import Print from './print';
  • function component() {
  • const element = document.createElement('div');
  • // lodash 是由当前 script 脚本 import 进来的
  • element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  • - element.onclick = Print.bind(null, 'Hello webpack!');
  • + // element.onclick = Print.bind(null, 'Hello webpack!');
  • return element;
  • }
  • document.body.appendChild(component());

最后,再次运行我们的构建:

  • ...
  • Asset Size Chunks Chunk Names
  • main.ad717f2466ce655fff5c.js 274 bytes 0 [emitted] main
  • vendors.55e79e5927a639d21a1b.js 69.5 KiB 1 [emitted] vendors
  • runtime.725a1a51ede5ae0cfde0.js 1.42 KiB 2 [emitted] runtime
  • index.html 353 bytes [emitted]
  • Entrypoint main = runtime.725a1a51ede5ae0cfde0.js vendors.55e79e5927a639d21a1b.js main.ad717f2466ce655fff5c.js
  • ...

我们可以看到,这两次构建中,vendor bundle 文件名称,都是 55e79e5927a639d21a1b

八、创建 library

除了打包应用程序,webpack 还可以用于打包 JavaScript library。

假设你正在编写一个名为 webpack-numbers 的小的 library,可以将数字 1 到 5 转换为文本表示,反之亦然,例如将 2 转换为 ‘two’。

基本的项目结构可能如下所示:

project

  • + |- webpack.config.js
  • + |- package.json
  • + |- /src
  • + |- index.js
  • + |- ref.json

初始化 npm,安装 webpack 和 lodash:

  • npm init -y
  • npm install --save-dev webpack lodash

src/ref.json

  • [
  • {
  • "num": 1,
  • "word": "One"
  • },
  • {
  • "num": 2,
  • "word": "Two"
  • },
  • {
  • "num": 3,
  • "word": "Three"
  • },
  • {
  • "num": 4,
  • "word": "Four"
  • },
  • {
  • "num": 5,
  • "word": "Five"
  • },
  • {
  • "num": 0,
  • "word": "Zero"
  • }
  • ]

src/index.js

  • import _ from 'lodash';
  • import numRef from './ref.json';
  • export function numToWord(num) {
  • return _.reduce(numRef, (accum, ref) => {
  • return ref.num === num ? ref.word : accum;
  • }, '');
  • }
  • export function wordToNum(word) {
  • return _.reduce(numRef, (accum, ref) => {
  • return ref.word === word && word.toLowerCase() ? ref.num : accum;
  • }, -1);
  • }

这个 library 的调用规范如下:

  • ES2015 module import:
  • import * as webpackNumbers from 'webpack-numbers';
  • // ...
  • webpackNumbers.wordToNum('Two');
  • CommonJS module require:
  • const webpackNumbers = require('webpack-numbers');
  • // ...
  • webpackNumbers.wordToNum('Two');
  • AMD module require:
  • require(['webpackNumbers'], function (webpackNumbers) {
  • // ...
  • webpackNumbers.wordToNum('Two');
  • });

consumer(使用者) 还可以通过一个 script 标签来加载和使用此 library:

  • <!doctype html>
  • <html>
  • ...
  • <script src="https://unpkg.com/webpack-numbers"></script>
  • <script>
  • // ...
  • // 全局变量
  • webpackNumbers.wordToNum('Five')
  • // window 对象中的属性
  • window.webpackNumbers.wordToNum('Five')
  • // ...
  • </script>
  • </html>

注意,我们还可以通过以下配置方式,将 library 暴露为:

  • global 对象中的属性,用于 Node.js。
  • this 对象中的属性。

完整的 library 配置和代码,请查看 webpack-library-example

1、基本配置

现在,让我们以某种方式打包这个 library,能够实现以下几个目标:

  • 使用 externals 选项,避免将 lodash 打包到应用程序,而使用者会去加载它。
  • 将 library 的名称设置为 webpack-numbers
  • 将 library 暴露为一个名为 webpackNumbers 的变量。
  • 能够访问其他 Node.js 中的 library。

此外,consumer(使用者) 应该能够通过以下方式访问 library:

  • ES2015 模块。例如 import webpackNumbers from 'webpack-numbers'
  • CommonJS 模块。例如 require('webpack-numbers').
  • 全局变量,在通过 script 标签引入时。

我们可以从如下 webpack 基本配置开始:

webpack.config.js

  • const path = require('path');
  • module.exports = {
  • entry: './src/index.js',
  • output: {
  • path: path.resolve(__dirname, 'dist'),
  • filename: 'webpack-numbers.js',
  • },
  • };

2、外部化 lodash

现在,如果执行 webpack,你会发现创建了一个体积相当大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash 当作 peerDependency。也就是说,consumer(使用者) 应该已经安装过 lodash 。因此,你就可以放弃控制此外部 library ,而是将控制权让给使用 library 的 consumer。

这可以使用 externals 配置来完成:

webpack.config.js

  • const path = require('path');
  • module.exports = {
  • entry: './src/index.js',
  • output: {
  • path: path.resolve(__dirname, 'dist'),
  • filename: 'webpack-numbers.js',
  • },
  • + externals: {
  • + lodash: {
  • + commonjs: 'lodash',
  • + commonjs2: 'lodash',
  • + amd: 'lodash',
  • + root: '_',
  • + },
  • + },
  • };

这意味着你的 library 需要一个名为 lodash 的依赖,这个依赖在 consumer 环境中必须存在且可用。

3、暴露 library

对于用法广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。为了让你的 library 能够在各种使用环境中可用,需要在 output 中添加 library 属性:

webpack.config.js

  • const path = require('path');
  • module.exports = {
  • entry: './src/index.js',
  • output: {
  • path: path.resolve(__dirname, 'dist'),
  • filename: 'webpack-numbers.js',
  • + library: 'webpackNumbers',
  • },
  • externals: {
  • lodash: {
  • commonjs: 'lodash',
  • commonjs2: 'lodash',
  • amd: 'lodash',
  • root: '_',
  • },
  • },
  • };

这会将你的 library bundle 暴露为名为 webpackNumbers 的全局变量,consumer 通过此名称来 import。为了让 library 和其他环境兼容,则需要在配置中添加 libraryTarget 属性。这个选项可以控制以多种形式暴露 library。

webpack.config.js

  • const path = require('path');
  • module.exports = {
  • entry: './src/index.js',
  • output: {
  • path: path.resolve(__dirname, 'dist'),
  • filename: 'webpack-numbers.js',
  • library: 'webpackNumbers',
  • + libraryTarget: 'umd',
  • },
  • externals: {
  • lodash: {
  • commonjs: 'lodash',
  • commonjs2: 'lodash',
  • amd: 'lodash',
  • root: '_',
  • },
  • },
  • };

有以下几种方式暴露 library:

  • 变量:作为一个全局变量,通过 script 标签来访问(libraryTarget:'var')。
  • this:通过 this 对象访问(libraryTarget:'this')。
  • window:在浏览器中通过 window 对象访问(libraryTarget:'window')。
  • UMD:在 AMD 或 CommonJS require 之后可访问(libraryTarget:'umd')。

如果设置了 library 但没有设置 libraryTarget,则 libraryTarget 默认指定为 var,详细说明请查看 output 文档。查看 output.libraryTarget 文档,以获取所有可用选项的详细列表。

(1) 发布准备

遵循 生产环境 指南中提到的步骤,来优化生产环境下的输出结果。那么,我们还需要将生成 bundle 的文件路径,添加到 package.json 中的 main 字段中。

package.json

  • {
  • ...
  • "main": "dist/webpack-numbers.js",
  • ...
  • }

或者,按照这个 指南,将其添加为标准模块:

  • {
  • ...
  • "module": "src/index.js",
  • ...
  • }

这里的 key(键) main 是参照 package.json 标准,而 module 是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。

(2) 发布 library

现在,你可以 将其发布为一个 npm package,并且在 unpkg.com 找到它,并分发给你的用户。

  • 注册npm仓库账号
  • https://www.npmjs.com 上面的账号
  • $ npm adduser
  • 上传包
  • $ npm publish

坑:403 Forbidden

  • 查看npm源:npm config get registry
  • 切换npm源方法一:npm config set registry http://registry.npmjs.org
  • 切换npm源方法二:nrm use npm
CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐