故事的背景
再一个秋高气爽的日子里,小主打算创造一个工具,这个工具可以根据项目中的代码自动生成文档页面。这个工具需要将项目中JS文件的注释解析出来,将一些用于描述项目的markdown解析出来,再加上点润色后的HTML,构成一个漂亮的文档站。需求如下:
- 解析JSON文件中的配置,用以生成文档站的导航,页面,读取JS文件的路径。
- 清理即将用于存放生成文档的目标目录。
- 将静态资源复制到目标目录。
- 解析工程文件中的注释,并将其归类整理,生成指定格式数据。
- 解析指定markdown格式文件,并生成指定格式数据。
- 根据数据使用模板文件生成代码块。
- 将生成的代码块加上统一的title&footer生成最终的页面
当每一个小部分所需要的代码Ready后,需要一个工具把他们组合起来,能够让他们按顺序依次执行。
选择一个自动化管理工具
*以下文字摘抄于从零单排之gulp实战 *
grunt由于是基于文件读写的,前一个任务做完写好文件,下个任务再去读文件进行操作。所以频繁的磁盘io读写会很浪费性能。而且由于grunt的输入输出格式没有统一规范化,导致目前线上的各种库的入口文件还有出口文件配置五花八门,而且职责混乱。越来越被人诟病。
于是gulp出来了。基于node的stream机制,最大程度的减少了磁盘io消耗。所有的插件具有了统一的入口,插件都是在流的中间对输入流操作,输出更改过的流给下一个插件。并且每个人只做自己的事,职责分明。 以下文字摘抄自
如果你对Grunt 足够熟悉,就会注意到,Gulp和Grunt的工作方式很不一样。Grunt不使用数据流,而是使用文件,对文件执行单个任务然后保存到新的文件中,每个任务都会重复执行所有进程,文件系统频繁的处理任务会导致Grunt的运行速度比Gulp慢。
看了这个me就选择了gulp……
gulp 基础
创建一个任务
首先是基于orchestrator的任务管理,这个官网写的就很好了,摘抄一下:定义一个使用 Orchestrator 实现的任务(task)。
gulp.task(name[, deps], fn)
//for example
gulp.task('somename', function() {// 做一些事});
- name | String
任务的名字,如果你需要在命令行中运行你的某些任务,那么,请不要在名字中使用空格。
- deps | Array
一个包含任务列表的数组,这些任务会在你当前任务运行之前完成。
gulp.task('mytask', ['array', 'of', 'task', 'names'], function() {// 做一些事});
注意: 你的任务是否在这些前置依赖的任务完成之前运行了?请一定要确保你所依赖的任务列表中的任务都使用了正确的异步执行方式:使用一个 callback,或者返回一个 promise 或 stream。
- fn | Function
该函数定义任务所要执行的一些操作。通常来说,它会是这种形式:
gulp.src().pipe(someplugin())。
使任务支持异步
任务可以异步执行,如果 fn 能做到以下其中一点:
- 接受一个 callback
// 在 shell 中执行一个命令
var exec = require('child_process').exec;
gulp.task('jekyll', function(cb) {
// 编译 Jekyll
exec('jekyll build', function(err) {
if (err) return cb(err); // 返回 error
cb(); // 完成 task
});
});
- 返回一个 stream
gulp.task('somename', function() {
return gulp.src('client/**/*.js')
.pipe(minify())
.pipe(gulp.dest('build'));
});
- 返回一个 promise
var Q = require('q');
gulp.task('somename', function() {
var deferred = Q.defer();
// 执行异步的操作
setTimeout(function() {
deferred.resolve();
}, 1);
return deferred.promise;
});
注意: 默认的,task 将以最大的并发数执行,也就是说,gulp 会一次性运行所有的 task 并且不做任何等待。
gulp进一步
- orchestrator.主要实现了task方法,还有解决任务依赖关系。
文件转换系统,通过gulp.src()或得的vinyl对象格式如下:
{
cwd: cwd,
base: base,
stat: 使用 fs.Stats得到的结果,
path: 文件路径,
contents: 文件的内容
}
之后再把所有的可读流合并成一个可读流,使用的是ordered-read-streams 然后传递给一个转换流,也就是我们之后会编写的插件。处理完再写入到一个dst生成的转换流里面,写入文件。整个过程就结束了。
由于dst是一个转换流,所以可以继续pipe到其他的插件上。
以上摘抄自 从零单排之gulp实战(http://purplebamboo.github.io/2014/11/30/gulp-analyze/]),
尝试在控制台输出接收的参数。内容如下:
File "doc.js" <Buffer 2f 2a 2a 0a 20 2a 20 43 72 65 61 74 65 64 20 62 79 20 65 76 61 20 6f 6e 20 31 35 2f 31 30 2f 38 2e 0a 20 2a 2f 0a 2f 2a 2a 0a 20 2a 20 e5 bc b9 e5 b1 ... >>
对其JSON.stringify,打印如下:
{"history": [
"/Users/eva/……/src/alert.js",
"/Users/eva/……/src/kami/doc/src/doc.js",
"/Users/eva/……/src/kami/doc/docsite/doc.js"],
"cwd": "/Users/eva/……/src/kami/doc",
"base": "/Users/eva/……/src/kami/doc/docsite",
"stat": {
"dev": 16777217,
"mode": 33261,
"nlink": 1,
"uid": 501,
"gid": 20,
"rdev": 0,
"blksize": 4096,
"ino": 13274234,
"size": 6172,
"blocks": 16,
"atime": "2015-10-19T13:41:28.000Z",
"mtime": "2015-10-19T12:18:36.000Z",
"ctime": "2015-10-19T12:18:36.000Z",
"birthtime": "2015-10-19T12:18:36.000Z"
},
"_contents": {
"type": "Buffer",
"data": [47,42,………………]}}
由此可以清晰地看到对象vinyl的真实面貌。
回到我们的文档站
gulp 安装
npm install -save-dev gulp
学会像gulp一样思考
gulp流的起始为文件的读取gulp.src
gulp流的结束为文件的存取gulp.dest
gulp中每一个插件在处理数据的时候都是以pipe函数接收一种特定格式的数据,先将数据转换成需要的格式处理后再转成这种数据格式return出去,交由下一个部分去处理。这种处理数据的形式理解为流,这种数据格式为buffer或者是二进制流。
接触到的gulp插件简介
代码验证工具JSHint (gulp-jshint)
文件合并 (gulp-concat)
清理档案 (gulp-clean)
更改文件名(gulp-rename)
模板(gulp-template)
markdown 编辑工具(gulp-markdown)
从clean开始
使用地址
packagedata 为配置文件的JSON格式串
输入read:flase 配置项 意味着不会真正的读文件内容 提高删除的效率
clean为自定义任务名 在命令行中可以通过 gulp clean 调用这个任务
数组表示该任务执行的依赖
gulp.task(‘clean’, [],function(){
return gulp.src(packagedata.outputpath,{read:false})
.pipe(clean());
});
将文件存储到指定地点
在clean完成之后,在进行template文件的复制。因此下面添加了task得依赖在第二个参数中。
gulp.task('store',['clean'],function(){
gulp.src(packagedata.templatedirepath + '/**')
.pipe(gulp.dest(packagedata.outputpath + '/'));
});
编写插件
1.引入npm包
可以引入through-gulp
或者是through2
var through = require('through-gulp');
2.如何使用
function packdoc (callback2){
var stream = through(function (file, encoding, callback) {
var htmlStr, jsStr, that = this;
var html = file.contents.toString('utf-8');
var data = {
page: page,
title: 'Kami',
footer: 'Kami',
banner: {
title: 'Kami',
description: '为移动而生的组件库'
},
menus: package.menu
};
file.contents = new Buffer(JSON.stringify(data));
this.push(file);
callback();
});
return stream;
}
module.exports = packdoc;
调用插件完成packJS任务
var packdoc = require('./lib/test.js');
gulp.task('packJs',['store'], function(){
return gulp.src(packagedata.JsSource)
.pipe(concat('widget.js'))
.pipe(gulp.dest(packagedata.outputpath + '/tmp'))
.pipe(packdoc())
.pipe(dest(packagedata.outputpath +'/tmp', {ext:'.json'}))
.pipe(gulp.dest('./'));
});
关于文件的合并concat 使用地址
使用through2的同学这里看,听说through2比gulp-through好一些。
参考文章:插件编写入门
基于现有的插件写一个封装md未指定格式的插件
var gulp = require('gulp');
var fs = require('fs');
var through = require('through-gulp');
var data = fs.readFileSync('./package.json');
var package = JSON.parse(data.toString('utf-8'));
var path = require('path');
function packMd(){
var stream = through(function(file, enc, callback){
var html = file.contents.toString();
var fileName = path.basename(file.path, '.html');
var currentItem ={};
package.menu.forEach(function(item){
if(item.name === fileName){
currentItem = item;
}
});
var data = {
page: {
type: 'html',
title: currentItem.title,
content: html,
sidebar: currentItem.sidebar || [],
menu: package.menu
},
title: 'Kami',
footer: 'Kami',
banner: {
title: 'Kami',
description: '为移动而生的组件库'
},
menus: package.menu
};
file.contents = new Buffer(JSON.stringify(data));
this.push(file);
callback();
});
return stream;
}
module.exports = packMd;
完成packMd任务
gulp.task('packMd',['store'], function(){
return gulp.src(packagedata.markdowndir + '/*.markdown')
.pipe(markdownCompress())
.pipe(packMd())
.pipe(dest(packagedata.outputpath +'/tmp', {ext:'.json'}))
.pipe(gulp.dest('./'))})
对于pack后的文件使用模板生成文档站
gulp.task('compile', ['packMd','packJs'], function(){
packagedata.pageConfig.forEach(function(item){
gulp.src(packagedata.templatepath)
.pipe(artTemplate(require(packagedata.outputpath+'/tmp/'+ item.name +'.json')))
.pipe(rename({
dirname: packagedata.outputpath,
extname:'.html',
basename:item.name
})).pipe(gulp.dest('./'));})})
对于上述代码描述:
对于定义的配置文件进行遍历,生成多个文档站页面
使用模板文件和数据生成文件
由于artTemplate并没有gulp插件,因此此处又做了一个小插件封装
var artTemplateEn = require('art-template');
var gulp = require('gulp');
var through = require('through-gulp');
function artTemplate(resource) {
var resource = resource;
var stream = through(function(file, enc, callback){
var html = file.contents.toString();
artTemplateEn.config('escape', true);
artTemplateEn.helper('anchor', function(name) {
return name
? name.replace(/[\.:]/g, '-')
: ''";
});
artTemplateEn.helper('txt', function(html) {
return html
? html.replace(/\<\/?[^\>]*\>/g, '')
: ''";
});
var render = artTemplateEn.compile(html);
var newContent = render(resource);
file.contents = new Buffer(newContent);
this.push(file);
callback();
});
return stream;
}
module.exports = artTemplate;
使用rename插件对于生成的文件更名
最后写一个默认的task
gulp.task('default',['compile' ] ,function(){console.log('I'm ready');});