【第02课:Webpack 基本使用】

您所在的位置:网站首页 webpack现在有哪几个版本 【第02课:Webpack 基本使用】

【第02课:Webpack 基本使用】

2022-06-11 02:42| 来源: 网络整理| 查看: 265

在本章节中将简要的对 Webpack 基本使用做一个演示。通过本章节的学习,能在项目中快速配置一个 Webpack 打包文件并完成项目的打包工作。在本章节,我们不牵涉到 Webpack 中那些深入的知识点,后续会针对这些知识点做更加细致的讲解。但是,正如在上一个章节中看到的那样,我很早就在 Webpack 配置中引入了 CommonChunkPlugin,因此在本章节,将在 Webpack 配置文件中继续使用这个插件。希望能对该插件引起足够的重视,并学会如何在自己的项目中使用它。

其中 CommonChunkPlugin 是 Webpack 用于创建一个独立的文件,即所谓的 common chunk。这个 chunk 会包含多个入口文件中共同的模块。通过将多个入口文件公共的模块抽取出来可以在特定的时间进行缓存,这对于提升页面加载速度是很好的优化手段。

Webpack 打包例子讲解 CommonChunkPlugin 参数详解

开始具体的例子之前先看下这个插件支持的配置和详细含义。同时,也给出官网描述的几个例子:

