Webpack代码拆分实战:按需加载、CSS提取与框架分离

作者:测试鸭时间:2026-10-22 06:09:55浏览:477

本文深入解析Webpack代码拆分的三种核心策略:利用require.ensure实现按需加载,通过extract-text-webpack-plugin提取CSS以优化并行加载,以及使用CommonsChunkPlugin分离业务代码与框架代码。文章详细阐述了配置方法、潜在问题及缓存优化方案,帮助开发者提升Web应用加载性能。

Webpack代码拆分概述

在近期学习VUE的过程中接触到了webpack,特此记录一篇关于webpack代码拆分的笔记。

将项目所有代码打包成一个文件通常并非最佳实践,因为某些代码更适合独立管理。例如:

  1. 样式表,期望通过link标签引入
  2. 低频使用的模块,期望在需要时异步加载
  3. 框架代码,期望利用浏览器缓存那些不易变动的部分

以下是阅读webpack官方文档时整理的笔记,部分表述经过简化处理,旨在使其更易于理解。

按需加载拆分

按需加载拆分

webpack能够将代码划分为不同的逻辑块,并在需要时进行加载。

使用require.ensure() 来拆分代码

require.ensure() 是一种基于CommonJS规范的异步加载模块策略。在代码中通过 require.ensure([<fileurl>]) 引用模块,具体用法如下:

代码如下:

require.ensure(dependencies: String[], callback: function(require), chunkName: String)

第一个参数指定依赖的模块,第二个参数是一个函数,在该函数内部可以使用require加载其他模块。webpack会收集ensure中的依赖,将其打包成单独的文件,并在后续使用时通过jsonp异步加载。


require.ensure(['./a'], function(require){
  let b = require('./b');
  let a = require('./a');
  console.log(a+b)
});

上述代码中,a和b会被打包在一起。当代码执行到该部分时,会异步加载,加载完成后执行函数内的逻辑。


let a = require('./a');
require.ensure(['./a'], function(require){
  let b = require('./b');
  console.log(a+b)
});

如果按以下方式编写,a不会与b打包在一起,因为a已经被打包在主代码中。


require.ensure(['./c'], function(require){
  let a = require('./a');
  console.log(a)
});

require.ensure(['./c'], function(require){
  let b = require('./b');
  console.log(b)
});

在上述代码中,两个模块都依赖了c模块。此时会生成两个模块,其中都包含c模块。因为在实际应用中,上述两个require.ensure 的执行顺序和是否执行是不确定的,因此需要将c模块分别打包进两个模块中。

require.ensure 还可以传入第三个参数,用于指定打包的包名。针对上述情况,c模块被打包进了两个包中。如果事先明确这两个包都会被使用,可以将它们合并为一个,从而避免c模块被打包两次的问题。因此,可以将chunkName指定为相同的名字。


require.ensure(['./c'], function(require){
  let a = require('./a');
  console.log(a)
}, 'd');

require.ensure(['./c'], function(require){
  let b = require('./b');
  console.log(b)
}, 'd');

这样,上述两个require.ensure 拆出来的包就合并为同一个了。

CSS拆分

CSS拆分

开发者可能希望将工程中所有引入的CSS拆分为单个文件,这样可以利用缓存,并利用CSS和JavaScript并行加载来加速Web应用。

使用css-loader

为了加载CSS,这里需要用到css-loader,配置方法如下:


module: {
  loaders: [{
    test: /\.css$/,
    exclude: /node_modules/,
    loader: 'css-loader'
  }]
}

这样在代码中就可以写如下代码:


let css = require('./main.css');
console.log('' + css);

通过require一个CSS得到其内容,当然这里require('./main.css')实际得到的是一个对象,需要调用其toString方法将其转换为字符串。在代码中引用一段CSS,这通常不是我们想要的。为此可以使用style-loader,在代码执行时,会将这些CSS插入到style标签中,只是这里CSS还是存在于JS中的,是后来动态插入到页面中的:


module: {
  loaders: [{
    test: /\.css$/,
    exclude: /node_modules/,
    loader: 'style-loader!css-loader'
  }]
}

更多时候,是希望将CSS拆分为单个文件,然后使用link标签嵌入到HTML中,CSS和JavaScript可以并行加载,CSS还可以被缓存下来。

使用 extract-text-webpack-plugin 来拆分CSS

为了使用这个插件,首先需要通过npm来安装它:


npm i --save-dev extract-text-webpack-plugin

然后在webpack的配置文件中使用该插件:


var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = function () {
  return {
    entry: './index.js',
    output: {
      path: './build',
      filename: 'bundle.js'
    },
    module: {
      loaders: [{
        test: /\.css$/,
        exclude: /node_modules/,
        // 在 loader 中使用该插件
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
      }]
    },
    plugins: [
      // 将其添加在插件中
      new ExtractTextPlugin({ filename: 'bundle.css', disable: false, allChunks: true })
    ]
  }
}

