从零构建 webpack 脚手架(基础篇)
webpack 是一个现代 JavaScript 应用程序的静态模块打包工具,它对于前端工程师来说可谓是如雷贯耳,基本上现在的大型应用都是通过 webpack 进行构建的。
webpack 具有高度可配置性,它拥有非常丰富的配置。在过去一段时间内曾有人将熟练配置 webpack 的人称呼为 “webapck 工程师”。当然,这称呼只是个玩笑话,但也能从侧面了解到 webpack 配置的灵活与复杂。
为了能够熟练掌握 webpack 的使用,接下来通过几个例子循序渐进的学习如何使用 webpack。
以下 Demo
都可以在 Github 的 webpack-example 中找到对应的示例,欢迎 star~
本篇文章内容略长,建议先马后看。由于知乎不支持代码折叠,因此建议直接看原文 从零构建 webpack 脚手架(基础篇) | Anran758's blog 以获得更好的阅读体验。
目录预览
- 起步
- 使用配置
- Plugin
- loader
- css
- image
- HTML 资源引入
- lodash template
- 静态目录
- html-loader
- 总结
起步
从 webpack@v4.0.0
开始,就可以不用再引入配置文件来打包项目。若没有提供配置的话,webpack 将按照默认规则进行打包。默认情况下 src/index
是项目的源代码入口,打包后的代码会输出到 dist/main.js
上。
首先来初始化一个项目,项目名为 getting-started:
# 创建项目文件夹
mkdir getting-started
# 进入项目目录
cd getting-started
# npm 项目
npm init -y
初始化项目后,项目目录会新增一个 package.json
,该文件记录了项目依赖的相关信息。若想要使用 webpack 的话需要安装它的依赖: webpack
(本体)和 webpack-cli
(可以在命令行操作 webpack 的工具):
# -D 和 --save-dev 选项都可以用于安装开发依赖
# npm i --save-dev webpack webpack-cli
npm i -D webpack webpack-cli
# 或者使用 yarn 安装开发依赖
yarn add -D webpack webpack-cli
接着创建 webpack 所需的默认入口文件 src/index.js
以及测试模块所用的 src/log.js
文件。此时的项目结构大致如下:
.
├── package.json
+ ├── src
+ │ ├── index.js
+ │ └── log.js
└── node_modules
javascript:
// src/log.js
export const log = (name) => console.log(`Hello ${name}!`);
// src/index.js
import { log } from './log'
log('anran758');
src/log.js
导出了一个工具函数,它负责向控制台发送消息。src/index.js
是默认的入口文件,它引入 log
函数并调用了它。
上面的代码很简单,像这种模块化的代码按照传统 <script src>
引入的话,浏览器是不能正确执行的。可以在根目录上创建一个 index.html
引入 js 脚本来测试一下:
<!-- /index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<!-- 引入脚本 -->
<script src="./src/index.js"></script>
</body>
</html>
创建文件后,将上例代码复制到 `index.html` 中。保存并打开该文件,看看浏览器能否正确处理模块逻辑。不出意外的话,文件在浏览器打开后,浏览器开发者工具会抛出错误信息:
Uncaught SyntaxError: Cannot use import statement outside a module
言下之意就是说浏览器不能正确的解析 ES module
语句,此时 webpack 就可以派上用场啦~ 在 package.json
中的 scripts
字段中添加如下命令:
"scripts": {
+ "build": "webpack"
- "test": "echo \"Error: no test specified\" && exit 1"
},
在命令行输入 npm run build
调用 webpack
对当前项目进行编译,编译后的结果会输出到 dist/main.js
文件中(即便本地没有 dist 目录,它都会自动创建该目录)。输出文件后,修改 index.html
对 js 的引用:
<!-- /index.html -->
<body>
+ <script src="./dist/main.js"></script>
- <script src="./src/index.js"></script>
</body>
重新刷新页面后就能看到 log
正确的输出了 Hello anran758!
。点击 log 右侧的链接,可以跳转至 Source
面板,将代码格式化后可以清晰地看到编译后 js 的变化:
![](https://pic2.zhimg.com/80/v2-689a2893c88c6eac461617dab72eebe5_720w.jpg)
使用配置
当然,上例代码只不过是小试牛刀。对于正式的项目会有更复杂的需求,因此需要自定义配置。webpack
主要有两种方式接收配置:
第一种: 通过 Node.js
API引入 webpack 包,在调用 webpack 函数时传入配置:
const webpack = require("webpack");
const webpackConfig = {
// webpack 配置对象
}
webpack(webpackConfig, (err, stats) => {
if (err || stats.hasErrors()) {
// 在这里处理错误
}
// 处理完成
});
第二种: 通过 webpack-cli
在终端使使用 webpack 时指定配置。
webpack [--config webpack.config.js]
两种方法内配置都是相似的,只是调用的形式不同。本篇先使用 webpack-cli
来做示例。
webpack 接受一个特定的配置文件,配置文件要求导出一个对象、函数、Promise
或多个配置对象组成的数组。
现在将上一章的 Demo 复制一份出来,并重命名为 getting-started-config,在该目录下新建 webpack.config.js
文件,文件内容如下:
const path = require('path');
module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),
// 输出 bundle 的名称
filename: "bundle.js",
}
}
上面的配置主要是定义了程序入口、编译后的文件输出目录。然后在 src/index.js
中修改一些内容用来打包后测试文件是否被正确被编译:
import { log } from './log'
+ log('本节在测试配置噢');
- log('anran758');
随后在终端输入 num run build
进行编译,可以看到 dist
目录下多了个 bundle.js
。
$ npm run build
> webpack --config ./webpack.config.js
Hash: 3cd5f3bbfaf23f01de37
Version: webpack 4.43.0
Time: 117ms
Built at: 05/06/2020 1:01:37 PM
Asset Size Chunks Chunk Names
bundle.js 1010 bytes 0 [emitted] main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 123 bytes {0} [built]
| ./src/index.js 62 bytes [built]
| ./src/log.js 61 bytes [built]
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
由于我们输出的文件名被修改了,此时还得修改 html
的引入路径。但每改一次输出目录,HTML
中的引入路径也得跟着改,这样替换的话就比较容易出纰漏。那能不能让 webpack 自动帮我们插入资源呢?答案是可以的。
Plugin
webpack 提供插件(plugin)的功能,它可以用于各种方式自定义 webpack 构建过程。
html-webpack-plugin 可以在运行 webpack 时自动生成一个 HTML
文件,并将打包后的 js
代码自动插入到文档中。下面来安装它:
npm i --D html-webpack-plugin
安装后在 webpack.config.js
中使用该插件:
const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",
// 输出配置
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),
// 输出 bundle 的名称
filename: "bundle.js",
},
+ plugins: [
+ new HtmlWebpackPlugin({
+ title: 'Test Configuration'
+ })
+ ],
}
重新编译后 HTML
也被输出到 dist
目录下。查看 dist/index.html
的源码可以发现:不仅源码被压缩了,同时 <script>
标签也正确的引入了 bundle.js
。
后续目录展示会将node_modules
、package-lock.json
、yarn.lock
这种对项目架构讲解影响不大的目录省略掉..
此时目录结构如下:
.
├── dist
│ ├── bundle.js
│ ├── index.html
│ └── main.js
├── index.html
├── package.json
├── src
│ ├── index.js
│ └── log.js
└── webpack.config.js
处理完资源自动插入的问题后,还有一个问题需要我们处理:虽然 webpack 现在能自动生成 HTML
并插入脚本,但我们还得在 HTML
中写其他代码逻辑呀,总不能去改 /dist/index.html
文件吧?
这个问题也很好解决。html-webpack-plugin
在初始化实例时,传入的配置中可以加上 template
属性来指定模板。配置后直接在指定模板上进行编码就可以解决这个问题了:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 起点或是应用程序的起点入口
entry: "./src/index",
// 输出配置
output: {
// 编译后的输出路径
// 注意此处必须是绝对路径,不然 webpack 将会抛错(使用 Node.js 的 path 模块)
path: path.resolve(__dirname, "dist"),
// 输出 bundle 的名称
filename: "bundle.js",
},
plugins: [
// html-webpack-plugin
// https://github.com/jantimon/html-webpack-plugin#configuration
new HtmlWebpackPlugin({
title: 'Test Configuration',
+ template: path.resolve(__dirname, "./index.html"),
})
],
}
使用模板后 html-webpack-plugin
也会自动将脚本插入到模板中。因此可以将模板中的 <script>
给去掉了。为了测试输出的文件是否使用了模板,在 <body>
内随便插入一句话,重新打包后预览输出的文件是否包含这句话:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Test Config</title>
- <title>Test</title>
</head>
<body>
+ <p>Test Config</p>
- <script src="./dist/main.js"></script>
</body>
</html>
修改文件后,重新打包就能看到模板也被压缩输出至 /dist/index.html
了,script
标签也正常的插入了。
清理目录
现在来看编译后的目录,我们发现 dist/mian.js
这文件是使用配置之前编译出来的文件,现在我们的项目已经不再需要它了。这种历史遗留的旧文件就应该在每次编译之前就被扔进垃圾桶,只输出最新的结果。
clean-webpack-plugin 或 rimraf 可以完成清理功能。前者是比较流行的 webpack 清除插件,后者是通用的 unix 删除命令(安装该依赖包后 windows 平台也能用)。如果仅是清理 /dist
目录下文件的话,个人是比较倾向使用 rimraf
的,因为它更小更灵活。而 clean-webpack-plugin
是针对 webpack 输出做的一系列操作。
在终端安装依赖:
npm i -D rimraf
rimraf
的命令行的语法是: rimraf <path> [<path> ...]
,我们在 package.json
的 scirpts
中修改 build
的命令:
"scripts": {
+ "build": "rimraf ./dist && webpack --config ./webpack.config.js"
- "build": "webpack --config ./webpack.config.js"
}
重新运行脚本:
$ npm run build
> rimraf ./dist && webpack --config ./webpack.config.js
Hash: 763fe4b004e1c33c6876
Version: webpack 4.43.0
Time: 342ms
Built at: 05/06/2020 2:35:49 PM
Asset Size Chunks Chunk Names
bundle.js 1010 bytes 0 [emitted] main
index.html 209 bytes [emitted]
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 123 bytes {0} [built]
| ./src/index.js 62 bytes [built]
| ./src/log.js 61 bytes [built]
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
Child HtmlWebpackCompiler:
1 asset
Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
1 module
这样 webpack 输出的 /dist
目录始终是最新的东西。
loader
在正常的页面中,引入 css
样式表会让页面变得更美观。引入图片可以让页面内容更丰富。
然而 webpack 本体只能处理原生的 JavaScript 模块,你让它处理 css
或图片资源,它是无法直接处理的。为了处理这种问题,webpack 提供了 loader 的机制,用于对模块外的源码进行转换。
loader
一般是单独的包,我们可以在社区找到对应 loader
来处理特定的资源。在使用前通过 npm
安装到项目的开发依赖中即可。loader
可以通过配置、内联或 Cli 这三种方式来使用。下文主要以 配置
的方式来使用。
css
往常引入 css
样式表无非就是在 html
中通过 <link>
标签引入。现在想通过 webpack 来管理依赖得需要安装对应的 loader
来处理这些事。
css-loader 可以让 webpack 可以引入 css
资源。光有让 webpack 识别 css 的能还不够。为了能将 css
资源进行导出,还要安装 mini-css-extract-plugin 插件:
现在将上一节的 Demo 复制并重名为 getting-started-loader-css。进入新的项目目录后安装依赖:
npm install -D css-loader mini-css-extract-plugin
在更改配置之前,为了使项目结构更清晰,咱们按照文件类型重新调整源码目录结构。将 src
下的 js
文件都放进 js
文件夹中。同时创建 /src/css/style.css
样式表。调整后的目录结构如下:
.
├── package.json
├── src
│ ├── index.html
│ ├── css
│ │ └── style.css
│ └── js
│ ├── index.js
│ └── log.js
└── webpack.config.js
现在将 Flexbox 布局用例 中结尾的 Demo 迁移到项目中,测试一下效果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<div class="panels">
<div class="panel panel1">
<p class="item name">Alice</p>
<p class="item index">I</p>
<p class="item desc">Pixiv Content ID: 65843704</p>
</div>
<div class="panel panel2">
<p class="item name">Birthday</p>
<p class="item index">II</p>
<p class="item desc">Pixiv Content ID: 70487844</p>
</div>
<div class="panel panel3">
<p class="item name">Dream</p>
<p class="item index">III</p>
<p class="item desc">Pixiv Content ID: 65040104</p>
</div>
<div class="panel panel4">
<p class="item name">Daliy</p>
<p class="item index">IV</p>
<p class="item desc">Pixiv Content ID: 64702860</p>
</div>
<div class="panel panel5">
<p class="item name">Schoolyard</p>
<p class="item index">V</p>
<p class="item desc">Pixiv Content ID: 67270728</p>
</div>
</div>
</body>
</html>
CSS 源码:
html {
font-family: 'helvetica neue';
font-size: 20px;
font-weight: 200;
background: #f7f7f7;
}
body,
p {
margin: 0;
}
.panels {
display: flex;
min-height: 100vh;
overflow: hidden;
}
.panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: white;
background: #ececec;
text-align: center;
box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.1);
transition: font-size 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11),
flex 0.7s cubic-bezier(0.61, -0.19, 0.7, -0.11), background 0.2s;
font-size: 20px;
background-size: cover;
background-position: center;
cursor: pointer;
}
.panel1 {
background-color: #f4f8ea;
}
.panel2 {
background-color: #fffcdd;
}
.panel3 {
background-color: #beddcf;
}
.panel4 {
background-color: #c3cbd8;
}
.panel5 {
background-color: #dfe0e4;
}
.item {
flex: 1 0 auto;
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.5s;
font-size: 1.6em;
font-family: 'Amatic SC', cursive;
text-shadow: 0 0 4px rgba(0, 0