如何基于yeoman快速编写脚手架工具(系列:使用篇上)

如何基于yeoman快速编写脚手架工具(系列:使用篇上)

由于关于yeoman编写脚手架的文章太少,所以推出此文,注意本文介绍的yoeman generator-generator是基于 0.8.0以前的版本,0.1.0以后的版本将在下一次文章分享。

原理将在系列二中讲述。

脚手架生成器(scaffold generator)能够帮助我们快速建立起一个项目的基础目录结构和构建任务,使得我们开发项目时能够遵循一定的开发规范,工作流方式,帮助我们提高开发和协作效率。脚手架生成器主要的工作包括:

  1. 生成规范的项目目录结构

  2. 根据用户输入自动生成具体的项目信息,如项目名称,作者等

  3. 开发过程中自动编译lesscoffee等任务,自动安装项目基本依赖模块

  4. 统一环境,开启静态server服务,实时预览项目效果

  5. 开启watch,当文件发生变化时实时更新到页面预览,如接入 browserSync 服务(实现类似livereload的功能)

  6. 质量保障,单元测试

  7. 构建和发布

如上面所述,一个脚手架生成器能帮助我们规范项目,保障质量以及提高开发效率。现在社区也有非常多的工具,例如generator-webapp,但是在实际的环境下我们很多时候需要根据团队需求和业务现况来自定义我们自己的脚手架。编写一个简单的符合我们自己的脚手架生成器其实非常简单,本文将介绍基于yeoman来实现。因为yeoman提供了很多常用的工具函数供我们使用,例如处理用户输入,任务队列,文件操作等,并且社区已经具有非常多的工具提供了很多实用的API,借用它正可以让我们站在巨人的肩膀上实现我们小小的梦想。

下面将具体介绍如何利用yeoman编写一个简单的脚手架工具。

安装依赖

所有的generator都是基于yo来执行的,所以我们得先安装好这个全局依赖

npm install -g yo

基本规范约束

命名规范

所有generator都必须以generator-为前缀,例如generator-webapp,而初始化项目时都是通过yo xxx来执行的,例如使用generator-webapp来初始一个通用的web项目时是通过执行yo webapp

下面以generator-example为例,其原理就是在generator-example模块安装时保证是全局的,也即是在linuxmac下模块安装在/url/local/lib/node_modules下,当执行yo example的时候它就会去这个全局目录下寻找generator-example模块的入口来执行。

目录规范

目录规范如下,当执行yo example时会默认进入执行app/index.js,如果还需要一个sub-generators则创建generator-exmaple/test/index.js,并通过yo name:test来执行

|-generator-example
  |-package.json
  |-app/
     |--index.js

代码规范

package.jsonkeywords字段必须包含yeoman-generator,且必须依赖yeoman-generator

