脚手架的Package模块开发

  • 图解高性能脚手架架构设计方法
  • 封装通用的Package和Command类
  • 基于缓存+Node多进程实现动态命令加载和执行
  • 将业务逻辑和脚手架彻底解耦

mj-cli-dev脚手架初始化+全局参数注册

安装commander库

js 复制代码
cnpm i -S commander@6.2.1

在core/lib/index.js中修改;

js 复制代码
function registerCommand() {
  program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version)
    .option("-d, --debug", "是否开启调试模式", false);

  // 开启debug模式
  program.on("option:debug", function () {
    console.log(program.debug);
    if (program.debug) {
      process.env.LOG_LEVEL = "verbose";
    } else {
      process.env.LOG_LEVEL = "info";
    }
    log.level = process.env.LOG_LEVEL;
    log.verbose("test");
  });

  // 未知命令的监听
  program.on("command:*", function (obj) {
    const variableCommands = program.commands.map((cmd) => cmd.name());

    if (variableCommands.length > 0) {
      console.log(colors.red("可用的命令:" + variableCommands.join(",")));
    }
    console.log(colors.red("未知的命令:" + obj[0]));
  });

  // 没有输入命令时
  program.parse(process.argv);
  if (program.args && program.args.length < 1) {
    // 打印出帮助文档
    program.outputHelp();
  }
}

mj-cli-dev脚手架命令的注册

commands文件夹下创建一个包;

js 复制代码
lerna create @mj-cli-dev/init

在init/lib/index.js修改

js 复制代码
"use strict";

module.exports = init;

function init(projectName, cmdObj) {
  console.log(111);
  console.log("init", projectName, cmdObj.force);
}

core/lib/index.js中的registerCommand方法中注册命令:

js 复制代码
program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version)
    .option("-d, --debug", "是否开启调试模式", false);

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

当前mj-cli-dev脚手架架构痛点分析

当前脚手架架构如图:

这样的架构设计已经可以满足一般的脚手架需求,但是有以下两个问题: 1.core安装速度满:所有的package都集成在core中,因为当命令比较多时,会减慢core的安装速度; 2.灵活性差:init命令只能使用@mj-cli-dev/init包,对于集团公司而言,每个bu的init命令可能都各不相同,可能需要实现init命令动态化,如

  • 团队A使用@mj-cli-dev/init作为初始化模块;
  • 团队B使用自己开发的@mj-cli-dev/my-init作为初始化模块;
  • 团队C使用自己开发的@mj-cli-dev/your-init作为初始化模块;

这时就对我们的架构设计提出了挑战,要求我们能够动态加载init模块,这将增加架构的复杂度,但大大提升脚手架的可扩展性,将脚手架框架和业务逻辑解耦;

高性能脚手架架构设计

1.init命令动态加载; 2.动态加载时缓存形式做的; 3.new initCommand是多进程的,提升性能;

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

require的用法:

1.绝对路径

require("/xxx/yyy/index.js")

2.相对路径

