4-2 脚手架执行流程开发 -- 封装通用的 Package & Commond 类

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

1.1 指定本地调试文件路径 targetPath

  • core>cli>bin>index.js
function registerCommand() {
  program
    .name(Object.keys(pkg.bin)[0])
    .usage('<command> [options]')
    .version(pkg.version)
    .option('-d, --debug', '是否开启调试模式', false)
    .option('-tp, --targetPath <targetPath>', '是否指定本地调试文件路径', '')

  program
    .command('init [projectName]')
    .option('-f, --force', '是否强制初始化项目')
    .action(exec)

  // 指定targetPath
  program.on('option:targetPath', function() {
    process.env.CLI_TARGET_PATH = program._optionValues.targetPath
  })

  program.parse(process.argv)
  if(program.args && program.args.length < 1) {
    program.outputHelp()
  }
}
  • commands>init>lib>index.js
function init(projectName, cmdObj) {
  console.log('init', projectName, cmdObj.force, process.env.CLI_TARGET_PATH);
}

1.2 动态执行库 exec 模块创建

lerna create exec

function exec() {
  console.log('targetPath', targetPath)
  console.log('homePath', homePath)
}
module.exports = exec;

1.3 创建 npm 模块通用类 Package

  • models>package>lib>index.js
'use strict';

class Package {
  constructor(options) {
  }
}

module.exports = Package;
  • core>exec>lib>index.js
const Package = require('@zmoon-cli-dev/package')
const log = require('@zmoon-cli-dev/log')
function exec() {
  const targetPath = process.env.CLI_TARGET_PATH
  const homePath = process.env.CLI_HOME_PATH
  log.verbose('targetPath', targetPath)
  log.verbose('homePath', homePath)
  const pkg = new Package({
      targetPath
  })
  console.log(pkg);
}
module.exports = exec;

1.4 Package 类的属性、方法定义及构造函数

1. Package 类的属性、方法定义

class Package {
  constructor(options) {
    // package的路径
    this.targetPath = options.targetPath
    // package的存储路径
    this.storePath = options.storePath
    // package的name
    this.packageName = options.name
    // package的version
    this.packageVersion = options.version
  }
  // 判断当前Package是否存在
  exists() {}
  // 安装Package
  install() {}
  // 更新Package
  update() {}
  // 获取入口文件的路径
  getRootFilePath() {}
}

2. Package 构造函数逻辑开发

  • models>package>lib>index.js
const { isObject } = require('@zmoon-cli-dev/utils') 
class Package {
  constructor(options) {
    if(!options) {
      throw new Error('Package类的options参数不能为空!')
    }
    if(!isObject(options)) {
      throw new Error('Package类的options参数必须为对象!')
    }
    // package的路径
    this.targetPath = options.targetPath
    // package的存储路径
    this.storePath = options.storePath
    // package的name
    this.packageName = options.packageName
    // package的version
    this.packageVersion = options.packageVersion
  }
}
  • utils>utils>lib>index.js
function isObject(o) {
  return Object.prototype.toString.call(o) === '[object Object]'
}
  • core>exec>lib>index.js
const SETTINGS = {
  init: '@zmoon-cli-dev/init'
}

function exec() {
  const targetPath = process.env.CLI_TARGET_PATH
  const homePath = process.env.CLI_HOME_PATH

  const cmdObj = arguments[arguments.length-1]
  const cmdName = cmdObj.name()
  const packageName = SETTINGS[cmdName]
  const packageVersion = 'latest'

  const pkg = new Package({
    targetPath,
    packageName,
    packageVersion
  })
}

1.5 Package 类获取文件入口路径

pkg-dir 应用+解决不同操作系统路径兼容问题

1. 获取package.json所在目录 -- pkg-dir

const pkgDir = require('pkg-dir').sync
getRootFilePath() {
  const dir = pkgDir(this.targetPath)
}

2. 读取package.json -- require() js/json/node

const pkgFile = require(path.resolve(dir, 'package.json'))

3. 寻找main/lib -- path

if(pkgFile && pkgFile.main) {}

4. 路径的兼容(macOS/windows)

if(pkgFile && pkgFile.main) {
  return formatPath(path.resolve(dir, pkgFile.main))
}
  • utils>format-path>lib>index.js
