脚手架开发(2)-注册阶段
点击查看脚手架系列文章总览【正在更新】
个人网站: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-serviceadd [options] <plugin> [pluginOptions] install a plugin and invoke its generator in an already created projectinvoke [options] <plugin> [pluginOptions] invoke the generator of a plugin in an already created projectinspect [options] [paths...] inspect the webpack config in a project with vue-cli-serviceserve [options] [entry] serve a .js or .vue file in development mode with zero configbuild [options] [entry] build a .js or .vue file in production mode with zero configui [options] start and open the vue-cli uiinit [options] <template> <app-name> generate a project from a remote template (legacy API, requires @vue/cli-init)config [options] [value] inspect and modify the configoutdated [options] (experimental) check for outdated vue cli service / pluginsupgrade [options] [plugin-name] (experimental) upgrade vue cli service / pluginsmigrate [options] [plugin-name] (experimental) run migrator for an already-installed cli plugininfo print debugging information about your environmentRun 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 commandCommands: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
关注公众号【前端筱园】,不错过每一篇文章
脚手架开发(2)-注册阶段相关推荐
- 脚手架(一)——脚手架开发入门
脚手架 1. 脚手架是什么 2. 为什么要开发脚手架 3. 脚手架实现原理 4. 脚手架开发 4.1 开发流程 4.2 安装脚手架 4.3 脚手架开发难点 1. 脚手架是什么 脚手架本质是一个操作系统 ...
- 前端脚手架开发(1)
脚手架实现原理 当我们在终端输入vue create xx的时候 终端会去全局环境变量中,找到vue指令的方法 查看create-react-app的位置,所有通过npm -g安装的包都会放入该目录下 ...
- Python中str()与repr()函数的区别——repr() 的输出追求明确性,除了对象内容,还需要展示出对象的数据类型信息,适合开发和调试阶段使用...
Python中str()与repr()函数的区别 from:https://www.jianshu.com/p/2a41315ca47e 在 Python 中要将某一类型的变量或者常量转换为字符串对象 ...
- 软件开发之计划阶段: ”声控打鼓”游戏的”用户/场景”分析
"用户/场景"分析(a.k.a user scenarios)对于软件开发的计划阶段是十分重要的.只有明确了软件的用户群,以及软件所应用的场合,才能真正了解到所要开发的软件是否有价 ...
- 企业微信三方开发:注册企业微信服务商
其他链接 初识微信开发 企业微信三方开发:注册企业微信服务商 企业微信三方开发(一):回调验证及重要参数获取 企业微信三方开发(二):获取access_token 企业微信三方开发(三):网页授权登录 ...
- 亚马逊Amazon SP-API注册申请和授权对接开发和亚马逊SP-API开发人员注册资料的注意事项,PII申请的事项
关于亚马逊Amazon SP-API注册申请和授权对接开发和亚马逊SP-API开发人员注册资料的注意事项, 以及PII申请的事项,我简单聊几句吧. 不聊注册过程什么的,网上这类文章太多了,只说几个关键 ...
- 【Lilishop商城】No4-2.业务逻辑的代码开发,涉及到:会员B端第三方登录的开发-平台注册会员接口开发
仅涉及后端,全部目录看顶部专栏,代码.文档.接口路径在: [Lilishop商城]记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑,其中重点包括接口 ...
- Vue教程03-Vue脚手架开发环境
Vue脚手架开发环境 1.Vue开发环境的安装 1.1安装Node JS 1.2全局安装Vue脚手架 1.3安装HBuilderX 1.4强烈推荐安装以下工具软件 2.HBuilderX创建Vue项目 ...
- 软件开发的六大阶段 (指针经典原创)
软件开发的六大阶段 第一阶段:调研阶段 本阶段我们将组成企业项目调研组到企业进行现场调研,企业也部分需组织相应人员进行配合.整个调研工作将历时三星期到一个月左右时间.调研内容按以下方面进行 ...
最新文章
- AI一分钟 | 妈呀!连地铁都开始无人驾驶了,飞机还远吗;北京无人驾驶新规出台,终于知道李彦宏该不该被罚了(12月19日)
- Lync常识之Lync客户端有哪些
- oracle取消dataguard,【DataGuard】Oracle DataGuard 数据保护模式切换
- 备战双十一,大数据告诉你哪家快递公司最强?
- 2019年, SGG论文汇总
- C语言与Java怎么沟通_c语言初学指针,对于java面向对象的初理解
- Django的model中日期字段设置默认值的问题
- kaldi语音识别实战pdf_FSMN网络结构在语音识别声学模型的实践
- struts2 OGNL表达式
- 外卖平台系统开发需要注意什么?快跑者外卖系统好吗?
- Json与List的相互转换 [谷歌的Gson.jar和阿里的fastJson.jar]
- Xcode8自带注释不管用解决办法
- 【计算机网络】计算机网络基础知识笔记
- Linux capability初探
- 小学二年级计算机课游戏,小学体育课游戏_求10种左右适合小学一二年级学生体育课上做的游戏...
- 2019年安徽省模块七满分多少_2019年安徽中考总分是多少 考试科目及分值
- “地图易“制图工具——零代码制作漂亮业务地图
- 电脑变慢,4K对齐来解决
- 高德地图——地图图层
- MindManager2022免序列号弹窗解除功能限制