webpack 入门教程:打包入口和打包输出

87 阅读10分钟

上一篇文章中,我们讲解了如何使用 webpack 打包一个简单的应用,期间浅尝辄止地介绍了打包过程中会遇到的一些基本概念。

不过,先要订正上一篇文章里的有一个优化点。webpack 从 5.20.0+ 开始,增加了 output.clean 的支持,这样就不需要再额外引入 CleanWebpackPlugin 了。

module.exports = {
  //...
  output: {
    clean: true, // Clean the output directory before emit.
  },
};

diff 状况如下:

在此之后呢,我们就要去一一深入webpack 中的核心概念和需要掌握的重要技能点了。

当然,首当其冲要讲的就是 打包输入(Entry)打包输出(Output) 了。顾名思义,Entry 用来指定打包的入口文件,而 Output 则是用于制定打包产物的输出位置以及输出形式。

在上一篇文章中,我们就简单使用了下它们:

// webpack.dev.config.js
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
    },
    mode: 'development',
};
  • entry 的值是一个字符串,指定打包的入口文件是 src/index.js,而
  • output.filename 则是表明将打包结果输出到 dist/bundle.js

下面就要详细介绍。

打包入口

entry 的可不只支持字符串类型,还有对象、数组甚至是函数。

多样的 entry 类型

entry 为字符串的场景,我们已经介绍过,就不多赘述了。

// webpack.prod.config.js
module.exports = {
  mode: 'production',
  entry: './src/index.js',
}

以上的配置,只是定义了一个打包入口,只有一个打包输出。

npm run build

> webpack-demo@1.0.0 build
> webpack --config webpack.prod.config.js

asset index.html 234 bytes [emitted]
asset main.js 84 bytes [emitted] [minimized] (name: main)
orphan modules 59 bytes [orphan] 1 module
./src/index.js + 1 modules 146 bytes [built] [code generated]
webpack 5.94.0 compiled successfully in 305 ms

输出文件 dist/mian.js。

如果要定义多个打包入口(比如对于多页应用),就要使用 entry 的对象形式。

module.exports = {
  mode: 'production',
  entry: {
    index: './src/index.js',
    lib: './src/lib.js',
  },
};
// src/lib.js
window.Util = {
  escapeHtml(str) {
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
  },
}

以上,我们将 entry 定义成包含了 2 个属性 index、lib 的对象。

index、lib 这两个属性名有一个专有名词来描述,即「chunk name」。chunk 的字面意思是「代码块」,在打包领域,我们将从入口文件开始、由入口文件及其内部依赖所组成的依赖树理解成是一个 chunk,从 chunk 得到的打包产物成为 bundle

chunk、entry 以及 bundle 三者的关系如下:

默认情况下,打包的输出文件名由 chunk name 决定的。删除 dist/ 目录,再次执行打包:

npm run build

> webpack-demo@1.0.0 build
> webpack --config webpack.prod.config.js

asset lib.js 140 bytes [emitted] [minimized] (name: lib)
asset index.js 84 bytes [emitted] [minimized] (name: index)
orphan modules 59 bytes [orphan] 1 module
./src/index.js + 1 modules 146 bytes [built] [code generated]
./src/lib.js 174 bytes [built] [code generated]
webpack 5.94.0 compiled successfully in 225 ms

./src/index.js 和 ./src/lib.js 最终会输出成 dist/index.js 和 dist/lib.js 2 个文件(在不指定 output 的情况下)。

如果项目只有一个入口文件,那么他的 chunk nanme 就是 main。也就是说:

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

等同于

module.exports = {
  entry: {
    main: './src/index.js'
  },
}

正因为如此,默认单入口打包的输出文件才会是 dist/main.js。

除了对象,entry 还能设置数组:

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

当 entry 是一个数组时,数组的最后一个元素才是实际的入口文件,而前面的资源是用来合并到入口文件里去的。

也就是说:

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

的写法,等同于

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

效果类似:

最后,entry 甚至可以是一个函数,函数的返回类型可以是字符串、对象或数组,其含义与单独将 entry 定义成字符串、对象或数是一样的。

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

