Skip to content

webpack的资源输入输出

资源处理流程

在一切流程的最开始,我们需要指定一个或多个入口(entry),也就是告诉 Webpack 具体从源码目录下的哪个文件开始打包,这些存在依赖关系的模块会在打包时被封装为一个 chunk。Webpack 会从入口文件开始检索,并将具有依赖关系的模块生成一棵依赖树,最终得到一个 chunk。由这个 chunk 得到的打包产物我们一般称之为 bundle。

配置资源入口

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

  • 确定入口模块位置,告诉 Webpack 从哪里开始进行打包。
  • 定义 chunk name。如果工程只有一个入口,那么默认其 chunk name 为“main”;如果工程有多个入口,我们需要为每个入口定义 chunk name,来作为该 chunk 的唯一标识。

context

context 可以理解为资源入口的路径前缀,在配置时要求必须使用绝对路径的形式。

其目的是为了让代码更加简洁,在多入口的时候往往省略。

entry

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

字符串

直接传入文件路径

数组类型

将多个资源与县合并,在打包时把最后一个元素作为实际入口

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

上面的配置等同于:

js
// webpack.config.js
module.exports = {
  entry: "./src/index.js",
};

// index.js
import "babel-polyfill";

对象

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

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

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

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

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

函数类型入口

用函数定义入口时,只要返回上面介绍的任何配置形式即可,如:

js
// 返回一个字符串型的入口
module.exports = {
  entry: () => "./src/index.js",
};

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

传入一个函数的优点在于我们可以在函数体里添加一些动态的逻辑来获取工程的入口。另外,函数也支持返回一个 Promise 对象来进行异步操作。

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

实例

单页应用

对于单页应用(SPA)来说,一般定义单一入口即可。

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

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

提取 vendor

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

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

js
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.splitChunk),将 app 与 vendor 这两个 chunk 中的公共模块提取出来。通过这样的配置,app.js 产生的 bundle 将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的 bundle,这也就达到了我们提取 vendor 的目标。由于 vendor 仅仅包含第三方模块,这部分不会经常变动,因此可以有效地利用客户端缓存,在用户后续请求页面时会加快整体的渲染速度。

多页应用

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

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

在上面的配置中,入口与页面是一一对应的关系,这样每个 HTML 只要引入各自的 JS 就可以加载其所需要的模块。 另外,对于多页应用的场景,我们同样可以使用提取 vendor 的方法,将各个页面之间的公共模块进行打包。如:

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

配置资源出口

接着我们来看资源输出相关的配置,所有与出口相关的配置都集中在 output 对象里。请看下面的例子:

js
const path = require("path");
module.exports = {
  entry: "./src/app.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "assets"),
    publicPath: "/dist/",
  },
};

filename

filename 的作用是控制输出资源的文件名,其形式是字符串。也可以是相对路径,路径中目录不存在会自动创建。

“在多入口的场景中,我们需要为对应产生的每个 bundle 指定不同的名字,Webpack 支持使用一种类似模板语言的形式动态地生成文件名,如:

module.exports = { entry: { app: './src/app.js', vendor: './src/vendor.js', }, output: { filename: '[name].js', }, };

在资源输出时,上面配置的 filename 中的[name]会被替换为 chunk name,因此最后项目中实际生成的资源是 vendor.js 与 app.js。

除了[name]可以指代 chunk name 以外,还有其他几种模板变量可以用于 filename 的配置中,如表所示。

变量名称功能描述
[hash]指代 Wcbpack 此次打包所有资源生成的 hash
[chunkhash]指代当前 chunk 内容的 hash
[id]指代当前 chunk 的 id
[query]指代 filename 配置项中的 query
  • 当有多个 chunk 存在时对不同的 chunk 进行区分。如[name][chunkhash][id],它们对于每个 chunk 来说都是不同的。
  • 控制客户端缓存。表中的[hash][chunkhash]都与 chunk 内容直接相关,在 filename 中使用了这些变量后,当 chunk 的内容改变时,可以同时引起资源文件名的更改,从而使用户在下一次请求资源文件时会立即下载新的版本而不会使用本地缓存。[query]也可以起到类似的效果,只不过它与 chunk 内容无关,要由开发者手动指定。

