关于webpack依赖动态懒加载

前言

起因是因为一个基于vue-cli3.0的项目突然反馈vendor包过大,为了减少用户的白屏时间开始做优化。


webpack4的splitChunk插件

用过vue-cli3.0的同学应该熟悉,其舍弃了以前常用的build文件夹下的webpack.config.js文件配置,配置内容全部放到vue.config.js文件中,实际上关于webpack的配置其实和之前大同小异。打包拆分不得不聊到常用的CommonsChunkPlugin

旧项目常用的方式就是通过webpack.optimize.CommonsChunkPlugin(opts),加载该插件进行代码分割。但是其存在很多问题:

  • 它可能导致下载更多的超过我们使用的代码
  • 它在异步chunks中是低效的
  • 配置繁琐,很难使用
  • 难以被理解

在webpack4抛弃了CommonsChunkPlugin,换成了更先进的SplitChunksPlugin。它们的区别就在于,CommonChunksPlugin 会找到多数模块中都共有的东西,并且把它提取出来(common.js),也就意味着如果你加载了 common.js,那么里面可能会存在一些当前模块不需要的东西。

SplitChunksPlugin 采用了完全不同的 heuristics 方法,它会根据模块之间的依赖关系,自动打包出很多很多(而不是单个)通用模块,可以保证加载进来的代码一定是会被依赖到的。

下面是一个简单的例子,假设我们有 4 个 chunk,分别依赖了以下模块:

关于webpack依赖动态懒加载_第1张图片

根据 CommonChunksPlugin的默认配置,会打包成:
关于webpack依赖动态懒加载_第2张图片

SplitChunksPlugin会打包成:
关于webpack依赖动态懒加载_第3张图片

显然进一步优化了空间。

当然这不是本次讨论的重点,因为vue-cli3.0默认情况下已经是使用了SplitChunksPlugin的配置,查看vue-cli service config文件夹下的app.js,有一段链式的webpackConfig配置了最终打包的chunks配置。

if (isProd && !process.env.CYPRESS_ENV) {
  webpackConfig
    .optimization.splitChunks({
      cacheGroups: {
        vendors: {
          name: `chunk-vendors`,
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: `chunk-common`,
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    })
}

通常该默认情况可以满足大部分应用场景,但是考虑我们项目的特殊性,我需要额外提高chunk-vendors的minChunks项,让一些偶尔出现但是频率没有太高的依赖滚出vendors。


动态懒加载

先来聊聊import和require的区别。
require/exports 出生在野生规范当中,什么叫做野生规范?即这些规范是 JavaScript 社区中的开发者自己草拟的规则,得到了大家的承认或者广泛的应用。比如 CommonJS、AMD、CMD 等等。
import/export 则是名门正派。TC39 制定的新的 ECMAScript 版本,即 ES6(ES2015)中包含进来。

const PAGE_A = require.ensure([], () => {require("a")}。早期写vue-router,习惯以这种形式去完成异步加载。后续日常开发中,常用的就是 import from 来引入资源(千万避免全局引入ui组件,可能会导致资源包异常的大)webpack官方就指出,应该用import来代替require.ensure

// import 官方案例
// 局部引入
function determineDate() {
  import('moment').then(function(moment) {
    console.log(moment().format());
  }).catch(function(err) {
    console.log('Failed to load moment', err);
  });
}
// 导入整个模块
import('./component').then(Component => /* ... */);
// 使用await
async function determineDate() {
  const moment = await import('moment');
  return moment().format('LLLL');
}
determineDate().then(str => console.log(str));

相比较而言,import使用了promise的封装,只接受一个参数,就是引用包的地址,语法十分简单。

由于webpack需要将所有import()的模块都进行单独打包,所以在工程打包阶段,webpack会进行依赖收集。webpack会找到所有import()的调用,将传入的参数处理成一个正则,如:

import('./app'+path+'/util') => /^\.\/app.*\/util$/

也就是说,import参数中的所有变量,都会被替换为【.*】,而webpack就根据这个正则,查找所有符合条件的包,将其作为package进行打包。
所以import的正确姿势,应该是尽可能静态化表达包所处的路径,最小化变量控制的区域。
如我们要引用一堆页面组件,可以使用import('./pages/'+ComponentName),这样就可以实现引用的封装,同时也避免打包多余的内容。但是webpack会保证该路径下所有可能引入的文件是可用的,即会预请求。

官方指出,在import内部添加注释,可以完成chunkname命名、打包模式等功能。4.6+还支持Prefetching/Preloading来提前加载/预加载资源。(prefetch用于未来会发生的场合,preload用于当前场合)

// Single target
import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  'module'
);
// Multiple possible targets
import(
  /* webpackInclude: /\.json$/ */
  /* webpackExclude: /\.noimport\.json$/ */
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  `./locale/${language}`
);

的确是可以完美取代require了


分析结果

借助webpack-bundle-analyzer,可以清晰的查看,打包后之后项目的文件大小以及其构成。对于做性能优化有很大的帮助。具体使用方法不再详述,建议直接移步官方文档。


心得

其实大部分是关于webpack的使用方式。老的require.ensure也好,新的import也好,其实本质还是交给webpack去打包处理,在最后选择如何去引入。
重要的是webpack的配置,即便用了vue-cli3.0依然要考虑自定义配置如何去完成,再细化一点就是import的引入方式。


参考

一步一步的了解webpack4的splitChunk插件
require和import的区别
webpack import() 动态加载模块踩坑
webpack-bundle-analyzer

你可能感兴趣的:(关于webpack依赖动态懒加载)