webpack 代码分片,你都清楚了吗?

1,498 阅读6分钟

为什么要配置代码分片

如果你阅读过我 webpack 专栏中关于单页应用的配置,或者自己配置过单页应用,应该知道,对于单页应用来说,如果不进行配置,它最终会将项目中的 js 打包成一个 js 文件。

这样做会产生什么问题呢?

我们要知道,如果我们采用 react 去写项目,那么我们的项目必然依赖 react 相关文件。在打包的时候,会将 react 相关文件打包进去。

这些文件与我们自己写的 js 结合在一起的时候,打包结果的单个 js 文件就会非常大,影响加载速度,造成首屏空白。

我们还知道,react 相关文件是几乎不会变动的。而不会变动的 js ,我们如果能够将它缓存在浏览器中,就能够减小文件的下载体积,提高页面加载速度。

基于此,我们可不可以将一些公用的、不经常变动的 js 文件,和 涉及项目业务的、经常变动的 js 文件,分开打包呢?

更进一步,我们能不能让单个页面,只加载 该页面相关的 js 文件呢?

提取公共文件

多页应用

本多页应用代码可从 单页应用转多页,你猜 webpack+react 怎么配 - 掘金 (juejin.cn) 中获取。

我们运行 npm run build 对代码进行打包,得到如下结果:

Snipaste_2022-05-05_15-19-18.png

从打包js的体积来看,应该是每个 js 文件都把 react 库相关代码打包进去了。

我们先把 react 库相关的代码分离出来,变成公用的 vendors.js。

这是生产环境相关配置,我们在 webpack.common.js 中修改如下

const packagejson = require("./package.json");
//...
module.exports = {
	entry:{...entry,vendors:Object.keys(packagejson.dependencies)},
    // 我们将 package.json 中的依赖库都指定为公共文件
    //...
}

此处将依赖文件都指定为公共文件 vendors , 然后需要在入口文件需要指定依赖对象。

注意: vendors 也可以自己指定文件,内容为数组形式。

我们修改 src/config 中 entry 及 plugin 如下:

//...
entry[pageName] = {
            import:entryFile,
            dependOn:'vendors'
        }
        
htmlWebpackPlugins.push(
            new HtmlWebpackPlugin({
                template: `./src/page/${pageName}/index.html`,
                filename:`${pageName === 'home'?'index':pageName}.html`,
                chunks:[pageName,'vendors'],// 添加 vendors 引入
                scriptLoading: 'blocking',
            }),
        )
//...

此前,entry 是 { 名字:路径} 键值对,我们修改为

entry:{
	[name]:{// 入口文件的名字
        import:path,// 引入路径
        dependOn:'vendors'
	}
}

运行 npm run build 打包,我们得到如下文件:

Snipaste_2022-05-05_16-34-28.png

可以看到我们已经成功地将公共文件都提取出来了。

打开 html 文件,可以发现 html 文件中正常引入公共 js 及界面对应 js。

Snipaste_2022-05-05_16-36-54.png

单页应用

单页应用的配置与多页应用相似,添加 vendors ,修改对应的入口文件配置、HtmlWebpackPlugin 即可。

不足

它的不足显而易见,它只能打包出一个公共文件。

如果我们需要打包多个公共文件、并且希望分开打包呢?

打包的内容需要手动指定。

能不能自动化的通过模块的引入来判定公共内容呢?

适配更加复杂的场景

自动打包公共文件

optimization.SplitChunks(简称SplitChunks)是Webpack 4为了改进CommonsChunk-Plugin而重新设计和实现的代码分片特性。它不仅比CommonsChunkPlugin功能更加强大,还更简单易用。

复原我们的代码,在 webpack.pro.js 写入如下代码:

module.exports = merge(common, { 
    optimization: {
        splitChunks:{
            chunks:'all'
        },// 新增内容
        minimize: true,
        minimizer: [
            // 添加 css 压缩配置
            new OptimizeCssAssetsPlugin({}),
            new TerserPlugin()
        ]
    },
});

运行 npm run build,得到如下打包结果:

Snipaste_2022-05-05_17-48-12.png 我们可以看到,简单的配置后,我们指定的是 ’all', 它自动检测出了所有 chunk 中的公共内容,并将它打包到了公共文件中,然后在 html 文件中自动引入了,不需要我们额外配置,操作起来非常方便。它打包的模块也在对应的 txt 文档中有说明。

它是以什么作为依据来检测出公共内容的呢?

以下内容来自官方文档 v4.webpack.docschina.org/plugins/spl…

