脚手架

1. 学习要点

  • 探讨脚手架的本质和功能
  • 脚手架在前端工程中的角色和特征
  • 典型脚手架案例分析
  • yeoman脚手架集成方案

2. 脚手架的功能和本质

用一句话概括功能是:创建项目初始文件。

脚手架的本质: 方案的封装

3. 脚手架在前端工程汇总的角色和特征

用完即弃的发起者角色 脚手架的价值在于

  1. 快速生成配置
  2. 降低框架的学习成本
  3. 令业务开发人员关注业务逻辑本身

Vue-cil就是一个非常典型的例子,提供了模板选择,编译,本地开发服务器等功能模块。

局限于本地的执行环境

无论是本地工具链的工程化形式 还是持续集成的工程化形式,脚手架的执行环境始终局限于本地。所以必须解决操作系统兼容性的问他们。

多样性的实现模式 无论具体模式如何,优秀的脚手架工具应遵循的原则是一致的。

  • 与构建、开发、部署等功能模块联动,在创建项目时生成对应的配置
  • 自动安装依赖模块
  • 动态可配置
  • 底层高度可扩展
  • 丰富而不繁琐的配置项
  • 支持多种运行环境,比如命令行和Node.js API
  • 兼容主流操作系统

4. 开源脚手架案例剖析

Sails.js 针对服务器端的脚手架方案

yeoman 脚手架方案

5. 集成Yeoman 封装脚手架方案

整体的流程是: 收集用户的配置信息 ---> 将动态的配置信息转换成静态的文件内容(yeoman 使用的是ejs引擎) ---> 将生成的文件复制到目标文件夹。

封装脚手架方案

创建一个完整的空的脚手架文件目录。

微信图片_20190513173144.png

  • app目录是源码文件
  • index.js是执行入口文件
  • templates 是脚手架所封装方案的项目文件源码
  • _prompts是用户提示用户配置的选项内容的

或者使用 npm install -g generator-generator 的模板创建一个generator项目这样index里面会有相应的代码了。 然后根据下面步骤编写 index.js文件代码。这个代码并不完整,只是用来后面讲解各部分内容时的一个参考

'use strict';
const Generators  = require('yeoman-generator');
const chalk = require('chalk');
const yosay = require('yosay');
const Path = require('path');
const _ = require('lodash');

module.exports = class extends Generators  {
    constructor(args, opts) {
        super(args, opts);
        // argument appname is not required
        this.argument('appname', {
            desc: 'project name',
            type: String,
            required: false
        });
        // current选项代表是否在当前目录文件夹中创建项目
        this.option('current', {
            desc: 'generate app in current folder',
            type: Boolean,
            alias: 'c',
            required: false,
            default: false
        });
    }

    prompting() {
        // 这个方法负责用户提示和配置收集
        this.log(
            yosay(`Welcome to the wicked ${chalk.red('generator-vuegenerator')} generator!`)
        );

        const prompts = [].concat(require('./_prompts/_js.js'))
            .concat(require('./_prompts/_style.js'))
            .concat(require('./_prompts/_html.js'));

        return this.prompt(prompts).then(res => {
           let appname = res.appname || this.options.appname;
           let options = Object.assign({}, res, {
               appname
           });
           this.pkg = options.nodeModules;
           this.renderOpts = options;
        });
    }

    writing() {
        // 负责文件操作
        // destFolder 项目所在的目录
        // current 是否在当前目录创建
        const destFolder  = this.options.current ? "": Path.join(this.options.appname , '/');
        // 生成boi-conf.js文件
        this.fs.copyTpl(
            this.templatePath('boi-conf.ejs'),
            this.destinationPath(Path.join(destFolder,'boi-conf.js')),
            this.renderOpts
        );
        this.fs.copyTpl(
            this.templatePath('index.app.ejs'),
            this.destinationPath(Path.join(destFolder,'src','index.'+ opts.appname + '.html')),
            this.renderOpts
        );
    }

    install() {
        // 负责依赖模板的安装
        if(!this.options.current){
            // 如果当前目录不是根目录 要先进入根目录
            process.chdir(Path.join(process.cwd(),this.options.appname));
        }
        if(this.pkg && _.isArray(this.pkg) && this.pkg.length > 0){
            this.npmInstall(
                this.pkg,{
                    'save-dev': true,
                    'skipMessage': true
                }
            )
        }
    }
};

  1. 收集用户配置。

