点击查看脚手架系列文章总览【正在更新】
个人网站:www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇文章

在上篇文章,已经完成了第一个阶段:准备阶段,在准备阶段做了许多基础工作,目的为保证满足脚手架的运行环境。

现在开始进入第二阶段:注册阶段,主要功能是完成命令的解析,以及命令的动态加载的实现。

前期改造

首先介绍两个常用的脚手架命令行交互工具包:yargscommander

他们给我们在开发脚手架提供了极大的方便,功能大致相同,本篇文章使用的是 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 命令可以输出全部 OptionsCommands

> 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 中的 mainbin 中定义,因此首选需要找到 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. 脚手架(一)——脚手架开发入门

    脚手架 1. 脚手架是什么 2. 为什么要开发脚手架 3. 脚手架实现原理 4. 脚手架开发 4.1 开发流程 4.2 安装脚手架 4.3 脚手架开发难点 1. 脚手架是什么 脚手架本质是一个操作系统 ...

  2. 前端脚手架开发(1)

    脚手架实现原理 当我们在终端输入vue create xx的时候 终端会去全局环境变量中,找到vue指令的方法 查看create-react-app的位置,所有通过npm -g安装的包都会放入该目录下 ...

  3. Python中str()与repr()函数的区别——repr() 的输出追求明确性,除了对象内容,还需要展示出对象的数据类型信息,适合开发和调试阶段使用...

    Python中str()与repr()函数的区别 from:https://www.jianshu.com/p/2a41315ca47e 在 Python 中要将某一类型的变量或者常量转换为字符串对象 ...

  4. 软件开发之计划阶段: ”声控打鼓”游戏的”用户/场景”分析

    "用户/场景"分析(a.k.a user scenarios)对于软件开发的计划阶段是十分重要的.只有明确了软件的用户群,以及软件所应用的场合,才能真正了解到所要开发的软件是否有价 ...

  5. 企业微信三方开发:注册企业微信服务商

    其他链接 初识微信开发 企业微信三方开发:注册企业微信服务商 企业微信三方开发(一):回调验证及重要参数获取 企业微信三方开发(二):获取access_token 企业微信三方开发(三):网页授权登录 ...

  6. 亚马逊Amazon SP-API注册申请和授权对接开发和亚马逊SP-API开发人员注册资料的注意事项,PII申请的事项

    关于亚马逊Amazon SP-API注册申请和授权对接开发和亚马逊SP-API开发人员注册资料的注意事项, 以及PII申请的事项,我简单聊几句吧. 不聊注册过程什么的,网上这类文章太多了,只说几个关键 ...

  7. 【Lilishop商城】No4-2.业务逻辑的代码开发,涉及到:会员B端第三方登录的开发-平台注册会员接口开发

    仅涉及后端,全部目录看顶部专栏,代码.文档.接口路径在: [Lilishop商城]记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑,其中重点包括接口 ...

  8. Vue教程03-Vue脚手架开发环境

    Vue脚手架开发环境 1.Vue开发环境的安装 1.1安装Node JS 1.2全局安装Vue脚手架 1.3安装HBuilderX 1.4强烈推荐安装以下工具软件 2.HBuilderX创建Vue项目 ...

  9. 软件开发的六大阶段 (指针经典原创)

     软件开发的六大阶段      第一阶段:调研阶段 本阶段我们将组成企业项目调研组到企业进行现场调研,企业也部分需组织相应人员进行配合.整个调研工作将历时三星期到一个月左右时间.调研内容按以下方面进行 ...

最新文章

  1. AI一分钟 | 妈呀!连地铁都开始无人驾驶了,飞机还远吗;北京无人驾驶新规出台,终于知道李彦宏该不该被罚了(12月19日)
  2. Lync常识之Lync客户端有哪些
  3. oracle取消dataguard,【DataGuard】Oracle DataGuard 数据保护模式切换
  4. 备战双十一,大数据告诉你哪家快递公司最强?
  5. 2019年, SGG论文汇总
  6. C语言与Java怎么沟通_c语言初学指针,对于java面向对象的初理解
  7. Django的model中日期字段设置默认值的问题
  8. kaldi语音识别实战pdf_FSMN网络结构在语音识别声学模型的实践
  9. struts2 OGNL表达式
  10. 外卖平台系统开发需要注意什么?快跑者外卖系统好吗?
  11. Json与List的相互转换 [谷歌的Gson.jar和阿里的fastJson.jar]
  12. Xcode8自带注释不管用解决办法
  13. 【计算机网络】计算机网络基础知识笔记
  14. Linux capability初探
  15. 小学二年级计算机课游戏,小学体育课游戏_求10种左右适合小学一二年级学生体育课上做的游戏...
  16. 2019年安徽省模块七满分多少_2019年安徽中考总分是多少 考试科目及分值
  17. “地图易“制图工具——零代码制作漂亮业务地图
  18. 电脑变慢,4K对齐来解决
  19. 高德地图——地图图层
  20. MindManager2022免序列号弹窗解除功能限制

热门文章

  1. 重新认识java(十一)---- java中的数组
  2. 关于win7 32bit连接win10共享打印机0x0000011b解决办法
  3. C# wave mp3 播放器探寻
  4. 顺丰菜鸟之争落幕:今日12时起恢复数据传输
  5. HHC6003: Error: The file Itircl.dll has not been
  6. 美丽即可用抑或可用即美丽?
  7. 关于Cisco路由器配置DHCP全面详解
  8. 高斯-赛德尔迭代(Gauss–Seidel method)c语言实现
  9. JAVA 知识点 | Hook
  10. Linux 修改用户名(同时修改用户组名和家目录)