webpack
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 的文件的大小必须大于等于这个值
}
children 属性
其中在 Webpack 中很多 chunk 产生都是通过 require.ensure 来完成的。先看看下面的例子:
import asd from './req'
//home.js
if( asd() ) {
require.ensure([], () => {
const Dabao = require('./dabao');
});
} else {
require.ensure([], () => {
const Xiaobao = require('./xiaobao');
});
}
//main.js
import React from 'react';
import common from './common'
import xiaobao from './xiaobao'
两个文件一个是 通过require.ensure 引入,一个直接引入,那么通过require.ensure引入也生成两个文件 0.js 和 1.js.
通过配置 children,可以将动态产生的这些 chunk 的公共的模块也抽取出来。如果配置了多个入口文件(假如还有一个 main1.js),那么这些动态产生的 chunk 中可能也会存在相同的模块(此时 main1、main 会产生四个动态 chunk )。而这个 children 配置就是为了这种情况而产生的。通过配置 children,可以将动态产生的这些 chunk 的公共的模块也抽取出来。
当配置children后我们抽取公共模块的chunks集合
这时候在插件commonChunkPlugin中的抽取公共chunk的代码:
commonChunks.forEach(function processCommonChunk(commonChunk, idx) {
let usedChunks;
if(Array.isArray(selectedChunks)) {
usedChunks = chunks.filter(chunk => chunk !== commonChunk && selectedChunks.indexOf(chunk.name) >= 0);
} else if(selectedChunks === false || asyncOption) {
usedChunks = (commonChunk.chunks || []).filter((chunk) => {
// we can only move modules from this chunk if the "commonChunk" is the only parent
return asyncOption || chunk.parents.length === 1;
});
//(1)
var util = require('util');
console.log('------------->commonChunk',util.inspect(commonChunk, {showHidden:true,depth:4}));
//如果name=['main','main1']那么表示以入口文件开始单独打包,此时的commonChunk就是我们的main.js和main1.js
//其chunks属性表示所有require.ensure的产生的chunk
} else {
if(commonChunk.parents.length > 0) {
compilation.errors.push(new Error("CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + commonChunk.name + ")"));
return;
}
//如果found>=idx表示该chunk后续会作为一个独立的chunk来处理(独立打包),所以此处不做修改
//这里的chunks集合是包括所有entry中配置的和在name数组中配置的,如果entry中不存在这个chunk而name中存在,直接创建一个空的chunk!
usedChunks = chunks.filter((chunk) => {
const found = commonChunks.indexOf(chunk);
if(found >= idx) return false;
return chunk.hasRuntime();
});
}
let asyncChunk;
if(asyncOption) {
asyncChunk = compilation.addChunk(typeof asyncOption === "string" ? asyncOption : undefined);
asyncChunk.chunkReason = "async commons chunk";
asyncChunk.extraAsync = true;
asyncChunk.addParent(commonChunk);
commonChunk.addChunk(asyncChunk);
commonChunk = asyncChunk;
}
const reallyUsedModules = [];
if(minChunks !== Infinity) {
const commonModulesCount = [];
const commonModules = [];
usedChunks.forEach((chunk) => {
chunk.modules.forEach((module) => {
const idx = commonModules.indexOf(module);
if(idx < 0) {
commonModules.push(module);
commonModulesCount.push(1);
} else {
commonModulesCount[idx]++;
}
});
});
const _minChunks = (minChunks || Math.max(2, usedChunks.length));
commonModulesCount.forEach((count, idx) => {
const module = commonModules[idx];
if(typeof minChunks === "function") {
if(!minChunks(module, count))
return;
} else if(count < _minChunks) {
return;
}
if(module.chunkCondition && !module.chunkCondition(commonChunk))
return;
reallyUsedModules.push(module);
});
}
if(minSize) {
const size = reallyUsedModules.reduce((a, b) => {
return a + b.size();
}, 0);
if(size < minSize)
return;
}
const reallyUsedChunks = new Set();
reallyUsedModules.forEach((module) => {
usedChunks.forEach((chunk) => {
if(module.removeChunk(chunk)) {
reallyUsedChunks.add(chunk);
}
});
commonChunk.addModule(module);
module.addChunk(commonChunk);
});
if(asyncOption) {
for(const chunk of reallyUsedChunks) {
if(chunk.isInitial()) continue;
chunk.blocks.forEach((block) => {
block.chunks.unshift(commonChunk);
commonChunk.addBlock(block);
});
}
asyncChunk.origins = Array.from(reallyUsedChunks).map((chunk) => {
return chunk.origins.map((origin) => {
const newOrigin = Object.create(origin);
newOrigin.reasons = (origin.reasons || []).slice();
newOrigin.reasons.push("async commons");
return newOrigin;
});
}).reduce((arr, a) => {
arr.push.apply(arr, a);
return arr;
}, []);
} else {
usedChunks.forEach((chunk) => {
chunk.parents = [commonChunk];
chunk.entrypoints.forEach((ep) => {
ep.insertChunk(commonChunk, chunk);
});
commonChunk.addChunk(chunk);
});
}
if(filenameTemplate)
commonChunk.filenameTemplate = filenameTemplate;
});
我们看看其中的chunk.hasRuntime函数:
hasRuntime() {
if(this.entrypoints.length === 0) return false;
return this.entrypoints[0].chunks[0] === this;
}
我们看看chunk.entrypoints内部表示(见data.js下名字为main的chunk):
entrypoints:
[ Entrypoint { name: 'main', chunks: [ [Circular], [length]: 1 ] },
[length]: 1 ]
所以只有顶级chunk才会有执行环境。我们顺便看看在commonchunkplugin的处理方式:
//usedChunks是已经抽取了公共模块的chunk
usedChunks.forEach(function(chunk) {
chunk.parents = [commonChunk];
chunk.entrypoints.forEach(function(ep) {
ep.insertChunk(commonChunk, chunk);
//在每一个移除了公共代码的chunk之前插入commonChunk
});
//每一个移除了公共chunk的chunk.entrypoints添加一个chunk
commonChunk.addChunk(chunk);
});
我们顺便也给出EntryPoint的代码:
class Entrypoint {
constructor(name) {
this.name = name;
this.chunks = [];
}
unshiftChunk(chunk) {
this.chunks.unshift(chunk);
chunk.entrypoints.push(this);
}
insertChunk(chunk, before) {
const idx = this.chunks.indexOf(before);
if(idx >= 0) {
this.chunks.splice(idx, 0, chunk);
} else {
throw new Error("before chunk not found");
}
chunk.entrypoints.push(this);
}
getFiles() {
let files = [];
for(let chunkIdx = 0; chunkIdx < this.chunks.length; chunkIdx++) {
for(let fileIdx = 0; fileIdx < this.chunks[chunkIdx].files.length; fileIdx++) {
if(files.indexOf(this.chunks[chunkIdx].files[fileIdx]) === -1) {
files.push(this.chunks[chunkIdx].files[fileIdx]);
}
}
}
return files;
}
}
module.exports = Entrypoint;
usedChunks.forEach(function(chunk,index) {
var util = require('util');
console.log('------------->before'+chunk.name,util.inspect(chunk.entrypoints, {showHidden:true,depth:2}));
chunk.parents = [commonChunk];
chunk.entrypoints.forEach(function(ep) {
ep.insertChunk(commonChunk, chunk);
});
var util = require('util');
console.log('------------->end'+chunk.name,util.inspect(chunk.entrypoints, {showHidden:true,depth:2}));
commonChunk.addChunk(chunk);
});
commonChunkPlugin抽取之前的chunk
Chunk {
id: null,
ids: null,
debugId: 1000,
name: 'main',
//chunk对应的name
modules: [],
//该chunk来自于哪些module,main这个chunk来自于src/index.js,该module包含两个RequireEnsureDependenciesBlock
entrypoints:
[ Entrypoint { name: 'main', chunks: [ [Circular], [length]: 1 ] },
[length]: 1 ],
//入口文件为main:'./src/index.js',而entryPoint对应的chunk为对当前chunk的循环引用
chunks:[],//当前chunk的子级chunks有哪些,如require.ensure都是当前chunk的子级chunk
parents: [ [length]: 0 ],
//当前chunk的父级chunk集合,没有经过commonChunkPlugin处理main是顶级chunk
blocks: [ [length]: 0 ],
//module.blocks表示模块包含的块RequireEnsureDependenciesBlock等的个数,chunk.block表示当前chunk包含的block的个数
origins:
//当前chunk从哪些模块得到
[ { module:
NormalModule {
dependencies: [ [Object], [length]: 1 ],
blocks: [ [Object], [Object], [length]: 2 ],
variables: [ [length]: 0 ],
context: '/Users/klfang/Desktop/webpack-chunkfilename/src',
reasons: [ [length]: 0 ],
debugId: 1000,
lastId: null,
id: null,
portableId: null,
index: 0,
index2: 12,
depth: 0,
used: true,
usedExports: true,
providedExports: true,
chunks: [ [Circular], [length]: 1 ],
warnings: [ [Object], [length]: 1 ],
dependenciesWarnings: [ [length]: 0 ],
errors: [ [length]: 0 ],
dependenciesErrors: [ [length]: 0 ],
strict: true,
meta: {},
request: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/babel-loader/lib/index.js!/Users/klfang/Desktop/webpack-chunkfilename/node_modules/eslint-loader/index.js!/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
userRequest: '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
rawRequest: './src/index.js',
parser:
Parser {
_plugins: [Object],
options: undefined,
scope: undefined,
state: undefined },
resource: '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
loaders: [ [Object], [Object], [length]: 2 ],
//module.fileDependencies: An array of source file paths included into a module. This includes the source JavaScript file itself (ex: index.js), and all dependency asset files (stylesheets, images, etc) that it has required. Reviewing dependencies is useful for seeing what source files belong to a module.
//这个module没有引入相应的css/html/image等
fileDependencies:
[ '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
[length]: 1 ],
contextDependencies: [ [length]: 0 ],
error: null,
_source:
OriginalSource {
_value: '\'use strict\';\n\n// var $ = require(\'jquery\');\n\n// $(\'body\').html(\'Hello\');\n\n\n// import $ from \'jquery\';\n// $(\'body\').html(\'Hello\');\n\n\n// import Button from \'./Components/Button\';\n// const button = new Button(\'google.com\');\n// button.render(\'a\');\n\n//code splitting\nif (document.querySelectorAll(\'a\').length) {\n require.ensure([], function () {\n var Button = require(\'./Components/Button\').default;\n var button = new Button(\'google.com\');\n button.render(\'a\');\n });\n}\n\nif (document.querySelectorAll(\'h1\').length) {\n require.ensure([], function () {\n var Header = require(\'./Components/Header\').default;\n new Header().render(\'h1\');\n });\n}',
_name: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/babel-loader/lib/index.js!/Users/klfang/Desktop/webpack-chunkfilename/node_modules/eslint-loader/index.js!/Users/klfang/Desktop/webpack-chunkfilename/src/index.js' },
assets: {},
built: true,
_cachedSource: null,
issuer: null,
building: undefined,
buildTimestamp: 1487137260364,
cacheable: true },
loc: undefined,
name: 'main' },
[length]: 1 ],
files: [ [length]: 0 ],
// An array of output filenames generated by the chunk.
//You may access these asset sources from the compilation.assets table.
//表示这个chunk产生的输出文件,此处为顶级chunk没有输出文件产生
_removeAndDo:{},
addChunk:{},
addParent:{},
//入口模块
entryModule:
NormalModule {
dependencies:
[ ConstDependency {},
[length]: 1 ],
blocks:
[ RequireEnsureDependenciesBlock {
dependencies: [ [Object], [Object], [Object], [length]: 3 ],
blocks: [ [length]: 0 ],
variables: [ [length]: 0 ],
chunkName: null,
chunks: [ [Object], [length]: 1 ],
module: [Circular],
loc: SourceLocation { start: [Object], end: [Object] },
expr:
Node {
type: 'CallExpression',
start: 313,
end: 488,
loc: [Object],
range: [Object],
callee: [Object],
arguments: [Object] },
range: [ 345, 486, [length]: 2 ],
chunkNameRange: null,
parent: [Circular] },
RequireEnsureDependenciesBlock {
dependencies: [ [Object], [Object], [Object], [length]: 3 ],
blocks: [ [length]: 0 ],
variables: [ [length]: 0 ],
chunkName: null,
chunks: [ [Object], [length]: 1 ],
module: [Circular],
loc: SourceLocation { start: [Object], end: [Object] },
expr:
Node {
type: 'CallExpression',
start: 543,
end: 678,
loc: [Object],
range: [Object],
callee: [Object],
arguments: [Object] },
range: [ 575, 676, [length]: 2 ],
chunkNameRange: null,
parent: [Circular] },
[length]: 2 ],
variables: [ [length]: 0 ],
context: '/Users/klfang/Desktop/webpack-chunkfilename/src',
reasons: [ [length]: 0 ],
debugId: 1000,
lastId: null,
id: null,
portableId: null,
index: 0,
index2: 12,
depth: 0,
used: true,
usedExports: true,
providedExports: true,
chunks: [ [Circular], [length]: 1 ],
warnings: [],
dependenciesWarnings: [ [length]: 0 ],
errors: [ [length]: 0 ],
dependenciesErrors: [ [length]: 0 ],
strict: true,
meta: {},
request: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/babel-loader/lib/index.js!/Users/klfang/Desktop/webpack-chunkfilename/node_modules/eslint-loader/index.js!/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
userRequest: '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
rawRequest: './src/index.js',
parser:
Parser {
_plugins: {},
options: undefined,
scope: undefined,
state: undefined },
resource: '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
loaders:
[ { loader: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/babel-loader/lib/index.js' },
{ loader: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/eslint-loader/index.js' },
[length]: 2 ],
fileDependencies:
[ '/Users/klfang/Desktop/webpack-chunkfilename/src/index.js',
[length]: 1 ],
contextDependencies: [ [length]: 0 ],
error: null,
_source:
OriginalSource {
_value: '\'use strict\';\n\n// var $ = require(\'jquery\');\n\n// $(\'body\').html(\'Hello\');\n\n\n// import $ from \'jquery\';\n// $(\'body\').html(\'Hello\');\n\n\n// import Button from \'./Components/Button\';\n// const button = new Button(\'google.com\');\n// button.render(\'a\');\n\n//code splitting\nif (document.querySelectorAll(\'a\').length) {\n require.ensure([], function () {\n var Button = require(\'./Components/Button\').default;\n var button = new Button(\'google.com\');\n button.render(\'a\');\n });\n}\n\nif (document.querySelectorAll(\'h1\').length) {\n require.ensure([], function () {\n var Header = require(\'./Components/Header\').default;\n new Header().render(\'h1\');\n });\n}',
_name: '/Users/klfang/Desktop/webpack-chunkfilename/node_modules/babel-loader/lib/index.js!/Users/klfang/Desktop/webpack-chunkfilename/node_modules/eslint-loader/index.js!/Users/klfang/Desktop/webpack-chunkfilename/src/index.js' },
assets: {},
built: true,
_cachedSource: null,
issuer: null,
building: undefined,
buildTimestamp: 1487137260364,
cacheable: true } }
}
我们看看commonchunkplugin中的处理方式(else部分):
if(Array.isArray(selectedChunks)) {
usedChunks = chunks.filter(function(chunk) {
if(chunk === commonChunk) return false;
//此时commonChunk的内容是已经存在于最终的文件中了,如果它不是手动创建的chunk
//去掉下例的jquery,得到usedChunks集合
return selectedChunks.indexOf(chunk.name) >= 0;
});
} else if(selectedChunks === false || asyncOption) {
usedChunks = (commonChunk.chunks || []).filter(function(chunk) {
// we can only move modules from this chunk if the "commonChunk" is the only parent
//只是把一级子chunk的公共内容提取出来,如果有一个子chunk的父级chunk有两个那么不会被提取出来。
return asyncOption || chunk.parents.length === 1;
});
} else {
//如果当前的这个chunk有多个父级chunk,那么不会提取的
if(commonChunk.parents.length > 0) {
compilation.errors.push(new Error("CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (" + commonChunk.name + ")"));
return;
}
usedChunks = chunks.filter(function(chunk) {
var found = commonChunks.indexOf(chunk);
if(found >= idx) return false;
return chunk.hasRuntime();
});
}
chunks
通过 chunks 参数来选择来源的 chunk。这些 chunk 必须是 common-chunk 的子级 chunk。如果没有指定,那么默认选中所有的入口 chunk。下面给出一个例子:
module.exports = {
entry : {
main : './src/main.js',
home : './src/home.js',
common : ['jquery'],
common2 : ['react']
},
output : {
path: path.join(__dirname, 'build'),
filename: '[name].js'
},
plugins: [
new CommonsChunkPlugin({
name: "common",
minChunks: 2,
chunks : ["main","home"]
})
]
}
main","home" 公共模块会被打包到 common里面
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;
}
});
第二个例子
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;
}
})
其中 CommonsChunkPlugin 插件还有一个更加有用的配置,即用于将 Webpack 打包逻辑相关的一些文件抽取到一个独立的 chunk 中。但是此时配置的 name 应该是 entry 中不存在的,这对于线上缓存很有作用。因为如果文件的内容不发生变化,那么 chunk 的名称不会发生变化,所以并不会影响到线上的缓存。比如下面的例子:
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
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
}),
]
runtime
当代码在浏览器中运行的时候,Webpack 使用 runtime 和 manifest 来处理应用中的模块化关系。其中包括在模块存在依赖关系的时候,加载和解析特定的逻辑,而解析的模块包括已经在浏览中加载完成的模块和那些需要懒加载的模块本身。
manifest
一旦应用程序中,如 index.html 文件、一些 bundle 和各种静态资源被加载到浏览器中,会发生什么?精心安排的 /src 目录的文件结构现在已经不存在,所以 Webpack 如何管理所有模块之间的交互呢?这就是 manifest 数据用途的由来……