Skip to content

Latest commit

 

History

History

webpack-code-splitting

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

对于大型Web项目来说,把所有代码打包成一个JavaScript文件并不明智,因为这会导致生成的bundle.js非常庞大,需要花费更多的时间来加载它,导致用户体验下降。本文将介绍Webpack强大的代码分离(Code Splitting)功能,通过该特性我们可以将一个bundle.js文件拆分为多个chunk文件,实现在运行时按需异步加载相关资源。本文将从chunk的角度讲解Webpack的代码分离特性。

项目目录结构如下所示:

Project
  |--buildOutput
  |--node_modules
  |--.babelrc
  |--package.json
  |--README.md
  |--webpack.config.js
  |--src
     |--a.js
     |--b.js
     |--c.js
     |--d.js
     |--e.js
     |--page1.js
     |--page2.js
     |--page3.js

a.js文件如下所示:

module.exports = "module a";

b.js文件如下所示:

module.exports = "module b";

c.js文件如下所示:

module.exports = "module c";

d.js文件如下所示:

module.exports = "module d";

如上所示,a.jsb.jsc.jsd.js这四个文件都是普通的CommonJS模块。

chunk,英文直译过来是数据块的意思,我们可以把一个chunk看做是一个文件,这个文件里可以包含一个或多个模块(比如a.js、b.js等)。

chunk总的来说可以分为entry chunknormal chunknormal chunk指的就是non-entry chunk

我们首先看一下最简单的entry chunk

1. entry chunk

page1.js中引入了a.jsb.js模块,如下所示:

import a from "./a.js";
import b from "./b.js";

console.log("module a: ", a);
console.log("module b: ", b);

page2.js中引入了c.jsd.js模块,如下所示:

import c from "./c.js";
import d from "./d.js";

console.log("module c: ", c);
console.log("module d: ", d);

1.1 entry为字符串

webpack.config.js中的entry用于设置打包的入口文件,即要将哪些资源进行打包。output.pathoutput.filename分别用于设置打包的输出目录和输出文件。

entry的值为字符串路径时,这个entry属于单一入口(single entry)。

我们将webpack.config.js配置如下所示:

entry: "./src/page1.js",
output: {
    path: path.join(__dirname, "buildOutput"),
    filename: "page1.bundle.js"
}

此处,我们将entry配置成一个stirng值,即一个文件路径。page1.js中引入了a.jsb.js模块,执行npm start进行打包,在buildOutput目录下生成打包文件page1.bundle.js,该文件就是一个入口chunk(entry chunk),即根据entry生成的打包文件。

打开page1.bundle.js文件我们可以看到其中定义了webpackJsonp()__webpack_require__()之类的函数,通过这些方法可以在浏览器中加载相应的模块资源,我们把这些在运行时Webpack加载资源的逻辑代码叫做webpack runtime。就像require.js用于加载AMD模块资源一样,webpack runtime用于加载Webpack打包后的资源,它是在浏览器环境中加载和使用Webpack资源的关键。

所以

page1.bundle.js = webpack runtime + a.js + b.js

我们对filename做点修改,将设置为filename: "[id].[name].bundle.js",此处的[id]表示chunk id,[name]表示chunk name,执行npm start重新进行打包,在buildOutput目录下生成打包文件0.main.js,也就是说我们生成的entry chunk的id为0,chunk name为main。在只有一个entry chunk的情况下,将filename设置为类似于"[id].[name].bundle.js"这样的值意义不大,大家知道其输出文件名的含义即可,我们会后面的multiple entry中讲解"[id].[name].bundle.js"的使用场景。

1.2 entry为字符串数组

entry还可以配置为一个字符串路径数组,这种entry也属于单一入口(single entry)。

修改webpack.config.js如下所示:

entry: ["./src/page1.js", "./src/page2.js"],
output: {
    path: path.join(__dirname, "buildOutput"),
    filename: "page12.bundle.js"
}

我们将entry设置为字符串数组,每个字符串都表示一个路径,Webpack会将每个路径所对应的文件一起打包,生成一个打包文件。执行npm start,在buildOutput目录下生成打包文件page12.bundle.js

page12.bundle.js = webpack runtime + a.js + b.js + c.js + d.js

page12.bundle.js是一个entry chunk,其chunk id为0,chunk name为main,可以设置filename: "[id].[name].bundle.js"进行验证。

1.3 entry为Object对象

entry的值为字符串路径或者是字符串路径数组时,这种entry属于single entry(单一入口)。一般情况下,single entry打包会生成一个输出文件(不考虑Code Splitting)。

entry的值为Object对象时,这种entry叫做multiple entry(多入口)。一般情况下,multiple entry打包会生成多个输出文件。

我们修改webpack.config.js如下所示:

entry: {
    page1: ["./src/page1.js"],
    page2: ["./src/page2.js"]
},
output: {
    path: path.join(__dirname, "buildOutput"),
    filename: "[id].[name].bundle.js
}

执行npm start进行打包,在buildOutput目录下生成打包文件0.page1.bundle.js1.page2.bundle.js

entry的值为Object对象时,该对象中的每个key-value键值对都会创建一个entry chunk,其key值相当于chunk name,value值表示该entry chunk要打包哪些资源文件,value值可以为字符串路径或字符串路径数组。在本例中,key分别为page1page2,所以会产生两个entry chunk。

