webpack中分割代码的三种方式


官网教程:code-splitting

最近在开发一个新的功能,需要在某一条件下引入新包,而之前的场景则完全不需要。这种情况下,动态加载+代码分割正可以派上用场。在动手之前,先看看官网是怎么说的吧。

entry - 配置多入口、多页面

在entry对象中写入多个入口文件即可。

weboack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

这样做的弊端主要由两个。第一,如果有公共的代码块,那么打包之后的两个Bundle里都会包含重复的模块。第二,不够灵活,不能被webpack通过splitChunks的规则灵活地优化。

因此,在entry里直接写多个入口的方式,适用于本身就是多入口的项目,而非一般的spa。可以通过代码结构,用函数构造出合理的entry和相应的htmlwebpackplugin,来进行打包。

splitChunks - 智能抽取公共代码

webpack还给出了智能分割代码的功能:splitChunks,可以用给定的策略去抽取公共代码,避免重复。

webpack.config.js

 optimization: {
    minimizer: [
      new UglifyJsPlugin({
        exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
        cache: true,
        parallel: true,
        sourceMap: true,
        extractComments: false,
        uglifyOptions: {
          output: {
            // 移除注释
            comments: false,
          },
        },
      }),
      new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.css$/g,
        cssProcessorOptions: {
          parser: safeParser,
          autoprefixer: {
            disable: true,
          },
          discardComments: {
            removeAll: true, // 移除注释
          },
        },
        canPrint: true,
      }),
    ],
    splitChunks: {
      minChunks: 3,
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor',
          chunks: 'all',
        },
      },
    },
    // 可取'single', 'multiple', default 为false
    // 此处等于 runtime的chunkname 即为'runtime'
    runtimeChunk: 'single',
  }

具体配置写在[optimization.splitChunks ](https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks)中。此外还可以通过mini-css-extract-pluginbundle-loaderpromise-loader等别的社区贡献的工具进行代码分割。

dynamic-import - 按需动态加载

语法分为两种,一种是import(),另一种是webpack原来的老语法require.ensure。注意使用import时,要配置对应的Babel插件。

main.js

async function lala() {
  const { default: socketcluster } = await import(
  /* webpackChunkName: "socket" */
    /* webpackMode: "lazy" */
    'socketcluster-client')
  console.log(typeof socketcluster, socketcluster)
}

lala()

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ],
  "plugins": [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-syntax-dynamic-import"
  ],
  "env": {
    "development": {
      "presets": [
        "@babel/preset-env"
      ]
    }
  }
}

webpack.config.js

  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/',
    chunkFilename: '[name].chunk.js',
  },

注意main.js里的注释,这是webpack的魔法注释,可以让chunkFileName里的name获得注释里写入的值。不过据说webpack5会采用更棒的方式来决定chunkFileName。

魔法注释还包括一些其他的选项,比如:

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

webpackPrefetchwebpackPreload: prefetch是未来导航可能需要的资源,preload是当前导航也许需要的资源。当然,这需要浏览器支持,其实就是在浏览器的标签里加入了rel="prefetch"或rel=“preload”

  • preload的资源是和父模块平行加载的,而prefetch的资源则在父模块加载完毕后开始加载。
  • preload的资源优先级中等,会立即下载。prefetch的资源会等到浏览器空闲时再下载。
  • preload的资源是父模块立即需要的,prefetch的资源则是未来任意时间需要用到的。

下载和需求优先级上,preload > prefetch。

tips - 踩坑小提示

  1. webpackChunkName 魔法注释为什么会失效?

检查你的.babelrcwebpack.config.js,有没有移除注释的配置。魔法注释,当然注释得存在才能生效。所以.babelrc里不能有comments: false,webpack的uglifyjs等插件中也不能设置comments: falseextractComments

  1. @babel/plugin-syntax-dynamic-import为什么失效?

你可能同时使用了@babel/plugin-syntax-dynamic-importdynamic-import-node

@babel/plugin-syntax-dynamic-importdynamic-import-node 在一定程度上是彼此冲突的。后者主要是给Node使用的,把import语法转译为一个被延迟的require()二者的区别已经被讨论过。简单来说,dynamic-import-node,包括它的babel-7版本,都是社区贡献的插件。而@babel/plugin-syntax-dynamic-import是babel官方出品的(看前缀那个@就知道啦),只是为了让babel能够解析动态的import(),需要配合别的打包工具如webpack,rollup或者原生实现一起使用。

避免过度优化!结局

经过打包和压缩(uglify/terser)之后,动态引入部分的代码大小为40.2KB,app主体代码为896 KiB。如果说对于移动端,40KB的代码还值得分离的话,那么再经过gzip压缩后,app主体代码为242KB,分离出的代码大小只有11KB左右,比起多发一个网络请求,倒不如索性打包到一起了。

于是乎,虽然试验了一下code-splitting,但是一番衡量后,最后还是回到了原点。不过这个过程还是蛮有意思的(勉强挽尊吧,哎)。

注意,gzip压缩并不是在webpack打包这一步做的。实际上,webpack虽然提供了这个选项,比如利用compression-webpack-plugin,但多数情况下都不会选择使用。这是因为很多流行的静态服务器主机如Surge/Netlify等都已经做了自动gzip静态文件的事情。本项目中,服务端采用了express,利用了[compress](https://github.com/expressjs/compression)来进行gzip压缩,减小response body的大小。所以webpack直接打包出来的文件会比实际上线后,浏览器network中显示的文件size要大的多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章