背景
在刚刚接触前端时候,对于形形色色的前端工具,总是分不清其中的区别。为什么这里是这个工具,那里又是另外一个。已经有了一个,为什么又冒出来一个。这里我们通过对比 Gulp 和 webpack,介绍一下前端构建工具的其中一类:基于任务的构建。这方面在知乎上也有相关讨论。
Gulp
首先就是 Gulp 是什么呢?
A toolkit to automate & enhance your workflow.
这是官方的定义。但是哪怕翻译成中文也不好理解。接下来,我们通过完成一个小例子来理解 Gulp。 假设现在有一个前端小项目,使用 less 编写样式,typescript 编写逻辑,使用 Gulp 来进行构建,应该怎么做呢?
安装
首先我们通过 pnpm init 初始化项目。然后安装相关依赖,gulp-clean-css 和 gulp-less
处理less 文件,gulp-typescript 和 typescript gulp-uglify
处理 typescript 文件, gulp-clean
用于每次构建前删除相关目录。所有的依赖如下。
json
"devDependencies": {
"gulp": "^4.0.2",
"gulp-clean": "^0.4.0",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-htmlmin": "^5.0.1",
"gulp-less": "^5.0.0",
"gulp-typescript": "6.0.0-alpha.1",
"gulp-uglify": "^3.0.2",
"typescript": "^5.2.2"
}
项目内容
在 src 目录下,新建项目的入口文件 index.html
xml
|--src
|--index.html
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="./css/style.min.css" rel="stylesheet" />
<title>Gulp Demo</title>
</head>
<body>
<div class="one">
hello
</div>
<div class="two">
world
</div>
<div class="container">
</div>
<script src="./js/main.min.js"></script>
</body>
</html>
新建相关的 less 文件
less
|--src
|--css
|--one.less
|--two.less
|--var.less
// one.less
@import './var.less';
.one {
color: @one-color;
}
// two.less
@import './var.less';
.two {
color: @two-color;
}
// var.less
@one-color: green;
@two-color: red;
新建项目相关的 typescript 文件
typescript
|--src
|--js
|--index.ts
// index.ts
function add(a: number, b: number): number {
return a + b;
}
const container = document.querySelector('.container');
container!.innerHTML = `${add(1, 2)}`
最后项目的结构为
css
|--src
|--css
|--one.less
|--two.less
|--var.less
|--js
|--index.ts
|--index.html
在 package.json 中添加一个构建命令
json
"scripts": {
"build": "gulp"
}
Gulp 任务
上面的 index.html 文件是不可以直接在浏览器运行的。 所以接下来,我们使用 Gulp 对项目进行构建处理。在项目的根目录创建 Gulp 配置文件 gulpfile.js,运行 Gulp 的时候,会自动读取这个文件。
在 Gulp 中,一个任务就是一个js 异步函数,首先我们处理 less 文件,将其转化为 css 文件,并压缩。
scss
function lessProcess() {
return src("src/**/*.less")
.pipe(less())
.pipe(concat("style.min.css"))
.pipe(cleanCss())
.pipe(dest("dist/css"));
}
这里首先读取 src 目录下的所有 less 文件,然后传给 Gulp 的 less 插件处理,处理以后,使用 concat 插件进行拼接,然后使用 cleanCss 压缩,最后输出到 dist/css 目录下。
接下来处理 typescript 文件。同样新建一个 typescript 的处理任务。
less
function tsProcess() {
return src("src/**/*.ts")
.pipe(
ts({
lib: ["es2015", "dom"],
module: "es2015",
target: "es2015",
})
)
.pipe(concat("main.min.js"))
.pipe(uglify())
.pipe(dest("dist/js"));
}
任务的处理流程同 less 文件差不多。读取 src 目录下的 ts 文件,传给 ts 插件,最后压缩输出到 dist/js 目录下。
对于 html 文件,我们使用 htmlMin 对其进行压缩处理。
php
function htmlProcess() {
return src("src/**/*.html")
.pipe(htmlMin({ collapseWhitespace: true }))
.pipe(dest("dist"));
}
由于,我们会对项目进行多次构建,所以需要在每次构建前将 dist 目录进行删除。这样才能保证不会出现构建之间的干扰。所以,我们添加一个删除 dist 目录的任务。
php
function cleanDist() {
return src("dist", { read: false, allowEmpty: true }).pipe(clean());
}
接下来,使用 Gulp 将四个任务组合起来。
ini
exports.default = series(
cleanDist,
parallel([lessProcess, tsProcess, htmlProcess])
);
series 和 parallel 是 Gulp 中的两种任务运行方式,series 表示串行,parallel 表示并行。这里的意思中,首先运行任务 cleanDist,然后并行运行任务 lessProcess、 tsProcess 和 htmlProcess。 最后 gulpfile.js 的文件内容是:
php
const { series, parallel, src, dest } = require("gulp");
const less = require("gulp-less");
const concat = require("gulp-concat");
const cleanCss = require("gulp-clean-css");
const ts = require("gulp-typescript");
const uglify = require("gulp-uglify");
const htmlMin = require("gulp-htmlmin");
const clean = require("gulp-clean");
function cleanDist() {
return src("dist", { read: false, allowEmpty: true }).pipe(clean());
}
function lessProcess() {
return src("src/**/*.less")
.pipe(less())
.pipe(concat("style.min.css"))
.pipe(cleanCss())
.pipe(dest("dist/css"));
}
function tsProcess() {
return src("src/**/*.ts")
.pipe(
ts({
lib: ["es2015", "dom"],
module: "es2015",
target: "es2015",
})
)
.pipe(concat("main.min.js"))
.pipe(uglify())
.pipe(dest("dist/js"));
}
function htmlProcess() {
return src("src/**/*.html")
.pipe(htmlMin({ collapseWhitespace: true }))
.pipe(dest("dist"));
}
exports.default = series(
cleanDist,
parallel([lessProcess, tsProcess, htmlProcess])
);
运行pnpm build
命令,会在 dist 目录生成以下文件
css
|--dist
|--css
|--style.min.css
|--js
|--main.min.js
index.html
运行 index.html 文件以后,就可以看到项目结果。
Gulp 小结
可以感受到,Gulp 的使用方式,就是建立一个一个的任务,将这些任务组合起来,交给 Gulp 自动执行。对于 Gulp 本身,只是负责执行这些任务,而任务的具体内容,则需要开发者自己编写或使用已有插件。
webpack
在这一小节中,我们使用 webpack 来构建上面的小项目。首先修改一下 index.html 和 index.ts。 删除 index.html 中对 css 和 js 的引用。
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack Demo</title>
</head>
<body>
<div class="one">
hello
</div>
<div class="two">
world
</div>
<div class="container">
</div>
</body>
</html>
然后修改 index.ts,在这里引入样式文件。
typescript
import '../css/one.less';
import '../css/two.less';
function add(a: number, b: number): number {
return a + b;
}
const container = document.querySelector('.container');
container!.innerHTML = `${add(1, 2)}`
接下来安装相关依赖。
json
"devDependencies": {
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"style-loader": "^3.3.3",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
修改构建脚步命令:
json
"scripts": {
"build": "webpack"
},
在项目根目录下,新建 webpack.config.js 文件
javascript
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/js/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.min.js",
clean: true, // 清空输出目录
},
module: {
rules: [
{
test: /.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".ts"],
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
};
由于我们使用了 typescript 和 ts-loader,需要新建一个 tsconfig.json 文件,这里内容为空就可以了。
运行pnpm build
命令,会在 dist 目录下生成 index.html 和 main.min.js 文件,浏览器打开 index.html,运行结果同上一小节的结果一样。
webpack 的方式中,我们在 webpack.config.js 文件中配置了入口entry: "./src/js/index.ts"
,webpack 会从这个文件开始,递归收集所有的依赖文件,对收集的文件使用 loader 处理,最后打包成一个文件,输出到 dist 目录下。
对比
实际上,在上面的小项目中,我们做了一个取巧,就是只有一个 indext.ts 文件。如果存在多个 typescript 文件,文件之间通过 import 引用。那么上面的 Gulp 任务是无法处理的。下面我们试一下。
在src 目录下新建一个 subract.ts
typescript
// subract.ts
const subtract = (a: number, b: number): number => {
return a - b;
}
export default subtract;
然后在 index.ts 文件中引入
typescript
// index.ts
import '../css/one.less';
import '../css/two.less';
import sub from './subtract';
function add(a: number, b: number): number {
return a + b;
}
const container = document.querySelector('.container');
container!.innerHTML = `${add(1, 2)}`
console.log(sub(1, 2));
使用 Gulp 的方式,只会将 index.ts 和 subract.ts 分别编译为 js 代码,然后简单拼接起来,依然不能运行。使用 webpack 的方式,则可以生成可运行的代码。
如果我们想要使 Gulp 的方式生效,则需要使用插件解决 typescript 文件之间的引用问题。
所以可以看到,Gulp 和 webpack 完全就不是一类东西。虽然都可以用来处理前端项目,但是使用的场景却不太一样。在需要处理引用依赖的情况下,比如需要合并多个通过 import 关联的文件,可能更适合 webpack。如果是一些文件复制,文件内容的替换,文件拼接,文件压缩,这些情况下,就可以考虑使用 Gulp。
还有一种用法就是,可以将 webpack 直接集成到 Gulp 里面使用。比如下面这段来自 @ant-design/tools 的代码。
我们可以感受到,Gulp 并不算是一个前端的构建工具,而是一个任务运行器。我们可以将一些重复性的工作,转为 Gulp 的一个任务,然后让 Gulp 来重复执行这个工作。
在前端构建工具快速发展的今天,大家在一般的业务开发中,基本很少会使用到 Gulp。所以就会有种 Gulp 已经过时的感觉。但是在某些场景下面,Gulp 依然在被广泛使用,甚至不可替代。比如当我最近在项目上对组件库的样式进行处理的时候,需要使用tailwind 生成样式文件,添加对 tailwind 生成文件的引用,需要对所有的 less 文件进行复制,还需要使用 babel 将 js 代码转为 ast,然后对代码内容修改。这些任务都可以转化为一个一个的 Gulp 任务,使用 Gulp 执行。
在开源方面,也可以看到一些 Gulp 的使用。比如,下面的代码是 antd 使用 Gulp 对文件内容进行更改,将 less 引用替换为 css 引用。
总结
我们首先创建了一个小项目,然后使用 Gulp 和 webpack 分别对其处理,最后对比了两种方式的区别。Gulp 同之前的 AMD 和 Browserify 有一点不一样就是,Gulp 虽然用的少,但是在某些情况下,还是有使用场景。而 AMD 和 Browserify 在现在的 web 项目开发中,感觉基本没有使用场景了。除了 Gulp,还有一个类似的工具 Grunt,因为功能类似就不展开说了。构建工具的历史内容就这么多了,后续我们看看 webpack 以及 rollup 又是怎么一回事呢?