使用函数的一个优点,就是可以在里面添加一些判断逻辑,动态定义入口文件。

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

另外,webpack 还支持返回 Promise 来支持异步操作。

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

以上配置,在运行 npm run build 之后,需要等待 1 秒钟之后才会开始打包。

context

webpack 的入口文件的路径可不是只由 entry 决定的,还依赖 context 设置。

以下的配置:

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

其实等同于

module.exports = {
  context: path.join(process.cwd(), '.')
  entry: './src/index.js',
}

实际上,context 指代打包入口的路径前缀,是可选的,默认指向项目当前的工作目录(current working directory)。这在多入口打包时,能让 entry 的编写更加简洁。

举例来说:以下两种配置效果一样,入口都是 <项目根路径>/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',
};

打包效果:

当然,因为没有 src/scripts/index.js 文件打包报错了,不过验证了 context 的作用。

单页/多页应用案例

webpack 多 chunk name 的 entry 设置,让我们既可以打包单页应用(SPA),也可以打包多页应用。 案例如下:

单页应用:

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

多页应用:

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

对多页应用来苏红,每个页面(HTML)对应一个独立的 bundle。

当然,针对入口打包我们还可以做一些优化,就是将一些公共模块(vendor,比如 react、react-dom)从业务代码中抽离出来,这样打包出来的文件尺寸就会比较小(只包含业务代码)。

webpack 4 之后,我们可以通过 optimization.splitChunks 来优化,后续文章中会介绍,本篇就不再赘述了。

打包输出

讲完打包入口,再来说打包输出,与打包输出相关的配置集中在 output 对象中。

filename

之前关于 output 的应用比较简单:

module.exports = {
  output: {
    filename: 'bundle.js',
  },
};

以上,filename 用于指定输出文件名,默认输出的目录是 dist(由 path 属性决定)。

module.exports = {
  output: {
    filename: 'bundle.js',
    // 输出目录默认是当前项目下的 dist 目录
    path: path.join(process.cwd(), 'dist'),
  },
};

当然,filename 还可以是一个相对路径:

module.exports = {
    output: {
        filename: './js/main.js',
    },
};

执行打包,文件被输出到 dist/js/main.js。

一如既往的,如果打包文件的父级目录不存在,webpack 会自动创建。

输出占位符

更加方便的是,对于多入口配置,filename 中还可以使用类似 [placeholder] 形式动态生成文件名。以下面为例:

// webpack.prod.config.js
module.exports = {
  mode: 'production',
  entry: {
    pageA: './src/pageA.js',
    pageB: './src/pageB.js',
  },
  output: {
    filename: '[name].js',
    clean: true
  },
// pageA.js
document.getElementById("app").innerHTML = `Page A`

// pageB.js
document.getElementById("app").innerHTML = `Page B`

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

除了 [name],其他还支持的占位符还包括:

在实际项目中,我们使用比较多的是 [name],它与 chunk 是一一对应的关系,并且可读性较高。如果要控制客户端缓存,最好还要加上 [chunkhash]

module.exports = {
    entry: {
        pageA: './src/pageA.js',
        pageB: './src/pageB.js',
    },
    output: {
        filename: '[name]@[chunkhash].js',
    },
    plugins: [
      new HtmlWebpackPlugin({
        filename: 'pageA.html',
        chunks: ['pagea'],
        template: './index.html'
      }),
      new HtmlWebpackPlugin({
        filename: 'pageB.html',
        chunks: ['pageB'],
        template: './index.html'
      }),
    ]
};

执行打包,结果类似下面这样:

output: {
  filename: '[name]@[chunkhash:5].js',
},

当然,还可以通过 [chunkhash:5] 方式缩短 hash 的长度到 5 位:

path

output.path 用来指定打包文件的输出目录,它的值必须是一个绝对路径。其默认值是当前的工作目录下的 dist/ 目录。

module.exports = {
    entry: './src/app.js',
    output: {
        filename: 'bundle.js',
        path: path.join(process.cwd(), 'dist'),
    },
};

默认 dist/ 目录的配置是从 webpack 4 起使用的,之前默认都是项目根目录。

ouput.path 类似 entry.context,不过一个是指定输出目录,一个是指定入口文件目录。除非我们需要更改它,否则不必单独配置。

与webpack-dev-server配合使用

为了避免开发环境和生产环境产生不一致而造成开发者的疑惑,我们可以将 webpack-dev-server 的 static 与 webpack 中的 output.path 保持一致。

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

这样,静态资源的服务地址与打包文件的输出目录是一样的,理解上就不容易出现问题。

publicPath

publicPath 是 output 下重要性仅次于 filaname 的一个属性。你的项目可能不需要设置 path,但基本上是要设置 publicPath 的。

publicPath 名字上很容易让人跟 path 混淆。不过,弄清他们的作用后,就不难区分了。

  • path:这个已经介绍过了,是用来指定打包文件的输出目录的,默认是当前项目下的 dist/。
  • publicPath:则用于指定打包出来的文件内部引用的资源的路径前缀。比如打包文件里动态引入的 CSS、JavaScript,或是图片、svg 等这类文件。

也就是说,path 控制的是打包时的输出行为;publicPath 则是控制打包后、运行时的资源引入行为

我们以下面的配置为例:

module.exports = {
  mode: 'production',
  entry: './src/app.js',
  output: {
    // publicPath: 'xxx',
    clean: true
  },
  // 为了支持解析图片,后续文章会将,暂时略过
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        type: 'asset/resource'
      },
    ]
  },
}
// app.js
import img from '../assets/images/avatar.jpg'

