gulp 已经成为很多项目的标配了,gulp 的插件生态也十分繁荣,截至 2015.1.5,npm 上已经有 10190 款 gulp 插件供我们使用。我们完全可以傻瓜式地搭起一套构建。
然而,我们经常会遇到一种情况,我们好不容易按照文档传入对应的参数调用了插件,却发现结果不如预期,这时候我们就要一点点去排错,这就要求我们对 gulp 插件的工作原理有一定的了解。本文以实现一个 gulp 插件为例,讲解一下 gulp 插件是如何工作的。
需求描述
通常,我们的构建资源为 js/css/html 以及其它的一些资源文件,在开发或发布阶段,js/css 会经过合并, 压缩, 重命名等处理步骤。
有些场景下,我们不能确定经过构建后生成 js/css 的名称或者数量,如此就不能在 HTML 文件中写死资源的引用地址,那么该如何实现一个 Gulp 的插件用以将最终生成的资源文件/地址注入到 HTML 中呢?
假设我们需要实现的插件是这样使用方式:
1 2 3 4 5 6 7 8 |
<html> <head> <!--InlineResource:\.css$--> </head> <body> <!--InlineResource:\.js$--> </body> </html> |
我们通过一个 HTML 注释用以声明需要依赖的资源,InlineResource 是匹配的关键词,":" 做为分割,/*.css$/,/*.js$/ 是声明要依赖的文件的正则匹配。
在 gulpfile.js 我们需要这边配置:
1 2 3 4 5 6 7 8 |
gulp.task('dist', function () { return gulp.src('index.html') .pipe(InjectResources( gulp.src(['*.js', '*.css']) .pipe(hash(/*添加MD5作为文件名*/)) )) .pipe(gulp.dest('dist')) }) |
这里简单介绍下其中的一些方法与步骤:
-
gulp.src('index.html') 会读取文件系统中当前目录下的 index.html,并生成一个可读的 Stream,用于后续的步骤消费
-
InjectResources(stream) 是我们将要实现的插件,它接受一个参数用以获取要注入到 HTML 中的 JS/CSS,此参数应该是一个 Stream 实例,用生成一个 Stream 实例,用于接收并处理上一步流进来的数据
-
hash(options) 是一个第三方插件,用于往当前流中的文件名添加 md5 串,如:gulp-hash
-
gulp.dest('dist') 用于将注入资源后的 HTML 文件生成到当前目录下
我们要关心的是第 2 点:如何接所有的资源文件并完成注入?
我们可以将该逻辑分成 4 个步骤
- 获取所有的 js/css 资源
- 获取所有的 HTML 文件
- 定位 HTML 中的依赖声明
- 匹配所依赖的资源
- 生成并注入依赖的资源标签
在开编之前,我们需要依赖一个重要的第三方库:map-stream
map-stream 用于获取当前流中的每一个文件数据,并且修改数据内容。
步骤 1 (JS/CSS 资源)
1 2 3 |
module.exports = function (resourcesStream) { // step 1: TODO => 这里要获取所有的js/css资源 } |
资源流会作为参数的形式传给 InjectResources 方法,在此通过一个异步的实例方法获取所有的文件对象,放到一个资源列表:
1 2 3 4 5 6 7 8 9 10 11 12 |
var resources = [] function getResources(done) { if (resources) return done(resources) // 由于下面的操作是异步的,此处要有锁... resourcesStream.pipe(mapStream(function (data, cb) { resources.push(data) cb(null, data) })) .on('end', function () { done(resources) }) } |
- mapStream 的处理方法中获取到的 data 是由 gulp.src 生成的 vinyl 对象,代表了一个文件
- 每一个 stream 都会在接受后抛出 end 事件
Note: mapStream 的处理方法中的 cb 方法,第二个参数可以用于替换当前处理的文件对象
到此,我们就完成了第一步的封装啦!
1 2 3 4 5 6 |
module.exports = function (resourcesStream) { // step 1: function getResources () { ... } } |
步骤 2 (HTML 文件)
1 2 3 4 5 6 7 8 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: TODO => 获取当前流中的所有目标HTML文件 return mapStream(function (data, cb) { }) } |
InjectResources 插件方法会返回一个 Writable Stream 实例,用于接收并处理流到 InjectResources 的 HTML 文件,mapStream 的返回值就是一个 writable stream。
此时,mapStream 的处理方法拿到的 data 就是一个 HTML 文件对象,接下来进行内容处理。
步骤 3 (定位依赖)
1 2 3 4 5 6 7 8 9 10 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { var html = data.contents.toString() // step 3: TODO => 获取HTML中的资源依赖声明 }) } |
我们拿到的 data 是一个 vinyl 对象,contents 属性是文件的内容,类型可能是 Buffer 也可能是 String, 通过 toStraing() 后可以获取到字符串内容。
所有的依赖声明都有 InlineResource 关键词,简单点的做法,可以通过正则来定位并替换 HTML 中的资源依赖:
1 2 3 |
html.replace(/<!--InlineResource:(.*?)-->/g, function (expr, fileRegexpStr){ // fileRegexp是用以匹配依赖资源的正则字符串 }) |
到此,我们完成了资源依赖的定位,下一步将是获取所依赖的资源用以替换。
步骤 4 (依赖匹配)
我们将通过步骤 1 定义的 getResources 方法获取所需的资源文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { // step 3: ✔ getResources(function (list) { html.replace(depRegexp, function (expr, fileRegexpStr) { var fileRegexp = new RegExp(fileRegexpStr) // step 4: TODO => 获取匹配的依赖 }) }) }) } |
由于 getResources 是异步方法,因此需要把替换处理逻辑包裹在 getResources 的回调方法中
根据依赖声明中的正则表达式,对资源列表一一匹配:
1 2 3 4 5 6 7 8 9 10 |
function matchingDependences(list, regexp) { var deps = [] list.forEach(function (file) { var fpath = file.path if (fileRegexp.test(fpath)) { deps.push(fpath) } }) return deps } |
到此只差最后一步,将资源转换为 HTML 标签并注入到 HTML 中
步骤 5 (资源转换/依赖注入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { // step 3: ✔ // step 4: ✔ // ... html.replace(depRegexp, function (expr, fileRegexpStr) { var deps = matchingDependences(list, fileRegexpStr) // step 5: 文件对象转换为HTML标签 }) }) } |
接下来的定义一个 transform 方法,用于将路径列表转换为 HTML 的资源标签列表,其中引入了 path 模块用于解析获取文件路径的一些信息,该模块是 node 内置模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var path = require('path') function transform(deps) { return deps.map(function (dep) { var ext = path.extname(dep) switch (ext) { case 'js': '<script>' + dep + '</script>' break case 'css': return '<link rel="stylesheet" href="' + dep + '">' break } return '' }).join('') } |
最终,我们将标签列表拼接为一个字符串来 HTML 中的依赖声明(注入):
1 2 3 4 5 6 7 8 9 |
html = html.replace(depRegexp, function (expr, fileRegexpStr) { var deps = matchingDependences(list, fileRegexpStr) // step 5: 文件对象转换为HTML标签 return transform(deps) }) // html文件对象 data.contents = new Buffer(html) // 把修改后的文件对象放回HTML流中 cb(null, data) |
到此也就完整地实现了一个拥有基本注入功能的插件~~~~~~
One More Thing
通过上面实现的示例步骤,可以清楚了解到 gulp 插件的工作原理。 但要做一个易用/可定制性高的插件,我们还要继续完善一下,例如:
- 比较资源的路径与 HTML 的路径,输出相对路径作为默认的标签资源路径
- 提供 sort 选项方法用于修改资源的注入顺序
- 提供 transform 选项方法用于定制标签中的资源路径
-
在依赖声明中支持 inline 声明,用以将资源内容内联到 HTML 中,例如:
1<!--InjectResources:*\.js$??inline--> -
支持命名空间,用于往同一个资源流中使用多次资源注入的区分,例如:
12345678gulp.src('index.html').pipe(InjectResources(gulp.src('asserts/*.js'), { name: 'asserts'})).pipe(InjectResources(gulp.src('components/*.js'), { name: 'components'}))... -
. . .
TransNet2013 2016 年 2 月 23 日
推测是 2016.1.5 日,截止
TAT.mandyluo 2016 年 2 月 24 日
呃呃,原来已经 2016 了,光阴似箭,日月如梭
shu 2016 年 2 月 2 日
对于类似 browserify 的多级依赖,合并内容时不确定在 gulp 流中的顺序,该怎么处理好呢?
TAT.mandyluo 2016 年 2 月 2 日
在最后有提到,提供 sort 选项方法,让用户自定义资源顺序
darcy.bai 2016 年 2 月 1 日
if (resources) 这里有问题吧, 空数组为真吧.
TAT.mandyluo 2016 年 2 月 24 日
谢谢指出
darcy 2016 年 1 月 28 日
太赞了
TAT.mandyluo 2016 年 2 月 24 日
阿里嘎多
send 2016 年 1 月 25 日
[good]
TAT.mandyluo 2016 年 2 月 24 日
thx