{ name: string, // or names: string[], // The chunk name of the commons chunk. An existing chunk can be selected by passing a name of an existing chunk. // If an array of strings is passed this is equal to invoking the plugin multiple times for each chunk name. // If omitted and `options.async` or `options.children` is set all chunks are used, otherwise `options.filename` // is used as chunk name. // When using `options.async` to create common chunks from other async chunks you must specify an entry-point // chunk name here instead of omitting the `option.name`. filename: string, //指定该插件产生的文件名称,可以支持 output.filename 中那些支持的占位符,如 [hash]、[chunkhash]、[id] 等。如果忽略这个这个属性,那么原始的文件名称不会被修改(一般是 output.filename 或者 output.chunkFilename,可以查看 compiler 和 compilation 部分第一个例子)。但是这个配置不允许和 `options.async` 一起使用 minChunks: number|Infinity|function(module, count) boolean, //至少有 minChunks 的 chunk 都包含指定的模块,那么该模块就会被移出到 common chunk 中。这个数值必须大于等于2,并且小于等于没有使用这个插件应该产生的 chunk 数量。如果传入 `Infinity`,那么只会产生 common chunk,但是不会有任何模块被移到这个 chunk中 (没有一个模块会被依赖无限次)。通过提供一个函数,也可以添加自己的逻辑,这个函数会被传入一个参数表示产生的 chunk 数量 chunks: string[], // Select the source chunks by chunk names. The chunk must be a child of the commons chunk. // If omitted all entry chunks are selected. children: boolean, // If `true` all children of the commons chunk are selected deepChildren: boolean, // If `true` all descendants of the commons chunk are selected async: boolean|string, // If `true` a new async commons chunk is created as child of `options.name` and sibling of `options.chunks`. // It is loaded in parallel with `options.chunks`. // Instead of using `option.filename`, it is possible to change the name of the output file by providing // the desired string here instead of `true`. minSize: number, //所有被移出到 common chunk 的文件的大小必须大于等于这个值 }

上面的 filename 和 minChunks 已经在注释中说明了,下面重点说一下其他的属性。

children 属性

其中在 Webpack 中很多 chunk 产生都是通过 require.ensure 来完成的。先看看下面的例子:

//main.js 为入口文件 if (document.querySelectorAll('a').length) { require.ensure([], () => { const Button = require('./Components/Button').default; const button = new Button('google.com'); button.render('a'); }); } if (document.querySelectorAll('h1').length) { require.ensure([], () => { const Header = require('./Components/Header').default; new Header().render('h1'); }); }

此时会产生三个 chunk,分别为 main 和其他两个通过 require.ensure 产生的 chunk,比如 0.entry.chunk.js 和 1.entry.chunk.js。如果配置了多个入口文件(假如还有一个 main1.js),那么这些动态产生的 chunk 中可能也会存在相同的模块(此时 main1、main 会产生四个动态 chunk )。而这个 children 配置就是为了这种情况而产生的。通过配置 children,可以将动态产生的这些 chunk 的公共的模块也抽取出来。

很显然,以前是动态加载的文件现在都必须在页面初始的时候就加载完成,那么对于初始加载肯在时间上有一定的副作用。但是存在一种情况,比如进入主页面后,需要加载路由 A、路由 B……等一系列的文件(网站的核心模块都要提前一次性加载),那么把路由 A、路由 B……这些公共的模块提取到公有模块中,然后和入口文件一起加载,在性能上还是有优势的。下面是官网提供的一个例子:

new webpack.optimize.CommonsChunkPlugin({ // names: ["app", "subPageA"] // (choose the chunks, or omit for all chunks) children: true, // (select all children of chosen chunks) // minChunks: 3, // (3 children must share the module before it's moved) })

对于 common-chunk-plugin 不太明白的,可以 查看这里。

async

上面这种 children 的方案会增加初始加载的时间,这种 async 的方式相当于创建了一个异步加载的 common-chunk,其包含 require.ensure 动态产生的 chunk 中的公共模块。这样,当访问特定路由的时候,会动态的加载这个 common chunk,以及特定路由包含的业务代码。下面也是官网给出的一个实例:

new webpack.optimize.CommonsChunkPlugin({ name: "app", // or names: ["app", "subPageA"] // the name or list of names must match the name or names // of the entry points that create the async chunks children: true, // (use all children of the chunk) async: true, // (create an async commons chunk) minChunks: 3, // (3 children must share the module before it's separated) })

names

该参数用于指定 common chunk 的名称。如果指定的 chunk 名称在 entry 中有配置,那么表示选择特定的 chunk。如果指定的是一个数组,那么相当于按照名称的顺序多次执行 common-chunk-plugin 插件。如果没有指定 name 属性,但是指定了 options.async 或者 options.children,那么表示抽取所有的 chunk 的公共模块,包括通过 require.ensure 动态产生的模块。其他情况下使用 options.filename 来作为 chunk 的名称。

注意:如果指定了 options.async 来创建一个异步加载的 common chunk,那么必须指定一个入口 chunk 名称,而不能忽略 option.name 参数。可以 点击这个例子 查看。

chunks

通过 chunks 参数来选择来源的 chunk。这些 chunk 必须是 common-chunk 的子级 chunk。如果没有指定,那么默认选中所有的入口 chunk。下面给出一个例子:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main: process.cwd()+'/example6/main.js', main1: process.cwd()+'/example6/main1.js', jquery:["jquery"] }, output: { path: process.cwd() + '/dest/example6', filename: '[name].js' }, plugins: [ new CommonsChunkPlugin({ name: "jquery", minChunks:2, chunks:["main","main1"] }) ] };

此时会发现在 jquery.js 的最后会打包进来 chunk1.js 和 chunk2.js。

/* 2 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(3); var chunk1=1; exports.chunk1=chunk1; /***/ }, /* 3 */ /***/ function(module, exports) { var chunk2=1; exports.chunk2=chunk2; /***/ }

关于 chunks 配置的使用可以 点击这里查看。所以,chunks 就是用于指定从那些 chunks 来抽取公共的模块,而 chunks 的名称一般都是通过 entry 来指定的,比如上面的 entry 为:

entry: { main: process.cwd()+'/example6/main.js', main1: process.cwd()+'/example6/main1.js', jquery:["jquery"] },

而 chunks 指定的为:

chunks:["main","main1"]

表明从 main、main1 两个入口 chunk 中来找公共的模块。

deepChildren

如果将该参数设置为 true,那么 common-chunk 下的所有的 chunk 都会被选中,比如 require.ensure 产生的 chunk 的子级 chunk,从这些 chunks 中来抽取公共的模块。

minChunks 为函数

可以给 minChunks 传入一个函数。CommonsChunkPlugin 将会调用这个函数并传入 module 和 count 参数。这个 module 参数用于指定某一个 chunks 中所有的模块,而这个 chunk 的名称就是上面配置的 name/names 参数。这个 module 是一个 NormalModule 实例,有如下的常用属性:

module.context:表示存储文件的路径,比如 '/my_project/node_modules/example-dependency'

module.resource:表示被处理的文件名称,比如 '/my_project/node_modules/example-dependency/index.js'

而 count 参数表示指定的模块出现在多少个 chunk 中。这个函数对于细粒度的操作 CommonsChunk 插件还是很有用的。可自己决定将那些模块放在指定的 common chunk 中,下面是官网给出的一个例子:

new webpack.optimize.CommonsChunkPlugin({ name: "my-single-lib-chunk", filename: "my-single-lib-chunk.js", minChunks: function(module, count) { //如果一个模块的路径中存在 somelib 部分,而且这个模块出现在 3 个独立的 chunk 或者 entry 中,那么它就会被抽取到一个独立的 chunk 中,而且这个 chunk 的文件名称为 "my-single-lib-chunk.js",而这个 chunk 本身的名称为 "my-single-lib-chunk" return module.resource && (/somelib/).test(module.resource) && count === 3; } });

而官网下面的例子详细的展示了如何将 node_modules 下引用的模块抽取到一个独立的 chunk 中:

new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: function (module) { // this assumes your vendor imports exist in the node_modules directory return module.context && module.context.indexOf("node_modules") !== -1; } })

因为 node_module 下的模块一般都是来源于第三方,所以在本地很少修改,通过这种方式可以将第三方的模块抽取到公共的 chunk 中。

还有一种情况就是,如果想把应用的 css/scss 和 vendor 的 css(第三方类库的 css)抽取到一个独立的文件中,那么可以使用下面的 minChunk() 函数,同时配合 ExtractTextPlugin 来完成。

new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: function (module) { // This prevents stylesheet resources with the .css or .scss extension // from being moved from their original chunk to the vendor chunk if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) { return false; } return module.context && module.context.indexOf("node_modules") !== -1; } })

这个例子在抽取 node_modules 下的模块的时候做了一个限制,即明确指定 node_modules 下的 scss/css 文件不会被抽取,所以最后生成的 vendor.js 不会包含第三方类库的 css/scss 文件,而只包含其中的 js 部分。 同时通过配置 ExtractTextPlugin 就可以将应用的 css 和第三方应用的 css 抽取到一个独立的 css 文件中,从而达到 css 和 js 分离。

其中 CommonsChunkPlugin 插件还有一个更加有用的配置,即用于将 Webpack 打包逻辑相关的一些文件抽取到一个独立的 chunk 中。但是此时配置的 name 应该是 entry 中不存在的,这对于线上缓存很有作用。因为如果文件的内容不发生变化,那么 chunk 的名称不会发生变化,所以并不会影响到线上的缓存。比如下面的例子:

new webpack.optimize.CommonsChunkPlugin({ name: "manifest", //这个 name 必须不在 entry 中 minChunks: Infinity })

但是你会发现抽取 manifest 文件和配置 vendor chunk 的逻辑不一样,所以这个插件需要配置两次:

[ new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: function(module){ return module.context && module.context.indexOf("node_modules") !== -1; } }), new webpack.optimize.CommonsChunkPlugin({ name: "manifest", minChunks: Infinity }), ]

你可能会好奇,假如有如下的配置:

module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Caching' }) ], output: { filename: '[name].[chunkhash].js', path: path.resolve(__dirname, 'dist') } };

那么如果 entry 中文件的内容没有发生变化,运行 webpack 命令多次,那么最后生成的文件名称应该是一样的,为什么会重新通过 CommonsChunkPlugin 来生成一个 manifest 文件呢?这个官网也有明确的说明:

因为 Webpack 在入口文件中会包含特定的样板文件,特别是 runtime 文件和 manifest 文件。而最后生成的文件的名称到底是否一致还与 Webpack 的版本有关,在新版本中可能不存在这个问题,但是在老版本中可能会存在,所以为了安全起见一般都会使用它。那么问题又来了,样板文件和 runtime 文件指的是什么?可以查 我的这个例子,把关注点放在文中说的为什么要提前加载最后一个 chunk 的问题上。下面就这部分做一下深入的分析:

runtime

当代码在浏览器中运行的时候,Webpack 使用 runtime 和 manifest 来处理应用中的模块化关系。其中包括在模块存在依赖关系的时候,加载和解析特定的逻辑,而解析的模块包括已经在浏览中加载完成的模块和那些需要懒加载的模块本身。

manifest

一旦应用程序中,如 index.html 文件、一些 bundle 和各种静态资源被加载到浏览器中,会发生什么?精心安排的 /src 目录的文件结构现在已经不存在,所以 Webpack 如何管理所有模块之间的交互呢?这就是 manifest 数据用途的由来……

当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。比如提供的 [这个例子](https://github.com/liangklfangl/commonsChunkPlugin_Config#单入口文件时候不能把引用多次的模块打印到 commonchunkplugin中),其中入口文件中有 main.js,入口文件中加载 chunk1.js 和 chunk2.js,而最后看到的就是下面转化为__webpack_require__ 后的内容:

webpackJsonp([0,1],[ /* 0 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(1); __webpack_require__(2); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(2); var chunk1=1; exports.chunk1=chunk1; /***/ }, /* 2 */ /***/ function(module, exports) { var chunk2=1; exports.chunk2=chunk2; /***/ } ]);

而 manifest 文件的作用就是在运行的时候通过__webpack_require__后的模块标识(Module identifier)来加载指定的模块内容。比如 manifest例子 生成的 manifest.json 文件内容是如下的格式:

{ "common.js": "common.js", "main.js": "main.js", "main1.js": "main1.js" }

这样就可以在源文件和目标文件之间有一个映射关系,而这个映射关系本身依然存在于打包后的输出目录,而不会因为 src 目录消失了而不知道具体的模块对应关系。而至于其中 moduleId 等的对应关系是由 Webpack 自己维护的,通过打包后的 可视化 可以了解。

CommonChunkPlugin 无法抽取单入口文件公共模块

上面讲了 Webpack 官网提供的例子以及原理分析,下面通过自己构造的几个例子来深入理解上面的概念。假如有如下的 Webpack 配置文件:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main:process.cwd()+'/example1/main.js', }, output: { path:process.cwd()+'/dest/example1', filename: '[name].js' }, devtool:'cheap-source-map', plugins: [ new CommonsChunkPlugin({ name:"chunk", minChunks:2 }) ] };

下面是入口文件内容:

//main.js require("./chunk1"); require("./chunk2"); console.log('main1.');

其中 chunk1.js 内容如下:

require("./chunk2"); var chunk1=1; exports.chunk1=chunk1;

而 chunk2.js 内容如下:

var chunk2=1; exports.chunk2=chunk2;

我们引入了 CommonsChunkPlugin,并将那些引入了两次以上的模块输出到 chunk.js 中。那么你肯定会认为,chunk2.js 被引入了两次,那么它肯定会被插件抽取到 chunk.js 中,但是实际上并不是这样。可以查看 main.js,内容如下:

webpackJsonp([0,1],[ /* 0 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(1); __webpack_require__(2); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(2); var chunk1=1; exports.chunk1=chunk1; /***/ }, /* 2 */ /***/ function(module, exports) { var chunk2=1; exports.chunk2=chunk2; /***/ } ]);

通过这个例子可知:“单入口文件时候不能把引用多次的模块打印到 CommonChunkPlugin 中”。

CommonChunkPlugin 抽取多入口文件公共模块

假如有如下的 Webpack 配置文件:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main:process.cwd()+'/example2/main.js', main1:process.cwd()+'/example2/main1.js', }, output: { path:process.cwd()+'/dest/example2', filename: '[name].js' }, plugins: [ new CommonsChunkPlugin({ name:"chunk", minChunks:2 }) ] };

其中 main1.js 内容如下:

require("./chunk1"); require("./chunk2");

而 main.js 内容如下:

require("./chunk1"); require("./chunk2");

而 chunk1.js 内容如下:

require("./chunk2"); var chunk1=1; exports.chunk1=chunk1;

而 chunk2.js 内容如下:

var chunk2=1; exports.chunk2=chunk2;

此时,很显然采用的是多入口文件模式,在相应的目录下会生成 main.js 和 main1.js,以及 chunk.js,而 chunk.js 中抽取的是 main.js 和 main1.js 中被引入了两次以上的模块,很显然 chunk1.js 和 chunk2.js 都会被引入到 chunk.js 中,下面是 chunk.js 中的部分代码:

/******/ ([ /* 0 */, /* 1 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(2); var chunk1=1; exports.chunk1=chunk1; /***/ }, /* 2 */ /***/ function(module, exports) { var chunk2=1; exports.chunk2=chunk2; /***/ } /******/ ]); CommonChunkPlugin 分离业务代码与框架代码

假如有如下的 Webpack 配置内容,同时 chunk1、chunk2、main1、main 的内容和上面保持一致。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main: process.cwd()+'/example3/main.js', main1: process.cwd()+'/example3/main1.js', common1:["jquery"], //只含有 jquery.js common2:["vue"] //只含有 vue.js 和加载器代码 }, output: { path: process.cwd()+'/dest/example3', filename: '[name].js' }, plugins: [ new CommonsChunkPlugin({ name: ["chunk",'common1','common2'], minChunks:2 //引入两次以及以上的模块 }) ] };

按照 CommonsChunkPlugin 的抽取公共代码的逻辑,会有如下的结果:

chunk.js 中保存的是 main.js 和 main1.js 的公共代码,即 chunk1.js 和 chunk2.js common1.js 中只有 jquery.js common2.js 中只有 vue.js,但是必须含有 Webpack 的加载器代码

其实道理很简单,chunk.js 中只有 chunk1.js 和 chunk2.js,而不存在被引入了两次的模块,最多引入次数的就是 chunk2.js,所以 common1.js 只含有 jquery.js。但是,正如前文所说,common2.js 必须最先加载。

minChunks 为 Infinity 配置

假如 Webpack 配置如下:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main: process.cwd()+'/example5/main.js', main1: process.cwd()+'/example5/main1.js', jquery:["jquery"] //minChunks: Infinity 时候框架代码依然会被单独打包成一个文件 }, output: { path: process.cwd() + '/dest/example5', filename: '[name].js' }, plugins: [ new CommonsChunkPlugin({ name: "jquery", minChunks:2//被引用两次及以上 }) ] };

上面的文件输出将会是如下内容:

main.js 包含去掉的公共代码部分 main1.js 包含去掉的公共代码部分 main1.js 和 main2.js 的公共代码将会被打包到 jquery.js 中,即 jquery.js 包含 jquery+ 公共的业务代码

其实,这个配置稍微晦涩难懂一点,假如将上面的 minChunks 配置修改为"Infinity",那么结果将截然不同:

main.js 原样打包 main1.js 原样打包 jquery 包含 jquery.js 和 webpack 模块加载器

因为将 minChunks 设置为 Infinity,也就是无穷大,那么 main.js 和 main1.js 中不存在任何模块被依赖的次数这么大,因此 chunk.js 和 chunk1.js 都不会被抽取出来。

chunks 指定那些入口文件中的公共模块会被抽取

继续修改 Webpack 配置如下:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); module.exports = { entry: { main: process.cwd()+'/example6/main.js', main1: process.cwd()+'/example6/main1.js', jquery:["jquery"] }, output: { path: process.cwd() + '/dest/example6', filename: '[name].js' }, plugins: [ new CommonsChunkPlugin({ name: "jquery", minChunks:2, chunks:["main","main1"] //main.js 和 main1.js 中都引用的模块才会被打包的到公共模块 }) ] };

此时 chunks 设置为 ["main","main1"],表示只有 main.js 和 main1.js 中都引用的模块才会被打包的到公共模块,而且必须是依赖次数为 2 次以上的模块。因此结果将会如下:

jquery.js 中包含 main1.js 和 main.js 中公共的模块,即 chunk1.js 和 chunk2.js,以及 jquery.js 本身 main1.js 表示是去掉公共模块后的文件内容 main.js 表示是去掉公共模块后的文件内容

我们也可以通过查看打包后的 jquery.js 看到结果验证,即 jquery.js 包含了 jquery.js 本身以及公共的业务代码:

/* 2 */ /***/ function(module, exports, __webpack_require__) { __webpack_require__(3); var chunk1=1; exports.chunk1=chunk1; /***/ }, /* 3 */ /***/ function(module, exports) { var chunk2=1; exports.chunk2=chunk2; /***/ } 本章小结

本章节主要通过 7 个例子展示了 Webpack 配合 CommonsChunkPlugin 的打包结果,但是为什么结果是这样,会在 Webpack 常见插件原理分析章节进行深入的剖析。本章节所有的例子代码你可以 点击这里 查看。文中的配置都是参考 Webpack 2 的,如果使用的是 Webpack 1,请升级。如果需要查看上面的例子的运行结果,请执行下面的命令:

npm install webpack -g git clone https://github.com/liangklfangl/commonsChunkPlugin_Config.git webpack //修改 webpack.config.js 并运行 webpack 命令


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3