脚手架初始化
全局参数注册
使用 commander 来全局添加注册命令
const commander = require('commander');const program = new commander.Command();// 命令注册function registerCommand () {// option:选项// 用法:.option('-n, --name <name>', 'your name', 'jsliang')// 第一个参数是选项定义,可以用 |,, 和 ' ' 空格连接// 第二个参数为选项描述// 第三个参数为选项参数默认值(可选)program.name(Object.keys(pkg.bin)[0]).usage('<command> [options]').version(pkg.version).option('-d, --debug', '是否开启调式模式', false);// .on : custom event listeners// 开启 debug 模式program.on('option:debug', function() {if (program.opts().debug) {process.env.LOG_LEVEL = 'verbose';} else {process.env.LOG_LEVEL = 'info';}log.level = process.env.LOG_LEVEL;log.verbose('test');})// 对未知命令监听program.on('command:*', function(obj) {const availableCommands = program.commands.map(cmd => cmd.name())console.log(colors.red('未知的命令:'+obj[0]));if(availableCommands.length > 0){console.log(colors.red('可用命令为:'+availableCommands.join(',')))}})program.parse(program.argv);if (program.args && program.args.length < 1) {program.outputHelp();}}
使用 command 注册命令,并将命令执行函数抽离出来单独维护
const init = require('@temp-cli-dev/init');// The argument may be <required> or [optional]program.command('init [projectName]').option('-f, --force', '是否强制初始化项目').action(init);
高性能脚手架架构设计

- 将 init 命令做成一个动态加载的形式,不同的团队能够使用不同的模块初始化
- 动态加载的脚手架通过缓存形式进行存储:执行哪个命令下载哪个命令
- 动态加载的时候,通过 node 多进程进行执行,深挖 cpu 性能
脚手架命令动态加载功能架构设计
命令的动态加载
一个脚手架会包含很多命令,如:初始化项目,构建,打包等。这么将每个命令全部独立出来,与脚手架主流程解耦,一旦其中某个命令出现了问题或者需要更新,只需要更新相应的命令即可,不需要去动整个架构以及其他的命令模块。每个命令都是一个独立的 npm 包,将脚手架安装到本地时,并不会去安装这些命令包,而是在使用的时候才去动态的安装,简称动态加载
支持本地调试
为了方便本地开发调试,需要支持执行本地文件,在参数中增加
--targetPath <targetPath>则会去执行targetPath下的文件,不会去下载线上的 npm
program.name(Object.keys(pkg.bin)[0]).usage('<command> [options]').version(pkg.version).option('-d, --debug', '是否开启调式模式', false).option('-tp, --targetPath <targetPath>', '是否指定本地调试文件路径', '');//指定targetPath// 是否执行本地代码,我们通过一个属性来进行标识:targetPathprogram.on('option:targetPath', function () {// 将命令中的参数写入环境变量中实现解耦,不同的项目都可访问到该变量process.env.CLI_TARGET_PATH = program.opts().targetPath;});
let targetPath = process.env.CLI_TARGET_PATH;const homePath = process.env.CLI_HOME_PATH;let storeDir ='';let pkg;const cmdObj = arguments[arguments.length - 1];const cmdName = cmdObj.name();const packageName = SETTINGS[cmdName];const packageVersion = 'latest';// 如果不存在targetPath,说明是执行线上的命令,手动设置缓存本地的targetPath路径及缓存路径if (targetPath) {pkg = new Package({targetPath,packageName,packageVersion})const rootFile = pkg.getRootFilePath();if (rootFile) { //新添加require(rootFile).apply(null,arguments);}}
支持动态更新
如果本地存在最新的版本命令包,则不需要每次都去线上下载。如果有了新的版本,则需要更新本地的包。
// 如果不存在targetPath,说明是执行线上的命令,手动设置缓存本地的targetPath路径及缓存路径if (!targetPath) {//生成缓存路径targetPath = path.resolve(homePath, CATCH_DIR);storeDir = path.resolve(targetPath, 'node_modules');pkg = new Package({targetPath,storeDir,packageName,packageVersion});if (await pkg.exists()) {// 更新packageawait pkg.update();} else {// 安装packageawait pkg.install();}}
// 安装 packageasync install() {return npminstall({root: this.targetPath, // 安装storeDir: this.storeDir, // 缓存路径registry: getDefaultRegistry(), // 下载源pkgs: [{name: this.packageName, // 需要下载的包名version: this.packageVersion // 包的版本}]});}// 更新 packageasync update() {//获取最新的npm模块版本号const latestPackageVersion = await getNpmLatestVersion(this.packageName);// 查询最新版本号对应的路径是否存在const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)// 如果不存在,则直接安装最新版本if(!pathExists(latestFilePath)){await npminstall({root:this.targetPath,storeDir:this.storeDir,registry:getDefaultRegistry(),pkgs:[{name:this.packageName,version:latestPackageVersion}]})this.packageVersion = latestPackageVersion} else {this.packageVersion = latestPackageVersion}return latestFilePath;}
找到入口文件并执行
一个 npm 包的入口文件会在 package.json 中的 main 或 bin 中定义,因此首选需要找到 package.json 的路径,然后再找到入口文件的路径,最后再执行。
const path = require('path');const pkgDir = require('pkg-dir').sync;const formatPath = require('@temp-cli-dev/format-path');// 获取入口文件路径getRootFilePath() {function _getRootFile(targetPath){// 1.获取package.json所在的目录 - pkg-dir// pkg-dir 从某个目录开始向上查找,直到找到存在 package.json 的目录,并返回该目录。如果未找到则返回 nullconst dir = pkgDir(targetPath);if (dir) {// 2.读取package.json - require() js/json/nodeconst pkgFile = require(path.resolve(dir, 'package.json'));// 3.寻找main/lib - pathif (pkgFile && pkgFile.main) {// 4.路径的兼容(macOS/windows)return formatPath(path.resolve(dir, pkgFile.main));}}return null;}if (this.storeDir) {return _getRootFile(this.cachFilePath);} else {return _getRootFile(this.targetPath);}}
解决不同操作系统路径兼容问题
'use strict';const path = require('path');module.exports = formatPath;// 路径的兼容(macOS/windows)function formatPath(nowPath) {const sep = path.sep;if (nowPath && typeof nowPath === 'string' && sep !== '/') {return nowPath.replace(/\\/g, '/');}return nowPath;}
