【架构师(第十一篇)】脚手架之命令注册和执行过程开发

1,953 阅读5分钟

脚手架命令动态加载功能架构设计图

123.png

是否执行本地代码

设置全局的 targetPath

// 设置全局的 targetPath
program.on('option:targetPath', () => {
    process.env.CLI_TARGET_PATH = params.targetPath;
});

新建一个 exec 包,动态加载命令

lerna create @hzw-cli-dev/exec

新建一个 package 包,放在 models 目录下面

lerna create @hzw-cli-dev/package

新建一个 command 包,放在 models 目录下面

lerna create @hzw-cli-dev/command

npminstall 用法

const npmInstall = require('npminstall');
const path = require('path');
const userHome = require('user-home');

// 直接调用安装方法
npmInstall({
  root: path.resolve(userHome, '.hzw-cli-dev'), // 模块路径
  storeDir: path.resolve(userHome, '', 'node_modules'), // 模块安装位置
  register: 'https://registry.npmjs.org', // 设置 npm 源
  pkgs: [ // 要安装的包信息
    {
      name: 'warbler-js',
      version: '',
    },
  ],
});

Package 类的实现

'use strict';

const { isObject } = require('@hzw-cli-dev/utils');
const { getRegister, getLatestVersion } = require('@hzw-cli-dev/get-npm-info');
const formatPath = require('@hzw-cli-dev/format-path');
const npmInstall = require('npminstall');
const fse = require('fs-extra');
const pathExists = require('path-exists').sync;
const pkgDir = require('pkg-dir').sync;
const path = require('path');
// Package 类 管理模块
class Package {
  /**
   * @description: 构造函数
   * @param {*} options 用户传入的配置信息
   * @return {*}
   */
  constructor(options) {
    if (!options) {
      throw new Error('Package 类的参数不能为空!');
    }
    if (!isObject(options)) {
      throw new Error('Package 类的参数必须是对象类型!');
    }
    // 获取 targetPath ,如果没有 则说明不是一个本地的package
    this.targetPath = options.targetPath;
    // 模块安装位置 缓存路径
    this.storeDir = options.storeDir;
    // package 的 name
    this.packageName = options.packageName;
    // package 的 Version
    this.packageVersion = options.packageVersion;
    // 缓存路径的前缀
    this.cacheFilePathPrefix = this.packageName.replace('/', '_');
  }

  /**
   * @description: 准备工作
   * @param {*}
   * @return {*}
   */
  async prepare() {}

  /**
   * @description: 获取当前模块缓存路径
   * @param {*}
   * @return {*}
   */
  get cacheFilePath() {}

  /**
   * @description: 获取最新版本模块缓存路径
   * @param {*}
   * @return {*}
   */
  getSpecificFilePath(packageVersion) {}

  /**
   * @description: 判断当前 package 是否存在
   * @param {*}
   * @return {*}
   */
  async exists() {}

  /**
   * @description: 安装 package
   * @param {*}
   * @return {*}
   */
  async install() {}

  /**
   * @description: 更新 package
   * @param {*}
   * @return {*}
   */
  async update() {}

  /**
   * @description:获取入口文件的路径
   * 1.获取package.json所在的目录 pkg-dir
   * 2.读取package.json
   * 3.找到main或者lib属性 形成路径
   * 4.路径的兼容(macOs/windows)
   * @param {*}
   * @return {*}
   */
  getRootFilePath() {}
module.exports = Package;

准备工作

  /**
   * @description: 准备工作
   * @param {*}
   * @return {*}
   */
  async prepare() {
    // 当缓存目录不存在的时候
    if (this.storeDir && !pathExists(this.storeDir)) {
      // 创建缓存目录
      fse.mkdirpSync(this.storeDir);
    }
    // 获取最新版本
    const latestVersion = await getLatestVersion(this.packageName);
    // 如果设定的版本号是最新的话,就赋值
    if (this.packageVersion === 'latest') {
      this.packageVersion = latestVersion;
    }
  }

获取当前模块缓存路径

  /**
   * @description: 获取当前模块缓存路径
   * @param {*}
   * @return {*}
   */
  get cacheFilePath() {
    return path.resolve(
      this.storeDir,
      `_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`,
    );
  }

获取最新版本模块缓存路径

  /**
   * @description: 获取最新版本模块缓存路径
   * @param {*}
   * @return {*}
   */
  getSpecificFilePath(packageVersion) {
    return path.resolve(
      this.storeDir,
      `_${this.cacheFilePathPrefix}@${packageVersion}@${this.packageName}`,
    );
  }

判断当前 package 是否存在

  /**
   * @description: 判断当前 package 是否存在
   * @param {*}
   * @return {*}
   */
  async exists() {
    // 如果 this.storeDir 存在 ,就是需要下载安装,否则就是本地安装
    if (this.storeDir) {
      // 获取具体版本号
      await this.prepare();
      return pathExists(this.cacheFilePath);
    } else {
      // 查看本地路径是否存在
      return pathExists(this.targetPath);
    }
  }

安装 package

