脚手架开发(2)-注册阶段

点击查看脚手架系列文章总览【正在更新】
个人网站: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-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 命令可以输出全部 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 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 中的 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
关注公众号【前端筱园】,不错过每一篇文章
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端筱园

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值