2018-04-30 webpack打包如何防止第三方依赖包体积过大造成打包速度慢的问题

为何第三方依赖包会引起打包的体积过大

ES6的模块化机制,当引入外部第三方依赖包时,无论是否已经引入,都会再次将其引入进来,这时候就会存在重复引入导致打包体积过大、打包速度过慢的问题。

如何解决重复引入第三方依赖包,引起的打包体积过大以及打包速度过慢的问题

  1. 使用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的方式去提取公共的第三方外部依赖包模块,还是在每一次构建的时候,都会去进行打包,像我前面说的,第三方外部依赖包模块不像业务代码,很少进行修改。
        //所以每一次都去进行打包,还是不妥当的,花费了很多时间在打包第三方外部依赖包上面。
    ]
};
  1. 使用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 加载和运行模块文件的打包,减小了打包体积,提高了打包的速度。