  /**
   * @description: 安装 package
   * @param {*}
   * @return {*}
   */
  async install() {
    await this.prepare();
    return npmInstall({
      root: this.targetPath, // 模块路径
      storeDir: this.storeDir, // 模块安装位置
      register: getRegister('npm'), // 设置 npm 源
      pkgs: [
        // 要安装的包信息
        {
          name: this.packageName,
          version: this.packageVersion,
        },
      ],
    });
  }

更新 package

  /**
   * @description: 更新 package
   * @param {*}
   * @return {*}
   */
  async update() {
    await this.prepare();
    // 获取最新版本号
    const latestVersion = await getLatestVersion(this.packageName);
    // 查询本地是否已经是最新版本
    const localPath = this.getSpecificFilePath(latestVersion);
    const isLocalLatestVersion = pathExists(localPath);
    console.log('🚀🚀 ~ Package ~ latestVersion', latestVersion);
    console.log('🚀🚀 ~ Package ~ localPath', localPath);
    console.log('🚀🚀 ~ Package ~ isLocalLatestVersion', isLocalLatestVersion);
    // 如果不是最新版本 安装最新版本
    if (!isLocalLatestVersion) {
      await npmInstall({
        root: this.targetPath, // 模块路径
        storeDir: this.storeDir, // 模块安装位置
        register: getRegister('npm'), // 设置 npm 源
        pkgs: [
          // 要安装的包信息
          {
            name: this.packageName,
            version: latestVersion,
          },
        ],
      });
      this.packageVersion = latestVersion;
    }
  }

获取入口文件的路径

  /**
   * @description:获取入口文件的路径
   * 1.获取package.json所在的目录 pkg-dir
   * 2.读取package.json
   * 3.找到main或者lib属性 形成路径
   * 4.路径的兼容(macOs/windows)
   * @param {*}
   * @return {*}
   */
  getRootFilePath() {
    function _getRootFIle(_path) {
      const dir = pkgDir(_path);
      if (dir) {
        const pkgFile = require(path.resolve(dir, 'package.json'));
        if (pkgFile && (pkgFile.main || pkgFile.lib)) {
          const rootPath =
            formatPath(path.resolve(dir, pkgFile.main)) ||
            formatPath(path.resolve(dir, pkgFile.lib));
          return rootPath;
        }
      }
      return null;
    }
    // 如果 this.storeDir 存在 ,就是需要下载安装,否则就是本地安装
    if (this.storeDir) {
      return _getRootFIle(this.cacheFilePath);
    }
    return _getRootFIle(this.targetPath);
  }

脚手架优化后执行过程

week4_01.png

Command 类的实现

'use strict';

// 引入版本比对第三方库 semver
const semver = require('semver');
const LOWEST_NODE_VERSION = '12.0.0';
// 引入颜色库 colors
const colors = require('colors/safe');
const log = require('@hzw-cli-dev/log');
const { isArray } = require('@hzw-cli-dev/utils');
class Command {
  /**
   * @description: 命令的准备和执行阶段
   * @param {*} argv : projectName  项目名称 , 命令的 options  , commander实例
   * @return {*}
   */
  constructor(argv) {
    if (!argv) {
      throw new Error('参数不可以为空!');
    }
    if (!isArray(argv)) {
      throw new Error('参数必须是数组类型!');
    }
    if (argv.length < 1) {
      throw new Error('参数列表不可以为空!');
    }
    this._argv = argv;
    let runner = new Promise((resolve, reject) => {
      let chain = Promise.resolve();
      chain = chain.then(() => this.checkNodeVersion());
      chain = chain.then(() => this.initArgs());
      chain = chain.then(() => this.init());
      chain = chain.then(() => this.exec());
      chain.catch((error) => log.error(error.message));
    });
  }

  /**
   * @description: 检查当前的 node 版本,防止 node 版本过低调用最新 api 出错
   * @param {*}
   * @return {*}
   */
  checkNodeVersion() {
    // 获取当前 node 版本号
    const currentVersion = process.version;
    log.test('环境检查 当前的node版本是:', process.version);
    // 获取最低 node 版本号
    const lowestVersion = LOWEST_NODE_VERSION;
    // 对比最低 node 版本号
    if (!semver.gte(currentVersion, lowestVersion)) {
      throw new Error(colors.red('错误:node版本过低'));
    }
  }

  /**
   * @description:初始化参数
   * @param {*}
   * @return {*}
   */
  initArgs() {
    this._cmd = this._argv[this._argv.length - 1];
    this._argv = this._argv.slice(0, this._argv.length - 1);
  }

  init() {
    throw new Error('command 必须拥有一个 init 方法');
  }

