1、脚手架命令注册

  1. //注册命令
  2. function registerCommander() {
  3. program
  4. .name(Object.keys(pkg.bin)[0])
  5. .usage('<command> [options]')
  6. .version(pkg.version)
  7. .option('-d,--debug', 'Is debugging mode turned on?', false)
  8. //初始化模式
  9. program
  10. .command('init [name]')
  11. .alias('i') //添加别名
  12. .option('-f,--force', '是否强制初始化')
  13. .action(init)
  14. //注册debug模式
  15. program //记得加上program 否则init 命令的name参数打印不对
  16. .on('option:debug', () => {
  17. if (program.opts()?.debug) {
  18. process.env.LOG_LEVEL = 'verbose'
  19. } else {
  20. process.env.LOG_LEVEL = 'info'
  21. }
  22. log.level = process.env.LOG_LEVEL
  23. log.verbose('test debug')
  24. })
  25. // 监听未注册的所有命令
  26. program
  27. .on('command:*', obj => {
  28. const availableCommand = program.commands.map(command => command.name)
  29. log.info(colors.red('未知的命令 ' + obj[0]))
  30. if (availableCommand.length) {
  31. log.info(colors.blue('支持的命令 ' + availableCommand.join(',')))
  32. }
  33. })
  34. .parse(process.argv)
  35. //判断是否输入命令, 没输入则显示帮助文档
  36. if (process.args && program.args.length < 1) {
  37. program.outputHelp()
  38. }
  39. }

lerna create @haha-cli-dev/init

  1. 'use strict'
  2. module.exports = init
  3. function init(projectName, cmdObj) {
  4. console.log('init', projectName, cmdObj)
  5. }

image.png
image.png

2、当前脚手架架构痛点

5、脚手架命令注册和执行过程开发 - 图3
问题:
1、脚手架安装速度慢:所有的package都集成在cli里,因此当命令较多,会减慢脚手架的安装速度。
2、灵活性差:init命令只能使用@haha-cli-dev/init包,对于大公司而言,每个团队的init可能各不相同,需要init命令动态化。虽然增加了脚手架的复杂度,但是大大提升了脚手架的可扩展性,将脚手架框架和业务逻辑解耦。

3、脚手架架构优化

5、脚手架命令注册和执行过程开发 - 图4
5、脚手架命令注册和执行过程开发 - 图5
指令执行代码 node -e

4、是否执行本地代码

  1. program
  2. .option('-tp,--targetPath <targetPath>', '是否指定本地调试文件路径?')
  3. .on('option:targetPath', () => {
  4. //本地调试代码地址
  5. process.env.CLI_TARGET_PATH = program._optionValues.targetPath
  6. })

lerna create exec
将初始化时改成exec

  1. //初始化模式
  2. program
  3. .command('init [name]')
  4. .description('初始化项目')
  5. .alias('i') //添加别名
  6. .option('-f,--force', '是否强制初始化')
  7. .action(exec)

lerna create packages 移到models中

  1. 'use strict'
  2. class Packages {
  3. constructor() {
  4. console.log('Packages')
  5. }
  6. }
  7. module.exports = Packages

exec中引用packages的内容

  1. 'use strict'
  2. const pkg = require('@haha-cli-dev/packages')
  3. function exec() {
  4. const _pkg = new pkg()
  5. console.log(_pkg)
  6. console.log(process.env.CLI_TARGET_PATH)
  7. console.log(process.env.CLI_HOME)
  8. //1、targetPath ->modulePath(通过targetPath 拿到实际的modulePath)
  9. //2、modulePath ->Package(npm模块) 将modulePath生成一个通用的package
  10. //3、Package.getRootFIle(获取入口文件) 这样以后扩展直接处理package的逻辑就可以,而不需要将getRootFIle暴露在外面
  11. //封装--->复用
  12. }
  13. module.exports = exec

image.png

