为何第三方依赖包会引起打包的体积过大
ES6的模块化机制,当引入外部第三方依赖包时,无论是否已经引入,都会再次将其引入进来,这时候就会存在重复引入导致打包体积过大、打包速度过慢的问题。
如何解决重复引入第三方依赖包,引起的打包体积过大以及打包速度过慢的问题
- 使用CommonsChunkPlugin提取公共模块,理想状态下是将第三方外部依赖包、业务代码、业务代码中的重复引入的公共部分和webpack的引导程序以及manifest 加载运行外部依赖包都分别打成一个包,减小打包体积,提高打包的速度。
const webpack = require("webpack"),
HtmlWebpackIncludeAssetsPlugin = require("html-webpack-include-assets-plugin"),
HtmlWebpackPlugin = require("html-webpack-plugin");
const PUBLIC_DIR = "/";
module.exports = {
entry: {
//login入口有引入testConfig.js以及reactConfig.js
login: `${APP_DIR}/login.js`,
//index入口也有引入testConfig.js以及reactConfig.js
index: `${APP_DIR}/index.js`,
//app入口没有引入testConfig.js,也没有引入reactConfig.js
app: `${APP_DIR}/app.js`,
//mobile入口有引入testConfig.js,却没有引入reactConfig.js
mobile: `${APP_DIR}/mobule.js`
},
plugins: [
//...
//首先我想把四个入口中的公共模块,包括引入的第三方外部依赖包都提取出来
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: 'js/common.[hash].js'
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "login.html",
template: `${ROOT_DIR}/login.html`,
chunks: ["common", "login"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "index.html",
template: `${ROOT_DIR}/index.html`,
chunks: ["common", "index"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "app.html",
template: `${ROOT_DIR}/app.html`,
chunks: ["common", "app"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "mobile.html",
template: `${ROOT_DIR}/mobile.html`,
chunks: ["common", "mobile"],
inject: "body"
})
//发现这样行不通,打包之后,common.[hash].js中只有webpack引导程序以及manifest 加载运行模块的代码,公共部分并没有办法提取出来。
//后来查找原因,原来是CommonsChunkPlugin中的minChunks属性默认设置为公共模块部分最小在全部入口全部引入,才会被提取合成公共代码。
//知道了原因,那就简单了,直接设置minChunks: 2,也就是说只要公共模块最小在两个入口引入,就可以被提取出来作为公共模块部分。
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: 'js/common.[hash].js',
minChunks: 2
}),
...
//这样设置之后发现是可以的,将公共的第三方的外部依赖包、testConfig.js、reactConfig.js、webpack引导程序以及manifest 加载运行模块部分都提取出来,打包至common.[hash].js中,减小了打包的体积,加快了打包速度。
//试想假如我只想提取testConfig.js模块
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: 'js/common.[hash].js',
//minChunks还可以是一个函数,接收两个参数,module是指入口所引入的每一个模块,count则是指module被几个入口所调用
//module有两个属性: context和resource
//context: 模块所存储的目录位置
//resource: 模块所执行的文件名称
//这里就是使用resource模块所执行的文件名称去匹配testConfig,且模块至少被两个入口所引入,这样就可以单独提取testConfig.js文件了
minChunks: function(module, count) {
return module.resource && /testConfig/.test(module.resource) && count >= 2;
}
}),
...
],
//将第三方公共依赖包与testConfig.js、reactConfig.js这种业务开发所使用的公共模块分离开,使得第三方公共依赖包分离在另一个包下
entry: {
//login入口有引入testConfig.js以及reactConfig.js
login: `${APP_DIR}/login.js`,
//index入口也有引入testConfig.js以及reactConfig.js
index: `${APP_DIR}/index.js`,
//app入口没有引入testConfig.js,也没有引入reactConfig.js
app: `${APP_DIR}/app.js`,
//mobile入口有引入testConfig.js,却没有引入reactConfig.js
mobile: `${APP_DIR}/mobule.js`,
//设置一个第三方外部依赖包的入口
vendor: ['react', 'react-redux', 'redux', 'react-router', 'redux-thunk', 'redux-logger', 'react-dom', 'react-addons', 'prop-types', 'moment', 'antd', 'babel-polyfill']
},
plugins: [
...
//这里无论入口是否引入了vendor入口数组里面的第三方依赖包,它都会对数组里面的第三方外部依赖包进行提取,当然这里的第三方外部依赖包是有限制的,module.context在这里起到了作用,只有模块所存储的路径含有"node_modules"的情况下,也就是说只有是在npm下载的第三方外部依赖包,才会被提取出来。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'js/vendor.[hash].js',
minChunks: function(module, count) {
return module.context && module.context.includes("nodule_modules");
}
}),
//这样提取第三方外部依赖包是可以的,直接提取到js文件夹里面的vendor.[hash].js
//上面以及所有的提取的公共模块文件加hash的原因,是为了防止浏览器的永久缓存机制,使得文件更新过后,使用的还是原来文件的内容。
//提取出了第三方外部依赖包之后,由于想到它们不是业务代码,很少进行修改,尽量的多利用浏览器永久缓存机制,所以为了防止每一次构建都会引起它们hash值的改变,不能利用浏览器永久缓存机制,要把webpack引导程序以及manifest 加载运行文件提取到另一个模块包中。
//这里manifest就是隐藏的webpack引导程序以及manifest 加载和运行模块文件的入口
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
filename: 'js/manifest.[hash].js',
minChunks: Infinity
}),
//这样提取webpack引导程序以及manifest 加载和运行文件是可以的,直接提取到js文件夹里面的manifest.[hash].js
//再提取出业务使用的像testConfig.js、reactConfig.js公共模块,想着应该可以满足需求,业务代码的公共部分一个模块包,第三方外部依赖包一个模块包,以及webpack引导程序和manfiest 加载运行模块文件一个模块包
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: 'js/common.[hash].js',
minChunks: 2
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "login.html",
template: `${ROOT_DIR}/login.html`,
chunks: ["manifest", "vendor", "common", "login"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "index.html",
template: `${ROOT_DIR}/index.html`,
chunks: [manifest", "vendor", "common", "index"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "app.html",
template: `${ROOT_DIR}/app.html`,
chunks: [manifest", "vendor", "common", "app"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "mobile.html",
template: `${ROOT_DIR}/mobile.html`,
chunks: [manifest", "vendor", "common", "mobile"],
inject: "body"
})
//发现这样实现是不可以的,第三方外部依赖包的模块包,webpack引导程序以及manifest 加载和运行模块文件的模块包被打出来了。
//但是common.[hash].js中却没有至少两个入口共同引入业务代码公共模块的部分,还是被分别打进了业务代码模块包中。
//且common.[hash].js中的代码实际上是webpack运行文件以及manfiest 加载和运行模块的文件部分,manfiest.[hash].js也不见了,好像common.[hash].js替换掉了。
//原因到现在还没有找到...
//CommonsChunkPlugin的缺点就在于:即使我使用了vendor的方式去提取公共的第三方外部依赖包模块,还是在每一次构建的时候,都会去进行打包,像我前面说的,第三方外部依赖包模块不像业务代码,很少进行修改。
//所以每一次都去进行打包,还是不妥当的,花费了很多时间在打包第三方外部依赖包上面。
]
};
- 使用DllPlugin、DllReferencePlugin、CommonsChunkPlugin以及HtmlWebpackIncludeAssetsPlugin实现对第三方外部依赖包模块、业务代码公共模块以及webpack引导程序和manifest 加载和运行模块文件都分别打成一个包,减小打包体积,提高打包的速度。
//使用DllPlugin和DllReferencePlugin,就不需要像CommonsChunkPlugin那样,对于第三方外部依赖包模块,每次都要去构建打包了,只需要另外配置一个webpack配置文件,就可以实现一劳永逸的体验。
//只要没有下载新的第三方外部依赖包模块,就不需要利用webpack.dll.config配置文件去打dll包,总体上减少了每次都构建第三方外部依赖包模块的时间。
//webpack.dll.config配置
const webpack = require("webpack"),
path = require("path");
const PUBLIC_DIR = "/",
DLL_DIR = path.resolve(__dirname, "../dll"),
ROOT_DIR = path.resolve(__dirname, "../..");
const webpackDllConfig = {
devtool: "source-map",
entry: {
vendor: ["react", "react-router", "redux", "react-redux", "redux-thunk", "redux-logger", "react-dom", "react-addons", "prop-types", "antd", "babel-polyfill"]
},
output: {
publicPath: PUBLIC_DIR,
path: DLL_DIR,
filename: "[name].dll.js",
library: "[name]_[chunkhash]"
},
plugins: [
//防止打包过程中出现错误,中断打包
new webpack.NoEmitOnErrorsPlugin(),
//谈一下DllPlugin,DllPlugin的机制是根据webpack制定的id映射到vendor入口中的第三方外部依赖包模块的路径上,生成映射关系,打包后,生成vendor.dll.js文件和vendor_manifest.dll.json文件,这个文件的内容是webpack制定的id与vendor入口中的第三方外部依赖包模块的映射数据,之后再使用DllReferencePlugin将vendor_manifest.dll.json文件引入到业务代码打包的配置文件的manfiest 加载和运行模块文件中,最后只要将vendor.dll.js引入到你所选择的业务代码入口就可以了。
//vendor.dll.js的作用是,根据vendor_manifest.dll.json中webpack制定的id与vendor入口中第三方外部依赖包模块路径的映射关系,业务代码入口所引入的第三方外部依赖包模块,都会通过vendor.dll.js全局函数进行处理,并根据所引入的第三方外部依赖包模块的id进行使用,且这样并不会把库文件中的代码也打包进去
new webpack.DllPlugin({
path: path.join(DLL_DIR, "[name]_manifest.dll.json"),
//name必须和output的library属性保持一致
name: "[name]_[chunkhash]",
context: ROOT_DIR
}),
//对打包代码进行压缩
new webpack.optimize.UglifyJsPlugin({
uglifyOptions: {
sourceMap: true,
compress: {
unused: false,
dead_code: false,
warnings: true
},
output: {
comments: true
}
}
})
]
};
export default webpackDllConfig;
//业务代码webpack打包配置文件
const webpack = require("webpack"),
path = require("path"),
//用来复制目录或者目录下的文件的插件
CopyWebpackPlugin = require("copy-webpack-config"),
//用来将vendor.dll.js插入到业务代码入口的插件,但不可选择插入的入口业务代码文件,默认会将所有的入口业务代码都插入vendor.dll.js
AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin"),
//用来将vendor.dll.js插入到业务代码入口的插件,可选择插入的入口业务代码文件
HtmlWebpackIncludeAssetsPlugin = require("html-webpack-include-assets-plugin"),
HtmlWebpackPlugin = require("html-webpack-plugin");
const PUBLIC_DIR = "/",
ROOT_DIR = path.resolve(__dirname, "../"),
BUILD_DIR = path.resolve(__dirname, "../build"),
DLL_DIR = path.resolve(__dirname, "../dll"),
IMAGE_DIR = path.resolve(__dirname, "../images"),
MANIFEST_DIR = require(path.resolve(__dirname, `${DLL_DIR}/vendor_manifest.dll.json`));
const webpackProdConfig = {
entry: {
//login入口有引入testConfig.js以及reactConfig.js
login: `${APP_DIR}/login.js`,
//index入口也有引入testConfig.js以及reactConfig.js
index: `${APP_DIR}/index.js`,
//app入口没有引入testConfig.js,也没有引入reactConfig.js
app: `${APP_DIR}/app.js`,
//mobile入口有引入testConfig.js,却没有引入reactConfig.js
mobile: `${APP_DIR}/mobule.js`
},
...
plugins: [
...
//这里就是将vendor_manfiest.dll.json第三方外部依赖包模块引入到业务代码的打包配置文件的manifest 加载和运行模块文件中
new webpack.DllReferencePlugin({
manifest: MANIFEST_DIR,
context: ROOT_DIR
}),
new CopyWebpackPlugin([{
context: ROOT_DIR,
from: IMAGE_DIR,
to: `${BUILD_DIR}/images`
},{
context: ROOT_DIR,
from: DLL_DIR,
to: `${BUILD_DIR}/dll`
}, {
context: ROOT_DIR,
from: "./dll/vendor.dll.js",
to: "js/"
}]),
//这里的AddAssetHtmlPlugin会将所有的业务代码入口都引入vendor.dll.js文件
//new AddAssetHtmlPlugin({
// filepath: "js/vendor.dll.js",
// hash: true
//}),
//这里的HtmlWebpackIncludeAssetsPlugin会有选择的将login、index、app入口文件引入vendor.dll.js,可实现按需加载
//append属性设置为false,是确保vendor.dll.js文件在业务代码包模块之前引入
new HtmlWebpackIncludeAssetsPlugin({
assets: ["js/vendor.dll.js"],
files: ["login.html", "index.html", "app.html"],
append: false,
hash: true
}),
//这样第三方外部依赖包模块就生成了,当每次没有新的第三方外部依赖包模块下载时,就可以直接利用vendor.dll.js全局函数处理对引入的webpack制定的id与vendor_manfiest.dll.json中的第三方外部依赖包模块路径的映射进行调用
new webpack.optimize.CommonsChunkPlugin({
name: "common",
filename: "js/common.[hash].js",
minChunks: 2
}),
//这里上面在CommonsChunkPlugin部分有介绍过,直接对业务代码中的公共模块部分进行提取,且至少在两个入口中有公共模块的引入
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
filename: "js/manifest.[hash].js",
minChunks: Infinity
}),
//再将webpack引导程序以及manfiest 加载运行模块文件从common.[hash].js文件中提取出来,这样就完成配置了
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "login.html",
template: `${ROOT_DIR}/login.html`,
chunks: ["manifest", "common", "login"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "index.html",
template: `${ROOT_DIR}/index.html`,
chunks: [manifest", "common", "index"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "app.html",
template: `${ROOT_DIR}/app.html`,
chunks: [manifest", "common", "app"],
inject: "body"
}),
new HtmlWebpackPlugin({
publicPath: PUBLIC_DIR,
filename: "mobile.html",
template: `${ROOT_DIR}/mobile.html`,
chunks: [manifest", "common", "mobile"],
inject: "body"
})
]
};
这种由DllPlugin、DllReferencePlugin和HtmlWebpackIncludAssetsPlugin(可实现按需加载)打包第三方外部依赖包模块,由CommonsChunkPlugin提取业务代码公共模块部分和webpack引导程序以及manifest 加载运行模块文件的方式,很好的实现了对第三方外部依赖包模块、业务代码公共模块以及webpack引导程序和manifest 加载和运行模块文件的打包,减小了打包体积,提高了打包的速度。