4、核心配置之Output:path与filename配置、publicPath详解、clean清理输出、多入口输出配置、library与libraryTarget
好,咱们接着聊 Webpack 的核心配置。上一章我们把 entry 入口讲透了,那打包出来的东西放哪儿?怎么命名?这就是 output 配置要干的事。
说实话,output 这块儿我刚开始学的时候觉得挺简单的,不就是指定个输出目录嘛。但后来踩了不少坑才发现,这里面的门道真不少。尤其是 publicPath,当年让我在部署环境上折腾了一整天。
path 与 filename:最基础的输出配置
先看最常用的两个配置项:path 和 filename。
path 指定输出目录,必须是绝对路径。我习惯用 Node.js 的 path.resolve() 来处理。
filename 指定输出文件的名称。你可以写死,比如 bundle.js,但更常见的是用占位符动态生成。
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
嗯,这是最基础的写法。但实际项目中,我们很少用固定的文件名。为什么?因为浏览器缓存。
你想想看,如果每次打包都叫 bundle.js,浏览器会认为文件没变,直接读缓存。你修了个 bug 重新部署,用户看到的还是旧代码。这就尴尬了。
所以我们会用 [contenthash] 这个占位符。它根据文件内容生成哈希值,内容变了哈希就变,文件名也就变了。浏览器一看文件名不同,乖乖去下载新文件。
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js'
}
这里 [name] 对应入口的名称,[contenthash:8] 取哈希的前8位。我还习惯把 JS 文件放到 js/ 子目录下,保持输出目录整洁。
[contenthash],开发环境用 [name] 就够了。开发时频繁改代码,每次哈希都变,编译速度会受影响。
publicPath:部署路径的坑
这个配置我当年栽过大跟头。简单说,publicPath 告诉 Webpack:打包后的资源在服务器上通过什么 URL 路径来访问。
举个例子。你的项目部署在 https://example.com/my-app/ 这个路径下,那 publicPath 就要设为 /my-app/。否则加载 JS、CSS、图片时路径会出错。
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
publicPath: '/my-app/'
}
我曾经把一个项目部署到 CDN 上,CDN 的域名是 https://cdn.example.com,资源放在 /assets/ 目录下。结果我忘了配 publicPath,所有资源都去根路径找,页面白屏了一下午。
后来怎么解决的?把 publicPath 设为 https://cdn.example.com/assets/,问题就解决了。
publicPath 的值要以 / 结尾,否则路径拼接会出问题。比如 /my-app 和 /my-app/ 差别很大。
常见的 publicPath 场景:
| 部署场景 | publicPath 示例 |
|---|---|
| 根路径部署 | '/' |
| 子路径部署 | '/my-app/' |
| CDN 部署 | 'https://cdn.example.com/assets/' |
| 相对路径 | './' 或 '' |
clean:告别手动删除 dist
每次打包前,你是不是还要手动删一下 dist 目录?我以前就是这么干的,直到发现了 clean: true。
在 Webpack 5 中,output.clean 可以自动清理输出目录。每次打包前,它会删除 path 指定的目录下的所有文件,然后重新生成。
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true
}
这个配置太省心了。你再也不用担心旧文件残留,也不用写什么 rm -rf dist 的脚本了。
clean: true 只清理 path 目录下的文件,不会动其他目录。放心用。
多入口输出配置
实际项目很少只有一个入口。比如你有个后台管理系统,登录页和首页是两套独立的页面,那就需要多入口。
多入口的配置其实很简单,entry 用对象形式,output.filename 用 [name] 占位符就行。
module.exports = {
entry: {
login: './src/login.js',
admin: './src/admin.js',
common: './src/common.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true
}
};
这样打包后会生成三个文件:login.abc123.js、admin.def456.js、common.ghi789.js。
我做过一个项目,有十几个入口页面。当时我用了 glob 库来动态扫描入口文件,省去了手动一个个配置的麻烦。代码大概是这样的:
const glob = require('glob');
const path = require('path');
const entries = {};
glob.sync('./src/pages/**/index.js').forEach(file => {
const name = path.basename(path.dirname(file));
entries[name] = file;
});
module.exports = {
entry: entries,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true
}
};
嗯,这个技巧在处理大型多页面应用时特别有用。
library 与 libraryTarget:打包成库
如果你不是打包一个应用,而是打包一个供别人使用的库(比如组件库、工具库),那就要用到 library 和 libraryTarget 了。
library 指定库的名称,libraryTarget 指定库的导出格式。
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-library.js',
library: 'MyLibrary',
libraryTarget: 'umd'
}
};
常见的 libraryTarget 有这些:
| libraryTarget | 说明 | 使用场景 |
|---|---|---|
var |
将库赋值给一个全局变量 | 直接通过 <script> 标签引入 |
commonjs |
通过 module.exports 导出 |
Node.js 环境 |
commonjs2 |
通过 module.exports 导出(更标准) |
Node.js 环境 |
amd |
AMD 规范 | RequireJS 等 |
umd |
兼容 CommonJS、AMD 和全局变量 | 通用库,推荐使用 |
module |
ES Module 规范 | 支持 import 的环境 |
我个人强烈推荐用 umd。为什么?因为它兼容性最好。不管用户是用 import、require 还是直接 <script> 标签引入,都能正常工作。
libraryTarget: 'commonjs',结果用户用 ES Module 方式引入时报错了。后来改成 umd,问题解决。所以,除非你有特殊需求,否则一律用 umd。
另外,如果你用的是 libraryTarget: 'module',记得把 experiments.outputModule 设为 true。这是 Webpack 5 的新特性,支持输出真正的 ES Module。
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-library.mjs',
library: {
type: 'module'
}
},
experiments: {
outputModule: true
}
嗯,这块内容比较多,但核心就一句话:打包应用用 filename 和 path,打包库用 library 和 libraryTarget。
好了,output 配置的核心内容就这些。下一章我们聊聊 module 和 rules,也就是 Webpack 怎么处理不同类型的文件。到时候见。