5、通用类packages

  1. 'use strict';
  2. const { isObject } = require('@hzw-cli-dev/utils');
  3. const { getRegister, getLatestVersion } = require('@hzw-cli-dev/get-npm-info');
  4. const formatPath = require('@hzw-cli-dev/format-path');
  5. const npmInstall = require('npminstall');
  6. const fse = require('fs-extra');
  7. const pathExists = require('path-exists').sync;
  8. const pkgDir = require('pkg-dir').sync;
  9. const path = require('path');
  10. // Package 类 管理模块
  11. class Package {
  12. /**
  13. * @description: 构造函数
  14. * @param {*} options 用户传入的配置信息
  15. * @return {*}
  16. */
  17. constructor(options) {
  18. if (!options) {
  19. throw new Error('Package 类的参数不能为空!');
  20. }
  21. if (!isObject(options)) {
  22. throw new Error('Package 类的参数必须是对象类型!');
  23. }
  24. // 获取 targetPath ,如果没有 则说明不是一个本地的package
  25. this.targetPath = options.targetPath;
  26. // 模块安装位置 缓存路径
  27. this.storeDir = options.storeDir;
  28. // package 的 name
  29. this.packageName = options.packageName;
  30. // package 的 Version
  31. this.packageVersion = options.packageVersion;
  32. // 缓存路径的前缀
  33. this.cacheFilePathPrefix = this.packageName.replace('/', '_');
  34. }
  35. /**
  36. * @description: 准备工作
  37. * @param {*}
  38. * @return {*}
  39. */
  40. async prepare() {}
  41. /**
  42. * @description: 获取当前模块缓存路径
  43. * @param {*}
  44. * @return {*}
  45. */
  46. get cacheFilePath() {}
  47. /**
  48. * @description: 获取最新版本模块缓存路径
  49. * @param {*}
  50. * @return {*}
  51. */
  52. getSpecificFilePath(packageVersion) {}
  53. /**
  54. * @description: 判断当前 package 是否存在
  55. * @param {*}
  56. * @return {*}
  57. */
  58. async exists() {}
  59. /**
  60. * @description: 安装 package
  61. * @param {*}
  62. * @return {*}
  63. */
  64. async install() {}
  65. /**
  66. * @description: 更新 package
  67. * @param {*}
  68. * @return {*}
  69. */
  70. async update() {}
  71. /**
  72. * @description:获取入口文件的路径
  73. * 1.获取package.json所在的目录 pkg-dir
  74. * 2.读取package.json
  75. * 3.找到main或者lib属性 形成路径
  76. * 4.路径的兼容(macOs/windows)
  77. * @param {*}
  78. * @return {*}
  79. */
  80. getRootFilePath() {}
  81. module.exports = Package;

6、npmInstall用法

  1. const npminstall = require('npminstall');
  2. (async () => {
  3. await npminstall({
  4. //安装根目录
  5. root: process.cwd(),
  6. pkgs: [
  7. { name: 'foo', version: '~1.0.0' },
  8. ],
  9. registry: 'https://registry.npmjs.org',
  10. //安装缓存目录
  11. storeDir: root + 'node_modules'
  12. });
  13. })().catch(err => {
  14. console.error(err);
  15. });

7、Command

  1. 'use strict'
  2. const semver = require('semver')
  3. const colors = require('colors')
  4. const LOWEST_NODE_VERSION = '12.0.0'
  5. class Command {
  6. constructor(argv) {
  7. if (!argv || argv.length === 0 || !argv[0]) {
  8. throw new Error('参数不能为空')
  9. }
  10. if (!Array.isArray(argv)) {
  11. throw new Error('参数必须是数组')
  12. }
  13. let runner = new Promise((resolve, reject) => {
  14. let chain = Promise.resolve()
  15. chain = chain
  16. .then(() => this.checkNodeVersion())
  17. .then(() => this.checkArgs(argv))
  18. .then(() => this.init())
  19. .then(() => this.exec())
  20. .catch(error => console.log(error.message))
  21. })
  22. }
  23. //检查node版本
  24. checkNodeVersion() {
  25. const currentVersion = process.version
  26. if (!semver.gte(currentVersion, LOWEST_NODE_VERSION)) {
  27. throw new Error(colors.red('错误:node版本过低'))
  28. }
  29. }
  30. //初始化参数
  31. checkArgs(argv) {
  32. this._cmd = argv[arguments.length - 1]
  33. this._argv = argv.slice(0, argv.length - 1)
  34. }
  35. init() {
  36. throw new Error('command 必须拥有一个 init 方法')
  37. }
  38. exec() {
  39. throw new Error('command 必须拥有一个 exec 方法')
  40. }
  41. }
  42. module.exports = Command

8、initCommand

  1. 'use strict'
  2. const Command = require('@haha-cli-dev/command')
  3. class initCommand extends Command {
  4. init() {
  5. this.projectName = this._argv[0]
  6. console.log('projectName', this.projectName)
  7. }
  8. }
  9. function init(argv) {
  10. return new initCommand(argv)
  11. }
  12. function exec() {}
  13. module.exports = init
  14. module.exports.initCommand = initCommand

9、exec

  1. 'use strict'
  2. const path = require('path')
  3. const childProcess = require('child_process')
  4. const packages = require('@haha-cli-dev/packages')
  5. const log = require('@haha-cli-dev/log')
  6. const SETTINGS = {
  7. init: '@haha-cli-dev/init'
  8. }
  9. const CACHE_DIR = 'dependencies'
  10. async function exec(...argv) {
  11. log.level = process.env.LOG_LEVEL
  12. let targetPath = process.env.CLI_TARGET_PATH
  13. const homePath = process.env.CLI_HOME_PATH
  14. let storeDir = ''
  15. const cmdObj = arguments[arguments.length - 1]
  16. const packageName = SETTINGS[cmdObj._name]
  17. let pkg
  18. //是否执行本地代码
  19. if (!targetPath) {
  20. targetPath = path.resolve(homePath, CACHE_DIR)
  21. storeDir = path.resolve(targetPath, 'node_modules')
  22. log.verbose('targetPath', targetPath)
  23. log.verbose('storeDir', storeDir)
  24. pkg = new packages({
  25. targetPath,
  26. storeDir,
  27. packageName,
  28. packageVersion: 'latest'
  29. })
  30. if (await pkg.exists()) {
  31. //更新
  32. pkg.update()
  33. } else {
  34. //初始化
  35. pkg.install()
  36. }
  37. } else {
  38. pkg = new packages({
  39. targetPath,
  40. packageName,
  41. packageVersion: 'latest'
  42. })
  43. }
  44. //获取本地代码入口文件
  45. const rootFile = pkg.getRootFilePath()
  46. log.verbose('rootFile', rootFile)
  47. if (rootFile) {
  48. //cli已经对全局的异常进行了捕获,为什么在这里仍需要使用try,catch捕获command的异常,因为在使用了program.action异步处理了。异常处理的都需要重新捕获异常
  49. try {
  50. const cmd = argv[argv.length - 1]
  51. const newCmd = Object.create(null)
  52. Object.keys(cmd).forEach(key => {
  53. if (cmd.hasOwnProperty(key) && !key.startsWith('_') && key !== 'parent') {
  54. newCmd[key] = cmd[key]
  55. }
  56. })
  57. argv[argv.length - 1] = newCmd
  58. try {
  59. //code是字符串 !!! require(路径(字符串))(参数(字符串))
  60. const code = `require('${rootFile}')(${JSON.stringify(argv)})`
  61. //在 node 子进程中调用 提高速度
  62. const cp = spawn('node', ['-e', code], {
  63. cwd: process.cwd(), //cwd 子进程的当前工作目录
  64. stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr
  65. })
  66. cp.on('error', function (error) {
  67. console.log(error.message)
  68. process.exit(1)
  69. })
  70. cp.on('exit', e => {
  71. log.verbose('命令执行成功', e)
  72. process.exit(e)
  73. })
  74. } catch (error) {
  75. console.log(error.message)
  76. }
  77. } catch (error) {
  78. console.log(error.message)
  79. }
  80. }
  81. function spawn(command, args, options = {}) {
  82. const win32 = process.platform === 'win32'
  83. const cmd = win32 ? 'cmd' : command
  84. const cmdArgs = win32 ? ['/c'].concat(command, args) : args
  85. return childProcess.spawn(cmd, cmdArgs, options)
  86. }
  87. }
  88. module.exports = exec