Webpack实战:入门、进阶与调优(第2版)
上QQ阅读APP看书,第一时间看更新

3.2 配置资源入口

Webpack通过context和entry这两个配置项来共同决定入口文件的路径。在配置入口时,我们实际上做了两件事。

·确定入口模块位置,告诉Webpack从哪里开始进行打包。

·定义chunk name。如果工程只有一个入口,那么默认其chunk name为main;如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的唯一标识。

3.2.1 context

context可以理解为资源入口的路径前缀,在配置时要求必须使用绝对路径的形式。请看下面两个例子:

// 以下两种配置达到的效果相同,入口都为 <工程根路径>/src/scripts/index.js
module.exports = {
    context: path.join(__dirname, './src'),
    entry: './scripts/index.js',
};
module.exports = {
    context: path.join(__dirname, './src/scripts'),
    entry: './index.js',
};

配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。context可以省略,默认值为当前工程的根目录。

3.2.2 entry

与context只能是字符串不同,entry的配置可以有多种形式:字符串、数组、对象、函数。我们可以根据不同的需求场景来选择entry的配置类型。

1. 字符串类型入口

直接传入文件路径:

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
    },
    mode: 'development',
};

2. 数组类型入口

传入一个数组的作用是将多个资源预先合并,这样Webpack在打包时会将数组中的最后一个元素作为实际的入口路径。如:

module.exports = {
    entry: ['babel-polyfill', './src/index.js'] ,
};

上面的配置等同于:

// webpack.config.js
module.exports = {
    entry: './src/index.js',
};
   
// index.js
import 'babel-polyfill';

3. 对象类型入口

如果想要定义多入口,则必须使用对象的形式。对象的属性名(key)是chunk name,属性值(value)是入口路径。如:

module.exports = {
    entry: {
        // chunk name为index,入口路径为./src/index.js
        index: './src/index.js',
        // chunk name为lib,入口路径为./src/lib.js
        lib: './src/lib.js',
    },
};

entry对象的属性值也可以为字符串或数组。如:

module.exports = {
    entry: {
        index: ['babel-polyfill', './src/index.js'],
        lib: './src/lib.js',
    },
};

在使用字符串或数组定义单入口时,我们没有办法更改chunk name,只能用默认的main。在使用对象来定义多入口时,则必须为每一个入口定义chunk name。

4. 函数类型入口

我们也可以使用匿名函数来定义入口。其实非常简单,只要使匿名函数的返回值为上面介绍的任何配置形式即可,如:

// 返回一个字符串型的入口
module.exports = {
    entry: () => './src/index.js',
};
   
// 返回一个对象型的入口
module.exports = {
    entry: () => ({
        index: ['babel-polyfill', './src/index.js'],
        lib: './src/lib.js',
    }),
};

使用函数的优点在于我们可以添加一些代码逻辑来动态地定义入口值。另外,函数也支持返回一个Promise对象来进行异步操作。

module.exports = {
    entry: () => new Promise((resolve) => {
        // 模拟异步操作
        setTimeout(() => {
            resolve('./src/index.js');
        }, 1000);
    }),
};

3.2.3 实例

下面我们看3个在不同场景下配置资源入口的例子。

1. 单页应用

对于单页应用(SPA),一般情况下定义单一入口即可。

module.exports = {
    entry: './src/app.js',
};

无论是框架、库,还是各个页面的模块,都由app.js单一的入口进行引用。这样做的好处是只会产生一个JS文件,依赖关系清晰。但这种做法也有弊端,所有模块都打包到一起,当应用的规模上升到一定程度之后会导致产生的资源体积过大,降低用户的页面渲染速度。

在Webpack默认配置中,当一个bundle大于250KB时(压缩前),Webpack会发出警告,如图3-4所示。

image-image009

图3-4 大于250KB的资源会有[big]提示

2. 提取vendor

试想一下,假如工程只生成一个JS文件并且它的体积很大,一旦产生代码更新,即便只有一点点改动,用户都要重新下载整个资源文件,这对于页面的性能是非常不友好的。

为了解决这个问题,我们可以使用提取vendor的方法。vendor的字面意思是“供应商”,在Webpack中则一般指工程所使用的库、框架等第三方模块集中打包而产生的bundle。请看下面这个例子:

module.exports = {
    context: path.join(__dirname, './src'),
    entry: {
        app: './src/app.js',
        vendor: ['react', 'react-dom', 'react-router'],
    },
};

在上面的配置中,app.js仍然和最开始一样,其内容也不需要做任何改变,只是我们添加了一个新的chunk name为vendor的入口,并通过数组的形式把工程所依赖的第三方模块放了进去。

那么问题来了,我们并没有为vendor设置入口路径,Webpack要如何打包呢?这时我们可以使用CommonsChunkPlugin(在Webpack 4之后CommonsChunkPlugin已被废弃,可以采用optimization.splitChunks,具体参考第6章内容),将app与vendor这两个chunk中的公共模块提取出来。通过这样的配置,app.js产生的bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle,从而达到我们提取vendor的目标。由于vendor仅仅包含第三方模块,这部分不会经常变动,因此可以有效地利用客户端缓存,在用户后续请求页面时加快整体的渲染速度。

3. 多页应用

对于多页应用的场景,为了尽可能减小资源的体积,我们希望每个页面都只加载各自必要的逻辑,而不是将所有页面打包到同一个bundle中,也就是说,每个页面都需要有一个独立的bundle。我们使用多入口来实现。请看下面的例子:

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js',
        pageC: './src/pageC.js',
    },
};

在上面的配置中,入口与页面是一一对应的关系,这样每个HTML只要引入各自的JS就可以加载其所需要的模块。

另外,对于多页应用的场景,我们同样可以使用提取vendor的方法,将各个页面之间的公共模块进行打包。如:

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js',
        pageC: './src/pageC.js',
        vendor: ['react', 'react-dom'] ,
    },
};

可以看到,我们将react和react-dom打包进了vendor,之后再配置optimization.splitChunks,将它们从各个页面中提取出来,生成单独的bundle即可。