开始

  1. 按上面的目录规范建好项目目录

  2. 基本的package.json如下

    {
        "name": "generator-example",
        "version": "0.1.0",
        "description": "just a simple example generator",
        "files": [        "app"
        ],
        "keywords": ["yeoman-generator"],
        "dependencies": {
            "yeoman-generator": "^0.17.3",
            "chalk": "^1.0.0",
            "yosay": "^1.0.2"
        }
    }
  3. npm install安装依赖

  4. 进入app/index.js,基本代码如下:

    var generators = require('yeoman-generator');  
    
    module.exports = generators.Base.extend({  //必须extend自
        method1 : function(){  //任务函数,当generator运行时会自动运行这些函数,这些函数有队列之分,下面会解释
            console.log('hello world...');
        }
    });
  5. 开发阶段,将模块暴露在全局目录node_modules下

    cd generator-example
    npm link//将模块link到`/usr/local/lib/node_modules`下并建立链接
  6. 至此,一个最简单的generator已经完成

    mkdir examplecd example
    yo example   //将会看到控制台输出 'hello world...'
  7. 增加功能,让它可以自动生成项目规范的目录结构。往generator-example增加一些文件,如下:

    |-generator-example
        |-package.json
        |-app/
            |--index.js
     |-templates
            |- _package.json   //项目基本信息
            |- _gulpfile.js    //书写构建任务
            |- _src            //项目源码
                |- less
                    |- index.less
                |- js
                    |- index.js

    也就是需要让生成器能够帮我们将新项目的结构初始化成templates下模板文件的结构,让我们编写一下模板文件,如下:

    -- _package.json

    /*默认用Underscore的模板语法来编写,在生成器中会使用用户的输入来填充这些模板内容*/
    {
        "name": "<%= pkgName %>",
        "version": "1.0.0",
        "description": "<%= description %>",
        "main": "index.js",
        "author": "<%= author %>",
        "repository": {
            "type": "git",
            "url": "<%= repo %>"
        },
        "keywords": [
            "plugin",
            "def"
        ],
        "license": "<%= license %>",
        "readmeFilename": "README.md",
        "dependencies": {
        },
        "devDependencies": {
            "gulp": "~3.8.11",
            "gulp-less": "~1.2.0",
            "gulp-sourcemaps": "~1.5.1"
        }
    }

    -- _gulpfile.js

    var gulp = require('gulp');
    var less = require('gulp-less');
    var sourcemaps = require('gulp-sourcemaps');
    gulp.task('less', function(){    
        return gulp.src('src/less/*.less')
            .pipe(sourcemaps.init())
            .pipe(less())
            .pipe(sourcemaps.write('./maps'))
            .pipe(gulp.dest('build'));
    });
    
    gulp.watch('src/less/*.less',['less']);
    
    gulp.task('default', ['less']);
  8. 生成器的生命周期

    当执行yo example启动生成器时,它会沿着它的生命周期执行如下特定名称的函数,这些特定名称的函数会放进一个队列里面按顺序执行,如果功能函数不是特定的函数名称,如上面的method1,则放进另一个队列default按顺序执行。这些特定的函数名称有:

    • initializing : 初始化阶段

    • prompting : 接受用户输入阶段

    • configuring : 保存配置信息和文件,如.editorconfig

    • default : 非特定的功能函数名称,如上面说到的method1

    • writing : 生成项目目录结构阶段

    • conflicts : 统一处理冲突,如要生成的文件已经存在是否覆盖等处理

    • install : 安装依赖阶段,如通过npmbower

    • end : 生成器即将结束

  9. 根据上面生成器的生命周期,我们修改app/index.js来完成生成器的工作,如下:

    --app/index.js

    'use strict';
    var generators = require('yeoman-generator');
    var chalk = require('chalk');
    var yosay = require('yosay');
    var path = require('path');
    module.exports = generators.Base.extend({
        initializing: function () {    //初始化准备工作
        },
    
        prompting: function () {  //接受用户输入
            var done = this.async(); //当处理完用户输入需要进入下一个生命周期阶段时必须调用这个方法
    
            //yeoman-generator 模块提供了很多内置的方法供我们调用,如下面的this.log , this.prompt , this.template , this.spawncommand 等
    
            // Have Yeoman greet the user.
            this.log(yosay('Welcome to the groundbreaking ' + chalk.red('example') + ' generator!'
            ));
            this.name = path.basename(process.cwd());
            this.license = 'ISC';
            this.description = '';
            this.author = '';
            var prompts = [
                {
                    type: 'input',
                    name: 'name',
                    message: 'name of app:', default: this.name
                },
                {
                    type: 'input',
                    name: 'description',
                    message: 'description:', default: this.description
                },
                {
                    type: 'list',   // 提供选择的列表
                    name: 'kissy',
                    message: 'which version of kissy',
                    choices: [
                        {
                            name: 'KISSY@1.4.x',
                            value: '1.4.x'
                        },
                        {
                            name: 'KISSY@6.0.x',
                            value: '6.0.x'
                        }
                    ]
                },
                {
                    type: 'input',
                    name: 'repo',
                    message: 'git repository:', default: this.repo
                },
                {
                    type: 'input',
                    name: 'license',
                    message: 'license:', default: this.license
                },
                {
                    type: 'input',
                    name: 'author',
                    message: 'author:', default: this.author
                }
    
            ];
            this.prompt(prompts, function (props) {
                this.name = props.name;
                this.pkgName = props.name;
                this.kissy = props.kissy;
                this.repo = props.repo;
                this.license = props.license;
                this.author = props.author;
                this.description = props.description;
    
                done();  //进入下一个生命周期阶段
            }.bind(this));
        },
    
        writing: {  //生成目录结构阶段
            app: function () {      //默认源目录就是生成器的templates目录,目标目录就是执行`yo example`时所处的目录。调用this.template用Underscore模板语法去填充模板文件
                this.template('_package.json', 'package.json');  //
                this.template('_gulpfile.js', 'gulpfile.js');
                this.copy('_src/less/index.less', 'src/less/index.less');
                this.copy('_src/js/index.js', 'src/js/index.js');
            }
        },
    
        install: function () {
            var done = this.async();
            this.spawnCommand('npm', ['install'])  //安装项目依赖
                .on('exit', function (code) {
                    if (code) {
                        done(new Error('code:' + code));
                    } else {
                        done();
                    }
                })
                .on('error', done);
        },
        end: function () {
            var done = this.async();
            this.spawnCommand('gulp')   //生成器退出前运行gulp,开启watch任务
                .on('exit', function (code) {
                    if (code) {
                        done(new Error('code:' + code));
                    } else {
                        done();
                    }
                })
                .on('error', done);
        }
    });
  10. 至此生成器的工作已经完成了,可以通过yo example验证使用了


最后

可以看到,自定义一个符合自己需求的生成器其实挺简单的。后面还可以做更多的事情,例如在gulpfile.js里面接入browserSync的功能,单元测试,编译打包任务等等。

yeoman还提供了更多的功能以及函数供我们使用,具体还得看yeoman官网


赞 (0) 评论 分享 ()