前面的文章webpack学习[一]中,我们介绍了webpack的一些基本概念,例如:入口、输出、loader、插件、模式等,本文将介绍如何使用webpack管理一个前端项目。
一、构建项目
首先我们创建一个目录webpack-study,并切换到该目录下
mkdir webpack-study
cd webpack-study
使用 npm 对项目进行初始化,生成package.json文件。
npm init
package.json 文件初始内容如下:
{
// 项目名称
"name": "webpack-study",
// 项目版本
"version": "1.0.0",
// 项目描述
"description": "",
// 入口文件
"main": "index.js",
// 脚本命令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
// 作者
"author": "ag_dubs <dali@npmjs.com>",
// 版本信息
"license": "ISC"
}
然后安装 webpack:
npm install webpack webpack-cli --save-dev
安装后可以看到package.json文件中多了如下内容:
"devDependencies": {
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
}
创建 ./src 目录,并创建入口文件index.js,该文件夹中存放项目源代码(即:用于书写和编辑的代码)。
创建 ./dist 目录,该文件夹用于存放分发代码(即:构建过程中,经过最小化和优化后产生的输出结果,最终将在浏览器中加载)
创建完成后,目录结构如下:
紧接着,我们在入口文件 index.js 中书写如下代码:
const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello Webpack';
document.body.appendChild(helloDiv);
书写完成后,我们在 cmd 终端执行npx webpack命令,该命令默认会将 src/index.js 作为入口起点(entry),生成 dist/main.js 作为输出(output)。
npx webpack
当我们没有设置配置文件时,上述命令是如下命令的简写形式:
npx webpack --entry <entry> --output-path <output-path>
npx webpack --entry src/index.js --output-path dist
其中npx 可以运行在初次安装的 webpack package 中的 webpack 二进制文件。该命令的主要作用可参考该博客.
该命令执行成功后,可以看到 ./dist 目录下会生成一个 main.js 文件。
然后我们在 ./dist 目录下创建一个index.html文件,书写如下代码来验证我们是否打包成功:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
如果打包成功,我们在浏览器中打开 index.html 文件可以看到 "Hello Webpack" 了:
创建配置文件
大多数项目会根据实际需求进行复杂的配置,所以webpack支持使用配置文件webpack.config.js进行打包配置,这比终端输入命令参数更加高效。
我们可以在项目根目录创建一个 webpack.config.js 文件,并添加如下内容:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
这时候就可以在终端运行如下命令进行打包
npx webpack --config webpack.config.js
如果
webpack.config.js存在,则webpack命令将默认选择使用它。我们在这里使用--config选项只是向你表明,可以传递任何名称的配置文件。(即:配置文件为webpack.config.js时可直接运行npx webpack进行打包)
package.json 中设置快捷方式
此外,我们可以在 package.json 文件的 scripts 中添加命令,为 webpack 命令设置一个快捷方式。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
配置完成后,我们可以直接在命令行执行如下命令进行打包:
npm run build
二、资源管理
2.1 加载CSS
若想在JavaScript模块中引入CSS文件,需要安装 styly-loader 和 css-loader:
styly-loader:把 CSS 插入到 DOM 中。css-loader:加载 CSS 文件并解析 import 的 CSS 文件,最终返回 CSS 代码
npm install --save-dev style-loader css-loader
然后在 webpack.config.js中添加如下配置:
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
}
]
}
注意:
style-loader和css-loader的先后顺序不能写错,因为模块 loader 链式调用(从下往上或从右往左),链中的每个 loader 都将对资源进行转换。链会逆序执行。第一个 loader 将其结果(被转换后的资源)传递给下一个 loader,依此类推。最后,webpack 期望链中的最后的 loader 返回 JavaScript。
我们在/src目录下新建一个index.css文件,添加如下样式:
.hello {
color: red
}
修改index.js内容如下:
import './index.css';
const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello Webpack';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);
然后执行npm run build进行打包,然后在浏览器中打开index.html,可以看到字体颜色变成红色了,这也说明CSS文件成功加载了。
除了 CSS 文件之外,现有的loader支持各种CSS风格,例如:postcss、sass、less等。
2.2 资源模块(asset module)
资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。主要包含以下四种类型:
asset/resource发送一个单独的文件并导出 URL。asset/inline导出一个资源的 data URI。asset/source导出资源的源代码。asset在导出一个 data URI 和发送一个单独的文件之间自动选择。
(1)resource 资源(asset/resource)
我们以加载图片资源为例,来展示asset/resource 、asset/inline两者之间的区别。
首先我们在配置文件中加入如下配置:
module: {
rules: [
...
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
]
}
使用webpack内置资源模块时,配置的是 type ,而不是 use。
然后我们在index.js文件中引入图片资源:
import './index.css';
import skyImg from './sky.jpg';
const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello Webpack';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);
const img = document.createElement('img');
img.src = skyImg;
document.body.appendChild(img);
执行 npm run build 进行打包。可以看到我们的图片文件被发送到 dist文件夹下。
这种资源模块类型下,我们可以通过如下两种配置方式自定义输出的文件名:
- 在 output 中配置
assetModuleFilename
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]'
},
- 配置 generator(如果两种方式都配置了,该种方式优先级更高)
module: {
rules: [
...
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]'
}
}
]
}
(2)inline 资源(asset/inline)
首先我们修改配置文件如下:
module: {
rules: [
...
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/inline'
}
]
}
执行打包命令后,我们在浏览器中查看网页源代码,可以看到图片被打包成 data URI 的形式。并且dist文件夹中没有任何文件输出。
webpack 输出的 data URI,默认是呈现为使用 Base64 算法编码的文件内容。如果要使用自定义编码算法,则可以指定一个自定义函数来编码文件内容:
const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
...
module: {
rules: [
{
test: /.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
return svgToMiniDataURI(content);
}
}
}
]
},
};
(3)source 资源(asset/source)
该类资源会导出文件的源代码。例如我们新建一个 txt 文件,输入如下内容:
hello webpack
添加如下配置:
module.exports = {
...
module: {
rules: [
{
test: /\.txt/,
type: 'asset/source'
}
]
},
};
修改 index.js 文件内容如下:
import './index.css';
import text from './text.txt';
const helloDiv = document.createElement('div');
helloDiv.innerHTML = text;
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);
打包之后,在浏览器打开 index.html 可以看到 txt 文件中的内容。
(4)通用资源(asset)
当资源类型为 asset 时,webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 8kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。
可以通过在 webpack 配置的 module rule 层级中,设置 Rule.parser.dataUrlCondition.maxSize选项来修改此条件:
module.exports = {
...
module: {
rules: [
{
test: /.txt/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
]
},
};
此外,也可以通过传递一个函数来指定是否使用 inline 模块,例如:
module.exports = {
//...
module: {
rules: [
{
//...
parser: {
dataUrlCondition: (source, { filename, module }) => {
const content = source.toString();
return content.includes('some marker');
},
},
},
],
},
};
2.3 加载图像(image)
加载图像资源,我们可以使用上面介绍的资源模块来处理。例如我们进行了如下配置后:
module: {
rules: [
...
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
]
}
- 在
import MyImage from './my-image.png'时,此图像将被处理并添加到output目录,并且MyImage变量将包含该图像在处理后的最终 url。在 - 使用 css-loader 时,如前所示,会使用类似过程处理你的 CSS 中的
url('./my-image.png')。loader 会识别这是一个本地文件,并将'./my-image.png'路径,替换为output目录中图像的最终路径。而- - html-loader 以相同的方式处理
<img src="./my-image.png" />。
当然对于一些较小的图像我们可以使用asset/inline进行加载。或者也可以使用asset根据图片大小选择如何打包。
2.4 加载字体(font)
对于字体这类资源我们也可以使用webpack内置的资源模块来加载,我们可进行如下配置:
module: {
rules: [
...
{
test: /.(woff|woff2|eot|ttf|otf)$/i,,
type: 'asset/resource'
}
]
}
然后我们在CSS文件中定义相关字体:
@font-face {
font-family: "Bitstream Vera Serif Bold";
src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf");
font-size: 14px;
font-weight: 500;
}
.hello {
color: red;
font-family: "Bitstream Vera Serif Bold"
}
打包后,在浏览器我们可以看到文字字体如下:
2.5 加载数据
对于一些数据文件,如 JSON 文件,CSV、TSV 和 XML,webpack也是可以加载的。其中json文件,webpack 自身就可以加载。如下json文件:
{
"title": "hello world"
}
我们使用import导入:
import jsonData from './data.json'
console.log(jsonData);
可以看到,json数据会被加载为一个JavaScript对象:
而对于 CSV、TSV 和 XML 我们需要安装csv-loader和xml-loader
npm install --save-dev csv-loader xml-loader
然后添加如下配置:
module: {
rules: [
...
{
test: /\.(csv|tsv)$/i,
use: 'csv-loader'
},
{
test: /\.xml$/i,
use: 'xml-loader'
}
]
}
假设项目中有如下数据文件data.csv和data.xml:
to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Mary</to>
<from>John</from>
<heading>Reminder</heading>
<body>Call Cindy on Tuesday</body>
</note>
webpack打包格式如下图所示:
2.6 自定义 JSON 模块 parser
通过使用自定义 parser 替代特定的 webpack loader,可以将任何 toml、yaml 或 json5 文件作为 JSON 模块导入。
首先安装 toml,yamljs 和 json5 的 packages:
npm install toml yamljs json5 --save-dev
然后添加如下配置:
const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');
module: {
rules: [
...
{
test: /\.toml$/i,
type: 'json',
parser: {
parse: toml.parse,
},
},
{
test: /\.yaml$/i,
type: 'json',
parser: {
parse: yaml.parse,
},
},
{
test: /\.json5$/i,
type: 'json',
parser: {
parse: json5.parse,
},
},
]
}
假设我们有如下数据文件:
src/data.toml
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z
src/data.yaml
title: YAML Example
owner:
name: Tom Preston-Werner
organization: GitHub
bio: |-
GitHub Cofounder & CEO
Likes tater tots and beer.
dob: 1979-05-27T07:32:00.000Z
src/data.json5
{
// comment
title: 'JSON5 Example',
owner: {
name: 'Tom Preston-Werner',
organization: 'GitHub',
bio: 'GitHub Cofounder & CEO\n\
Likes tater tots and beer.',
dob: '1979-05-27T07:32:00.000Z',
},
}
webpack打包后数据格式如下图所示:
正如 webpack 官方文档而言,有了这些加载资源的方法,就无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起使用。例如我们要开发某个组件,那么我们就可以将与该组件相关的所有的资源放在同一目录,这样使得代码更具有移植性。
三、管理输出
上面示例中我们都是单文件入口,而且打包结果都输出到main.js中。假设我们进行如下修改:
首先创建一个新文件 src/print.js
export default function printMe() {
console.log('I get called from print.js!');
}
然后我们在 index.js 文件中引入 printMe:
import './index.css';
import printMe from './print';
const helloDiv = document.createElement('div');
helloDiv.innerHTML = 'Hello world';
helloDiv.classList.add('hello');
document.body.appendChild(helloDiv);
const btn = document.createElement('button');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
helloDiv.appendChild(btn);
修改配置如下:
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
print: './src/print.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
}
}
然后执行npm run build 进行打包,我们可以看到dist 目录下会生成 print.bundle.js 和 index.bundle.js 文件:
这时我们在浏览器打开 index.html 文件,我们并不能看到所作的改变,这是因为我们还需要在index.html文件中修改引入的 js 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="print.bundle.js"></script>
<script src="index.bundle.js"></script>
</body>
</html>
这时候我们在浏览器中访问 index.html 中时才会显示新的内容:
例如上述这种情况,当我们修改了入口文件或者新增了入口文件,导致输出文件发生改变,这个时候我们需要在index.html 文件中手动引入新的文件。对于此,我们可以使用 HtmlWebpackPlugin。
3.1 HtmlWebpackPlugin
首选安装 HtmlWebpackPlugin:
npm install --save-dev html-webpack-plugin
然后在配置文件中添加如下配置:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin()
]
}
然后我们执行npm run build 命令进行打包,会自动在dist目录下生成一个新的index.html文件,该文件引入了print.bundle.js 和 index.bundle.js 。
该插件还有很多配置项,可参考该插件git仓库,这里介绍一些常用的配置:
| 配置项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
title | {String} | Webpack App | 生成的HTML文件的标题(<title>) 。如果配置了 template 会优先使用模板文件的title。 |
filename | {String|Function} | 'index.html' | 生成的HTML文件名。可以使用 [name] 替换 entry name 。也可以传一个函数,例如: (entryName) => entryName + '.html' |
template | {String} | 模板文件的绝对路径或相对路径。 默认使用 src/index.ejs(如果存在) | |
templateContent | {string|Function|false} | false | 模板内容 |
templateParameters | {Boolean|Object|Function} | false | 允许覆盖模板中使用的参数 |
inject | {Boolean|String} | true | 将所有资源注入到模板或模板内容中。 'body':所有 javascript 资源都将放置在 body 元素的底部。 'head':将把脚本放在 head 元素中 true: 会根据 scriptLoading 选项将其添加到 head/body 中。 false:将禁用自动注入 |
publicPath | {String|'auto'} | 'auto' | 用于脚本和链接标记的公共路径 |
scriptLoading | {'blocking'|'defer'|'module'} | 'defer' | 即 <script>标签的type |
favicon | {String} | 图标路径 | |
meta | {Object} | {} | 允许注入 meta 标签 例如: meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'} |
通常我们配置如下:
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
inject: 'body',
})
],
3.2 清理 dist 文件夹
我们每次生成的文件都会存放在 dist 文件夹,当我们多次打包后,dist文件夹会变得杂乱,而且有些文件是无用的,对此我们可以配置output.clean在每次构建之前清理 dist 文件夹。
const path = require('path');
module.exports = {
...
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
这样每次打包后 dist 文件夹中只有本次构建的文件。
四、开发环境配置
上述介绍了webpack的基本输入输出管理以及资源加载,下面将介绍开发环境中的一些配置,使得我们开发过程中更加方便。
以下配置仅适用于开发环境,所有首先配置 mode 为 development 。
4.1 使用 source map
当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。
例如:如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js。而我们可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。
为了更容易地追踪 error 和 warning,JavaScript 提供了 source maps 功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。
源映射 提供了一种将压缩文件中的代码映射回源文件中的原始位置的方法
webpack 中通过devtool选项开启 source map。source map 有很多选项,不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
这里以 inline-source-map 形式来展示 source map 的作用。例如,我们将上述的 print.js 文件内容修改如下:
export default function printMe() {
// 这里 console 拼写错误
consle.log('I get called from print.js!');
}
执行 npm run build 打包后,我们在浏览器打开 index.html,点击按钮可以看到控制台报错如下图:
可以看到,该报错中包含有发生错误的文件(print.js)和行号(2)的引用。这对于我们调试来说还是非常友好的。
注意:生产模式下,建议关闭 source map。
4.2 使用开发工具
前面例子中,每次编译代码时,都需要手动执行 npm run build 进行构建,比较繁琐。对此,webpack 提供了几种方式,能够在代码发生变换后自动编译代码。
(1)观察模式(watch mode)
通过 webpack watch 命令实现。当其中一个文件被更新,代码将被重新编译。
可在 package.json 添加一个用于启动 webpack watch mode 的 npm scripts:
{
"scripts": {
"watch": "webpack --watch",
},
}
然后我们就可以执行npm run watch 开启观察模式,命令执行后并不会退出命令行,当我们修改某个文件内容并保存时,webpack 会自动重新编译修改后的模块。
但是该种方式的唯一缺点在于,每次修改后,需要重新刷新浏览器才能看到修改后的实际结果。
(2)webpack-dev-server
webpack-dev-server 为你提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能。
首先,我们需要安装 web server:
npm install --save-dev webpack-dev-server
然后在配置文件中添加如下配置:
module.exports = {
mode: 'development',
...
// 告知 `webpack-dev-server`,将 `dist` 目录下的文件 serve 到 `localhost:8080` 下
devServer: {
static: './dist',
},
...
};
webpack-dev-server 会从 output.path 中定义的目录中的 bundle 文件提供服务,即文件将可以通过 http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename] 进行访问。
webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在 内存 中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。如果你的页面希望在其他不同路径中找到 bundle 文件,则可以通过 dev server 配置中的
devMiddleware.publicPath选项进行修改。
接着添加一个可以直接运行 dev server 的 script:
{
"scripts": {
"start": "webpack serve --open,
},
}
在命令行中运行 npm start,我们会看到浏览器自动加载页面。如果你更改任何源文件并保存它们,web server 将在编译代码后自动重新加载
(3)webpack-dev-middleware
webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行更多自定义设置。
官网上展示了一个webpack-dev-middleware 配合 express server 的示例。
首先,安装 express 和 webpack-dev-middleware:
npm install --save-dev express webpack-dev-middleware
然后添加如下配置:
module.exports = {
...
devServer: {
static: './dist',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: '/',
},
};
我们将会在 server 脚本使用 publicPath,以确保文件资源能够正确地 serve 在 http://localhost:3000 下。
然后自定义一个 server,即在根目录下添加 server.js 文件:
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
然后中终端执行node server.js 启动服务器。然后打开浏览器,访问 http://localhost:3000,即可看到webpack应用在运行了。