行业里有一种批评,说前端太浮躁,总是追逐新技术,感觉 grunt 还不熟悉,突然一夜之间满大街都在谈论 gulp 了。月影觉得不能怪技术发展太快,技术发展总是带来好处多于坏处,有时候我们确实需要鼓起勇气去"追求"技术潮流,当然理由是为了弄明白为什么有这些技术工具,而无关于什么浮躁之类的事儿。
也许是从业很多年有点累了,月影也对技术有些后知后觉,感觉 gulp 已经火了很久,才终于想起来写这篇文章,也许现在,很多工程师早已又去追求其他的什么类似的构建工具了。不管怎么样,如果你是一位前端工程师,你从来没有想过用构建工具优化网站这种事儿,或者你在工作中所在的团队和平台已经有成熟的工具,工作中不用自己再去琢磨 gulp 。你仍然可以暂时停下来阅读这篇文章,看看 gulp 这样的构建工具如何能帮你更简单地在构建的时候自动优化你的网站,也许你的个人博客也需要优化,也许你换了工作,要和之前熟悉不一样的构建工具,然而基本原理终归是"一招鲜吃遍天"的,不是吗?
Gulp 的官网title上对这个工具有一个比较准确的定义,叫做:基于流的自动化构建工具。如果你查看它的网页源代码,还会看到在<meta>
标签里有一段更详细的描述:
Gulp.js 是一个自动化构建工具,开发者可以使用它在项目开发过程中自动执行常见任务。Gulp.js 是基于 Node.js 构建的,利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。Gulp.js 源文件和你用来定义任务的 Gulp 文件都是通过 JavaScript(或者 CoffeeScript )源码来实现的。
所以,Gulp 是在项目开发过程中自动执行任务的一个工具,通过它可以方便地在开发时(或者发布前),对目标文件的内容进行I/O操作。
由于 Gulp 是基于流的,所以 Gulp 对于文件内容的操作就像是水槽对于水流,水流流经水槽,水槽将水流塑造成不同的形状。
既然是基于流的,在进一步理解 Gulp 前,我们最好先来理解什么是流。
在计算机系统中文件的大小是变化很大的,有的文件内容非常多非常大,而 Node.js 里文件操作是异步的,如果用一个形象的比喻来形容将一个文件的内容读取出来并写入另一个文件中,可以将它想象成文件内容像是水流,从一个文件"流"入另一个文件。
在node里面,读写文件可以用"流"来描述:
"use strict";
let fs = require("fs");
fs.createReadStream("./in.txt")
.pipe(fs.createWriteStream("./out.txt"));
上面的代码除了将 in.txt 文件中的内容输出到 out.txt 之外,不做其他任何事情,相当于复制了一份数据,从语法形式上可以看到,"数据流"从 fs.createReadStream 创建然后经过 pipe 流出,最后到 fs.createWriteStream。
在这输入流到输出流的中间,我们可以对"流"做一些事情:
"use strict";
let fs = require("fs");
let through = require("through2");
fs.createReadStream("./in.txt")
.pipe(through.obj(function (contents, enc, done) {
if(enc === "buffer"){
contents = contents.toString("utf-8");
enc = "utf-8";
}
done(null, contents, enc);
}))
.pipe(through.obj(function (contents, enc, done) {
done(null, contents.toUpperCase(), enc);
}))
.pipe(through.obj(function (contents, enc, done) {
contents = contents.split("").reverse().join("");
done(null, contents, enc);
}))
.pipe(fs.createWriteStream("./out.txt"));
在上面的代码里,我们通过 Node.js 的 through2 库(这是一个针对"流"的包装库),将输入流一步步转换成输出流,在中间的 pipes 中我们先是将 Buffer 转成 String,然后将它变成大写,最后再 reverse 然后传给输出流。
所以如果 in.txt 的文件内容是 hello world~
,那么 out.txt 的文件内容将是: ~DLROW OLLEH
。
月影觉得 Gulp 的文档其实写得挺烂的,点中文文档页面除了让你看入门指南、API文档、CLI文档、编写插件文档之外就没什么了,但实际上真要用 Gulp 的高级功能,这些文档简直就和教人如何画马一样:
既然 Gulp 是基于流的,我们就要理解 Gulp 如何控制和操作流。
然而在这之前,我们还要先看最基础的(还没安装 Gulp 的同学可以照前面那个入门指南安装一下~)
var gulp = require("gulp");
gulp.task("sync1", function() {
console.log("我是一个同步任务");
});
gulp.task("async", function(done) {
setTimeout(function(){
console.log("我是一个异步任务");
done();
}, 2000);
});
我们可以看到 Gulp 是基于任务的,gulp.task 可以定义一个任务,这样的话,我们在命令行下就可以通过 gulp 任务名
的方式来执行命令了:
$ gulp sync1
[18:27:12] Using gulpfile ~/Workspace/yuntu/myblog-master/item/photohome/gulpfile.js
[18:27:12] Starting "sync1"...
我是一个同步任务
[18:27:12] Finished "sync1" after 122 μs
$ gulp async
[18:27:48] Using gulpfile ~/Workspace/yuntu/myblog-master/item/photohome/gulpfile.js
[18:27:48] Starting "async"...
我是一个异步任务
[18:27:50] Finished "async" after 2 s
Gulp 的任务可以是同步和异步,在异步任务中确定任务完成,可以通过调用函数参数 done 来实现。
Gulp 也允许我们将任务组合起来执行:
var gulp = require("gulp");
var through = require("through2");
gulp.task("sync1", function() {
console.log("我是一个同步任务");
});
gulp.task("sync2", function() {
console.log("我是另一个同步任务");
});
gulp.task("sync3", function() {
console.log("我是又一个同步任务");
});
gulp.task("async", function(done) {
console.log("老大喊我去搬砖");
setTimeout(function(){
console.log("我是一个异步任务");
done();
}, 2000);
});
gulp.task("syncs", ["async", "sync1", "sync2", "sync3"],
function(){
console.log("砖搬完了!");
});
$ gulp syncs
[18:30:30] Using gulpfile ~/Workspace/yuntu/myblog-master/item/photohome/gulpfile.js
[18:30:30] Starting "async"...
老大喊我去搬砖
[18:30:30] Starting "sync1"...
我是一个同步任务
[18:30:30] Finished "sync1" after 142 μs
[18:30:30] Starting "sync2"...
我是另一个同步任务
[18:30:30] Finished "sync2" after 55 μs
[18:30:30] Starting "sync3"...
我是又一个同步任务
[18:30:30] Finished "sync3" after 43 μs
我是一个异步任务
[18:30:32] Finished "async" after 2 s
[18:30:32] Starting "syncs"...
砖搬完了!
[18:30:32] Finished "syncs" after 38 μs
我们看到说 gulp.task 可以有依赖,只要第二个参数传一个数组,中间加上依赖的任务就行了,而数组里面的这些任务是并行处理的,不会一个执行完才执行另一个(同步任务的输出比异步任务的结束早)。
以上是 Gulp 基本的任务模型。对于每个 task,Gulp 通常用来操作文件输入和输出流,因此 Gulp 封装了批量操作文件流的 api:
gulp.task("src-dist", function(){
gulp.src("./*.html")
.pipe(gulp.dest("./dist"));
});
上面的命令表示将当前目录下所有的 .html 文件匹配出来,依次输出到目标文件夹 ./dist 中去。
我们还可以用更高级的通配符:
gulp.task("src-dist", function(){
gulp.src("./**/*.html")
.pipe(gulp.dest("./dist"));
});
这样处理的 html 文件不仅仅匹配当前目录下的,还包括所有子目录里。关于输入这块,具体的用法还有很多,遵循的规范是glob模式,可以参考 node-glob
与上面说的 FileSystem 文件流类似,如果我们不做什么别的事情,那么我们就只是将文件从源 src,拷贝到了目的地 dest,其他的啥也没做,那么显然,我们可以做那么一些事情,在这里,我们尝试处理一下 index.html:
gulp.task("build-index", function(){
gulp.src("./index.html")
.pipe(through.obj(function(file, encode, cb) {
var contents = file.contents.toString(encode);
var HTMLMinifier = require("html-minifier").minify;
var minified = HTMLMinifier(contents, {
minifyCSS: true,
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
//console.log(minified);
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist"));
});
gulp.src 的输入流和 fileReadStream 会有一点点不一样,它的第一个参数不是一个 Buffer,而是一个包含文件信息和文件内容的对象,第二个参数是文件的编码,因此我们可以通过
var contents = file.contents.toString(encode);
将文件内容转成字符串。之后,我们使用 html-minifier 对文件的HTML内容和内联的样式、脚本进行压缩,这样就简单完成了首页 index.html 的优化!
前面只完成了优化的第一步,我们还没考虑外链资源该怎么处理呢,外链资源包括 js、 css 和图片。在处理之前,我们来约定一些规范:
页面 js 存放在 ./static/js 下,公共的库放在 ./static/js/lib 下,公共库只压缩不合并,页面 js 压缩并合并。
页面 css 存放在 ./static/css 下,公共的css放在 ./static/css/common 下,公共 css 只压缩不合并,页面 css 压缩并合并。
图片资源中小于3kb的图片以 base64 方式内联,图片放在 ./static/img 下。
压缩 js
function minifyAndComboJS(name, encode, files){
var fs = require("fs");
var UglifyJS = require("uglify-js");
var content = "";
files.forEach(function(js){
var minified = UglifyJS.minify(js).code;
content += minified;
});
if(content){
var combo = "static/js/" + name;
}
fs.writeFileSync(combo, content);
gulp.src(combo)
.pipe(gulp.dest("./dist/static/js"));
}
压缩 js lib
gulp.task("build-js-lib", function(){
gulp.src("./static/js/lib/**/*.js")
.pipe(through.obj(function(file, encode, cb) {
var UglifyJS = require("uglify-js");
var contents = file.contents.toString(encode);
var minified = UglifyJS.minify(contents,
{fromString:true}).code;
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist/static/js/lib"));
});
压缩 css
function minifyAndComboCSS(name, encode, files){
var fs = require("fs");
var CleanCSS = require("clean-css");
var content = "";
files.forEach(function(css){
var contents = fs.readFileSync(css, encode);
var minified = new CleanCSS().minify(contents).styles;
content += minified;
});
if(content){
var combo = "static/css/" + name;
}
fs.writeFileSync(combo, content);
gulp.src(combo)
.pipe(gulp.dest("./dist/static/css"));
}
压缩公共 css
gulp.task("build-common-css", function(){
gulp.src("./static/css/common/**/*.css")
.pipe(through.obj(function(file, encode, cb) {
var CleanCSS = require("clean-css");
var contents = file.contents.toString(encode);
var minified = new CleanCSS().minify(contents).styles;
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist/static/css/common"));
});
处理图片
//内联小图片
var imgs = $("img");
for(var i = 0; i < imgs.length; i++){
var img = $(imgs[i]);
var src = img.attr("src");
if(/^static\/img/.test(src)){
var stat = fs.statSync(src);
var ext = require("path").parse(src).ext;
if(stat.size <= 3000){
var head = ext === ".png" ? "data:image/png;base64," : "data:image/jpeg;base64,";
var datauri = fs.readFileSync(src).toString("base64");
img.attr("src", head + datauri);
}
}
}
压缩 HTML
contents = $.html();
//压缩 HTML
var HTMLMinifier = require("html-minifier").minify;
var minified = HTMLMinifier(contents, {
minifyCSS: true,
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
然后,在处理 index.html 的时候,我们可以使用 cheerio 来解析文件,将要处理的外链从文档中提取出来。
提取 js 和 css 外链处理
var $ = require("cheerio").load(contents, {decodeEntities: false});
//处理外链 css
var links = $("link");
var cssToCombo = [];
for(var i = 0; i < links.length; i++){
var link = $(links[i]);
if(link.attr("rel") === "stylesheet"){
var href = link.attr("href");
if(/^static\/css\/(?!common)/.test(href)){
cssToCombo.push(href);
if(cssToCombo.length == 1){
link.attr("href", "static/css/index.min.css");
}else{
link.remove();
}
}
}
}
minifyAndComboCSS("index.min.css", encode, cssToCombo);
//处理外链 js
var scripts = $("script");
var jsToCombo = [];
for(var i = 0; i < scripts.length; i++){
var s = $(scripts[i]);
//判断script标签确实是js
if(s.attr("type") == null
|| s.attr("type") === "text/javascript"){
var src = s.attr("src");
if(src){
//外链的js,默认只处理以/static/开头的资源
if(/^static\/js\/(?!lib)/.test(src)){
jsToCombo.push(src);
if(jsToCombo.length == 1){
s.attr("src", "static/js/index.min.js");
}else{
s.remove();
}
}
}
}
}
minifyAndComboJS("index.min.js", encode, jsToCombo);
最后是完整的代码:
var gulp = require("gulp");
var through = require("through2");
function minifyAndComboCSS(name, encode, files){
var fs = require("fs");
var CleanCSS = require("clean-css");
var content = "";
files.forEach(function(css){
var contents = fs.readFileSync(css, encode);
var minified = new CleanCSS().minify(contents).styles;
content += minified;
});
if(content){
var combo = "static/css/" + name;
}
fs.writeFileSync(combo, content);
gulp.src(combo)
.pipe(gulp.dest("./dist/static/css"));
}
function minifyAndComboJS(name, encode, files){
var fs = require("fs");
var UglifyJS = require("uglify-js");
var content = "";
files.forEach(function(js){
var minified = UglifyJS.minify(js).code;
content += minified;
});
if(content){
var combo = "static/js/" + name;
}
fs.writeFileSync(combo, content);
gulp.src(combo)
.pipe(gulp.dest("./dist/static/js"));
}
gulp.task("build-index", ["build-js-lib", "build-common-css"], function(){
gulp.src("./index.html")
.pipe(through.obj(function(file, encode, cb) {
var fs = require("fs");
var contents = file.contents.toString(encode);
var $ = require("cheerio").load(contents, {decodeEntities: false});
//处理外链 css
var links = $("link");
var cssToCombo = [];
for(var i = 0; i < links.length; i++){
var link = $(links[i]);
if(link.attr("rel") === "stylesheet"){
var href = link.attr("href");
if(/^static\/css\/(?!common)/.test(href)){
cssToCombo.push(href);
if(cssToCombo.length == 1){
link.attr("href", "static/css/index.min.css");
}else{
link.remove();
}
}
}
}
minifyAndComboCSS("index.min.css", encode, cssToCombo);
//处理外链 js
var scripts = $("script");
var jsToCombo = [];
for(var i = 0; i < scripts.length; i++){
var s = $(scripts[i]);
//判断script标签确实是js
if(s.attr("type") == null
|| s.attr("type") === "text/javascript"){
var src = s.attr("src");
if(src){
//外链的js,默认只处理以/static/开头的资源
if(/^static\/js\/(?!lib)/.test(src)){
jsToCombo.push(src);
if(jsToCombo.length == 1){
s.attr("src", "static/js/index.min.js");
}else{
s.remove();
}
}
}
}
}
minifyAndComboJS("index.min.js", encode, jsToCombo);
//处理内联图片
var imgs = $("img");
for(var i = 0; i < imgs.length; i++){
var img = $(imgs[i]);
var src = img.attr("src");
if(/^static\/img/.test(src)){
var stat = fs.statSync(src);
var ext = require("path").parse(src).ext;
if(stat.size <= 3000){
var head = ext === ".png" ? "data:image/png;base64," : "data:image/jpeg;base64,";
var datauri = fs.readFileSync(src).toString("base64");
img.attr("src", head + datauri);
}
}
}
contents = $.html();
//压缩 HTML
var HTMLMinifier = require("html-minifier").minify;
var minified = HTMLMinifier(contents, {
minifyCSS: true,
minifyJS: true,
collapseWhitespace: true,
removeAttributeQuotes: true
});
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist"));
});
gulp.task("build-js-lib", function(){
gulp.src("./static/js/lib/**/*.js")
.pipe(through.obj(function(file, encode, cb) {
var UglifyJS = require("uglify-js");
var contents = file.contents.toString(encode);
var minified = UglifyJS.minify(contents,
{fromString:true}).code;
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist/static/js/lib"));
});
gulp.task("build-common-css", function(){
gulp.src("./static/css/common/**/*.css")
.pipe(through.obj(function(file, encode, cb) {
var CleanCSS = require("clean-css");
var contents = file.contents.toString(encode);
var minified = new CleanCSS().minify(contents).styles;
file.contents = new Buffer(minified, encode);
cb(null, file, encode);
}))
.pipe(gulp.dest("./dist/static/css/common"));
});
我们用 Gulp 创建了一个非常简单的构建脚本,它可以压缩合并我们项目的 js 和 css 并处理小图片,我们还可以给它进一步增加其他功能,例如给压缩的文件添加版本号,或者根据内容计算签名以实现更新后不被缓存,我们还可以用 CDN 服务的 sdk 将资源发布到 CDN 并替换原始链接,同时,我们可以不用每次发布所有的文件,我们可以在开发的时候用 gulp.watch 来监控文件的修改,以实现增量的编译发布。
总之,我们可以用 gulp 来做许多有用的事情,来完善我们的构建脚本,而这一切都因为 gulp 基于流的构建以及 NPM 丰富的库变得非常简单。最后的最后,由于我们从头使用 through2 来处理任务,所以我们在具体实现功能的时候还是略微繁琐,事实上 gulp 提供了不少有用的插件,这些插件直接返回 stream 对象,可以让构建过程变得更简单,具体的可以多研究官方的文档。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。