2019独角兽企业重金招聘Python工程师标准>>>
原文:https://www.h5jun.com/post/gulp-build.html
什么是 Gulp?
Gulp 的官网title上对这个工具有一个比较准确的定义,叫做:基于流的自动化构建工具。如果你查看它的网页源代码,还会看到在<meta>
标签里有一段更详细的描述:
Gulp.js 是一个自动化构建工具,开发者可以使用它在项目开发过程中自动执行常见任务。Gulp.js 是基于 Node.js 构建的,利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。Gulp.js 源文件和你用来定义任务的 Gulp 文件都是通过 JavaScript(或者 CoffeeScript )源码来实现的。
所以,Gulp 是在项目开发过程中自动执行任务的一个工具,通过它可以方便地在开发时(或者发布前),对目标文件的内容进行I/O操作。
由于 Gulp 是基于流的,所以 Gulp 对于文件内容的操作就像是水槽对于水流,水流流经水槽,水槽将水流塑造成不同的形状。
既然是基于流的,在进一步理解 Gulp 前,我们最好先来理解什么是流。
Stream 流
在计算机系统中文件的大小是变化很大的,有的文件内容非常多非常大,而 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
月影觉得 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").minifyvar 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.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).codecontent += 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}).codefile.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).stylescontent += 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).stylesfile.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).extif(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").minifyvar 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标签确实是jsif(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")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})//处理cssvar 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()}}}}// 压缩cssminifyAndComboCSS("index.min.css", encode, cssToCombo)// 处理外链jsvar scripts = $("script")var jsToCombo = []for(var i = 0; i < scripts.length; i++){var s = $(scripts[i])// 判断script标签确实是jsif(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()} }}}}// 压缩jsminifyAndComboJS("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).extif(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)}}} // 压缩HTMLcontents = $.html()var HTMLMinifier = require("html-minifier").minifyvar 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"))
})// 压缩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).stylescontent += minified})if(content){var combo = "static/css/" + name}fs.writeFileSync(combo, content)gulp.src(combo).pipe(gulp.dest("./dist/static/css"))
}// 压缩css/common/
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).stylesfile.contents = new Buffer(minified, encode)cb(null, file, encode)})).pipe(gulp.dest("./dist/static/css/common"))
})// 压缩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).codecontent += 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}).codefile.contents = new Buffer(minified, encode)cb(null, file, encode)})).pipe(gulp.dest("./dist/static/js/lib"))
})
总结
我们用 Gulp 创建了一个非常简单的构建脚本,它可以压缩合并我们项目的 js 和 css 并处理小图片,我们还可以给它进一步增加其他功能,例如给压缩的文件添加版本号,或者根据内容计算签名以实现更新后不被缓存,我们还可以用 CDN 服务的 sdk 将资源发布到 CDN 并替换原始链接,同时,我们可以不用每次发布所有的文件,我们可以在开发的时候用 gulp.watch 来监控文件的修改,以实现增量的编译发布。
总之,我们可以用 gulp 来做许多有用的事情,来完善我们的构建脚本,而这一切都因为 gulp 基于流的构建以及 NPM 丰富的库变得非常简单。最后的最后,由于我们从头使用 through2 来处理任务,所以我们在具体实现功能的时候还是略微繁琐,事实上 gulp 提供了不少有用的插件,这些插件直接返回 stream 对象,可以让构建过程变得更简单,具体的可以多研究官方的文档。
PS
特别需要注意的是,在此Demo或者说小项目中,最后的压缩步骤需要node包管理工具,利用npm安装相应的包,而这涉及到nodejs的基础知识,例如在构建Demo一开始,可以利用npm init来创建package.json文件,利用npm install来安装包文件等。另外,在index.html中js和css等路径需要以static/开始,而不是./staitc/开始