在index.js的promptiong()方法里写,用户提示和收集配置信息。将配置信息放到renderOption变量里。将用户项目的第三方依赖模块放到pkg变量里。

  1. 转化动态内容

    • 将ejs源文件中内容依据renderOption 转化为静态内容
    • 转化后的ejs文件复制到项目目录并且修改后缀名 ejs文件编写示例
        boi.spec('style',{
            // 后缀类型
            ext: '<%= styleSyntax %>',
            // style文件夹相对于basic.source的目录
            source: 'style',
            // style 的输出目录相对于basic.output
            output: 'style',
            // 是否启用hash指纹
            useHash: true,
            autoprefix: false,
            // 是否启用CSS Sprites自动生成功能
            <% if(enableSprites){ %>
                sprites: {
                    // 散列图片目录
                    source: 'icons',
                    // 是否根据子目录分别编译输出
                    split: true,
                    // 是否识别retina命名标识
                    retina: true
                    postcssSpritesOpts: null
                },
            <% }else{%>
                sprites:fasle
            <% }%>
        })
    
  2. 自动安装依赖模板 在install()函数里面编写安装依赖的代码,注意一定要确定用户所创建的项目根目录是否为当前面目录,如果是新目录,需要首先进入目标目录再执行安装

集成到工程化体系中

目的: 1 统一工具栈 ,2 自动安装

  1. 集成到工程中使用的是 yeoman-environment这个模块,集成后我们使用命令 boi new < appname > --template < templateName > 来创建新项目

  2. 集成实现

    使用commander.js 实现命令行的交互封装

    const Cli  = require('commander');
    // 脚手架
    Cli.command('new [dir]')
     .description('generate a new project')
     .usage('[dir] [options]')
     .option('-t, --template [template]', 'specify template of new app')
     .action((dir, options) => {
         require('./features/generator.js')(dir, options.template);
     }).on('--help', () => {
         print('\n\n  Examples:\n');
         print('    $ boi new');
         print('    $ boi new page');
         print('    $ boi new app -p demo');
         print('    $ boi new app -p demo -t webapp\n');
     });
    

    commander.js命令执行以后将参数传递给 features/generator中。此模块实现具体的yeoman继承和执行工作。

    const Shell    = require('shelljs');
    const Yeoman   = require('yeoman-environment');
    const _        = require('lodash');
    const Path     = require('path');
    let YeomanRuntime = Yeoman.createEnv(); 
    module.exports = (dirname, template) => {
        let appname = '';
        let inCurrentDir = false;
        // 不指定TemplateName使用默认的boiapp模板
        const TemplateName = template && template.split(/\:/)[0] || 'boiapp';
        const GenerateTemplate = `generator-${TemplateName}`;
        const AppCommand = template || TemplateName;
        if (!dirname || dirname === '.' || dirname === './') {
            // 如果不指定appname则取值当前目录名称
            appname = _.last(process.cwd().split(/\//));
            inCurrentDir = true;
        } else {
            // 如果指定appname则创建子目录
            appname = dirname;
        }
    // to compate nvm system
    // 执行npm 命令
    Shell.exec('npm root -g', {
        async: true, // 是否异步
        silent: true // 不输出信息到console
    }, (code, stdout) => {
        // global template path
        const TemplatePath = Path.posix.join(_.trim(stdout), GenerateTemplate);
        try {
            const TemplateRealPath = require.resolve(TemplatePath); //判断模板是否安装了 如果安装则正常往下进行如果没有安装则抛出异常
            YeomanRuntime.register(TemplateRealPath, AppCommand);
            inCurrentDir ? YeomanRuntime.run(`${AppCommand} ${appname} -c`) : YeomanRuntime.run(`${AppCommand} ${appname}`);
        } catch (e) {
            Shell.exec(`npm install -g ${GenerateTemplate}`,{
                async: true, // 是否异步
                silent: true // 不输出信息到console
            },(code) => {
                if(code != 0){
                    // 如果失败则结束进程
                     process.exit();
                }
                YeomanRuntime.register(require.resolve(TemplatePath), AppCommand);
                inCurrentDir ? YeomanRuntime.run(`${AppCommand} ${appname} -c`) : YeomanRuntime.run(`${AppCommand} ${appname}`);
            });
        }
    });
    };
    

    6.总结

    以上代码都是实力代码,并不能直接运行,知识说明一种集成的形式,真正实现yeoman脚手架可以去查看yeoman或者使用完成的boi项目进行调试。