在实际工程中,我们使用比较多的是[name],它与 chunk 是一一对应的关系,并且可读性较高。如果要控制客户端缓存,最好还要加上[chunkhash],因为每个 chunk 所产生的[chunkhash]只与自身内容有关,单个 chunk 内容的改变不会影响其他资源,可以最精确地让客户端缓存得到更新。

path

path 指定资源输出的位置,其值为绝对路径,在 webpack4 之后可以不用单独配置,默认为 dist。

publicPath

publicPath 是一个非常重要的配置项,并且容易与 path 相混淆。从功能上来说,path 用来指定资源的输出位置,而 publicPath 则用来指定资源的请求位置。让我们详细解释这两个定义。

  • 输出位置:打包完成后资源产生的目录,一般将其指定为工程中的 dist 目录。
  • 请求位置:由 JS 或 CSS 所请求的间接资源路径。页面中的资源分为两种,一种是由 HTML 页面直接请求的,比如通过 script 标签加载的 JS;另一种是由 JS 或 CSS 请求的,如异步加载的 JS、从 CSS 请求的图片字体等。publicPath 的作用就是指定这部分间接资源的请求位置。

HTML 相关

与 HTML 相关,也就是说我们可以将 publicPath 指定为 HTML 的相对路径,在请求这些资源时会以当前页面 HTML 所在路径加上相对路径,构成实际请求的 URL。如:

js
// 假设当前HTML地址为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: ""; // 实际路径https://example.com/app/0.chunk.js
publicPath: "./js"; // 实际路径https://example.com/app/js/0.chunk.js
publicPath: "../assets/"; // 实际路径https://example.com/aseets/0.chunk.js

Host 相关

若 publicPath 的值以“/”开始,则代表此时 publicPath 是以当前页面的 host name 为基础路径的。如:

js
// 假设当前HTML地址为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "/"; // 实际路径https://example.com/0.chunk.js
publicPath: "/js/"; // 实际路径https://example.com/js/0.chunk.js
publicPath: "/dist/"; // 实际路径https://example.com/dist/0.chunk.js

CDN 相关

“上面两种配置都是相对路径,我们也可以使用绝对路径的形式配置 publicPath。这种情况一般发生于静态资源放在 CDN 上面时,由于其域名与当前页面域名不一致,需要以绝对路径的形式进行指定。当 publicPath 以协议头或相对协议的形式开始时,代表当前路径是 CDN 相关。如:

js
// 假设当前页面路径为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "http://cdn.com/" // 实际路径http://cdn.com/0.chunk.js
publicPath: "https://cdn.com/" // 实际路径https://cdn.com/0.chunk.js
publicPath: "//cdn.com/assets/" 实际路径 //cdn.com/assets/0.chunk.js

webpack-dev-server 的配置中也有一个 publicPath,值得注意的是,这个 publicPath 与 Webpack 中的配置项含义不同,它的作用是指定 webpack-dev-server 的静态资源服务路径。请看下面的例子:

js
const path = require("path");
module.exports = {
  entry: "./src/app.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  devServer: {
    publicPath: "/assets/",
    port: 3000,
  },
};

从上面可以看到,Webpack 配置中 output.path 为 dist 目录,因此 bundle.js 应该生成在 dist 目录。但是当我们启动 webpack-dev-server 的服务后,访问 localhost:3000/dist/bundle.js 时却会得到 404。这是因为 devServer.publicPath 配置项将资源位置指向了 localhost:3000/assets/,因此只有访问 localhost:3000/assets/bundle.js 才能得到我们想要的结果。

为了避免开发环境和生产环境产生不一致而造成开发者的疑惑,我们可以将webpack-dev-server的publicPath与Webpack中的output.path保持一致,这样在任何环境下资源输出的目录都是相同的。请看下面的例子:

js
const path = require('path');
module.exports = {
    entry: './src/app.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist') ,
    },
    devServer: {
        publicPath: '/dist/',
        port: 3000,
    },
};

小结

  • 在配置打包入口时,context相当于路径前缀,entry是入口文件路径。单入口的chunk name不可更改,多入口的话则必须为每一个chunk指定chunk name。
  • 当第三方依赖较多时,我们可以用提取vendor的方法将这些模块打包到一个单独的bundle中,以更有效地利用客户端缓存,加快页面渲染速度。
  • path和publicPath的区别在于path指定的是资源的输出位置,而publicPath指定的是间接资源的请求位置。

备案号:闽ICP备2024028309号-1