document.getElementById("app").innerHTML = `
  <h1>Hello Vanilla!</h1>
  <figure>
    <img src="${img}" alt="My Avatar" />
    <figcaption>My Avatar</figcaption>
  </figure>
`

引入了项目根目录下 assets 下的一张图片。下面开始测试,

publicPath 可以设置 3 种不同类型的值:

  1. HTML 相关

当设置的 publicPath 为空或是以 ./../ 开头的相对路径时,我们来看看效果。

output: {
  publicPath: '',
  clean: true
}

打包输出:

main.js 中输出代码:

依次类似,当 publicPath 分别位 ./../ 时,main.js 中输出代码如下:

可以看见,图片资源输出位置不变(一致位于 dist/ 目录下),而引用的图片地址就是"publicPath + 文件名"。

也就是说,publicPath 为空或是以 ./../ 开头的相对路径时时,被请求的资源是相对于 HTML 所在路径进行加载的。

举例来说,假设当前 HTML 地址为 https://example.com/app/index.html,我们要异步加载的资源文件叫 0.chunk.js。

那么不同取值下的 publicPath 对应的资源文件加载路径分别是:

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

注意,/js/ 最后一个 / 是必需的,否则输出会是类似 /js1058d90f4a32a506ab53.jpg 的错误地址。下同。

  1. Host 相关

同理,如果 publicPath 的值是以“/”开头时。以"/"、"/js/" 为例,输出结果如下:

那么 publicPath 则是以当前页面的 host name 为基础路径的。

举例来说,假设当前 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
  1. CDN 相关

当然,实际项目里考虑到性能,打包出来的静态资源通常会部署在单独的 CDN 上,与网页脚本的部署域名分开。这个时候,publicPath 的值就是一个以具体协议或相对协议开头的域名地址。

以"cdn.com/"、"//cdn.co…" 为例,输出结果如下:

举例来说,假设当前 HTML 地址为 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

简单来说,当为 publicPath 设置了值时,最终打包出来的结果地址就是"publicPath + 文件名"

当然,当你不设置 publicPath 时,其默认值为 'auto',它会根据当前加载资源的脚本位置(e.currentScript API)来计算,确保资源被正确加载。

总结

本文讲解了webpack 中 2 个最基础的核心概念:打包输入(Entry) 和 打包输出(Output)。Entry 用来指定打包的入口文件,而 Output 则是用于制定打包产物的输出位置以及输出形式。

希望本文讲解对你的工作能够有所帮助。感谢阅读,再见。