const path = require('path')

function formatPath(p) {
  if(p && typeof p === 'string') {
    // 分隔符
    const sep = path.sep
    if(sep === '/') {
      return p.replace(/\//g, '\\')
    } else {
      return p
    }
  }
  return p
}

1.6 利用 npminstall 库安装 npm 模块

  • models>package>lib>index.js
const npminstall = require('npminstall')
const { getDefaultRegistry } = require('@zmoon-cli-dev/get-npm-info')
// 安装Package
install() {
  return npminstall({
    root: this.targetPath, // 模块路径
    storeDir: this.storeDir,
    registry: getDefaultRegistry(),
    pkgs: [{
      name: this.packageName,
      version: this.packageVersion
    }]
  })
}
  • utils>get-npm-info>lib>index.js
function getDefaultRegistry(isOriginal = false) {
  return isOriginal ? 'https://registry.npmjs.org' : 'https://registry.npm.taobao.org'
}
  • core>exec>lib>index.js
const SETTINGS = {
  init: '@zmoon-cli-dev/init'
}
const CACHE_DIR = 'dependencies'

function exec() {
  let targetPath = process.env.CLI_TARGET_PATH
  const homePath = process.env.CLI_HOME_PATH
  let storeDir = ''
  let pkg

  const cmdObj = arguments[arguments.length-1]
  const cmdName = cmdObj.name()
  const packageName = SETTINGS[cmdName]
  const packageVersion = 'latest'

  if(!targetPath) {
    targetPath = path.resolve(homePath, CACHE_DIR) // 生成缓存路径
    storeDir = path.resolve(targetPath, 'node_modules')
    pkg = new Package({
      targetPath,
      storeDir,
      packageName,
      packageVersion
    })
    if(pkg.exists()) {
      // 更新Package
    } else {
      // 安装Package
      await pkg.install()
    }
  } else {
    pkg = new Package({
      targetPath,
      packageName,
      packageVersion
    })
  }
  const rootFile = pkg.getRootFilePath()
  if(rootFile) {
    require(rootFile).apply(null, arguments) // [] -> 参数列表
  }
}

1.7 Package 类判断模块是否存在

  • models>package>lib>index.js
// package的缓存目录前缀
// this.cacheFilePathPrefix = this.packageName.replace('/', '_')
async exists() {
  if(this.storeDir) {
    await this.prepare()
    return pathExists(this.cacheFilePath)
  } else {
    return pathExists(this.targetPath)
  }
}
async prepare() {
  if(this.packageVersion === 'latest') {
    this.packageVersion = await getNpmLatestVersion(this.packageName)
  }
}

// @imooc-cli/init 1.1.2 -> _@imooc-cli_init@1.1.2@@imooc-cli
get cacheFilePath() {
  return path.resolve(this.storeDir, `_${this.cacheFilePathPrefix}@${this.packageVersion}@${this.packageName}`)
}
  • utils>get-npm-info>lib>index.js
async function getNpmLatestVersion(npmName, registry) {
  const version = await getNpmVersions(npmName, registry)
  if(version) {
    // return version.sort((a, b) => semver.gt(b, a))[0]
    return version[version.length-1]
  }
  return null
}

1.8 Package 类更新模块

  • models>package>lib>index.js
// 最新版本存在则不需要更新
async update() {
  await this.prepare()
  // 1. 获取最新的npm模块版本号
  const latestPackageVersion = await getNpmLatestVersion(this.packageName)
  // 2. 查询最新版本号对应的路径是否存在
  const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)
  // 3. 如果不存在,则直接安装最新版本
  if(!pathExists(latestFilePath)) {
    return npminstall({
      root: this.targetPath, // 模块路径
      storeDir: this.storeDir,
      registry: getDefaultRegistry(),
      pkgs: [{
        name: this.packageName,
        version: latestPackageVersion
      }]
    })
  }
  return latestFilePath
}
async prepare() {
  if(this.storeDir && !pathExists(this.storeDir)) {
    fse.mkdirSync(this.storeDir) // 将当前路径不存在的文件都创建
  }
  if(this.packageVersion === 'latest') {
    this.packageVersion = await getNpmLatestVersion(this.packageName)
  }
}
getSpecificCacheFilePath(packageVersion) {
  return path.resolve(this.storeDir, `_${this.cacheFilePathPrefix}@${packageVersion}@${this.packageName}`)
}

1.9 Package 类获取缓存模块入口文件功能改造

  • models>package>lib>index.js
// 获取入口文件的路径
getRootFilePath() {
  function _getRootFile(targetPath) {
    // 1. 获取package.json所在目录 -- pkg-dir
    const dir = pkgDir(targetPath)
    if(dir) {
      // 2. 读取package.json -- require() js/json/node
      const pkgFile = require(path.resolve(dir, 'package.json'))
      // 3. 寻找main/lib -- path
      if(pkgFile && pkgFile.main) {
        // 4. 路径的兼容(macOS/windows)
        return formatPath(path.resolve(dir, pkgFile.main))
      }
    }
    return null
  }
  if(this.storeDir) {
    return _getRootFile(this.cacheFilePath)
  } else {
    return _getRootFile(this.targetPath)
  }
}

2 通用脚手架命令 Command 类封装

2.1 commander 脚手架初始化

  • models>command>lib>index.js
class Command {
  constructor() {}
  init() {} // 准备阶段
  exec() {} // 执行阶段

}
module.exports = Command

2.2 动态加载 initCommand + new initCommand

  • commands>init>lib>index.js
const Command = require('@zmoon-cli-dev/command')
class InitCommand extends Command {}
function init() {
  return new InitCommand()
}
module.exports = init
module.exports.InitCommand = InitCommand

2.3 Command constructor

  • commands>init>lib>index.js
const Command = require('@zmoon-cli-dev/command')
class InitCommand extends Command {}
function init(argv) {
  return new InitCommand(argv)
}
module.exports = init
module.exports.InitCommand = InitCommand
  • models>command>lib>index.js
class Command {
  constructor(argv) {
    console.log('Commond constructor', argv);
    this._argv = argv
    let runner = new Promise((resolve, reject) => {
      let chain = Promise.resolve()
      chain = chain.then(() => {})
    })
  }
  init() {
    throw new Error('init 必须实现')
  }
  exec() {
    throw new Error('init 必须实现')
  }
}

2.4 命令的准备阶段 -- 检查 node 版本

  • models>command>lib>index.js
const semver = require('semver')
const colors = require('colors/safe')
const log = require('@zmoon-cli-dev/log')

const LOWEST_NODE_VERSION = '12.0.0'

class Command {
  constructor(argv) {
    this._argv = argv
    let runner = new Promise((resolve, reject) => {
      let chain = Promise.resolve()
      chain = chain.then(() => this.checkNodeVersion())
      chain.catch(err => {
        log.error(err.message);
      })
    })
  }
  // 检查node版本
  checkNodeVersion() {
    // 1. 获取当前 node 版本号
    const currentVersion = process.version
    // 2. 比对最低版本号
    const lowestNodeVersion = LOWEST_NODE_VERSION
    if (!semver.gte(currentVersion, lowestNodeVersion)) {
      throw new Error(colors.red(`imooc-cli 需要安装v${lowestNodeVersion}以上版本的node.js`))
    }
  }
}

2.5 命令的准备阶段 -- 参数初始化

通过异步执行的命令都需要单独进行错误捕获
即每次新建 promise 都需要有单独的 catch

  • models>command>lib>index.js
class Command {
  constructor(argv) {
    // console.log('Commond constructor', argv);
    if(!argv) {
      throw new Error('参数不能为空!')
    }
    if(!Array.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(err => {
        log.error(err.message);
      })
    })
  }
  initArgs() {
    this.cmd = this._argv[this._argv.length-1]
    this._argv = this._argv.slice(0, this._argv.length-1)
  }
  // ...
  init() {
    throw new Error('init 必须实现!')
  }
  exec() {
    throw new Error('exec 必须实现!')
  }
}
  • commands>init>lib>index.js
class InitCommand extends Command {
  init() {
    this.projectName = this._argv[0] || ''
    this.force = !!this.cmd._optionValues.force
    log.verbose('projectName', this.projectName);
    log.verbose('force', this.force);
  }
  exec() {}
}
posted on 2022-11-25 15:04  pleaseAnswer  阅读(71)  评论(0编辑  收藏  举报