0.page1.bundle.js = webpack runtime + a.js + b.js

1.page2.bundle.js = webpack runtime + c.js + d.js

2. normal chunk

通过上面的示例,我想大家已经明白了什么是entry chunk,entry chunk包含webpack runtime,下面开始今天的正题Code Splitting。

Webpack允许我们在代码中创建分离点(Code Splitting Point),在分离点处将会产生一个新的normal chunk文件,normal chunk不包含webpack runtime。

我们在e.js中用ES6语法定义了一个Person类,如下所示:

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    setName(name) {
        this.name = name;
    }

    getName() {
        return this.name;
    }

    setAge(age) {
        this.age = age;
    }

    getAge() {
        return this.age;
    }

    toString() {
        return `name: ${this.name}, age: ${this.age}`;
    }
}

export default Person;

page3.js文件如下所示:

import a from "./a.js";
import b from "./b.js";

console.log("module a: ", a);
console.log("module b: ", b);

//创建代码分离点
require.ensure(["./c.js", "./d.js", "./e.js"], function() {
    const c = require("./c.js");
    const d = require("./d.js");
    const Person = require("./e.js");
    const person = new Person("ZhangSan", 28);
    console.log("module c: ", c);
    console.log("module d: ", d);
    console.log("person: ", person.toString());
}, "cde");

我们在page3.js中通过require.ensure([], function(){}, chunkName)创建了一个代码分离点,require.ensure(dependencies, callback, chunkName)方法能够保证dependencies这些依赖可以在callback回调中同步加载require,Webpack会将c.jsd.jse.js一起打包形成一个新的normal chunk文件(这个文件有可能很大),在浏览器中,满足某些条件的情况下,我们的代码会运行到分离点处时,此时Webpack就会异步加载之前打包生成的normal chunk文件,这样就实现了将某些功能从首屏资源文件中拆分出去,在浏览器中根据用户操作按需动态加载资源文件,这样可以加快首屏显示的速度,提升用户体验。

require.ensure(dependencies, callback, chunkName)方法中的dependencies可以保留空数组[],Webpack一样能智能地分析callback回调方法,从中找出callback回调中需要同步加载的资源文件并打包成normal chunck。

require.ensure(dependencies, callback, chunkName)方法最后有一个可选的chunkName参数,通过该参数可以给新生成的normal chunk设置chunk name,给其设置chunk name有两个好处:

  • 可以通过output.chunkFilename配置为[name]设置生成的normal chunk的文件名
  • 具有相同chunk name的多个normal chunk会合并为一个文件

修改webpack.config.js,配置如下所示:

entry: "./src/page3.js",
output: {
    path: path.join(__dirname, "buildOutput"),
    filename: "page3.bundle.js",
    chunkFilename: "[id].[name].js"
}

执行npm start进行打包,在buildOutput目录下生成打包文件page3.bundle.js1.cde.bundle.js

page3.bundle.js包含了webpack runtime和代码分离点处callback回调内部的代码逻辑,但是不包含c.jsd.jse.js1.cde.bundle.js中包含了c.jsd.jse.js的代码,不包含webpack runtime

page3.bundle.js = webpack runtime + 异步callback逻辑

1.cde.bundle.js = `c.js` + `d.js` + `e.js`

page3.bundle.js部分代码截图如下所示:

page3.js创建代码分离点的时,如果我们不传入chunkName参数,那么最终通过chunkFilename: "[id].[name].js"生成的normal chunk的文件名为1.1.js,可读性较差。

从上面的例子中我们可以看到通过require.ensure()方法可以异步加载CommonJS和ES6模块资源文件。其实通过require(dependencies, callback)也可以异步加载AMD模块,例如:

require(["module-a", "module-b"], function(a, b) {
  // ...
});

这种写法跟require.js中异步按需动态加载AMD模块的方式很类似,在此不再赘述。

3. 总结

  1. chunk分为entry chunk和normal chunk。

  2. entry chunk是入口文件,根据entry的数量,可以分为single entry和multiple entry。

    • 一般情况下,entry chunk = webpack runtime + modules。

    • 在不考虑Code Splitting的情况下,single entry只会生成一个chunk,即一个entry chunk,可通过output.fileName指定输出的文件名。

    • multiple entry会产生多个entry chunk,需要通过ouput.fileName指定各个entry chunk的文件名,而且一般会使用[id][name]等设置ouput.fileName的值,这样使得不同的entry chunk具有不同的文件名。

    • 如果使用Code Splitting创建了代码分离点,那么需要通过output.chunkFilename设置新生成的normal chunk文件名。

  3. normal chunk一般是被entry chunk在运行时动态加载的文件。

    • 一般情况下,normal chunk不包含webpack runtime,只包含一些modules代码。

    • 通过代码require.ensure([], function(...){})require([amd1, amd2], function(amd1, amd2){})可以设置代码的分离点(Code Splitting Point),Webpack会将其创建一个新的normal chunk。

    • 生成的normal chunk的文件名可以通过output.chunkFilename设定,在代码分离点处我们可以传入一个chunk name以便在output.chunkFilename中使用[name]作为输出的文件名。