2012 前后 构建工具 1.0(Grunt / Gulp 任务流)
初步认识
问题背景(为什么需要 Grunt/Gulp?)
在 模块化(阶段二) 之后,前端项目不再是几个小文件,而是:
- JS 代码越来越多,要压缩才能上线。
- CSS 文件越来越多,要合并、加前缀。
- 图片体积大,要压缩。
- 浏览器调试 → 改完代码要手动刷新,很低效。
- 团队协作混乱,需要一套"标准化构建流程"。
👉 于是,大家需要一个工具,来 自动执行重复的构建任务,避免手工操作。
解决方案:任务执行器
1. Grunt(2012)
- 思路:
- 一切基于 配置。
- 写一个
Gruntfile.js
,在里面定义任务(task)。 - Grunt 会按照你设定的顺序依次执行任务。
- 例子:
javascript
// Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
uglify: { // JS 压缩
build: {
src: 'src/app.js',
dest: 'dist/app.min.js'
}
},
cssmin: { // CSS 压缩
build: {
src: 'src/style.css',
dest: 'dist/style.min.css'
}
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['uglify', 'cssmin']);
};
👉 执行方式 :grunt
结果:把 JS 和 CSS 压缩到 dist
文件夹。
- 特点:配置多、灵活,但执行速度慢,因为是"写到磁盘 → 读出来 → 再写磁盘"的模式。
2. Gulp(2013)
- 思路:
- 用 代码流(stream) 代替繁琐配置。
- 文件在内存中传递,不需要反复写磁盘 → 更快。
- 例子:
javascript
// gulpfile.js
const { src, dest, series } = require('gulp');
const uglify = require('gulp-uglify');
const cleanCSS = require('gulp-clean-css');
function jsTask() {
return src('src/app.js')
.pipe(uglify()) // 压缩 JS
.pipe(dest('dist'));
}
function cssTask() {
return src('src/style.css')
.pipe(cleanCSS()) // 压缩 CSS
.pipe(dest('dist'));
}
exports.default = series(jsTask, cssTask);
👉 执行方式 :gulp
结果:同样压缩 JS 和 CSS,但比 Grunt 快。
- 特点 :
- API 更直观,代码风格比 Grunt 配置式更容易接受。
- "流水线思维" → 代码文件像水一样流过一条管道(pipe)。
典型任务
无论是 Grunt 还是 Gulp,当时前端工程化的"核心任务"是:
- JS 处理
- Uglify 压缩
- 合并多个文件
- Babel 转码(后期)
- CSS 处理
- 合并、压缩
- Autoprefixer 自动加前缀
- Sass/Less 转 CSS
- 图片处理
- 压缩(image-min)
- Base64 内联小图
- 开发体验
- Live Reload(改代码后自动刷新浏览器)
- 文件监听(watch)
👉 总结一句:构建工具 1.0 就是"自动化脚本集",解决重复劳动。
影响与局限
- 影响
- 提升开发效率,第一次让前端有了 "工程化的味道"。
- 为后来的 Webpack/Vite 奠定了"自动化构建"的思想基础。
- 局限
- 本质上只是"任务执行器",它不会帮你处理模块化(依赖图)。
- 依赖管理和打包还是要靠 Browserify、后来的 Webpack。
- 随着项目规模变大,配置/任务越来越复杂,维护成本高。
✅ 总结一句话:
Grunt/Gulp = 自动化流水线工人 ,帮你干重复的体力活(压缩、合并、刷新),但它 不懂模块化,只是执行任务。
压缩深入
我们聚焦 压缩(Minify) 这一任务,看看 Grunt 和 Gulp 各自怎么干的,以及"压缩前后到底发生了什么"。
背景:为什么要压缩?
在 2010 年前后,前端工程化刚起步,主要痛点是:
- 网速没现在快,带宽宝贵,文件越小越好。
- 服务器并发处理能力有限,减少流量、提升加载速度 成为刚需。
于是:压缩(Minify)= 自动构建工具里最重要的任务之一。
Grunt 的压缩方式(配置驱动)
Grunt 是"任务执行器"(Task Runner),压缩就是一个 task。
- 工作流:
- 安装插件(如
grunt-contrib-uglify
→ JS 压缩,grunt-contrib-cssmin
→ CSS 压缩)。 - 在
Gruntfile.js
里配置源文件与目标文件。 - 运行
grunt uglify
或grunt cssmin
,插件读取文件 → 压缩 → 写入目标目录。
- 安装插件(如
- 配置示例:
javascript
module.exports = function(grunt) {
grunt.initConfig({
uglify: {
build: {
files: {
'dist/app.min.js': ['src/app.js']
}
}
},
cssmin: {
build: {
files: {
'dist/style.min.css': ['src/style.css']
}
}
}
});
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.registerTask('default', ['uglify', 'cssmin']);
};
👉 运行结果:手动执行一次 → 输出压缩后的产物。
👉 特点:基于配置文件,一步一步执行,流程偏"串行"。
Gulp 的压缩方式(流式驱动)
Gulp 是"流式任务执行器",把文件看作 流(stream),边读边处理,效率比 Grunt 高。
- 工作流:
- 安装插件(如
gulp-uglify
→ JS 压缩,gulp-clean-css
→ CSS 压缩)。 - 在
gulpfile.js
里用代码写出"管道":源文件 → 压缩 → 输出。
- 安装插件(如
- 配置示例:
javascript
const { src, dest, series } = require('gulp');
const uglify = require('gulp-uglify');
const cleanCSS = require('gulp-clean-css');
function js() {
return src('src/*.js')
.pipe(uglify())
.pipe(dest('dist'));
}
function css() {
return src('src/*.css')
.pipe(cleanCSS())
.pipe(dest('dist'));
}
exports.default = series(js, css);
👉 运行结果:代码一改动,就能快速跑一遍 pipeline。
👉 特点:基于代码逻辑,支持并行/串行,效率更高,体验更"现代"。
压缩前后内容差别
以 JS 为例
- 压缩前:
plain
function add(a, b) {
// 求和函数
return a + b;
}
console.log(add(1, 2));
- 压缩后:
plain
function add(n,d){return n+d}console.log(add(1,2));
- 变化:
- 空格、换行 → 删除
- 注释 → 删除
- 变量名 → 缩短(有时会从
total
→t
) - 冗余代码 → 删除(比如死代码)
CSS 压缩
- 压缩前:
plain
body {
background: white;
margin: 0px;
}
- 压缩后:
plain
body{background:#fff;margin:0}
HTML 压缩
- 压缩前:
plain
<div>
<!-- 这是容器 -->
<span> Hello </span>
</div>
- 压缩后:
plain
<div><span>Hello</span></div>
运行上的差别
- 功能一致:压缩前和压缩后,执行结果是一样的。
- 区别在于性能 :
- 体积小 → 下载更快,尤其在 2G/3G 时代。
- 解析快 → 浏览器少解析无用字符。
- 变量混淆 → 增加代码保护(轻度的"防反编译")。
总结对比
维度 | Grunt 压缩 | Gulp 压缩 |
---|---|---|
工作方式 | 配置驱动,串行执行 | 流式驱动,代码式任务流 |
插件示例 | grunt-contrib-uglify , grunt-contrib-cssmin |
gulp-uglify , gulp-clean-css |
性能 | 文件 I/O 多次读写 | 流式处理,效率更高 |
场景 | 适合简单脚本 | 适合复杂任务流 |
一句话总结:
👉 Grunt :像"流水线工人,配置表写好,按部就班干活";
👉 Gulp:像"装配机器人,拿一堆零件,流过一遍全处理好"。
合并深入
前置思考
将多个文件合并成一个文件,以JS为例,这个合并是指将所有JS文件里的代码放到一个文件中吗?这样不会导致全局污染吗?
为什么要"合并"?
当时的背景(2010 年前后):
- HTTP/1.1 协议 的请求有开销,每发一个请求都要经历:
- DNS 解析
- TCP 三次握手
- HTTP 请求头、响应头传输
- 浏览器对同一域名的并发请求数有限(一般 6 个)。
👉 如果你有 50 个 JS 文件,每个都要请求一次,页面就会 卡得要命。
所以思路是:
把这 50 个 JS 文件合并成一个 app.js
,只发一次请求,速度立刻提升。
Grunt / Gulp 怎么"合并"?
就是字面意思:把文件内容拼接在一起。
例子:
- 文件
a.js
:
plain
function foo() {
console.log('foo');
}
- 文件
b.js
:
plain
function bar() {
console.log('bar');
}
- **合并后 **
**bundle.js**
:
plain
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
👉 真的是"字符串拼接" → 存到一个新文件。
👉 Grunt 用 grunt-contrib-concat
,Gulp 用 gulp-concat
。
全局污染问题
你说的对,单纯拼接文件确实会导致全局污染,因为:
- 当时的 JS 没有模块化标准(ESM 还没普及,CommonJS 也只是 Node.js 的方案)。
- 各个 JS 文件里写的函数、变量,全都挂在全局作用域(
window
)上。 - 合并之后,不同文件里的变量名可能冲突:
a.js
:
plain
var count = 1;
markdown
- `b.js`:
plain
var count = 2;
markdown
- 合并后,第二个 `count` 会直接覆盖第一个 → 出 Bug。
当时的解决方式
工程师们也知道会有全局污染,于是想了几种办法:
- 命名空间模式
- 把所有变量挂到一个对象上:
plain
var APP = APP || {};
APP.utils = {
add: function(a, b) { return a + b; }
};
markdown
- 避免直接污染 `window`。
- IIFE(立即执行函数表达式) - 每个文件外面包一个函数,形成私有作用域:
plain
(function() {
var count = 1;
console.log(count);
})();
markdown
- 这样 `count` 就不会泄漏到全局。
- 模块化雏形(AMD / CMD) - RequireJS(AMD)和 SeaJS(CMD)出现,用
define
/require
写模块:
plain
define(['dep'], function(dep) {
return { foo: function() {} }
});
markdown
- 构建工具把这些"模块"合并打包在一起,运行时由 `require.js` 管理。
总结
- 合并的本质:就是把多个文件内容拼到一个文件里,减少 HTTP 请求数。
- 风险:确实容易导致全局污染。
- 应对方法:IIFE、命名空间、早期模块化方案(AMD/CMD)。
- 意义 :虽然"粗糙",但为后来的 Webpack 时代"一切皆模块" 打下了基础。
👉 我们可以这么理解:
在 Grunt/Gulp 时代,合并是 性能驱动 的策略,而不是 模块化驱动 的。模块化真正的全面落地,是 Webpack / Rollup 之后的事。