美文网首页
多页面webpack构建优化不完全指北

多页面webpack构建优化不完全指北

作者: Kaku_fe | 来源:发表于2018-06-29 14:44 被阅读452次

    前言

    自从新项目的技术栈启用vue以后,项目的构建工具也自然而然的从原来的内部的工具切换成了webpack,在感受到HMR,各式各样loader的强大后,也随着项目的逐渐变大,依赖的模块越来越多,webpack的构建效率成为了制约团队开发效率的短板。因此,我们来介绍一下多页面下,我们是如何优化webpack的效率的(毕竟本文标题是不完全指北,如果还有其他更好的方法,欢迎留言给我)。

    项目背景

    我们的项目是基于vue多页面项目,webpack配置文件基于vue-cli进行改写,因此在webpack中存在多个entry,项目的大体结构如下

    |---src
        |---pages
            |---xxx1 - 某业务页面1
                |---App.vue - 该业务主入口vue组件
                |---xxx1.html - (与目录同名,业务模板文件)
                |---xxx1.js  - (与目录同名,业务主入口js文件)
            |---xxx2 - 某业务页面2
                |---App.vue - 该业务主入口vue组件
                |---xxx2.html - (与目录同名,业务模板文件)
                |---xxx2.js  - (与目录同名,业务主入口js文件)
    

    下面,我们基于这样的多页面结构具体讲述一下我们是如何对webpack进行构建优化(基于webpack3)

    公共代码提取

    使用过vue-cli的童鞋都知道,生成模板项目的时候默认使用了** CommonsChunkPlugin来作为code splite工具,本质上通过配置minChunk提出公共部分代码,便于在多页面中缓存(如:页面A和B都有vendor.js,那么访问了页面A,下一次访问页面B,B中的vendor.js直接加载内存中的就好了),从而达到性能提升的目的。当是该Plugin**也有不足,即他是动态编译和进行code splite。怎么理解呢,即每次打包构建,他都会执行一次重复的去执行code splite, 而且因为minChunk策略各不相同,每一次上线以后,提取的公共代码vendor.js内容可能因为版本的不同而不同,但是,像(vue, vuex vue-router)等三方库基本上是稳定的,不需要根据业务的变化而变化。因此,基于此我们可以提取出这些第三方库提前预构建好,而不是让他随着版本再次构建

    方法一

    最简单的方式莫过于直接将这些js合并压缩混淆挂载在全局节点上,但是如果这样做,我们在业务代码中就只能通过window下的属性来使用它们提供的各个功能,打破了模块化的封装,因此该方案并不好。

    方法二

    考虑到CommonChunkPlugin的局限性,webpack官方提供了另外一个插件DllPlugin,这个插件需要和DLLReferencePlugin配合使用。

    熟悉 Windows 的朋友就应该知道,DLL 所代表的含义。在 Windows 中,有大量的 .dll 文件,称为动态链接库。动态链接库提供了将应用模块化的方式,应用的功能可以在此基础上更容易被复用。

    因此我们的目的即使用DLL插件,将不修改的模块公共部分提取出来单独打包
    ,我们先建立webpack.dll.config.js,这个文件内容很简单。

    const path = require('path');
    const webpack = require('webpack');
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
    const config = require('../config');
    
    module.exports = {
        entry: {
            vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash'] // 所需要的打包前端公共模块
        },
        output: {
            path: path.join(__dirname, '../static/js'), // 打包后文件输出的位置
            filename: '[name].dll.js',
            /**
             * output.library
             * 将会定义为 window.${output.library}
             * 在这次的例子中,将会定义为`window.vendor_library`
             */
            library: '[name]_library'
        },
        plugins: [
            new webpack.DllPlugin({  //主要是使用这个插件去打包js
                /**
                 * path
                 * 定义 manifest 文件生成的位置
                 * [name]的部分由entry的名字替换
                 */
                path: path.join(__dirname, '.', '[name]-manifest.json'),
                /**
                 * name
                 * dll bundle 输出到那个全局变量上
                 * 和 output.library 一样即可。
                 */
                name: '[name]_library',
                context: path.join(__dirname, '..')
            }),
            new UglifyJsPlugin({    // 使用这个插件可以混淆打包完成的js
                uglifyOptions: {
                    compress: {
                        warnings: false
                    }
                },
                sourceMap: config.build.productionSourceMap,
                parallel: true
            })
        ]
    };
    

    执行 webpack --config build/webpack.dll.config.js后,webpack会自动生成2个文件,其中vendor.dll.js即合并打包后第三方模块。另外一个vendor-mainifest.json存储各个模块和所需公用模块的对应关系。

    将第三方模块打完包以后,我们就需要使用DLLReferencePlugin来将它和我们的业务代码进行融合,我们修改webpack.base.config(vue-cli生成配置),添加plugin如下:

    plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,  // 与DllPlugin中的那个context保持一致
                manifest: require('./vendor-manifest.json')
            }),
            ......
    ]
    

    同时,我们还需要手动的将vendor.dll.js插入类似index.html这样的模板文件才可以生效

    <script src="/vendor.dll.js"></script>
    

    这样就完成了使用dll插件提取公共第三方库的操作,一般情况下,我们不会增加或者减少第三方库,但是一旦出现这种情况,我们都需要手动重新去打一个包来进行替换。那么有没有更自动的方式来完成这件事呢?

    方法三

    AutoDllPlugin出现在了我的视野,这个插件自动同时相当于完成了DllReferencePluginDllPlugin的工作,只需要在webpack.base.config中添加

    plugins: [
        new AutoDllPlugin({
                inject: true, // will inject the DLL bundles to html
                context: path.join(__dirname, '..'),
                filename: '[name]_[hash].dll.js',
                path: 'res/js',
                plugins: mode === 'online' ? [
                    new UglifyJsPlugin({
                        uglifyOptions: {
                            compress: {
                                warnings: false
                            }
                        },
                        sourceMap: config.build.productionSourceMap,
                        parallel: true
                    })
                ] : [],
                entry: {
                    vendor: ['vue/dist/vue.esm.js', 'vuex', 'axios', 'vue-router', 'babel-polyfill', 'lodash']
                }
         })
    ]
    

    ,不需要额外的webpack.dll.config.js配置以及不需要手动将打完好的包拷贝到对应的模板文件中。

    小结

    大多数情况,我们推荐方法3,不过方法3相比方法2,增加了每次启动重新构建一次新的vendor.js,开发阶段首次启动会构建一次新的vendor,增加一些额外的时间(实测下来影响并不大),不过也避免了更新第三方库增减而忘记打包对业务产生的影响

    多进程构建

    webpack和其他大部分js工具相同都是单线程对项目进行处理,
    然而 Webpack 这个工具强就强在流程设计的扩展性如此之强,可以人为的加上多进程处理。
    其在编译文件流程如下:

    1. 开始编译 (Compiler#run)
    2. 开始编译入口文件 (Compilation#addEntry)
        2.1 开始编译文件 (Compilation#buildModule => NormalModule#build)
        2.2 执行 Loader 得到文件结果 (NormalModule#runLoaders)
        2.3 根据结果解析依赖 (NormalModule#parser.parse)
        2.4 处理依赖文件列表 (Compilation#processModuleDependencies)
        2.5 开始编译每个依赖文件 (异步,从这里开始递归操作: 编译文件->解析依赖->编译依赖文件->解析深层依赖...)
    

    这里的关键在于递归操作 2.5 开始编译每个依赖文件 这一步是异步设计,每个依赖文件的编译彼此之间互不影响。不过虽然是异步的,但还是跑在一个线程里。但是这样的设计却带来了多进程的可行性。

    编译文件中主要的耗时操作在于 Loader 对源文件的转换操作,而 Loader 的可异步的设计使得转换操作的执行并不被限制在同一线程内。下面对 Loader 进行改造,使其支持多进程并发:

    2.2 执行 Loader 得到文件结果
        LoaderWrapper 作为新的 Loader 入口接收文件输入信息
        LoaderWrapper 创建一个子进程 (child_process#fork) (这一步可维护一个进程池)
        子进程中,通过调用原始 Loader,转换输入文件,然后把最终结果传递给父进程
        父进程将收到的结果作为 Loader 结果传递给 Webpack
    

    HappyPack 的实现就是这个流程,我们来使用babel-loader作为例子,来讲解一下HappyPack如何配置

    通常情况下,我们使用的babel-loader如下所示

    webpack.base.config.js
    ...
    module: {
        rules: [
          ...
          {
            test: /\.js$/,
            include: [resolve('src'), resolve('lib'),resolve('test'), resolve('node_modules/webpack-dev-server/client')], // 通过合理配置include也可以对提升构建性能
            use: [
              {
                loader: 'babel-loader'
              },
            ],
            exclude: /node_modules/ // 通过合理配置exclude也可以对提升构建性能
          }
    }
    

    转换成HappyPack,配置改写为

    const HappyPack = require('happypack');
    const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
    
    // 省略其他配置
    module.exports = {
        module: {
            rules: [
                {
                    test: /\.js$/,
                    include: [resolve('src'), resolve('lib'), resolve('test'), resolve('node_modules/webpack-dev-server/client')],
                    use: [
                        {
                            loader: 'happypack/loader?id=happybabel'  // 将loader换成happypack并将id指向插件id参数
                        },
                    ],
                    exclude: /node_modules/
                }
            ]
        },
       plugins: [
            new HappyPack({  // HappyPack插件
                id: 'happybabel',
                loaders: ['babel-loader?cacheDirectory=true'],
                threadPool: happyThreadPool,
            })
        ]
    }
    

    HappyPack不只可以对babel-loader进行处理,其他vue-loader,css-loader等都可以用他进行加速优化,只需要如上增加实例以及改写loader即可。使用HappyPack整体优化后,在我们的项目中,构建速度基本可以提高70%。

    多页面html-webpack-plugin优化

    作为webpack中的第一大插件html-webpack-plugin,大家应该或多或少的使用过,这个插件会根据你的模板代码,通过不同的模板引擎构建出对应的html,ejs甚至ftl文件,在标准的SPA中,该插件性能不会性能瓶颈,但是如果你使用的是多页面,该插件的构建速度绝对是地狱级别的,
    如,我只是简单修改了一个vue文件的一个文案,在阶段居然花费了16s,这大大减慢了开发效率,感受不到HMR的优势

    image.png

    我们找到html-webpack-pluginemit事件钩子,注入事件代码

    image.png

    我们发现,他会对每一个入口文件都执行一遍emit中所有代码逻辑


    image.png

    ,

    因此,我们需要考虑,如何只在自己修改到的入口,执行emit下面的流程就好了。

    在浏览了很多issure后,发现已经有现有的轮子帮助我们完成了判断和缓存的功能.
    html-webpack-plugin-for-multihtml

    修改配置代码代码

    const HtmlWebpackPlugin = require('html-webpack-plugin-for-multihtml');
    // 省略其他代码
    
    plugins:[
      new HtmlWebpackPlugin({
              template: filePath,
              filename: `${filename}.html`,
              chunks: ['manifest', 'vendor', filename],
              inject: true,
               multihtmlCache: true  // 增加该配置
      })
    ]
    

    该插件通过在webpack done钩子函数中设置相关变量,来保证原html-webpack-plugin插件中emit仅触发一次全部流程。来达到提速的效果。升级以后,修改文案,HMR的速度从原来的秒级改为毫秒级。

    image.png

    参考文档

    HappyPack - Webpack 的加速器

    相关文章

      网友评论

          本文标题:多页面webpack构建优化不完全指北

          本文链接:https://www.haomeiwen.com/subject/rpzqyftx.html