webpack打包多页面,怎么按需分chunk

·  阅读 2789

「12-01更新」:今天同事提示,其实 html-webpack-plugin 已经支持这个事儿了,参考:javascript - How to use Webpack 4 SplitChunksPlugin with HtmlWebpackPlugin for Multiple Page Application? - Stack Overflow

前几天,同事在群里问了这么一个问题:一个 vue-cli 启动的项目,使用多页面打包方式,打包出两个应用a和b。a应用引入vant库ipunt组件,b引入vant库list组件。如果把vant打成一个单独的包,里面会既有input也有list,所以a应用加载了它并不需要的list组件,b应用加载了它不需要的input组件。怎么优化?

其实vue-cli自带的分离逻辑,公共chunk最多只会分离出两部分,即chunk-vendorschunk-common。这可以通过在vue项目下执行vue inspect来查看。

其中 chunk-vendors 表示,将 node_modules 下所有被引用的模块打成一个chunk。可以看出,这样的粒度是很粗的。对一般应用可能足够,对于文章开头提到的需求就不能满足了。所以我们需要对webpack做配置,覆盖vue的默认配置。

vue-cli使用 vue.config.js 可以配置webpack,将配置写在 configureWebpack 字段即可,参考 vue-cli官方文档

我们新创建一个vue-cli多页面应用来作为演示,你可以下载下来看看。github链接见:github.com/neilzhang61…

在我们的示例项目 vant-tree-shaking 中,有两个应用入口 indexsubpage ,其中 index.html 页面用到了 vant 的 Button 组件和 Switch 组件,subpage.html 页面用到了 vant 的 BadgeSwitch

我们希望的是,共用的 Switch 允许被打包进 chunk-vendors ,只有 index 页面用到的 Button 能被打包进 chunk-vant-index 中,只有 subpage 页面用到的 Badge 能被打包进 chunk-vant-subpage 中。

实现这一需求的的核心文件是 vue.config.js ,其内容如下:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/main.js',
      // 模板来源
      template: 'public/index.html',
      // 在 dist/index.html 的输出
      filename: 'index.html', 
      // 当使用 title 选项时,
      // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk。
      chunks: ['chunk-vendors', 'chunk-vant-index', 'index']
    },
    // 当使用只有入口的字符串格式时,
    // 模板会被推导为 `public/subpage.html`
    // 并且如果找不到的话,就回退到 `public/index.html`。
    // 输出文件名会被推导为 `subpage.html`。
    subpage: {
      entry: 'src/main2.js',
      chunks: ['chunk-vendors', 'chunk-vant-subpage', 'subpage']
    }
  },
  productionSourceMap: false,
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin()
    ],
    optimization: {
      splitChunks: {
        cacheGroups: {
          vendors: {
            name: 'chunk-vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            chunks: 'initial'
          },
          vantIndex: {
            name: 'chunk-vant-index',
            // test: /[\\/]node_modules[\\/](vant)[\\/]/,
            test (module) {
              // console.log(module)
              const size = module._chunks.size
              let chunkName = ''
              if (size === 1) {
                chunkName = [...module._chunks.values()][0].name
              }

              let path = module.resource
              if (!path) return false
              path = path.replace(/\\/g, '/')
              const result = path && /node_modules\/vant\n*/.test(path) && size === 1 && chunkName === 'index'
              return result
            },
            minSize: 1,
            priority: -5,
            chunks: 'all',
          },
          vantSubpage: {
            name: 'chunk-vant-subpage',
            // test: /[\\/]node_modules[\\/](vant)[\\/]/,
            test (module) {
              // console.log(module)
              const size = module._chunks.size
              let chunkName = ''
              if (size === 1) {
                chunkName = [...module._chunks.values()][0].name
              }

              let path = module.resource
              if (!path) return false
              path = path.replace(/\\/g, '/')
              const result = path && /node_modules\/vant\n*/.test(path) && size === 1 && chunkName === 'subpage'
              return result
            },
            minSize: 1,
            priority: -5,
            chunks: 'all',
          },
        }
      },
    }
  },
}
复制代码

这个文件中,起作用的就是 vantIndexvantSubpage 这两个对象。解释一下

  • name 字段表示,生成文件时,文件名叫什么。
  • test 字段可以是正则表达式或一个函数。当其为函数时,如果返回true,则会将匹配到的 modele 分到此 chunk。
  • minSize 字段表示一个chunk的最小字节限制。默认为 20000 字节。我们将此改为1,不然chunk太小的话,即使满足test条件,也不会分离出单独的chunk来了。
  • priority 字段表示优先级,当有多个规则都能匹配到的话,优先使用哪个。允许为负数,数字越大的优先级越高。
  • chunks 字段表示从哪些chunks里面抽取代码,有三个可选字符串值 initial、async、all

可以看出,为了达到我们的目的,主要是决定 vantIndexvantSubpage 中的 test 函数什么时候返回 true。

vantSubpage 为例,满足以下条件时,test 函数应该返回 true:

  1. 当前 module 是 vant 包下的模块。即 /node_modules\/vant\n*/.test(path) 为 true,其中的 pathmodule.resource 可以取到。
  2. 当前 module 只被一个页面依赖。即 module._chunks.size === 1,其中 module._chunks 是一个 Set,它存放的是依赖此module 的页面的描述对象。当它的长度为 1 时,就表示只有一个页面依赖此 module
  3. 当前 module 被依赖的页面的名字是 subpage,即 chunkName === 'subpage'

vantIndex 同理。

这样我们执行 yarn buildnpm run build后,就可以以我们希望的方式分离chunk了。

其中的 chunk-vant-index.d01042a3.jschunk-vant-subpage.53f1df41.js 就是我们分离出来的两个页面不能共享的chunks。

我们使用 BundleAnalyzerPlugin 插件也可以看出 chunk-vendors.b4fd1d08.js 中只有vant的公共代码和 Switch 组件的代码,chunk-vant-index.d01042a3.js 中只有 Button 组件的代码,chunk-vant-subpage.53f1df41.js 中只有 Badge。我们的目的已经达到。

虽然在此文章举的例子的场景下,分割出chunk的意义有限,但是了解了webpack分chunks的方式后,有其他需求也能轻松实现了。

参考

  1. vue-cli3中的vue.config.js文件怎么配置,使得chunk-vendors被分离,以给不同的页面引用? · Issue #3018 · vuejs/vue-cli
  2. Webpack 优化分包之 minSize 和 maxSize | 张星海的个人博客-星博客-前端开发
  3. SplitChunksPlugin | webpack
分类:
前端
标签:
分类:
前端
标签: