点击查看脚手架系列文章总览【正在更新】
个人网站:www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇文章
在上篇文章,已经完成了第一个阶段:准备阶段,在准备阶段做了许多基础工作,目的为保证满足脚手架的运行环境。
现在开始进入第二阶段:注册阶段,主要功能是完成命令的解析,以及命令的动态加载的实现。
前期改造
首先介绍两个常用的脚手架命令行交互工具包:yargs、commander
他们给我们在开发脚手架提供了极大的方便,功能大致相同,本篇文章使用的是 commander
作为例子。
设置基础信息
以 vue-cli
作为例子,输入 vue --help
时,可以看到 vue
支持的命令和配置参数,以帮助我们更好的使用它。输入 vue -V
可以看到当前的版本信息。
Usage: vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
create [options] <app-name> create a new project powered by vue-cli-service
add [options] <plugin> [pluginOptions] install a plugin and invoke its generator in an already created project
invoke [options] <plugin> [pluginOptions] invoke the generator of a plugin in an already created project
inspect [options] [paths...] inspect the webpack config in a project with vue-cli-service
serve [options] [entry] serve a .js or .vue file in development mode with zero config
build [options] [entry] build a .js or .vue file in production mode with zero config
ui [options] start and open the vue-cli ui
init [options] <template> <app-name> generate a project from a remote template (legacy API, requires @vue/cli-init)
config [options] [value] inspect and modify the config
outdated [options] (experimental) check for outdated vue cli service / plugins
upgrade [options] [plugin-name] (experimental) upgrade vue cli service / plugins
migrate [options] [plugin-name] (experimental) run migrator for an already-installed cli plugin
info print debugging information about your environment
Run vue <command> --help for detailed usage of given command.
这些功能通过 commander
可以很轻松的帮我们实现。
const { Command } = require('commander');
const pkg = require('../package.json');
const program = new Command();
program
.name(pkg.name) // 设置脚手架名称
.usage('st <command> [options]') // 设置usage
.version(pkg.version); // 设置版本号
注册参数
在上篇文章提到了一个是否开启 debug
模式,如果在命令后面加上 -d
或 --debug
参数则表示开启 debug
模式。
之前的处理方式需要自己在 process.argv.slice
中自行判断,使用 commder
可以很方便的实现。
第一步:注册参数
使用 option
方法,第一个参数表示参数是什么,可以使用 -d, --debug
的方式定义,前面的表示别名,第二个参数是描述信息。
program
.name(pkg.name)
.usage('st <command> [options]')
.version(pkg.version)
.option('-d, --debug', '是否开启debug模式');
第二步:监听参数
使用 program.on
可以监听输入内容是否包含某个参数,如果包含,则会执行相应的内容。
program.on('option:debug', () => {
const { debug } = program.opts();
if (debug) {
log.level = 'verbose';
} else {
log.level = STEAMED_CLI_LOG_LEVEL;
}
process.env.LOG_LEVEL = log.level;
});
注册命令
通过 .command()
或 .addCommand()
可以配置命令,有两种实现方式:为命令绑定处理函数,或者将命令单独写成一个可执行文件
- command:命令名称
- description:自定义描述信息
- option:设置可选配置参数
- action:自定义处理逻辑
program
.command('init [projectName]')
.description('初始化项目')
.option('-f, --force', '是否强制初始化项目')
.action(() => {
// 自定义处理逻辑
});
打印帮助信息
使用了 command
后,通过 st --help
可以打印出帮助信息,在某些情况下,也需要打印帮助信息,如:输入参数为空,输入命令不存在等,此时打印出帮助信息更有利于用户的使用。
判断参数为空
只需要判断 process.argv.length
的长度是否小于3,如果小于3,则说明没有出入任何参数,使用 .outputHelp()
方法可以打印帮助信息。
if (process.argv.length < 3) {
program.outputHelp();
}
输入不存在的命令
输入不存在的命令时会产生报错,使用 .showHelpAfterError()
方法可以在产生报错后打印帮助信息。
program.showHelpAfterError();
解析
最后一步也是必不可少的一步 ,需要将参数传入到 program.parse
方法中进行解析。program.parse(process.argv)
。
演示
输入 --help
命令可以输出全部 Options
和 Commands
。
> st --help
Usage: @steamed/cli st <command> [options]
Options:
-V, --version output the version number
-d, --debug 是否开启debug模式
-tp, --targetPath <targetPath> 指定本地路径
-h, --help display help for command
Commands:
init [options] [projectName] 初始化项目
help [command] display help for command
如果命令中存在 options
,可以通过 st <cmmand> --help
的方式开发 command
的详细配置说明。
> st init --help
初始化项目
Options:
-f, --force 是否强制初始化项目
-h, --help display help for command
命令的动态加载
开发一个大型的脚手架,犹如修建一栋房子,并不是一气呵成,而是需要各个部分相互协调组成起来的。将它拆分成各个模块,一旦出现问题,可以很方便的定位和解决问题。
一个脚手架会包含很多命令,如:初始化项目,构建,打包等。这么将每个命令全部独立出来,与脚手架主流程解耦,一旦其中某个命令出现了问题或者需要更新,只需要更新相应的命令即可,不需要去动整个架构以及其他的命令模块。
流程设计
每个命令都是一个独立的 npm
包,将脚手架安装到本地时,并不会去安装这些命令包,而是在使用的时候才去动态的安装,简称动态加载。
大体需要3个步骤:
- 输入命令,如初始化命令:
st init
- 安装执行命令对相应的
npm
到本地 - 执行命令包
支持本地调试
为了方便本地开发调试,需要支持执行本地文件,在参数中增加 -tp <targetPath>
则会去执行 targetPath
下的文件,不会去下载线上的 npm
。
支持动态更新
如果本地存在最新的版本命令包,则不需要每次都去线上下载。如果有了新的版本,则需要更新本地的包。
找到入口文件并执行
一个 npm
包的入口文件会在 package.json
中的 main
或 bin
中定义,因此首选需要找到 package.json
的路径,然后再找到入口文件的路径,最后再执行。
Package 类
我们需要开发一个名叫 pageage
的包,用来定义一个 Package
类,它主要提供这样几个功能:判断包是否存在、npm
包的安装、npm
包的更新、获取 npm
包的入口文件【具体实现会在后面提到】。
将上面的流程通过绘图的方式来表示:
exec 包
对于所有的命令执行他们的流程都是相同的,因此可以将上面的整个流程抽离成一个新的包
包名:@steamed/exec
路径:core/exec
作用:命令统一处理
1. 初始化参数
const commandMap = { // 命令名=>包名
'init': '@steamed/init'
}
const CACHE_DIR = 'dependencies/'; // 缓存文件夹名称
const command = process.argv[2]; // 获取输入的参数
let targetPath = process.env.STEAMED_ALI_TARGET_PATH; // 获取targetPath,可以通过 -tp 参数指定本地路径
const homePath = process.env.STEAMED_CLI_HOME_PATH; // 用户路径
const packageName = commandMap[command]; // 通过命令找到对应的包名
const packageVersion = 'latest'; // 设置包的版本,latest表示最新版本
let storeDir = ''; // 指定缓存路径
if (!targetPath) { // 如果不存在targetPath,说明是执行线上的命令,手动设置缓存本地的targetPath路径及缓存路径
targetPath = path.resolve(homePath, CACHE_DIR);
storeDir = path.resolve(targetPath, 'node_modules');;
}
2. 创建 Package
如果存在 storeDir
值,是执行缓存中的包,也就是线上的包。需要判断缓存中是否存在此包,不存在的话需要安装,否则进行更新。
如果不存在 storeDir
值,表示执行的是本地路径文件,不存在安装、更新等操作,针对于本地路径是否存在,可以在 -tp
参数监听中进行统一处理。
let pkg;
if (storeDir) {
pkg = new Package({
targetPath,
storeDir,
packageName,
packageVersion
});
} else {
pkg = new Package({
targetPath,
packageName,
packageVersion
});
}
3. 更新/安装命令包
if (await pkg.exists()) {
await pkg.update();
} else {
await pkg.install();
}
4. 获取入口文件并执行
const rootFilePath = pkg.getRootFilePath();
if (rootFilePath) {
require(rootFilePath).call(this, Array.from(arguments));
}
完整代码请访问Github:https://github.com/DengZhanyong/steamed
package 包
包名:@steamed/package
路径:models/package
作用:Package类,提供
npm
包的安装、更新、判断本地是否存在、获取入口文件路径
此包导出的是一个 Package
类,该类接收 4 个参数。
- targetPath:本地路径
- storeDir:缓存路径
- packageName:
npm
包名 - packageVersion: 包版本号
class Package {
constructor(props) {
if (!props) {
throw new Error('请传递参数');
}
if (!isObject(props)) {
throw new Error('参数应该是一个对象');
}
this.targetPath = props.targetPath; // 本地路径
this.storeDir = props.storeDir; // 缓存路径
this.packageName = props.packageName; // 包名
this.packageVersion = props.packageVersion; // 版本
}
// 判断包是否存在
exists() {}
// 安装 npm 包
install() {}
// 更新 npm 包
update() {}
// 获取到执行文件路径
getRootFilePath() {}
}
如何在本地安装一个线上的 npm
包
我们在项目中要安装一个 npm
包的话,只需要执行 npm install packageName@version
即可。
这里可以使用一个名叫 npminstall
的库来完成,
const npmInstall = require('npminstall');
const { getDefaultRegistry } = require('@steamed/get-npm-info');
npmInstall({
root: this.targetPath, // 安装路径
storeDir: this.storeDir, // 缓存路径
registry: getDefaultRegistry(), // 使用的源地址,在我们自己的get-npm-info包中已实现
pkgs: [{
name: this.packageName,
version: this.packageVersion
}]
});
通过 npmInstall
库下载到本地的 npm
包名格式为 _${cacheFilePathPrefix}@${packageVersion}@${packageName}
,其中前缀 cacheFilePathPrefix
对于普通包来说就是它本身,对于组织包来说需要将包名中 /
符号改为 _
,例如 @streamed/init
=> @streamed_init
。
可以抽离出一个获取本地缓存包路径的属性,方便其他地方使用:
this.cacheFilePathPrefix = this.packageName.replace('/', '_');
get cacheFilePath() {
return path.resolve(this.storeDir, `_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`);
}
如何获取入口文件路径
一个包的入口文件在 package.json
中的 main
属性上定义,那么首先需要找到 package.json
文件的路径。
脚手架支持传递一个本地路径,这个本地路径是用户自己传入的,对于一个本地项目来说,用户可以传入项目的根路径,也可以传入 package.json
的路径,还可以传入项目下的任意文件路径。对于这些情况,脚手架都应该支持,并正确的找到 package.json
的路径。
庆幸的是,这个功能也有现成的库给我们提供了该功能,名叫 pkg-dir
。它可以帮我们找到某个 node.js
项目或 npm
项目的根路径,package.json
所在位置处于根路径下一级。
const pkgDir = require('pkg-dir');
getRootFilePath() {
function _getRootFile(targetPath) {
const dir = pkgDir.sync(targetPath);
if (dir) {
const pkgFile = require(path.resolve(dir, 'package.json'))
if (pkgFile && pkgFile.main) {
return formatPath(path.resolve(dir, pkgFile.main));
}
}
return null;
}
return _getRootFile(this.storeDir ? this.cacheFilePath : this.targetPath);
}
如何判断是否需要更新
- 获取最新版本号(此方法在
get-npm-info
中实现) cacheFilePath
中包含了包名及版本号,相同的包不同的版本对应不同的路径,判断cacheFilePath
在本地是否存在,如果不存在,则说明本地缓存中没有最新版本包,此时就需要更新。更新的本质是下载最新的包到本地缓存中,并不会删除已安装的其他版本的包。
完整代码请访问Github:https://github.com/DengZhanyong/steamed
点击查看脚手架系列文章总览【正在更新】
个人网站:www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇文章