脚手架
vue-cli, create-react-app、react-native-cli 等都是非常优秀的脚手架,通过脚手架,我们可以快速初始化一个项目,无需自己从零开始一步步配置,有效提升开发体验。尽管这些脚手架非常优秀,但是未必是符合我们的实际应用的,我们可以定制一个属于自己的脚手架(或公司通用脚手架),来提升自己的开发效率。
脚手架的作用
- 减少重复性的工作,不需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。
- 可以根据交互动态生成项目结构和配置文件。
- 多人协作更为方便,不需要把文件传来传去。
实现的功能
在开始之前,我们需要明确自己的脚手架需要哪些功能。vue init template-name project-name 、create-react-app project-name。我们这次编写的脚手架(eos-cli)具备以下能力(脚手架的名字爱叫啥叫啥,随便写):
- eos init template-name project-name 根据远程模板,初始化一个项目(远程模板可配置)
- eos config set 修改配置信息
- eos config get [] 查看配置信息
- eos –version 查看当前版本号
- eos -h
大家可以自行扩展其它的 commander,本篇文章旨在教大家如何实现一个脚手架。
本项目完整代码请戳: https://github.com/ytanck/eos-cli
或
本项目完整代码请戳: https://github.com/ytanck/yang-cli
需要使用的第三方库
- babel-cli/babel-env: 语法转换
- commander: 命令行工具
- download-git-repo: 用来下载远程模板
- ini: 格式转换
- inquirer: 交互式命令行工具
- ora: 显示loading动画
- chalk: 修改控制台输出内容样式
- log-symbols: 显示出 √ 或 × 等的图标
关于这些第三方库的说明,可以直接npm上查看相应的说明,此处不一一展开。
初始化项目
创建一个空项目(eos-cli),使用 npm init 进行初始化。
安装依赖
1
| npm install babel-cli babel-env chalk commander download-git-repo ini inquirer log-symbols ora
|
目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ├── bin │ └── www //可执行文件 ├── dist ├── ... //生成文件 └── src ├── config.js //管理eos配置文件 ├── index.js //主流程入口文件 ├── init.js //init command ├── main.js //入口文件 └── utils ├── constants.js //定义常量 ├── get.js //获取模板 └── rc.js //配置文件 ├── .babelrc //babel配置文件 ├── package.json ├── README.md
|
babel 配置
开发使用了ES6语法,使用 babel 进行转义,
.bablerc
1 2 3 4 5 6 7 8 9 10 11 12
| { "presets": [ [ "env", { "targets": { "node": "current" } } ] ] }
|
eos 命令
node.js 内置了对命令行操作的支持,package.json 中的 bin 字段可以定义命令名和关联的执行文件。在 package.json 中添加 bin 字段
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13
| { "name": "eos-cli", "version": "1.0.0", "description": "脚手架", "main": "index.js", "bin": { "eos": "./bin/www" }, "scripts": { "compile": "babel src -d dist", "watch": "npm run compile -- --watch" } }
|
www 文件
行首加入一行 #!/usr/bin/env node 指定当前脚本由node.js进行解析
1 2
| #! /usr/bin/env node require('../dist/main.js');
|
链接到全局环境
开发过程中为了方便调试,在当前的 eos-cli 目录下执行 npm link,将 eos 命令链接到全局环境。
启动项目
处理命令行
利用 commander 来处理命令行。
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| import program from 'commander'; import { VERSION } from './utils/constants'; import apply from './index'; import chalk from 'chalk';
let actionMap = { init: { description: 'generate a new project from a template', usages: [ 'eos init templateName projectName' ] }, config: { alias: 'cfg', description: 'config .eosrc', usages: [ 'eos config set <k> <v>', 'eos config get <k>', 'eos config remove <k>' ] }, }
Object.keys(actionMap).forEach((action) => { program.command(action) .description(actionMap[action].description) .alias(actionMap[action].alias) .action(() => { switch (action) { case 'config': apply(action, ...process.argv.slice(3)); break; case 'init': apply(action, ...process.argv.slice(3)); break; default: break; } }); });
function help() { console.log('\r\nUsage:'); Object.keys(actionMap).forEach((action) => { actionMap[action].usages.forEach(usage => { console.log(' - ' + usage); }); }); console.log('\r'); } program.usage('<command> [options]');
program.on('-h', help); program.on('--help', help);
program.version(VERSION, '-V --version').parse(process.argv);
if (!process.argv.slice(2).length) { program.outputHelp(make_green); } function make_green(txt) { return chalk.green(txt); }
|
下载模板
download-git-repo 支持从 Github、Gitlab 下载远程仓库到本地。
get.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { getAll } from './rc'; import downloadGit from 'download-git-repo';
export const downloadLocal = async (templateName, projectName) => { let config = await getAll(); let api = `${config.registry}/${templateName}`; return new Promise((resolve, reject) => { downloadGit(api, projectName, (err) => { if (err) { reject(err); } resolve(); }); }); }
|
init 命令
命令行交互
在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。命令行交互利用 inquirer 来实现:
1 2 3 4 5 6 7 8 9 10 11 12
| inquirer.prompt([ { name: 'description', message: 'Please enter the project description: ' }, { name: 'author', message: 'Please enter the author name: ' } ]).then((answer) => { });
|
视觉美化
在用户输入之后,开始下载模板,这时候使用 ora 来提示用户正在下载模板,下载结束之后,也给出提示。
1 2 3 4 5
| import ora from 'ora'; let loading = ora('downloading template ...'); loading.start();
loading.succeed();
|
init.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import { downloadLocal } from './utils/get'; import ora from 'ora'; import inquirer from 'inquirer'; import fs from 'fs'; import chalk from 'chalk'; import symbol from 'log-symbols';
let init = async (templateName, projectName) => { if (!fs.existsSync(projectName)) { inquirer.prompt([ { name: 'description', message: 'Please enter the project description: ' }, { name: 'author', message: 'Please enter the author name: ' } ]).then(async (answer) => { let loading = ora('downloading template ...'); loading.start(); downloadLocal(templateName, projectName).then(() => { loading.succeed(); const fileName = `${projectName}/package.json`; if(fs.existsSync(fileName)){ const data = fs.readFileSync(fileName).toString(); let json = JSON.parse(data); json.name = projectName; json.author = answer.author; json.description = answer.description; fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8'); console.log(symbol.success, chalk.green('Project initialization finished!')); } }, () => { loading.fail(); }); }); }else { console.log(symbol.error, chalk.red('The project already exists')); } } module.exports = init;
|
config 配置
1
| eos config set registry vuejs-templates
|
config 配置,支持我们使用其它仓库的模板,例如,我们可以使用 vuejs-templates 中的仓库作为模板。这样有一个好处:更新模板无需重新发布脚手架,使用者无需重新安装,并且可以自由选择下载目标。
config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { get, set, getAll, remove } from './utils/rc';
let config = async (action, key, value) => { switch (action) { case 'get': if (key) { let result = await get(key); console.log(result); } else { let obj = await getAll(); Object.keys(obj).forEach(key => { console.log(`${key}=${obj[key]}`); }) } break; case 'set': set(key, value); break; case 'remove': remove(key); break; default: break; } }
module.exports = config;
|
?rc.js
.eosrc 文件的增删改查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import { RC, DEFAULTS } from './constants'; import { decode, encode } from 'ini'; import { promisify } from 'util'; import chalk from 'chalk'; import fs from 'fs';
const exits = promisify(fs.exists); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile);
export const get = async (key) => { const exit = await exits(RC); let opts; if (exit) { opts = await readFile(RC, 'utf8'); opts = decode(opts); return opts[key]; } return ''; }
export const getAll = async () => { const exit = await exits(RC); let opts; if (exit) { opts = await readFile(RC, 'utf8'); opts = decode(opts); return opts; } return {}; }
export const set = async (key, value) => { const exit = await exits(RC); let opts; if (exit) { opts = await readFile(RC, 'utf8'); opts = decode(opts); if(!key) { console.log(chalk.red(chalk.bold('Error:')), chalk.red('key is required')); return; } if(!value) { console.log(chalk.red(chalk.bold('Error:')), chalk.red('value is required')); return; } Object.assign(opts, { [key]: value }); } else { opts = Object.assign(DEFAULTS, { [key]: value }); } await writeFile(RC, encode(opts), 'utf8'); }
export const remove = async (key) => { const exit = await exits(RC); let opts; if (exit) { opts = await readFile(RC, 'utf8'); opts = decode(opts); delete opts[key]; await writeFile(RC, encode(opts), 'utf8'); } }
|
发布
npm publish 将本脚手架发布至npm上。其它用户可以通过 npm install eos-cli -g 全局安装。
即可使用 eos 命令。
项目地址
本项目完整代码请戳: https://github.com/ytanck/eos-cli