chunks 可以配置三种形式,all,async,initial

默认配置是是按需加载,即 async , 它会把异步加载(按需加载)的资源的公共文件提取出来,入口文件的公共文件不会动。

initial 会把入口文件的公共文件提取出来。

all 则是会将入口文件和异步加载资源的公共文件都提取出来。

它打包公共文件遵循以下条件:

  • 可以被多次引用的模块或者说来自于 node_modules 文件下的模块。
  • 打包出来的公共文件大小得大于 30kb。
  • 按需加载的时候,既通过 srcipt 引入的 js 文件,不能超过 5 个。
  • 首次加载的时候,通过 script 引入的 js 文件,不能超过 3 个。

默认配置如下:

splitChunks: {
      chunks: 'async',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }

其中,cacheGroups 是公共文件的分离规则。

比如,默认配置中,vendors 开头的 js 文件会提取出素有 node_modules 中符合的条件,default 则用于多次被引用的模块。当然,如果我们不修改默认配置,default 的形成条件是它的大小必须大于 30 kb。

打包多个公共文件

我们如果想实验多个公共文件打包,修改配置如下:

splitChunks:{
    chunks:'all',
        minSize:1
},

这样,只要公共文件超过了 1kb,而且被引用了两次以上,就会形成新的公共文件。

我们分别在 firstpage 和 secondpage 中引入 mybutton 组件。

示例修改 firstPage/index.jsx 内容如下:

import React from 'react';
import Style from './index.less';
import MyButton from '../../component/MyButton';

export default class Index extends React.Component{
    render(){
        return <div className={Style.first_page}>FIRST-PAGE-Change
                    <div>
                        <MyButton />
                    </div>
                </div>
    }
}

打包得到如下文件:

Snipaste_2022-05-06_14-08-33.png 得到打包公共文件如下,这样我们就成功地打包了多个公共文件。

单页应用按需加载资源

异步加载资源是指,我们可以不用一次性加载全部 js,而是等到我们需要用的时候才进行加载。

我们可以使用 import 进行异步加载,webpack 支持这种写法。

我们可以写出如下代码:(代码参照书籍: Webpack实战:入门、进阶与调优)

if(condition){
    import('./a.js').then(a=>{
        console.log(a)
    })
}else{
    import('./b.js').then(b=>{
        console.log(b)
    })
}

在做单页应用时,我们可以通过这种方式,通过判断路由的变更去加载对应页面需要的组件,这样能够尽可能的减少每个界面加载的文件。

接下来,我们使用单页应用实践一下。接下来使用的代码来自 命中注定拯救前端的,应该是 webpack(三) - 掘金 (juejin.cn) 文中已经做了简单配置的单页应用。

同样的,我们在 webpack.pro.js 中配置如下:

optimization: {
        splitChunks:{
            chunks:'initial',
            minSize:1
        },
        //...
    },

配置 react 单页应用的按需加载,我们可以引入外部库来实现。

npm i react-loadable -D

修改 src/app.jsx 代码如下:

import React from 'react';
import { BrowserRouter,Route,Routes,Link,} from 'react-router-dom';
import Loadable from 'react-loadable';
import Home from './page/home';

const Loading = () => <div>Loading...</div>;

const FirstPage = Loadable({
  loader: () => import('./page/firstPage'),
  loading: Loading,
})
const SecondPage = Loadable({
    loader: () => import('./page/secondPage'),
    loading: Loading,
  })

export default class App extends React.Component{

    // 

    render(){
        return <BrowserRouter>
            <h1>APP Page</h1>
            <div><Link to='/home'>to Home</Link></div>
            <div><Link to='/firstPage'>to firstPage</Link></div>
            <div><Link to='/secondPage'>to secondPage</Link></div>
            <div>
                <h1>Page-Content</h1>
                <Routes>
                    <Route exact path="/" element={<Home />} />
                    <Route exact path="/home" element={<Home />} />
                    <Route exact path="/firstPage" element={<FirstPage />} /> 
                    <Route exact path="/secondPage" element={<SecondPage/>} />
                </Routes>
            </div>
        </BrowserRouter>
    }
}

使用 npm run start 运行,我们打开调试器,发现,只有我们点击对应的链接、顶部路由改变时,才会加载界面对应的 js。

我们运行 npm run build,得到如下内容。可以看到,此时,已经分的非常细了。

Snipaste_2022-05-07_11-58-02.png

结语

到底,代码分片的内容就基本结束了,如果对你们有帮助,记得给我点个赞~