- 图解高性能脚手架架构设计方法
- 封装通用的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;