脚手架
1. 学习要点
- 探讨脚手架的本质和功能
- 脚手架在前端工程中的角色和特征
- 典型脚手架案例分析
- yeoman脚手架集成方案
2. 脚手架的功能和本质
用一句话概括功能是:创建项目初始文件。
脚手架的本质: 方案的封装
3. 脚手架在前端工程汇总的角色和特征
用完即弃的发起者角色 脚手架的价值在于
- 快速生成配置
- 降低框架的学习成本
- 令业务开发人员关注业务逻辑本身
Vue-cil就是一个非常典型的例子,提供了模板选择,编译,本地开发服务器等功能模块。
局限于本地的执行环境
无论是本地工具链的工程化形式 还是持续集成的工程化形式,脚手架的执行环境始终局限于本地。所以必须解决操作系统兼容性的问他们。
多样性的实现模式 无论具体模式如何,优秀的脚手架工具应遵循的原则是一致的。
- 与构建、开发、部署等功能模块联动,在创建项目时生成对应的配置
- 自动安装依赖模块
- 动态可配置
- 底层高度可扩展
- 丰富而不繁琐的配置项
- 支持多种运行环境,比如命令行和Node.js API
- 兼容主流操作系统
4. 开源脚手架案例剖析
Sails.js 针对服务器端的脚手架方案
yeoman 脚手架方案
5. 集成Yeoman 封装脚手架方案
整体的流程是: 收集用户的配置信息 ---> 将动态的配置信息转换成静态的文件内容(yeoman 使用的是ejs引擎) ---> 将生成的文件复制到目标文件夹。
封装脚手架方案
创建一个完整的空的脚手架文件目录。
- 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
}
)
}
}
};
- 收集用户配置。
在index.js的promptiong()方法里写,用户提示和收集配置信息。将配置信息放到renderOption变量里。将用户项目的第三方依赖模块放到pkg变量里。
转化动态内容
- 将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 <% }%> })
自动安装依赖模板 在install()函数里面编写安装依赖的代码,注意一定要确定用户所创建的项目根目录是否为当前面目录,如果是新目录,需要首先进入目标目录再执行安装
集成到工程化体系中
目的: 1 统一工具栈 ,2 自动安装
集成到工程中使用的是 yeoman-environment这个模块,集成后我们使用命令 boi new < appname > --template < templateName > 来创建新项目
集成实现
使用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项目进行调试。