1、为什么要进行代码拆分?

我们先引入一个应用场景,然后对这个场景进行分析,了解为什么需要拆分代码。
首先,安装第三方库lodash,然后在代码分割.js中引入并编写业务代码。

// 导入第三方库
const _ = require('lodash')

// 业务逻辑代码
console.log(_.join(['a', 'b', 'c']))

接着,配置webpack.config.js进行打包

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  entry: './代码分割.js',
  output: {
    path: path.resolve(__dirname, 'build'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    publicPath: __dirname + '/build/', // 引用的路径或者 CDN 地址
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ],
    
  },
  plugins: [
    new CleanWebpackPlugin() // 会删除上次构建的文件,然后重新构建
  ]
}

打包完后会生成一个名为main.bundle.js文件,我们在index.html中引入,打开浏览器执行。
7.png

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script src="./build/main.bundle.js"></script>
</body>
</html>

浏览器控制台会输出a,b,c
如果我们改动index.js中的业务代码,改为:

// 导入第三方库
const _ = require('lodash')

// 业务逻辑代码
console.log(_.join(['a', 'b', 'c'], '***'))

控制台结果变为:a\*\*\*b\*\*\*c

现在我们抛出一个问题,当我们把引入的第三方库和业务代码放在一起打包,这样会有什么问题?

假设上面第三方库lodash大小为1M,业务代码为1M,假设打包后的main.bundle.js2M

浏览器每次打开index.html时,都需要去加载main.bundle.js,然后执行业务代码。加载2M代码是很耗时的,但是浏览器有缓存机制,第二次加载同一文件时会从缓存中读取,刷新页面时网页加载速度更快。但是事与愿违,我们的业务代码更新很频繁,导致无论是首次加载还是再次加载都会很慢,那如何去解决这个问题呢?

答案很明显,第三方库lodash代码基本上是不会变的,如果我们能够将业务代码和第三方库代码分开加载,那么第三方库的加载就可用到缓存机制,整个页面的加载时间也会缩短。

在多个js文件都引入了同样的库或者代码的场景下也是可以进行拆分,避免重复加载。

webpack4 之前是使用 commonsChunkPlugin 来拆分公共代码,v4 之后被废弃,并使用 splitChunksPlugins

在使用 splitChunksPlugins 之前,首先要知道 splitChunksPluginswebpack 主模块中的一个细分模块,无需 npm 引入,只需要配置即可。

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  entry: './代码分割.js',
  output: {
    path: path.resolve(__dirname, 'build'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    publicPath: __dirname + '/build/', // 引用的路径或者 CDN 地址
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  },
  // 拆分代码配置项
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  plugins: [
    new CleanWebpackPlugin() // 会删除上次构建的文件,然后重新构建
  ]
}

我们使用cnpm run dev在开发模式下打包文件,开发模式下打包不会压缩文件,方便查看
8.png
可以看到代码拆分成了两个文件,打开main.bundle.js文件可以看到里卖弄存放的都是业务代码,没有lodash的代码
9.png
打开vendors~main.js文件可以看到lodash代码都在里面
10.png
这样就完成了代码拆分,拆分完的两个文件都要引入到index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script src="./build/vendors~main.js"></script>
  <script src="./build/main.bundle.js"></script>
</body>
</html>

浏览器执行index.html文件控制台结果:a\*\*\*b\*\*\*c

上面采用拆分代码的模式是all,另外还有async、initial,我们来了解一下

  • async: 只优化动态加载的代码,其他类型的代码正常打包。
  • initial: 针对原始 bundle 代码进行优化。
  • all: 针对所有代码进行优化。
  • function(chunk)自定义拆分函数

详情可以参考本人另一篇文章:
https://segmentfault.com/a/1190000020759399

分割出来的文件名为vendors~main.js,怎么来的,我们来分析一下:

当在splitChunks配置项中没有添加cacheGroups对象中的name属性时,默认会在文件名前面加上vendors字段。现在我们来配置一下name属性更改分割的文件名。

// 拆分代码配置项
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          chunks: 'all',
          name: 'test' // 定义分割文件名
        }
      }
    }
  },

6.png
写到这,有细心的人可能会问cacheGroups对象的作用是干嘛的?

cacheGroups is a plain object with key being the name of chunk and value being some configuration of that chunk. By default, Webpack ships with vendors and default cacheGroups but let’s turn those off by setting their value to false, else it will just confuse you to understand code splitting.

主要概念就是cacheGroups对象中的key是分割块的名称,value是分割块的相关配置。

2、如何拆分代码?

现在有如下代码:

// a,js
import './common'
console.log('A')
export default 'A'

// b.js
import './common'
console.log('B')
export default 'B'

// common.js
console.log('公共模块')
export default 'common'

// index.js
// 异步代码
import(/* webpackChunkName: 'a'*/ './a').then(function(a) {
  console.log(a)
})

import(/* webpackChunkName: 'b'*/ './b').then(function(b) {
  console.log(b)
})

function getComponent() {
  // 使用异步的形式导入 lodash,default: _ 表示用 _ 代指 lodash
  return import('lodash').then(({ default: _ }) => {
    var element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'world'], '-')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})

目录结构为:
7.png
webpack配置:

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  entry: {
    main: './code_split_test/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'build'), // 打包文件的输出目录
    filename: '[name].bundle.js', // 代码打包后的文件名
    publicPath: __dirname + '/build/', // 引用的路径或者 CDN 地址
    chunkFilename: '[name].js' // 代码拆分后的文件名
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      }
    ]
  },
  // 拆分代码配置项
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        lodash: {
          name: 'lodash',
          test: /[\\/]node_modules[\\/]/,
          priority: 10
        },
        common: {
          name: 'common',
          minSize: 0, //表示在压缩前的最小模块大小,默认值是 30kb,如果没设置为0,common模块就不会抽离为公共模块,因为原始大小小于30kb
          minChunks: 2, // 最小公用次数
          priority: 5, // 优先级
          reuseExistingChunk: true // 公共模块必开启
        },
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  },
  plugins: [
    new CleanWebpackPlugin(), // 会删除上次构建的文件,然后重新构建
    new BundleAnalyzerPlugin()
  ]
}

打包结果:
8.png

我们来分析一下文件之间的依赖关系:
10.png
index.js依赖于a.js、b.js、lodash.js,并且是动态加载,a.jsb.js都依赖common.js,是同步加载。

很明显,如果我们采用aysnc模式拆分,分割出的a.jsb.js里面都会存在common.jscommon.js模块不会被提取成公共模块,得不到复用。
因此,我们可以采用allcommon.js模块会被提取成共享模块。
9.png

现在将打包后的主文件main.bundle.js引入到index.html中,奇怪的是主文件的引入并没有使得分割后的文件自动引入
1.png
然后我去查看了一下主文件里面是否含有引入分割文件的脚本代码,发现是有的
2.png

3.png
这就很无语了,但是当我把ES6转译有关配置注释掉再打包执行index.html文件,发现是可行的

  // module: {
  //   rules: [
  //     {
  //       test: /\.js$/,
  //       exclude: /node_modules/,
  //       use: {
  //         loader: 'babel-loader'
  //       }
  //     }
  //   ]
  // },

4.png
这个问题到底是为什么呢?直到现在我也还没有搞懂,希望哪位大佬注意到后能帮我解决一下疑惑。

参考文章:
https://itxiaohao.github.io/passages/webpack4-code-splitting/


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。


引用和评论

0 条评论