require("./index.js)

3.内置模块

requeire(fs)

4.npm包

require("npmlog")

registerCommand方法中加一个otpion:

js 复制代码
program
    .name(Object.keys(pkg.bin)[0])
    .usage("<command> [options]")
    .version(pkg.version)
    .option("-d, --debug", "是否开启调试模式", false)
    .option("-tp, --targetPath <targetPath>", "是否指定本地调试文件路径", "");

targetPath放到环境变量中,然后init模块可从环境变量中取到;

js 复制代码
  // 指定targetPath targetPath放到环境变量?
  program.on("option:targetPath", function () {
    process.env.CLI_TARGET_PATH = program.targetPath;
    console.log("targetPath", program.targetPath);
  });

init/lib/index.js

js 复制代码
"use strict";

module.exports = init;

function init(projectName, cmdObj) {
  console.log("init", projectName, process.env.CLI_TARGET_PATH);
}

接着判断targetPath是否存在; 创建一个exec的库,用来写动态init的逻辑;

js 复制代码
lerna create @mj-cli-dev/exec

exec方法步骤:

1.targetPath => modulePath

2.modulePath => Package(npm模块)

3.Package提供一个方法getRootFile(获取入口文件)

这样能更好的进行封装;封装 => 复用

首先在models文件夹下创建新的模块Package;

js 复制代码
lerna create @mj-cli-dev/package

在exec下对package进行引用,在package.json中添加;

js 复制代码
"dependencies": {
    "@mj-cli-dev/package": "file:../../models/package"
},

在core模块init命令调用exec方法:

js 复制代码
program
    .command("init [projectName]")
    .option("-f, --force", "是否强制初始化项目")
    .action(exec);

在exec中调用package:

js 复制代码
"use strict";
module.exports = exec;
const Package = require("@mj-cli-dev/package");
const log = require("@mj-cli-dev/log");

const SETTINGS = {
  init: "@mj-cli-dev/init",
};

function exec() {
  // init模块的真实路径
  const targetPath = process.env.CLI_TARGET_PATH;
  const homePath = process.env.CLI_HOME_PATH;

  const cmdObj = arguments[arguments.length - 1];
  const packageVersion = "latest";
  const pkg = new Package({
    targetPath,
    storePath: "",
    packageName: SETTINGS[cmdObj.name()],
    packageVersion,
  });
}

在package中创建class:

js 复制代码
"use strict";
const { isObject } = require("@mj-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;
  }
  // 判断当前Package是否存在
  exists() {}

  // 安装Package
  install() {}

  // 更新Package
  update() {}

  // 获取入口文件
  getRootFilePath() {
    // 1.获取pacakage.json所在目录-pkg-dir@5.0.0
    // 2.读取package.json-require()
    // 3.main/lib-path
    // 4.路径的兼容(macOs/windows)
  }
}
module.exports = Package;

utils模块中添加isObject方法:

js 复制代码
function isObject(obj) {
  return Object.prototype.toString.call(obj) === "[object Object]";
}

终端输入命令:

js 复制代码
mj-cli-dev init --targetPath /Users/Minjie/Documents/vue3/mj-cli-dev/commands/init --debug test-project --force

为了路径的兼容(macOs/windows),需要在utils目录下新建format-path:

js 复制代码
lerna create @mj-cli-dev/format-path

// 安装npminstall@4.10.0
npminstall({
    root: path.resolve(userHome, "./mj-cli-dev"), // 模块路径
    storeDir: path.resolve(userHome, ".mj-cli-dev", "node_modules") , // 实际存储的位置
    registry: "https://registry.npmjs.org"//指定源
    pkgs: [{
        name: "mj-cli-dev", version: "1.0.0"
    }]
})

install的实现调用npminstall库;

实现exists方法:

js 复制代码
// 安装path-exists@4.0.0

get-npm-info模块中增加获取最新版本的方法:

js 复制代码
async function getNpmLatestVersion(npmName, registry) {
  const versions = await getNpmVersions(npmName, registry);
  if (versions) {
    versions.sort((a, b) => semver.gt(b, a));
    return versions[0];
  } else {
    return null;
  }
}

exists方法生成缓存的路径,例如:/Users/Minjie/mj-cli-dev/dependencies/node_modules/_mj-cli-dev@1.0.0@mj-cli-dev

package的update方法:

js 复制代码
//安装fs-extra@9.0.1

package/lib/index完整代码:

js 复制代码
"use strict";
const path = require("path");
const fse = require("fs-extra");
const pkgDir = require("pkg-dir").sync;
const pathExists = require("path-exists");
const { isObject } = require("@mj-cli-dev/utils");
const formatPath = require("@mj-cli-dev/format-path");
const {
  getDefaultRegistry,
  getNpmLatestVersion,
} = require("@mj-cli-dev/get-npm-info");

