脚手架核心流程开发

绘制 imooc-cli 脚手架架构设计图

实现 imooc-cli 脚手架准备过程代码

准备阶段分为以下步骤

  • 检测脚手架版本号
  • 检测 node 版本号
  • 检查 root 账户和降级权限
  • 检查用户主目录
  • 入参检查及debug模式开发
  • 检查环境变量
  • 检查新版本及提示更新

检测脚手架版本号

const log = require('@zjw-cli/log');
const pkg = require('../package.json');

function core() {
    checkVersion();
}

function checkVersion() {
    log.notice('cli', pkg.version);
}

module.exports = core;

检测 node 版本号

node 版本号可以通过 process.version 获取

const semver = require('semver');
const colors = require('colors/safe');
const constant = require('./const');

function core() {
    try {
        ...
        checkNodeVersion();
    } catch (error) {
        log.error(error.message)
    }
}

function checkNodeVersion() {
    const currentVersion = process.version;
      const lowestVersion = constant.LOWEST_NODE_VERSION;
    if (!semver.gte(currentVersion, lowestVersion)) {
        throw new Error(colors.red(`当前 node 版本:${currentVersion},最低 node 版本:${lowestVersion},请升级 node`))
    }
}

检查 root 账户和降级权限

当使用 root 账号创建的文件,别的账号是无法删除修改的,日常开发特别注意这一点。

使用 process.geteuid() 来获取账号权限

  • 普通账号(macOS):501
  • root账号:0(通过 sudo 来执行)

使用 root-check 来自动完成账号降级

function checkRoot() {
    const rootCheck = require('root-check');
    rootCheck()
    console.log(process.geteuid()) // 501
}
// 经过 root-check 处理后,在使用 sudo 执行,结果就是 501
// 原理:通过 process.setuid 、 process.setgid 来动态修改了用户及其分组的权限

检查用户主目录

function checkUserHome() {
    if (!userHome || !pathExists(userHome)) {
        throw new Error('当前登录用户主目录不存在')
    }
}

入参检查及debug模式开发

function core() {
    try {
        ...
        checkInputArgs();
        log.verbose('debug', 'test debug modal');
    } catch (error) {
        log.error(error.message)
    }
}

/**
 * @description 检查入参
 */
function checkInputArgs() {
    const args = require('minimist')(process.argv.slice(2));
    checkArgs(args)
}

/**
 * @description 更加入参动态修改 log level,开启 debug 模式
 * @param {Object} args 
 */
function checkArgs(args) {
    log.level = args.debug ? 'verbose' : 'info';
}

检查环境变量

可以在环境变量中存储用户信息,配置信息等数据。

  • 通过 dotenv 这个库实现,dotenv默认获取的是当前项目根目录中的 .env 文件。可以通过 path 属性来修改。
  • .env 文件中存储的数据格式是 key=value 这种格式的,获取到 .env 文件后会将里面的数据挂载到 process.env 上。
function checkEnv() {
    const dotenvPath = path.resolve(userHome, '.env')
    // 读取 .env 文件中的数据,并配置到环境变量中
    require('dotenv').config({ path: dotenvPath });
    createDefaultConfig();
    log.verbose('环境变量缓存文件路径', process.env.CLI_HOME_PATH)
}

/**
 * @description 创建环境变量缓存文件路径
 */
function createDefaultConfig() {
    const cliConfig = {};
    cliConfig.cliHomePath = process.env.CLI_HOME
        ? path.join(userHome, process.env.CLI_HOME)
        : path.join(userHome, constant.DEFAULT_CLI_HOME);
    process.env.CLI_HOME_PATH = cliConfig.cliHomePath;
}

检查新版本

  1. 获取npm 上所有的版本号
  2. 比较当前版本和 npm 上最新的版本
  3. 存在新版本提示更新
// core/cli/lib/index.js
async function checkGlobalUpdate() {
    const { getNpmSemverVersion } = require('@zjw-cli/get-npm-info');
    const lastVersion = await getNpmSemverVersion(pkg.version, pkg.name);
    if (lastVersion && semver.gt(lastVersion, pkg.version)) {
        throw new Error(colors.red(`请升级版本,最新版本:${lastVersion},升级命令:npm i ${pkg.name} -g`));
    }
}

// utils/get-npm-info/lib/index.js
'use strict';

const axios = require('axios');
const urlJoin = require('url-join');
const semver = require('semver');

// 优先调用淘宝源
function getRegistry(isOriginal = false) {
    return isOriginal ? 'https://registry.npmjs.org/' : 'https://registry.npm.taobao.org/';
}

function getNpmInfo(npmName, registry) {
    if (!npmName) return null;
    const targetRegistry = registry || getRegistry();
    const url = urlJoin(targetRegistry, npmName);
    return axios.get(url).then(res => {
        if (res.status === 200) {
            return res.data;
        } else {
            return {}
        }
    }).catch(err => Promise.reject(err))
}

async function getVersions(npmName, registry) {
    const data = await getNpmInfo(npmName, registry);
    return data.versions ? Object.keys(data.versions) : []
}

function getSemverVersions(baseVersion, versions) {
    return versions
        .filter(version => semver.satisfies(version, `>${baseVersion}`))
        .sort((a, b) => semver.gt(b, a) ? 1 : -1);
}

