基于vue-cli的webpack打包优化实践及探索



  • 转眼已经是2019年,短短三四年时间,webpack打包工具成为了前端开发中必备工具,曾经一度的面试题都是问,请问前端页面优化的方式有哪些?大家也是能够信手拈来的说出缓存、压缩文件、CSS雪碧图以及部署CDN等等各种方法,但是今天不一样了,可能你去面试问的就是,请问你是否知道webpack的打包原理,webpack的打包优化方法有哪些?所以该说不说的,笔者闲着没事研究了一下webpack的打包优化,可能大家都有看过类似的优化文章~ 但是笔者还是希望能够给大家一些新的启发~

    1、准备工作:测速与分析bundle

    既然我们要优化webpack打包,肯定要提前对我们的bundle文件进行分析,分析各模块的大小,以及分析打包时间的耗时主要是在哪里,这里主要需要用到两个webpack插件,speed-measure-webpack-plugin和webpack-bundle-analyzer,前者用于测速,后者用于分析bundle文件。

    具体配置

    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
    const smp = new SpeedMeasurePlugin({
      outputFormat:"human",
    });
    module.exports = {
    configureWebpack: smp.wrap({
    plugins: [
      new webpack.ProvidePlugin({
    $: "zepto",
    Zepto: "zepto",
      }),
      new BundleAnalyzerPlugin(),
    ],
    optimization: {
      splitChunks: {
    cacheGroups: {
      echarts: {
    name: "chunk-echarts",
    test: /[\\/]node_modules[\\/]echarts[\\/]/,
    chunks: "all",
    priority: 10,
    reuseExistingChunk: true,
    enforce: true,
      },
      demo: {
    name: "chunk-demo",
    test: /[\\/]src[\\/]views[\\/]demo[\\/]/,
    chunks: "all",
    priority: 20,
    reuseExistingChunk: true,
    enforce: true,
      },
      page: {
    name: "chunk-page",
    test: /[\\/]src[\\/]/,
    chunks: "all",
    priority: 10,
    reuseExistingChunk: true,
    enforce: true,
      },
      vendors: {
    name: "chunk-vendors",
    test: /[\\/]node_modules[\\/]/,
    chunks: "all",
    priority: 5,
    reuseExistingChunk: true,
    enforce: true,
      },
    },
      },
    },
      })
    }
    

    由于是基于vue-cli脚手架的,所以其实vue-cli中已经帮你做了一些优化的工作,可以看到,原先项目最初的配置设置了splitchunk,进行代码分割,这在大型项目中是很有必要的,毕竟你不希望你的用户阻塞加载一个5MB大小的JS文件,所以做代码分割和懒加载是很有必要的。
    说远了,我们来看看这个配置,你需要用smp对配置进行再包裹,因为SpeedMeasurePlugin会对你的其他Plugin对象包裹一层代理,这样的目的是为了能够知道plugin开始和结束的时间~
    其次,BundleAnalyzerPlugin就跟普通的plugin一样,加载plugins数组的后面即可。
    接下来我们看一下最初的打包时间以及包内容分析:

    1285305325-5da3049249267_articlex.png

    2704246753-5da304b49a25a_articlex.png

    可以看到项目中较大的三个包,其中两个包是我们的第三方依赖,three.js、lottie、lodash、echarts等。

    2、开始逐步优化

    2.1缩小文件查找和处理范围

    这是webpack优化中的常规操作,基本就是对模块和文件查找的优化,以及减少loader对一些不必要模块的处理,但是vue-cli中的loader并没有暴露给我们操作,所以其内置的loader处理无法由我们进行优化,但是其实vue-cli中的配置项已经对loader的查找路径进行了优化,如果你的项目也是使用了vue-cli,你可以通过以下命令行查看你现有的配置文件是怎样的:

    npx vue-cli-service inspect > output.js
    

    具体可以翻阅vuecli官方文档。

    resolve:{
      modules: [path.resolve(__dirname, 'node_modules')],
      alias:{
    'three':path.resolve(__dirname, './node_modules/three/build/three.min.js'),
    'zepto$':path.resolve(__dirname, './node_modules/zepto/dist/zepto.min.js'),
    'swiper$':path.resolve(__dirname, './node_modules/swiper/dist/js/swiper.min.js'),
    'lottie-web$':path.resolve(__dirname, './node_modules/lottie-web/build/player/lottie.min.js'),
    'lodash$':path.resolve(__dirname, './node_modules/lodash/lodash.min.js'),
      }
    },
    module:{
      noParse:/^(vue|vue-router|vuex|vuex-router-sync|three|zepto|swiper|lottie-web|lodash)$/
    },
    
    • 通过modules指定查找第三方模块的路径。
    • 通过alias指定第三方模块直接查找到打包构建好的压缩js文件。
    • 通过module指定noparse,对第三方模块不再进行分析依赖。

    优化效果:2s?

    1078467593-5da30775cc06c_articlex.png

    可以看到时间就减少了两三秒,在30s波动,感觉没有多大差别。

    2.2尝试使用happypack

    由于在进行webpack优化前,翻阅了很多有关webapck优化的文章,所以笔者也想尝试一下用happypack来优化打包时间。
    在想要用happypack进行的打包之前,大抵有这两种说法:
    1、webpack4中已经默认是多线程打包了,所以happypack打包效果不明显;
    2、vue不支持happypack打包,需要设置thread-loader。
    但是笔者想了一下,还是试试看把,大不了我只对JS和CSS文件设置happypack。
    但是问题又来了,vue-cli内置封装了loader,这个时候我要怎么拿到它的配置,改写里面的loader配置呢。
    通过翻阅vue-cli的官方文档我们可以看到以下使用介绍:

    configureWebpack
    Type: Object | Function
    如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中。
    如果这个值是一个函数,则会接收被解析的配置作为参数。该函数及可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。
    

    为此,笔者特地调试进了vue-cli的源码一探究竟:
    流程介绍:
    由于我们执行命令行vue-cli-service build,其实是先去node_modules的.bin文件夹下查找相应的可执行文件,.bin下的vue-cli-service会映射到相应的第三方库内的执行文件。
    所以我们可以找到这个可执行文件的地址:
    /node_modules/@vue/cli-service/bin/vue-cli-service.js
    找到了入口,接下来我们想要进入nodejs的调试,在以往的开发中,我们会通过node --inspect app.js的方式启动一个后台服务,然后在谷歌浏览器里进入调试界面(F12选择绿色的那个小按钮)
    但是这里却犯了难,由于我们的打包构建是一次执行的,不同于一个后台服务,是实时监听的,服务一直启动着。查阅了一下,如果是普通的nodejs文件想要调试的话,需要通过这样的方式:

    node --inspect-brk=9229 app.js
    

    所以,为了强行走进去vue-cli的源码进行调试,可看vue-cli的处理流程,我们需要这样输入以下命令行:

    node --inspect-brk=9229 node_modules/@vue/cli-service/bin/vue-cli-service.js build
    

    上面的这个命令行,等价于vue-cli-service build。
    通过这样的方式,我们终于走进了vue-cli的源码,看了它的执行流程,你可以在对应的位置打下断点,查看此时的作用域内的变量数据。
    3587485713-5da30fa9cc4c2_articlex.png
    可以看到vue-cli源码里的这一段操作,会执行我们传入的函数,判断函数有没有返回值来决定是否要merge进其内部配置的config。
    通过这段代码我们可以看出,如果我们configWepack配置为函数,之后通过参数的形式获取到config配置项,本身是一个对象,对象是保留引用的形式,所以如果我们直接对传入的config对象进行修改,就可以实现我们最初的目标!修改vue-cli内置的loader!
    当然,除了断点进入里面看配置,刚才也说了,我们可以通过命令行输出为一个output文件查看现有的配置。
    这里可以给大家截图看一下vue-cli内部的配置:
    1019530989-5da310988559c_articlex.png
    可能有点废话了,但是通过断点的方式,我们可以看到vue-cli其实已经对js文件设置了exclude,同时也帮我们设置好了cache-loader,意味着webpack常规的优化方式之一,使用cache-loader缓存它也帮我们做了。
    回到最初的起点,我们想要处理的是针对JS和CSS的loader,于是模仿大多数的配置,我进行了以下修改:

      configureWebpack:(config)=>{
        console.log("webpack config start");
        let originCssRuleLoader = config.module.rules[6].oneOf[0].use;
        let newCssRuleLoader = 'happypack/loader?id=css';
        config.module.rules[6].oneOf[0].use = newCssRuleLoader
        config.module.rules[6].oneOf[1].use = newCssRuleLoader
        config.module.rules[6].oneOf[2].use = newCssRuleLoader
        config.module.rules[6].oneOf[3].use = newCssRuleLoader
        ...//other code
     }
    

    尝试对css的loader配置进行修改。之后对plugins进行一下配置:

    plugins: [
        new HappyPack({
          id: 'css',
          threads: 4,
          loaders: originCssRuleLoader
        }),
      ],
    

    本以为这样就OK了,但是很遗憾的告诉大家,报错了...
    1402828501-5da31256a47e8_articlex.png
    可以看到报错的内容,是在处理vue文件的时候,出了错误。
    如何解决
    笔者百度了,也谷歌了,大抵是说happypack不支持vue-loader,同时,根据报错也查了一下处理的方案,通过设置parallel参数,也还是无效。
    笔者甚至怀疑是自己的happypack配置不对,于是我把配置原样移植配置到另一个非vue项目中,一切运行正常。
    答案:此题无解~
    原因分析:
    由于vue文件中会含有CSS,所以vue-loader会提取出其中的css,交给其他loader处理,vue-loader-plugin会通过在vue文件后面加上查询字符串来告诉其他loader,针对这个文件要做处理。意味着什么呢?我们的vue-loader在处理文件的时候,通知其他loader处理,但是此时的loader配置已经被我们改写成了happypack,而vue又与happypack不兼容,最终导致了报错。很遗憾的告诉大家,vue-cli接入happypack--失败。
    (注:这一部分主要是笔者在webpack优化过程中的探索,虽然最终不能让自己的webpack打包很好的优化,但是在这个探索的过程中,我们也可以学到很多~包括 vue-cli对配置对象的处理?如何调试普通文件nodejs代码?vue-loader中对vue文件的处理流程?vue-loader-plugin帮我们做了什么事?而这些都是要自己慢慢翻阅,慢慢踩坑去了解的~)

    2.3使用dllplugin

    和大多数的webpack优化教程一样,笔者也尝试了利用dllplugin进行优化,该插件的本质,是提取出我们常用的第三方模块,单独打成一个文件包,之后插入到我们的html页面中,这样我们以后每次打包,都不需要针对第三方模块进行处理,毕竟第三方模块动辄成千上万行。
    流程介绍:

    • 1、配置webpack.dll.js针对第三方库打包
    • 2、vue.config.js中配置plugin
    • 3、html中引入dll打包出来的js文件。(一般采用部署CDN的方式)

    由于项目中有很多大型的第三方库,类似three、echart等,所以笔者进行了以下配置:(webpack.dll.js)

    const webpack = require("webpack")
    const path = require("path")
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    module.exports = {
        entry: {
            vuebundle: [
                'vue',
                'vue-router',
                'vuex',
            ],
            utils:[
                'lodash',
                'swiper',
                'lottie-web',
                'three',
            ],
            echarts:[
                'echarts/lib/echarts',
                "echarts/lib/chart/bar",
                "echarts/lib/chart/line",
                "echarts/lib/component/tooltip",
                "echarts/lib/component/title",
                "echarts/lib/component/legend",
            ]
    
        },
        output: {
            path: path.resolve(__dirname, './static/'),
            filename: '[name].dll.js',
            library: '[name]_library'
        },
        plugins: [
            new webpack.DllPlugin({
                path: path.join(__dirname, 'build', '[name]-manifest.json'),
                name: '[name]_library'
            })
        ]
    }
    

    针对不同的库的大小进行划分,打了三个包,为啥不打成一个包?一个包那就太大了,你并不希望你的用户加载一个大型JS文件包而阻塞,影响页面性能。
    接下里是vue.config.js的配置:

    plugins: [
          new webpack.ProvidePlugin({
            $: "zepto",
            Zepto: "zepto",
          }),
          new DllReferencePlugin({
            manifest: require('./build/echarts-manifest.json'),
          }),
          new DllReferencePlugin({
            manifest: require('./build/utils-manifest.json'),
          }),
          new DllReferencePlugin({
            manifest: require('./build/vuebundle-manifest.json'),
          }),
          new BundleAnalyzerPlugin(),
        ]
    

    引入了DllPlugin。接下来配置HTML:
    (由于笔者没将DLL打包出来的js文件上传到CDN,所以只能本地自己起个node服务器返回静态资源了)

      <body>
         <div id="app"></div>
        <!-- built files will be auto injected -->
        <script type="text/javascript" src="http://localhost:3000/echarts.dll.js"></script>
        <script type="text/javascript" src="http://localhost:3000/utils.dll.js"></script>
        <script type="text/javascript" src="http://localhost:3000/vuebundle.dll.js"></script>
      </body>
    

    然后npm run serve,开始页面调试和开发~
    舒服~
    优化结果:
    3080003830-5da3173c36555_articlex.png
    由于少了大型第三方库,所以时间控制在了20s左右了。优化相对比较明显~

    3、优化与探索总结

    优化到这,基本就结束了。
    webpack常见的优化方式,优化路径查找、设置缓存、happypack以及dllplugin,前两项vue-cli已经帮我们做了一些,而happypack由于不和vue兼容,导致无法接入,dllplugin通过单独提取第三方库,取得了明显优化。
    当然,笔者也尝试剔除了一些项目中无用的代码,不过也是不痛不痒。
    webpack优化方式总结:

    • 1、优化模块查找路径
    • 2、剔除不必要的无用的模块
    • 3、设置缓存:缓存loader的执行结果(cacheDirectory/cache-loader)
    • 4、设置多线程:HappyPack/thread-loader
    • 5、dllplugin提取第三方库

    当然,这是针对开发的优化,如果是针对部署上的优化呢?我们可以设置splitchunk、按需加载、部署CDN等,这里就不展开了。

    最后

    希望这篇文章能够大家有所收获~ webpack已经是前端仔必备技能了~有空大家钻研一下webpack的配置和原理,也是会有所收获的!谢谢观看~


