webpack
Webpack 打包例子講解
CommonChunkPlugin 參數(shù)詳解
{
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,
//指定該插件產(chǎn)生的文件名稱烈菌,可以支持 output.filename 中那些支持的占位符宽菜,如 [hash]、[chunkhash]、[id] 等涩维。如果忽略這個這個屬性,那么原始的文件名稱不會被修改(一般是 output.filename 或者 output.chunkFilename,可以查看 compiler 和 compilation 部分第一個例子)。但是這個配置不允許和 `options.async` 一起使用
minChunks: number|Infinity|function(module, count) boolean,
//至少有 minChunks 的 chunk 都包含指定的模塊饰豺,那么該模塊就會被移出到 common chunk 中。這個數(shù)值必須大于等于2允蜈,并且小于等于沒有使用這個插件應(yīng)該產(chǎn)生的 chunk 數(shù)量冤吨。如果傳入 `Infinity`,那么只會產(chǎn)生 common chunk饶套,但是不會有任何模塊被移到這個 chunk中 (沒有一個模塊會被依賴無限次)漩蟆。通過提供一個函數(shù),也可以添加自己的邏輯妓蛮,這個函數(shù)會被傳入一個參數(shù)表示產(chǎn)生的 chunk 數(shù)量
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 產(chǎn)生都是通過 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扔仓,可以將動態(tài)產(chǎn)生的這些 chunk 的公共的模塊也抽取出來。如果配置了多個入口文件(假如還有一個 main1.js)咖耘,那么這些動態(tài)產(chǎn)生的 chunk 中可能也會存在相同的模塊(此時 main1翘簇、main 會產(chǎn)生四個動態(tài) chunk )。而這個 children 配置就是為了這種情況而產(chǎn)生的儿倒。通過配置 children版保,可以將動態(tài)產(chǎn)生的這些 chunk 的公共的模塊也抽取出來。
當(dāng)配置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的產(chǎn)生的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后續(xù)會作為一個獨立的chunk來處理(獨立打包)彻犁,所以此處不做修改
//這里的chunks集合是包括所有entry中配置的和在name數(shù)組中配置的,如果entry中不存在這個chunk而name中存在凰慈,直接創(chuàng)建一個空的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函數(shù):
hasRuntime() {
if(this.entrypoints.length === 0) return false;
return this.entrypoints[0].chunks[0] === this;
}
我們看看chunk.entrypoints內(nèi)部表示(見data.js下名字為main的chunk):
entrypoints:
[ Entrypoint { name: 'main', chunks: [ [Circular], [length]: 1 ] },
[length]: 1 ]
所以只有頂級chunk才會有執(zhí)行環(huán)境。我們順便看看在commonchunkplugin的處理方式:
//usedChunks是已經(jīng)抽取了公共模塊的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對應(yīng)的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對應(yīng)的chunk為對當(dāng)前chunk的循環(huán)引用
chunks:[],//當(dāng)前chunk的子級chunks有哪些微谓,如require.ensure都是當(dāng)前chunk的子級chunk
parents: [ [length]: 0 ],
//當(dāng)前chunk的父級chunk集合森篷,沒有經(jīng)過commonChunkPlugin處理main是頂級chunk
blocks: [ [length]: 0 ],
//module.blocks表示模塊包含的塊RequireEnsureDependenciesBlock等的個數(shù),chunk.block表示當(dāng)前chunk包含的block的個數(shù)
origins:
//當(dāng)前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沒有引入相應(yīng)的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產(chǎn)生的輸出文件豺型,此處為頂級chunk沒有輸出文件產(chǎn)生
_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的內(nèi)容是已經(jīng)存在于最終的文件中了仲智,如果它不是手動創(chuàng)建的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的公共內(nèi)容提取出來,如果有一個子chunk的父級chunk有兩個那么不會被提取出來姻氨。
return asyncOption || chunk.parents.length === 1;
});
} else {
//如果當(dāng)前的這個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 參數(shù)來選擇來源的 chunk。這些 chunk 必須是 common-chunk 的子級 chunk肴焊。如果沒有指定前联,那么默認(rèn)選中所有的入口 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 為函數(shù)
可以給 minChunks 傳入一個函數(shù)抖韩。CommonsChunkPlugin 將會調(diào)用這個函數(shù)并傳入 module 和 count 參數(shù)蛀恩。這個 module 參數(shù)用于指定某一個 chunks 中所有的模塊,而這個 chunk 的名稱就是上面配置的 name/names 參數(shù)茂浮。這個 module 是一個 NormalModule 實例双谆,有如下的常用屬性:
module.context:表示存儲文件的路徑,比如 '/my_project/node_modules/example-dependency'
module.resource:表示被處理的文件名稱席揽,比如 '/my_project/node_modules/example-dependency/index.js'
而 count 參數(shù)表示指定的模塊出現(xiàn)在多少個 chunk 中顽馋。這個函數(shù)對于細(xì)粒度的操作 CommonsChunk 插件還是很有用的』闲撸可自己決定將那些模塊放在指定的 common chunk 中寸谜,下面是官網(wǎng)給出的一個例子:
new webpack.optimize.CommonsChunkPlugin({
name: "my-single-lib-chunk",
filename: "my-single-lib-chunk.js",
minChunks: function(module, count) {
//如果一個模塊的路徑中存在 somelib 部分,而且這個模塊出現(xiàn)在 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 打包邏輯相關(guān)的一些文件抽取到一個獨立的 chunk 中果善。但是此時配置的 name 應(yīng)該是 entry 中不存在的诊笤,這對于線上緩存很有作用。因為如果文件的內(nèi)容不發(fā)生變化巾陕,那么 chunk 的名稱不會發(fā)生變化讨跟,所以并不會影響到線上的緩存。比如下面的例子:
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
})
但是你會發(fā)現(xiàn)抽取 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
當(dāng)代碼在瀏覽器中運行的時候晾匠,Webpack 使用 runtime 和 manifest 來處理應(yīng)用中的模塊化關(guān)系。其中包括在模塊存在依賴關(guān)系的時候梯刚,加載和解析特定的邏輯凉馆,而解析的模塊包括已經(jīng)在瀏覽中加載完成的模塊和那些需要懶加載的模塊本身。
manifest
一旦應(yīng)用程序中乾巧,如 index.html 文件句喜、一些 bundle 和各種靜態(tài)資源被加載到瀏覽器中,會發(fā)生什么沟于?精心安排的 /src 目錄的文件結(jié)構(gòu)現(xiàn)在已經(jīng)不存在咳胃,所以 Webpack 如何管理所有模塊之間的交互呢?這就是 manifest 數(shù)據(jù)用途的由來……