const npminstall = require("npminstall");

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.storeDir = options.storeDir;
    // package的name
    this.packageName = options.packageName;
    // package的version
    this.packageVersion = options.packageVersion;
    console.log("Package constructor");
  }
  // 获取最新的版本
  async prepare() {
    // 缓存目录不存在,创建目录
    console.log("prepare--->", this.storeDir);
    if (this.storeDir && !pathExists(this.storeDir)) {
      fse.mkdirpSync(this.storeDir);
    }
    if (this.packageVersion === "latest") {
      this.packageVersion = await getNpmLatestVersion(this.packageName);
    }
  }
  // 生成当前版本的缓存路径
  get cacheFilepath() {
    return path.resolve(
      this.storeDir,
      `_${this.packageName}@${this.packageVersion}@${this.packageName}`
    );
  }
  // 生成指定版本的缓存路径
  getOneCacheFilePath(packageVersion) {
    return path.resolve(
      this.storeDir,
      `_${this.packageName}@${packageVersion}@${this.packageName}`
    );
  }
  // 判断当前Package是否存在
  async exists() {
    if (this.storeDir) {
      // 缓存模式
      // 1.获取最新的版本
      await this.prepare();
      // 2.生成缓存路径
      return pathExists(this.cacheFilepath);
    } else {
      return pathExists(this.targetPath);
    }
  }

  // 安装Package
  async install() {
    // 用最新的版本
    await this.prepare();
    return npminstall({
      root: this.targetPath,
      storeDir: this.storeDir,
      registry: getDefaultRegistry(true),
      pkgs: [
        {
          name: this.packageName,
          version: this.packageVersion,
        },
      ],
    });
  }

  // 更新Package
  async update() {
    await this.prepare();
    // 1.获取最新的npm版本号
    const newPackageVersion = await getNpmLatestVersion(this.packageName);
    // 2.查询最新版本号路径是否存在
    const latestFilePath = this.getOneCacheFilePath(newPackageVersion);
    // 3.不存在,直接安装最新版本

    if (!pathExists(latestFilePath)) {
      await npminstall({
        root: this.targetPath,
        storeDir: this.storeDir,
        registry: getDefaultRegistry(true),
        pkgs: [
          {
            name: this.packageName,
            version: latestFilePath,
          },
        ],
      });
      // 4. 更新版本号
      this.packageVersion = latestFilePath;
    }
  }

  // 获取入口文件
  getRootFilePath() {
    function _getRootFile(targetPath, packageName) {
      // 1.获取pacakage.json所在目录-pkg-dir@5.0.0
      const dir = pkgDir(targetPath);
      if (dir) {
        // 2.读取package.json-require()
        const pkgFile = require(path.resolve(dir, "package.json"));
        // 3.main/lib-path
        if ((pkgFile && pkgFile.bin[packageName]) || pkgFile.main) {
          // 4.路径的兼容(macOs/windows)
          const newPath = formatPath(
            path.resolve(dir, pkgFile.bin[packageName])
          );
          return newPath;
        }
      }
      return null;
    }
    if (this.storeDir) {
      // 使用缓存
      return _getRootFile(this.cacheFilepath, this.packageName);
    } else {
      // 不使用缓存的情况
      return _getRootFile(this.targetPath, this.packageName);
    }
  }
}
module.exports = Package;

exac/lib/index.js完整代码;

js 复制代码
"use strict";

const path = require("path");
const Package = require("@mj-cli-dev/package");
const log = require("@mj-cli-dev/log");

const SETTINGS = {
  init: "mj-cli-test",
};

const CACHE_DIR = "dependencies";

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

  const cmdObj = arguments[arguments.length - 1];
  const packageVersion = "latest";

  let storeDir, pkg;

  if (!targetPath) {
    // 本地包未安装
    targetPath = path.resolve(homePath, CACHE_DIR);
    storeDir = path.resolve(targetPath, "node_modules");

    pkg = new Package({
      targetPath,
      storeDir,
      packageName: SETTINGS[cmdObj.name()],
      packageVersion,
    });
    const tempPkg = await pkg.exists();
    if (tempPkg) {
      // 更新package
      await pkg.update();
      log.info("更新package--->", tempPkg);
    } else {
      // 安装package
      await pkg.install();
    }
  } else {
    // 本地包已安装
    pkg = new Package({
      targetPath,
      packageName: SETTINGS[cmdObj.name()],
      packageVersion,
    });
  }
  const rootFile = pkg.getRootFilePath();
  // mj-cli-test/lib/index没方法,require报错
  if (rootFile) {
    // require(rootFile).apply(null, arguments);
  }
}
module.exports = exec;
相关推荐
qianmoQ31 分钟前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
C#Thread36 分钟前
C#上位机--流程控制(IF语句)
开发语言·javascript·ecmascript
椰果uu1 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑1 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄1 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19891 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome
IT、木易1 小时前
跟着AI学vue第五章
前端·javascript·vue.js
薛定谔的猫-菜鸟程序员1 小时前
Vue 2全屏滚动动画实战:结合fullpage-vue与animate.css打造炫酷H5页面
前端·css·vue.js
春天姐姐2 小时前
vue3项目开发总结
前端·vue.js·git
谢尔登2 小时前
【React】React 性能优化
前端·react.js·性能优化