Log in to reply
 

最新帖子

最新内容

  • S

    You can use the Element.tagName property (it represents the tag name in uppercase):

    <pre class="snippet-code-js lang-js prettyprint-override">``` var some_element = document.getElementById('my_Id'); console.log(some_element.tagName); // => P

    <pre class="snippet-code-html lang-html prettyprint-override">```
    <p id="my_Id">Hello</p>

    read more
  • S
    var some_id = document.getElementById('my_Id'); var tag = some_id.tagName;

    read more
  • S

    I am curious if it is possible to get tag equivalent to jquery's $('tag_name') by just having an id at hand? For example:

    var some_id = document.getElementById('my_Id'); //and then do something like var get_tag = some_id.its_tag;

    Is something like this possible?

    I know we can get all tags into one place, like list, and then loop through that list and check each id, but curious if there is something that can be done elegantly?

    Thanks!

    Alternative is known, but would be great if original question actually has some answer too.

    var some_id = document.getElementById('my_Id'); //and then do something like var get_tag = some_id.its_tag;

    read more
  • S

    It might help to have a base class in typescript from which other widget classes may derive. Its only purpose is to provide the base class semantic so you can access the base class'es members without having to resort to weak typing.

    The trick is to remove all the members at runtime (in the constructor) -- otherwise you run into problems with the inheritance provided by the widget factory. For example, the option method would override the widget's original method which is not desired: we just want to be able to call it (in a statically typed way).

    class WidgetBase { public element:JQuery; constructor() { // remove all members, they are only needed at compile time. var myPrototype = (<Function>WidgetBase).prototype; $.each(myPrototype, (propertyName, value)=>{ delete myPrototype[propertyName]; }); } /** * Calles the base implementation of a method when called from a derived method. * @private */ public _super(arg1?:any, arg2?:any, arg3?:any, arg4?:any) { } /** * @private */ public _superApply(arguments) { } /** * Gets or sets the value of the widget option associated with the specified optionName. */ public option(optionName:string, value?:any):any { } // ... further methods from http://api.jqueryui.com/jQuery.widget/ }

    Then you can implement your own widget like this:

    class SmartWidget extends WidgetBase { constructor(){ super(); } public _create() { var mySmartOption = this.option('smart'); // compiles because of base class this.beSmart(mySmartOption); } public _setOption(key:string, value:any) { if (key === 'smart') { this.beSmart(value); } this._super(key, value); // compiles because of base class } private beSmart(smartOne:any){ // ... } } // register jQuery.widget("myLib.smartWidget", new SmartWidget()); // assuming you are using https://github.com/borisyankov/DefinitelyTyped declare interface JQuery{ smartWidget(); smartWidget(options:any); smartWidget(methodName:string, param1?:any, param2?:any, param3?:any, param4?:any); }

    And finally, you can use your widget:

    $(".selector").smartWidget({smart:"you"});

    read more
  • S

    I'm not sure you can write a class that implements the Widget interface, due to the lack of overloaded constructors. You could create a variable that is typed by the Widget interface.

    A standard jQuery plugin would be represent in almost pure JavaScript and wouldn't use modules or classes as it ends up being wrapped up as part of jQuery, which itself isn't a module or class.

    Here is an empty plugin called plugin that looks like any standard jQuery plugin, but you can see it takes advantage of the TypeScript type system and extends the JQuery interface to allow it to be called.

    /// <reference path="jquery.d.ts" /> interface JQuery { plugin(): JQuery; plugin(settings: Object): JQuery; } (function ($) { function DoSomething(someParamater: string) : void { } $.fn.plugin = function (settings) { var config = { settingA: "Example", settingB: 5 }; if (settings) { $.extend(config, settings); } return this.each(function () { }); }; })(jQuery);

    This would be called in the normal way.

    $('#id').plugin();

    So really, my answer is - you can't really do what you want because you are adding to the declared interfaces for jQuery rather than exposing them as modules. You could wrap the usage in a module, like an adaptor that abstracts the jQuery aspect away from the use in your TypeScript, or you can call your classes from inside the plugin, but the plugin or widget doesn't really fit into a module or class.

    read more

推荐阅读