最近碰到一个问题,项目是用vue-cli(v4.4.1)生成的webpack配置,修改splitChunks后,页面没有进行渲染(白屏),检查了一下以后发现有html的<srcipt>
中少了一些chunck,再仔细检查以后发现锅在HtmlWebpackPlugin。这是一个生成html的webpack插件,会在模板渲染后将入口相关的chunks插入html中。
HtmlWebpackPlugin中有几个关键选项
{
chunks: 'all', //数组或者'all',表示要将哪些chunks插入html
chunksSortMode: 'auto', //chunks插入顺序。
}
(如果不想花时间,后面代码可以跳过不读,只看结论)
chunksSortMode
chunksSortMode在HtmlWebpackPlugin v3.2.0中的取值有:
- Function:会执行Array.sort(chunksSortMode)来排序
- none:不排序,即按compilation.entrypoints的原本顺序
- manual:手动指定,按上面chunks的数组中的顺序
- dependency:按照chunk依赖关系的拓扑排序
- id:根据chunk id排序
- auto:webpack版本大于1就是dependency,否则是id
在v4.3.0中只有Function、none、manual、auto(none),而且manual的行为和v3.2.0不一样。
// v3.2.0
// chunks是经过配置中的chunks规则过滤后的所有chunks,options是HtmlWebpackPlugin的配置对象
module.exports.manual = (chunks, options) => {
const specifyChunks = options.chunks;
const chunksResult = [];
let filterResult = [];
if (Array.isArray(specifyChunks)) {
for (var i = 0; i < specifyChunks.length; i++) {
filterResult = chunks.filter(chunk => {
if (chunk.names[0] && chunk.names[0] === specifyChunks[i]) {
return true;
}
return false;
});
filterResult.length > 0 && chunksResult.push(filterResult[0]);
}
}
return chunksResult;
};
可以看到,如果HtmlWebpackPlugin配置项设为chunks: 'all',那么最后return的chunksResult是空数组,也就没有任何chunk会插入到html中。
// v4.3.0
// entryPointNames就是v3.2.0中的chunks,htmlWebpackPluginOptions就是v3.2.0中的options
module.exports.manual = (entryPointNames, compilation, htmlWebpackPluginOptions) => {
const chunks = htmlWebpackPluginOptions.chunks;
if (!Array.isArray(chunks)) {
return entryPointNames;
}
// Remove none existing entries from
// htmlWebpackPluginOptions.chunks
return chunks.filter((entryPointName) => {
return compilation.entrypoints.has(entryPointName);
});
};
v4.3.0就没有这个问题,另外,在entrypoints中过滤而不是options.chunks中,后面会讲到。
chunks
这个选项在两个版本间的行为更加不同。
// v3.2.0
const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
// Filter chunks (options.chunks and options.excludeCHunks)
let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
// Sort chunks
chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
filterChunks (chunks, includedChunks, excludedChunks) {
return chunks.filter(chunk => {
const chunkName = chunk.names[0];
// This chunk doesn't have a name. This script can't handled it.
if (chunkName === undefined) {
return false;
}
// Skip if the chunk should be lazy loaded
if (typeof chunk.isInitial === 'function') {
if (!chunk.isInitial()) {
return false;
}
} else if (!chunk.initial) {
return false;
}
// Skip if the chunks should be filtered and the given chunk was not added explicity
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
return false;
}
// Skip if the chunks should be filtered and the given chunk was excluded explicity
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
return false;
}
// Add otherwise
return true;
});
}
可以看到逻辑很简单,就是在options.chunks里的chunk就保留,不然就抛弃,如果options.chunks不是数组就全保留(所有entries中的所有chunk)。 所以多入口的话设为'all'是会出错的。
// v4.3.0
const entryNames = Array.from(compilation.entrypoints.keys());
const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);
const sortedEntryNames = self.sortEntryChunks(filteredEntryNames, this.options.chunksSortMode, compilation);
filterChunks (chunks, includedChunks, excludedChunks) {
return chunks.filter(chunkName => {
// Skip if the chunks should be filtered and the given chunk was not added explicity
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
return false;
}
// Skip if the chunks should be filtered and the given chunk was excluded explicity
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
return false;
}
// Add otherwise
return true;
});
}
看上去没啥区别,因为区别不在这,而在后面执行到计算assets文件路径的时候。
// v3.2.0
// Get assets
const assets = self.htmlWebpackPluginAssets(compilation, chunks);
htmlWebpackPluginAssets (compilation, chunks) {
const self = this;
...
const assets = {
...
chunks: {},
js: [],
css: [],
...
};
...
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkName = chunk.names[0];
assets.chunks[chunkName] = {};
// chunkFiles就是chunk.files文件名加上publicPath、hash等
const js = chunkFiles.find(chunkFile => /.js($|\?)/.test(chunkFile));
if (js) {
assets.chunks[chunkName].size = chunk.size;
assets.chunks[chunkName].entry = js;
assets.chunks[chunkName].hash = chunk.hash;
assets.js.push(js);
}
// Gather all css files
const css = chunkFiles.filter(chunkFile => /.css($|\?)/.test(chunkFile));
assets.chunks[chunkName].css = css;
assets.css = assets.css.concat(css);
}
...
return assets;
}
v3.2.0的assets中放的是options.chunks里经过过滤和排序后,各个chunk对应的js、css文件(chunk.files中的文件),也就是你写了哪些chunk,最多保留这些chunk中的文件。
// v4.3.0
// childCompilationOutputName不是重点不用管
// 通过之前的代码可以知道sortedEntryNames是options.chunks过滤和排序后的数组
const assets = self.htmlWebpackPluginAssets(compilation, childCompilationOutputName, sortedEntryNames);
htmlWebpackPluginAssets (compilation, childCompilationOutputName, entryNames) {
...
const assets = {
...
js: [],
css: [],
...
};
for (let i = 0; i < entryNames.length; i++) {
const entryName = entryNames[i];
// 关键是这里,Files是从entrypoints中获取的
const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
...
// 过滤热加载模块,文件名加上publicPath、hash等
...
entryPointPublicPaths.forEach((entryPointPublicPath) => {
...
// ext是文件类型,js或css
assets[ext].push(entryPointPublicPath);
});
}
return assets;
}
所以v4.3.0的HtmlWebpackPlugin的chunks选项的作用是将对应入口(把chunks中的每一项当作entry)的所有文件插入html中
名为chunks,实为entries
举个例子,entryA要加载的js文件是entryA.js、entryAentryB.js、vendorsentryA~entryB.js,chunks选项设为['entryA']。
在v3.2.0中最后插入html的只有entryA.js,因为entryA这个chunk中只有entryA.js。
在v4.3.0中最后插入html的有是entryA.js、entryAentryB.js、vendorsentryA~entryB.js,因为为entryA这个entry中包含了这些文件。
运行效果
我们在代码中打个断点看看
下面是在v3.2.0中
下面是在v4.3.0中
开头的问题
回过头来看开头的问题,vue-cli最新版用的也是v3.2.0的HtmlWebpackPlugin,然后vue-cli的入口配置中
pages: {
index: {
...
// 在这个页面中包含的块,默认情况下会包含
// 提取出来的通用 chunk 和 vendor chunk。
// 这个chunks就是HtmlWebpackPlugin配置中的chunks
chunks: ['chunk-vendors', 'chunk-common', 'index']
},
...
}
如果chunks不写,就默认是['chunk-vendors', 'chunk-common', page],因此如果分片后入口的chunk有变化,比如多了个pageA~pageB,这个chunk不会被插入到html中,造成缺失部分代码,所以页面没有渲染。 如果chunks设为'all',就会把所有chunk(包括其它入口的)插到html中,造成执行出错。所以怎么写都不对。如果你chunks设为'all'的同时chunksSortMode还设为manual,那么一个chunk都不会插到html中。
这样必须在chunks选项设置好该入口的所有chunks,但是执行分片前又不知道有哪些chunks。而且HtmlWebpackPlugin的所有hook都在计算assets之后。所以在v3.2.0中无解。。。
只能单独升级HtmlWebpackPlugin。
npm install --save-dev html-webpack-plugin
然后再vue.config.js中插入
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
configureWebpack: {
...
plugins: [
...
...Object.keys(pages).map(page =>
new HtmlWebpackPlugin({
filename: `${page}.html`,
chunks: [page],
template: pages[page].template,
// 模块热替换时依旧会使用vue-cli内置的HtmlWebpackPlugin,
// 疑似是vue-cli的问题,开发环境下加入hash,让其识别文件变化
hash: process.env.NODE_ENV === 'development',
})),
]
}
}
如果有其它好办法的话,欢迎告诉我。
相关链接: