学习总结篇,以能否造轮子来衡量学习效果。本篇主要介绍近期使用的一个cli工具
随着公司各端的业务进行,前端方面会沉淀出一些通用的解决方案和模板。此时,统一维护和管理就非常有必要了。allen-cli
就是基于这样的场景而诞生的。
这个项目脚手架,最终实现:整合各个模板,一键生成模板
目前实现的功能为:
allen init
命令选择一个脚手架模版进行下载,然后创建对应的app。项目的整体结构:
npm init
创建package.json
, 主要加上bin
命令
{ "bin": { "allen": "bin/allen", "allen-init": "bin/allen-init" }, }
一个CLI需要通过命令行输入各种参数,可以直接用nodejs的process相关api进行解析,但是更推荐使用commander
这个npm包可以大大简化解析的过程。
#!/usr/bin/env node const program = require('commander') console.log('version', require('../package').version) program .version(require('../package').version) .usage('<command> [项目名称]') .command('init', '创建新项目') .parse(process.argv)
allen-init
// NODE moudle // node.js 命令行解决方案 const program = require("commander"); // node.js path模块 const path = require("path"); // node.js fs模块 const fs = require("fs"); // 常见的交互式命令行用户接口的集合 const inquirer = require("inquirer"); // 使用shell模式匹配文件 const glob = require("glob"); // 活动最新的npm包 const latestVersion = require("latest-version"); // node.js 子进程 const spawn = require("child_process").spawn; // node.js 命令行环境的 loading效果, 和显示各种状态的图标 const ora = require("ora"); // The UNIX command rm -rf for node. const rm = require("rimraf").sync; async function main() { let projectRoot, templateName try { // 检测版本 let isUpate = await checkVersion(); // 更新版本 if (isUpate) await updateCli(); // 检测路径 projectRoot = await checkDir(); // 创建路径 makeDir(projectRoot) // 选择模板 let { git } = await selectTemplate(); // 下载模板 templateName = await dowload(rootName, git); // 本地配置 let customizePrompt = await getCustomizePrompt(templateName, CONST.CUSTOMIZE_PROMPT) // 渲染本地配置 await render(projectRoot, templateName, customizePrompt); // 删除无用文件 deleteCusomizePrompt(projectRoot) // 构建结束 afterBuild(); } catch (err) { log.error(`创建失败:${err.message}`) afterError(projectRoot, templateName) } }
创建文件和选择模板
// 创建路径 function makeDir (projectRoot) { if (projectRoot !== ".") { fs.mkdirSync(projectName); } } /** * 模板选择 */ function selectTemplate() { return new Promise((resolve, reject) => { let choices = Object.values(templateConfig).map(item => { return { name: item.name, value: item.value }; }); let config = { // type: 'checkbox', type: "list", message: "请选择创建项目类型", name: "select", choices: [new inquirer.Separator("模板类型"), ...choices] }; inquirer.prompt(config).then(data => { let { select } = data; let { value, git } = templateConfig[select]; resolve({ git, // templateValue: value }); }); }); }
下载模板, 用的是download-git-repo
const download = require('download-git-repo') const path = require('path') const ora = require('ora') const logSymbols = require("log-symbols"); const chalk = require("chalk"); const CONST = require('../conf/const') module.exports = function (target, url) { const spinner = ora(`正在下载项目模板,源地址:${url}`) target = path.join(CONST.TEMPLATE_NAME) spinner.start() return new Promise((resolve,reject) => { download(`direct:${url}`, target, { clone: true }, (err) => { if (err) { spinner.fail() console.log(logSymbols.fail, chalk.red("模板下载失败:(")); reject(err) } else { spinner.succeed() console.log(logSymbols.success, chalk.green("模板下载完毕:)")); resolve(target) } }) }) }
采用的是inquirer
的这个库
// 常见的交互式命令行用户接口的集合 const inquirer = require("inquirer");
如果需要将一些配置放在本地文件,则可以创建一些本地配置
/** * * @param target 模板路径 * @param fileName 读取文件名 */ function getCustomizePrompt (target, fileName) { return new Promise ((resolve) => { const filePath = path.join(process.cwd(), target, fileName) if(fs.existsSync(filePath)) { console.log('读取模板配置文件') let file = require(filePath) resolve(file) } else { console.log('该文件没有配置文件') resolve([]) } }) }
template.json
{ type: "confirm", name: "mobile", message: "是否用于移动端?" }, { type: "confirm", name: "flexible", message: "是否使用移动端适配?", when: function (answers) { return answers.mobile } },
// NODE moudle // node.js 命令行解决方案 const program = require("commander"); // node.js path模块 const path = require("path"); // node.js fs模块 const fs = require("fs"); // 常见的交互式命令行用户接口的集合 const inquirer = require("inquirer"); // 使用shell模式匹配文件 const glob = require("glob"); // 活动最新的npm包 const latestVersion = require("latest-version"); // node.js 子进程 const spawn = require("child_process").spawn; // node.js 命令行环境的 loading效果, 和显示各种状态的图标 const ora = require("ora"); // The UNIX command rm -rf for node. const rm = require("rimraf").sync;
在项目目录下运行npm i -g
,注册全局命令allen-cli
即可使用
C:\Users\XX\AppData\Roaming\npm
目录下会生成相应的可执行文件:
一个基本的脚手架CLI就完成了。
npm包传送门
:https://www.npmjs.com/package/allen-cli欢迎试用:npm i -g allen-cli
使用过Linux或者Unix的开发者,对于Shebang应该不陌生,它是一个符号的名称,#!
。这个符号通常在Unix系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序
。了解了Shebang之后就可以理解,增加这一行是为了指定用node执行脚本文件
。
当你输入一个命令的时候,npm是如何识别并执行对应的文件的呢?
具体的原理阮一峰大神已经在npm scripts 使用指南中介绍过。简单的理解:
就是输入命令后,会有在一个新建的shell中执行指定的脚本,在执行这个脚本的时候,我们需要来指定这个脚本的解释程序是node。
在一些情况下,即使你增加了这一行,但还是可能会碰到一下错误,这是为什么呢?
No such file or directory
为了解决这个问题,首先需要了解一下/usr/bin/env。我们已经知道,Shebang是为了指定脚本的解释程序,可是不同用户或者不同的脚本解释器有可能安装在不同的目录下,系统如何知道要去哪里找你的解释程序呢?
/usr/bin/env就是告诉系统可以在PATH目录中查找。
所以配置#!/usr/bin/env node
, 就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
看到这里你应该理解,为什么会出现No such file or directory的错误?因为你的node安装路径没有添加到系统的PATH中。所以去进行node环境变量配置就可以了。
npm 脚本的原理非常简单。每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
比较特别的是,npm run新建的这个 Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。