async function getNpmSemverVersion(baseVersion, npmName, registry) {
    const versions = await getVersions(npmName, registry);
    const newVersions = getSemverVersions(baseVersion, versions);
    return newVersions[0];
}

module.exports = {
    getNpmInfo,
    getVersions,
    getSemverVersions,
    getNpmSemverVersion
};

通过 commander 框架实现一个脚手架,包含自定义 option 和 command 功能

#!/usr/bin/env node

const commander = require('commander');
const program = new commander.Command();

const pkg = require('../package.json');

// 配置全局属性
program
    // 在 Commander 7 以前,选项的值是作为属性存储在 command 对象上的。 
    // 这种处理方式便于实现,但缺点在于,选项可能会与Command的已有属性相冲突。
    // 通过使用.storeOptionsAsProperties(),可以恢复到这种旧的处理方式,并可以不加改动的继续运行遗留代码。
    // Commander 7之后所有的属性都要通过 opts() 获取
    .storeOptionsAsProperties()
    // .name 和 .usage 用来修改帮助信息的首行提示。name 属性也可以从参数自动推导出来。
    .name(Object.keys(pkg.bin)[0])
    .usage('<command> [option]')
    .version(pkg.version)
    // 配置 option 并设置默认值
    .option('-d, --debug', '开启调试模式', false)
    // 配置 option,指定参数
    .option('-e, --envName <envName>', '输出环境变量名称');

// command 注册命令,注意:调用 command 方法返回的时命令对象而不是脚手架对象
const clone = program.command('clone <source> [destination]');
clone
    .description('clone a repository')
    .option('-f, --force', '是否强制克隆')
    .action((source, destination, cmdObj) => {
        console.log('clone command called', source, destination, cmdObj.force);
    });

// addCommand 注册子命令
const service = new commander.Command('service');

service
    .command('start [port]')
    .description('service start at same port')
    .action((port) => {
        console.log('service starting at ', port);
    });

service
    .command('stop')
    .description('stop service')
    .action(() => {
        console.log('service stop');
    });

program.addCommand(service);

/**
 * @description 独立执行的命令
 * 当.command()带有描述参数时,就意味着使用独立的可执行文件作为子命令。
 * Commander 将会执行一个新的命令。新的命令为: 脚手架命令-注册的命令,如下例中未配置 executableFile 前执行的是 'zjw-cli-test-install'。
 * 通过配置选项 executableFile 可以自定义名字。
 * 这种方式可以实现脚手架之间的嵌套调用
 */
program
    .command('install [name]', 'install package', {
        executableFile: 'zjw-cli', // 由原先的 zjw-cli-test-install 改为执行 zjw-cli
        // isDefault: true, // 默认执行该命令,即执行 zjw-cli-test 就是执行 zjw-cli-test install
        hidden: true // 在帮助文档中隐藏该命令
    })
    .alias('i')

// arguments 可以为最顶层命令指定命令参数。如下例:表示配置一个必选命令 username 和一个可选命令 password。
// arguments 指定的命令参数是泛指,只要不是 command 和 addCommand 注册的命令都会被捕获到。
// zjw-cli-test abc => username 就是 abc
// 可以向.description()方法传递第二个参数,从而在帮助中展示命令参数的信息。该参数是一个包含了 “命令参数名称:命令参数描述” 键值对的对象。
program
    .arguments('<username> [password]')
    .description('test command', {
        username: 'user to login',
        password: 'password for user, if required'
    })
    .action((username, password) => {
        console.log(username, password);
    });

/**
 * @description 自定义 help 信息
 * 方法一: 步骤一:重写 helpInformation,返回值是啥 help 信息就是啥;。
 * 方法二: 步骤一:重写 helpInformation,返回值空字符串; 步骤二: 调用 on 方法监听 --help,在回调函数中输出 help 信息。
 * 方法三: 步骤一:重写 helpInformation,返回值空字符串; 步骤二: 调用 addHelpText 方法
 *
 * 如果你不想清除内建的帮助信息,
 * 方法一:不重新 helpInformation,只监听 --help;
 * 方法二: 调用 addHelpText 方法(建议这种)
 */

program.helpInformation = function() {
    return 'output your help information\n';
}

/**
 * @description 实现 debug 模式
 * 方法一: 调用 on 方法监听 --debug
 * 方法二: 调用 on 方法监听 option:debug
 */
program.on('option:debug', () => {
    console.log('debug: ', program.debug)
    process.env.LOG_LEVEL = 'verbose'
})

/**
 * @description 监听未知的命令
 */
program.on('command:*', (obj) => {
    console.log(obj);
    console.error('未知的命令:', obj[0]);
    // 获取所有已注册的命令
    const availableCommands = program.commands.map(cmd => cmd.name());
    console.log('可用命令:', availableCommands.join(', '))
})

// 解析参数
program.parse(process.argv);

实现 node 对 ES Module 的支持

  1. 通过 webpack 来实现对 ES Module 支持,并且通过配置 loader,使用 babel 对 js 进行编译,来兼容低版本的 node。
  2. 将js文件后缀名为 .mjs,导入 .mjs。node 版本小于 14 需要加上 --experimental-modules 来对 mjs 进行支持。
Copyright © imooc-lego (2020 - present) all right reserved,powered by GitbookFile Modify: 2021-06-27 08:04:56

results matching ""

    No results matching ""