需要注意的是,对于webpack1和webpack2这个插件的配置方法是不同的,差别比较细微,详情请看官方文档extract text plugin for webpack 2

拆分业务代码与框架代码

拆分业务代码与框架代码

通常一个Web应用都会引用若干第三方库,这些第三方库通常比较稳定不会经常变动。但是如果将业务代码和框架代码打包在了一起,这样业务代码每次变动打包得到的结果都会变动,即使只改变了一个字符,浏览器也无法利用缓存,必须全部重新加载。因此,何不将第三方库单独打包在一起呢?

这里举个案例,一个React项目中使用了react和 react-dom 这两个包,我希望将他们打包在一起,将业务代码打包在一起。

下面一步一步来:

1. 安装react和react-dom:


npm i react react-dom --save

2. 配置entry,output和loader

先使用单入口,让代码工作起来。另外因为使用了React所以要使用babel-loader 来加载JS


// webpack.config.js

module.exports = {
  entry: 'index.js',
  output: {
    path: 'build/',
    filname: '[name]@[chunkhash].js'
  },
  module:{
    loaders:[{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel'
    }]
  }
}

3. 编写业务代码

index.js:


import React from 'react';
import ReactDOM from 'react-dom';


var Hello = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

ReactDOM.render(<Hello name={'world'} />, document.getElementById('app'));

index.html:


<div id="app"></div>
<!--entry 为一个字符串,这个 chunk 的名字会是 main, 因此这里引入 main.js -->
<script src="build/main.js"></script>

启动webpack-dev-server,打开浏览器这个时候应该能在页面上看到hello world,这说明工作正常。

4. 拆分框架代码

为了拆分框架代码,我们需要增加一个入口,在这个入口中要包含 react 和 react-dom


module.exports = {
  entry: {
    main: 'index.js',
    vendor: ['react', 'react-dom']
  }
  //...
}

单单像上面这样配置,打包后会得到main.js和vendor.js,但会发现在main.js中依然包含了React和React-DOM的代码,这是因为指定了入口后,webpack就会从入口文件开始将整个依赖打包进来,index.js中引用了React和React-DOM自然会被打包进去。要想达到之前所说的那个效果,还需要借助一个插件——CommonsChunkPlugin

5. 使用CommonsChunkPlugin

这个插件的功能是将多个打包结果中公共的部分抽取出来,作为一个单独的文件。它符合目前的场景,因为 main.js 和vendor.js 中存在一份公共的代码,那就是vendor.js 中的内容。(这个说法并不准确,这里只是指React和React-DOM都被打包进了这两个文件)


let webpack = require('webpack');

module.exports = {
  entry: {
    main: 'index.js',
    vendor: ['react', 'react-dom']
  },
  //...

  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor' // 指定一个希望作为公共包的入口
    })
  ]
}

再进行打包,这个时候会发现 main.js 中的代码不再包含React的代码了。看似实现了我们的需求,但真实应用下并没有这么简单,在实际项目中JS脚本通常都会给添加一个MD5的hash在后面,形如 app@709d9850745a4c8ba1d4.js这样每次打包后,如果文件内容变了,后面的hash也会变动。就以上场景,会发现当我们修改了业务代码后,得到的hash是不同的,因此每次都会得到两个不同的打包结果。业务代码改变了,拆分出来的框架包也变了,这显然不符合初衷——利用浏览器缓存。

这是因为webpack在打包的时候会产生一些运行时代码,比如__webpack_require__ 、webpackJsonp 等等,这些函数是用来帮助webpack完成模块加载等功能的,业务代码的改变会导致业务代码打包后的hash值改变,而在webpack的运行时代码中实际上是保存了打包后的结果的文件名的,因为它在异步加载模块的时候需要用到。因此,下面需要做的是将webpack的运行时代码拆分出来。

修改plugins如下,将name修改为names,并增加一个init的包名,执行打包,会发现webpack的运行时代码都被入该包内。


plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    names: ['vendor', 'init']
  })
]

这样一来,修改了业务代码后,vendor因为只引用了React和React-DOM,因此业务代码的改变不会改变vendor这个包的内容,hash也保持不变。但,也仅仅如此如果你引用了其他模块,webpack收集依赖的时候会给每个模块编一个号,引入其他模块会导致模块数改变,也就会导致编号改变,这个时候打包出来的vendor还是会改变。

那么到底该如何解决这个问题呢?在官方文档上没有找到解决方案。后面我会继续探索这一问题,找到解决方案后会及时更新到这里,如果你有解决方案,还请不吝赐教,谢谢。

希望本文所述对你有所帮助,webpack代码拆分内容就给大家介绍到这里了。希望大家继续关注我们的网站!

作者最新文章