基于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

    I'm using Ionic 3 and this cordova plugin mauron85/cordova-plugin-background-geolocation but the app doesn't stream coordinates when it's in the background in iOS.

    It works in Android, iOS emulator but not in a real iPhone. Also, the url option stops streaming after 1s

    My code so far:

    /** * * @param {boolean} status * @param {number} stepOn */ private registerCoords(status: boolean, stepOn?: number): void { let step: number = stepOn ? stepOn : 1; if (!STATIC.GEO_INTERVAL && status) { STATIC.GEO_INTERVAL = setInterval((() => { this.currentCoords = this.geolocationService.getCoords(); if (!this.isNetWork) { this.functions.f_alertToast('Network connection lost', 0); return this.registerCoords(false); } if (!this.currentCoords && !this.isProcessPicture) { this.geoLocationEvent = false; this.isGeoLocation = false; console.warn('[HomePage] registerCoords() -> setInterval() [GPS connection lost!!]'); } else if (this.currentCoords) { this.geoLocationEvent = true; this.isGeoLocation = true; } this.saveCoords(this.currentCoords, step); step = 1; }).bind(this), STATIC.GEO_TIMEOUT); this.finishLoad(); } if (status === false) { clearInterval(STATIC.GEO_INTERVAL); STATIC.GEO_INTERVAL = null; this.checkGeolocation(); } }

    config.xml

    <plugin name="cordova-plugin-mauron85-background-geolocation" spec="2.3.5"> <variable name="ICON" value="@mipmap/icon" /> <variable name="SMALL_ICON" value="@mipmap/icon" /> <variable name="ACCOUNT_NAME" value="@string/app_name" /> <variable name="ACCOUNT_LABEL" value="@string/app_name" /> <variable name="ACCOUNT_TYPE" value="$PACKAGE_NAME.account" /> <variable name="CONTENT_AUTHORITY" value="$PACKAGE_NAME" /> <variable name="GOOGLE_PLAY_SERVICES_VERSION" value="11+" /> <variable name="ANDROID_SUPPORT_LIBRARY_VERSION" value="23+" /> </plugin> <plugin name="cordova-plugin-geolocation" spec="2.4.3"> <variable name="GEOLOCATION_USAGE_DESCRIPTION" value="..." /> </plugin>

    package.json

    "@ionic-native/geolocation": "4.9.0", "cordova-plugin-geolocation": "2.4.3", "cordova-plugin-mauron85-background-geolocation": "2.3.5",

    read more
  • S

    Using java datatype java.math.BigDecimal instead of java.lang.Double resolved the issue. After using BigDecimal, HBase is getting populated with actual value instead of scientific notation.

    read more
  • S

    is it possible to display actual decimal value rather than to display in scientific notation in HBase Shell?

    Created HBase table via Phoenix script mentioned below. Populated data ("loginTime":0.000064444) into this HBase table (created via phoenix script) via Spark job in java.

    If the java datatype for this field is Double, then the value is getting displayed in scientific notation as "6.44443E-5" instead of displaying actual value. But if I make the java datatype is String, then actual value is getting displayed. How to get the actual value without scientific notation with java datatype as Double?

    Phoenix Script

    CREATE TABLE IF NOT EXISTS "LOGIN_TABLE"(recordId VARCHAR, "FACTS"."loginTime" VARCHAR, CONSTRAINT "LOGIN_TABLE_PK" Primary Key(recordId)) SALT_BUCKETS = 4

    Java Pojo

    public class TestObject { private Double loginTime; }

    read more
  • S

    As also seem by molbdnilo in a remark, in std::ostream & operator<<(std::ostream & out, Home const &h) you do the iteration

    std::for_each(h.getHabitant().begin(), h.getHabitant().end(), [&out](People const pe){

    that supposes h.getHabitant().begin() and h.getHabitant().end() are iterators on the same vector but

    std::vector<People> getHabitant() const{return this->habitant;}

    returns each time a new copy of the vector.

    If you do not want to modify getHabitant to return a const reference to habitant you have to memorize the vector on which you iterate.

    std::vector<People> v = h.getHabitant(); std::for_each(v.begin(), v.end(), [&out](People const pe){

    but I encourage you to modify getHabitant() to be

    const std::vector<People> & getHabitant() const {return this->habitant;}

    read more
  • S

    In the project to learn C++, I have create a software compose of two easy class (Home and People). People have for constructor :

    // CONSTRUCTOR People(): name("NoName"), first_name("NoFirstName"), age(0){} People(std::string n, std::string fn, int a) : name(n), first_name(fn), age(a){}

    And Home have :

    // CONSTRUCTOR Home(): adresse("NoName"){} Home(std::string addr): adresse(addr){}

    In my software, a home have a vector of people, where we can add resident or remove a resident.

    My error occur when I try to remove a resident in a home or when I try to print a home.

    Here the code of "removeResident" :

    void Home::removeHabitant(People const &p) { this->getHabitant().erase(std::remove(this->getHabitant().begin(), this->getHabitant().end(), p)); }

    Here the code of "operator<<" :

    std::ostream & operator<<(std::ostream & out, Home const &h) { out << h.getAddr() << "\n"; //OK if(h.getHabitant().size() > 0) // OK { try { std::for_each(h.getHabitant().begin(), h.getHabitant().end(), [&out](People const pe){ out << pe << "\n"; }); // ERROR } catch(People p) { std::cout << "Exception à l'element : " << p << std::endl; } } else // OK { out << "Aucun habitant !"; // OK } return out ; // OK }

    Here the output of my software :

    clang++ -Wall -std=c++11 -c -o obj/main.o src/main.cpp -I include clang++ -Wall -std=c++11 -c -o obj/People.o src/People.cpp -I include clang++ -Wall -std=c++11 -c -o obj/Home.o src/Home.cpp -I include clang++ -Wall -std=c++11 -o bin/main obj/main.o obj/People.o obj/Home.o ./bin/main Peoples's destructor ( NoFirstName - NoName - 0 ) 10 rue des Brouettes rouge Peoples's destructor Peoples's destructor ( Erwan - AUBRY - 21 ) Peoples's destructor ( Roger - DURAND - 20 ) Peoples's destructor terminate called after throwing an instance of 'std::logic_error' what(): basic_string::_M_construct null not valid makefile:6: recipe for target 'compile' failed make: *** [compile] Aborted

    Here the main file :

    #include <Home.hpp> #include <vector> #include <algorithm> #include <iterator> using namespace std; int main() { People erwan("AUBRY", "Erwan", 21); People roger("DURAND", "Roger", 20); People noName; // vector<People> lsPeople; // lsPeople.push_back(erwan); // lsPeople.push_back(roger); // copy(lsPeople.begin(), lsPeople.end(), ostream_iterator<People>(cout, "|")); Home home1("10 rue des Brouettes rouge"); home1.addHabitant(erwan); home1.addHabitant(roger); cout << noName << endl; cout << home1 << endl; // cout << home1[0] << endl; // home1.removeHabitant(roger); // cout << home1[0] << endl; return 0; }

    After several research I think it's the cause of the home class, so here is the code of home .hpp :

    #ifndef HOME_INCLUDED #define HOME_INCLUDED #include <People.hpp> #include <vector> class Home { private: std::string adresse; std::vector<People> habitant; public: // CONSTRUCTOR Home(): adresse("NoName"){} Home(std::string addr): adresse(addr){} // DESTRUCTOR ~Home(){std::cout << "Home's destructor" << std::endl;} // GETTER std::string getAddr() const{return this->adresse;} std::vector<People> getHabitant() const{return this->habitant;} // SETTER void setAddr(std::string const val){this->adresse = val;} void addHabitant(People const &p){this->habitant.push_back(p);} void removeHabitant(People const &p); // OPERATOR People & operator[](unsigned int const val){return this->habitant[val];} }; std::ostream & operator<<(std::ostream & out, Home const &h); #endif

    I hope you have any idea for my problems.

    PS: sorry for my english, and sorry if I have make anything of badness I'm new as help seeker in StackOverflow

    read more

推荐阅读