  exec() {
    throw new Error('command 必须拥有一个 exec 方法');
  }
}

module.exports = Command;

注:第四周 第五章全部以及第七章全部 没看懂,后面需要再复习一下,但是不影响主课程的进行

exec 的实现

'use strict';

const Package = require('@hzw-cli-dev/package');
const log = require('@hzw-cli-dev/log');
const path = require('path');

const cp = require('child_process');

// package 的映射表
const SETTINGS = {
  // init: '@hzw-cli-dev/init',

  init: '@imooc-cli/init',
};

// 缓存目录
const CACHE_DIR = 'dependencies';

/**
 * @description: 动态加载命令
 * 1.获取 targetPath
 * 2.获取 modulePath
 * 3.生成一个 package
 * 4.提供一个 getRootFIle 方法获取入口文件
 * 5.提供 update 更新方法以及 install 安装方法
 * @return {*}
 */
async function exec(...argv) {
  // 获取 targetPath
  let targetPath = process.env.CLI_TARGET_PATH;
  // 获取 homePath
  const homePath = process.env.CLI_HOME_PATH;
  log.verbose('targetPath', targetPath);
  log.verbose('homePath', homePath);
  // 获取 commander 实例
  const cmdObj = argv[argv.length - 1];
  // 获取命令名称 exp:init
  const cmdName = cmdObj.name();
  // 获取 package 的 name  exp:根据 init 命令 映射到 @hzw-cli-dev/init 包
  const packageName = SETTINGS[cmdName];
  // 获取 package 的 version
  const packageVersion = 'latest';
  // 模块安装路径
  let storeDir = '';
  // package 类
  let pkg = '';

  // 如果  targetPath 不存在
  if (!targetPath) {
    targetPath = path.resolve(homePath, CACHE_DIR); // 生成缓存路径
    storeDir = path.resolve(targetPath, 'node_modules');
    // 创建 Package 对象
    pkg = new Package({
      storeDir,
      targetPath,
      packageName,
      packageVersion,
    });
    // 如果当前 package 存在
    if (await pkg.exists()) {
      // 更新 package
      pkg.update();
    } else {
      // 安装 package
      await pkg.install();
    }
  } else {
    // 如果 targetPath 存在
    pkg = new Package({
      targetPath,
      packageName,
      packageVersion,
    });
  }

  // 获取模块入口
  const rootFile = pkg.getRootFilePath();
  if (!rootFile) {
    throw new Error('模块不存在入口文件!');
  }
  // 执行模块
  try {
    // 在当前进程中调用
    // rootFile && require(rootFile)(argv);
    // 在 node 子进程中调用
    const cmd = argv[argv.length - 1];
    const newCmd = Object.create(null);
    // 给参数瘦身
    Object.keys(cmd).forEach((key) => {
      if (cmd.hasOwnProperty(key) && !key.startsWith('_') && key !== 'parent') {
        newCmd[key] = cmd[key];
      }
    });
    argv[argv.length - 1] = newCmd;
    // 组合code
    const code = `require('${rootFile}')(${JSON.stringify(argv)})`;
    const childProcess = spawn('node', ['-e', code], {
      cwd: process.cwd(),
      stdio: 'inherit',
    });
    childProcess.on('error', (error) => {
      log.error(error.message);
      process.exit(1);
    });
    childProcess.on('exit', (e) => {
      log.verbose('命令执行成功');
      process.exit(e);
    });
  } catch (error) {
    console.log(error.message);
  }
}

/**
 * @description: 封装一个 spawn 方法,兼容 mac 和 windows
 * windows : cp.spawn('cmd',['/c','node','-e',code],{})
 * mac : cp.spawn('node', ['-e', code],{})
 * @param {*} command 'cmd'
 * @param {*} args ['/c','node','-e',code]
 * @param {*} options {}
 * @return {*} cp.spawn(cmd, cmdArgs, options)
 */

/** 所有可能的值
const process = require('process');
var platform = process.platform;
switch (platform) {
  case 'aix':
    console.log('IBM AIX platform');
    break;
  case 'darwin':
    console.log('Darwin platform(MacOS, IOS etc)');
    break;
  case 'freebsd':
    console.log('FreeBSD Platform');
    break;
  case 'linux':
    console.log('Linux Platform');
    break;
  case 'openbsd':
    console.log('OpenBSD platform');
    break;
  case 'sunos':
    console.log('SunOS platform');
    break;
  case 'win32':
    console.log('windows platform');
    break;
  default:
    console.log('unknown platform');
}
 */

function spawn(command, args, options = {}) {
  const win32 = process.platform === 'win32';
  const cmd = win32 ? 'cmd' : command;
  const cmdArgs = win32 ? ['/c'].concat(command, args) : args;
  return cp.spawn(cmd, cmdArgs, options);
}

module.exports = exec;