脚手架初始化

全局参数注册

使用 commander 来全局添加注册命令

  1. const commander = require('commander');
  2. const program = new commander.Command();
  3. // 命令注册
  4. function registerCommand () {
  5. // option:选项
  6. // 用法:.option('-n, --name <name>', 'your name', 'jsliang')
  7. // 第一个参数是选项定义,可以用 |,, 和 ' ' 空格连接
  8. // 第二个参数为选项描述
  9. // 第三个参数为选项参数默认值(可选)
  10. program.name(Object.keys(pkg.bin)[0])
  11. .usage('<command> [options]')
  12. .version(pkg.version)
  13. .option('-d, --debug', '是否开启调式模式', false);
  14. // .on : custom event listeners
  15. // 开启 debug 模式
  16. program.on('option:debug', function() {
  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');
  24. })
  25. // 对未知命令监听
  26. program.on('command:*', function(obj) {
  27. const availableCommands = program.commands.map(cmd => cmd.name())
  28. console.log(colors.red('未知的命令:'+obj[0]));
  29. if(availableCommands.length > 0){
  30. console.log(colors.red('可用命令为:'+availableCommands.join(',')))
  31. }
  32. })
  33. program.parse(program.argv);
  34. if (program.args && program.args.length < 1) {
  35. program.outputHelp();
  36. }
  37. }

使用 command 注册命令,并将命令执行函数抽离出来单独维护

  1. const init = require('@temp-cli-dev/init');
  2. // The argument may be <required> or [optional]
  3. program.command('init [projectName]')
  4. .option('-f, --force', '是否强制初始化项目')
  5. .action(init);

高性能脚手架架构设计

week4_01.jpg

  • 将 init 命令做成一个动态加载的形式,不同的团队能够使用不同的模块初始化
  • 动态加载的脚手架通过缓存形式进行存储:执行哪个命令下载哪个命令
  • 动态加载的时候,通过 node 多进程进行执行,深挖 cpu 性能

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

    week4_02.jpg

    命令的动态加载

    一个脚手架会包含很多命令,如:初始化项目,构建,打包等。这么将每个命令全部独立出来,与脚手架主流程解耦,一旦其中某个命令出现了问题或者需要更新,只需要更新相应的命令即可,不需要去动整个架构以及其他的命令模块。

    每个命令都是一个独立的 npm 包,将脚手架安装到本地时,并不会去安装这些命令包,而是在使用的时候才去动态的安装,简称动态加载

支持本地调试

为了方便本地开发调试,需要支持执行本地文件,在参数中增加 --targetPath <targetPath> 则会去执行 targetPath下的文件,不会去下载线上的 npm

  1. program.name(Object.keys(pkg.bin)[0])
  2. .usage('<command> [options]')
  3. .version(pkg.version)
  4. .option('-d, --debug', '是否开启调式模式', false)
  5. .option('-tp, --targetPath <targetPath>', '是否指定本地调试文件路径', '');
  6. //指定targetPath
  7. // 是否执行本地代码,我们通过一个属性来进行标识:targetPath
  8. program.on('option:targetPath', function () {
  9. // 将命令中的参数写入环境变量中实现解耦,不同的项目都可访问到该变量
  10. process.env.CLI_TARGET_PATH = program.opts().targetPath;
  11. });
  1. let targetPath = process.env.CLI_TARGET_PATH;
  2. const homePath = process.env.CLI_HOME_PATH;
  3. let storeDir ='';
  4. let pkg;
  5. const cmdObj = arguments[arguments.length - 1];
  6. const cmdName = cmdObj.name();
  7. const packageName = SETTINGS[cmdName];
  8. const packageVersion = 'latest';
  9. // 如果不存在targetPath,说明是执行线上的命令,手动设置缓存本地的targetPath路径及缓存路径
  10. if (targetPath) {
  11. pkg = new Package({
  12. targetPath,
  13. packageName,
  14. packageVersion
  15. })
  16. const rootFile = pkg.getRootFilePath();
  17. if (rootFile) { //新添加
  18. require(rootFile).apply(null,arguments);
  19. }
  20. }

