使用gulp实现文档自动生成工具

故事的背景

再一个秋高气爽的日子里,小主打算创造一个工具,这个工具可以根据项目中的代码自动生成文档页面。这个工具需要将项目中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');});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容