支持动态更新

如果本地存在最新的版本命令包,则不需要每次都去线上下载。如果有了新的版本,则需要更新本地的包。

  1. // 如果不存在targetPath,说明是执行线上的命令,手动设置缓存本地的targetPath路径及缓存路径
  2. if (!targetPath) {
  3. //生成缓存路径
  4. targetPath = path.resolve(homePath, CATCH_DIR);
  5. storeDir = path.resolve(targetPath, 'node_modules');
  6. pkg = new Package({
  7. targetPath,
  8. storeDir,
  9. packageName,
  10. packageVersion
  11. });
  12. if (await pkg.exists()) {
  13. // 更新package
  14. await pkg.update();
  15. } else {
  16. // 安装package
  17. await pkg.install();
  18. }
  19. }
  1. // 安装 package
  2. async install() {
  3. return npminstall({
  4. root: this.targetPath, // 安装
  5. storeDir: this.storeDir, // 缓存路径
  6. registry: getDefaultRegistry(), // 下载源
  7. pkgs: [
  8. {
  9. name: this.packageName, // 需要下载的包名
  10. version: this.packageVersion // 包的版本
  11. }
  12. ]
  13. });
  14. }
  15. // 更新 package
  16. async update() {
  17. //获取最新的npm模块版本号
  18. const latestPackageVersion = await getNpmLatestVersion(this.packageName);
  19. // 查询最新版本号对应的路径是否存在
  20. const latestFilePath = this.getSpecificCacheFilePath(latestPackageVersion)
  21. // 如果不存在,则直接安装最新版本
  22. if(!pathExists(latestFilePath)){
  23. await npminstall({
  24. root:this.targetPath,
  25. storeDir:this.storeDir,
  26. registry:getDefaultRegistry(),
  27. pkgs:[{
  28. name:this.packageName,
  29. version:latestPackageVersion
  30. }
  31. ]
  32. })
  33. this.packageVersion = latestPackageVersion
  34. } else {
  35. this.packageVersion = latestPackageVersion
  36. }
  37. return latestFilePath;
  38. }

找到入口文件并执行

一个 npm 包的入口文件会在 package.json 中的 main 或 bin 中定义,因此首选需要找到 package.json 的路径,然后再找到入口文件的路径,最后再执行。

  1. const path = require('path');
  2. const pkgDir = require('pkg-dir').sync;
  3. const formatPath = require('@temp-cli-dev/format-path');
  4. // 获取入口文件路径
  5. getRootFilePath() {
  6. function _getRootFile(targetPath){
  7. // 1.获取package.json所在的目录 - pkg-dir
  8. // pkg-dir 从某个目录开始向上查找,直到找到存在 package.json 的目录,并返回该目录。如果未找到则返回 null
  9. const dir = pkgDir(targetPath);
  10. if (dir) {
  11. // 2.读取package.json - require() js/json/node
  12. const pkgFile = require(path.resolve(dir, 'package.json'));
  13. // 3.寻找main/lib - path
  14. if (pkgFile && pkgFile.main) {
  15. // 4.路径的兼容(macOS/windows)
  16. return formatPath(path.resolve(dir, pkgFile.main));
  17. }
  18. }
  19. return null;
  20. }
  21. if (this.storeDir) {
  22. return _getRootFile(this.cachFilePath);
  23. } else {
  24. return _getRootFile(this.targetPath);
  25. }
  26. }

解决不同操作系统路径兼容问题

  1. 'use strict';
  2. const path = require('path');
  3. module.exports = formatPath;
  4. // 路径的兼容(macOS/windows)
  5. function formatPath(nowPath) {
  6. const sep = path.sep;
  7. if (nowPath && typeof nowPath === 'string' && sep !== '/') {
  8. return nowPath.replace(/\\/g, '/');
  9. }
  10. return nowPath;
  11. }

参考资料

  1. Week4-脚手架命令注册和执行过程开发
  2. 脚手架开发(2)-注册阶段
  3. github 代码库
  4. github 代码库