在开发脚手架之前,我们先了解下脚手架开发的流程图。

脚手架架构图

Canvas 1.png

脚手架拆包策略

  • 核心流程:core
  • 命令:commands
    • 初始化
    • 发布
    • 清除缓存
  • 模型层:models
    • Command命令
    • Project项目
    • Component组件
    • Npm模块
    • Git仓库
  • 支持模块:utils
    • Git操作
    • 云构建
    • 工具方法
    • API请求
    • Git API

1632656091942.jpg

命令执行流程

  • 准备阶段

core准备阶段.png

  • 命令注册

    core命令阶段.png

  • 命令执行

5fe4a3a408c7620016001303.jpeg

准备阶段

  • 检查版本号
  1. // 检查版本
  2. function checkPkgVersion() {
  3. log.info('cli', pkg.version);
  4. }
  • 检查node版本
  1. // 检查node版本
  2. checkNodeVersion() {
  3. //第一步,获取当前Node版本号
  4. const currentVersion = process.version;
  5. const lastVersion = LOWEST_NODE_VERSION;
  6. //第二步,对比最低版本号
  7. if (!semver.gte(currentVersion, lastVersion)) {
  8. throw new Error(colors.red(`code-robot-cli 需要安装v${lastVersion}以上版本的Node.js`));
  9. }
  10. }
  • 检查root权限
  1. // 检查root启动
  2. function checkRoot() {
  3. //使用后,检查到root账户启动,会进行降级为用户账户
  4. const rootCheck = require('root-check');
  5. rootCheck();
  6. }
  • 检查用户主目录
  1. // 检查用户主目录
  2. function checkUserHome() {
  3. if (!userHome || !pathExists(userHome)) {
  4. throw new Error(colors.red('当前登录用户主目录不存在!!!'));
  5. }
  6. }
  • 检查入参
  1. // 检查入参
  2. function checkInputArgs() {
  3. const minimist = require('minimist');
  4. args = minimist(process.argv.slice(2));
  5. checkArgs();
  6. }
  7. function checkArgs() {
  8. if (args.debug) {
  9. process.env.LOG_LEVEL = 'verbose';
  10. } else {
  11. process.env.LOG_LEVEL = 'info';
  12. }
  13. log.level = process.env.LOG_LEVEL;
  14. }
  • 检查环境变量
  1. // 检查环境变量
  2. function checkEnv() {
  3. const dotenv = require('dotenv');
  4. const dotenvPath = path.resolve(userHome, '.env');
  5. if (pathExists(dotenvPath)) {
  6. config = dotenv.config({
  7. path: dotenvPath
  8. });
  9. }
  10. createDefaultConfig();
  11. log.verbose('环境变量', process.env.CLI_HOME_PATH);
  12. }
  13. function createDefaultConfig() {
  14. const cliConfig = {
  15. home: userHome
  16. }
  17. if (process.env.CLI_HOME) {
  18. cliConfig['cliHome'] = path.join(userHome, process.env.CLI_HOME);
  19. } else {
  20. cliConfig['cliHome'] = path.join(userHome, constants.DEFAULT_CLI_HOME);
  21. }
  22. process.env.CLI_HOME_PATH = cliConfig.cliHome;
  23. }
  • 检查是否是最新版本
  1. // 检查是否是最新版本,是否需要更新
  2. async function checkGlobalUpdate() {
  3. //1.获取当前版本号和模块名
  4. const currentVersion = pkg.version;
  5. const npmName = pkg.name;
  6. //2.调用npm API,获取所有版本号
  7. const { getNpmSemverVersion } = require('@code-robot-cli/get-cli-info');
  8. //3.提取所有版本号,比对哪些版本号是大于当前版本号
  9. const lastVersion = await getNpmSemverVersion(currentVersion, npmName);
  10. if (lastVersion && semver.gt(lastVersion, currentVersion)) {
  11. //4.获取最新的版本号,提示用户更新到该版本
  12. log.warn(colors.yellow(`请手动更新${npmName},当前版本:${currentVersion},最新版本:${lastVersion}
  13. 更新命令:npm install -g ${npmName}`))
  14. }
  15. }

命令注册

注册init阶段

  1. //命名的注册
  2. function registerCommand() {
  3. program
  4. .name(Object.keys(pkg.bin)[0])
  5. .usage('<command> [options]')
  6. .version(pkg.version)
  7. .option('-d, --debug', '是否开启调试模式', false)
  8. .option('-tp, --targetPath <targetPath>', '是否指定本地调试文件路径', '');
  9. program
  10. .command('init [projectName]')
  11. .option('-f, --force', '是否强制初始化项目')
  12. .action(init); //init 单独解析一个命令 exec动态加载模块
  13. //开启debug模式
  14. program.on('option:debug', function () {
  15. if (program.debug) {
  16. process.env.LOG_LEVEL = 'verbose';
  17. } else {
  18. process.env.LOG_LEVEL = 'info';
  19. }
  20. log.level = process.env.LOG_LEVEL;
  21. log.verbose('test');
  22. });
  23. //指定targetPath
  24. program.on('option:targetPath', function () {
  25. process.env.CLI_TARGET_PATH = program.targetPath;
  26. });
  27. //对未知命令的监听
  28. program.on('command:*', function (obj) {
  29. const availabelCommands = program.commands.map(cmd => cmd.name());
  30. log.verbose(colors.red('未知命令:' + obj[0]));
  31. if (availabelCommands.length > 0) {
  32. log.verbose(colors.blue('可用命令:' + availabelCommands.join(',')));
  33. }
  34. })
  35. program.parse(process.argv);
  36. //用户没有输入命令的时候
  37. if (program.args && program.args.length < 1) {
  38. program.outputHelp();
  39. console.log();
  40. }
  41. }

当前架构图

通过准备阶段和命令初始化init阶段,我们创建了如下一些package:
5fe4a37908dd3d1b13720561.jpeg

这样的架构设计已经可以满足一般脚手架需求,但是有以下两个问题:

1.cli安装速度慢:所有的package都集成在cli里,因此当命令较多时,会减慢cli的安装速度

2.灵活性差:init命令只能使用@code-robot-cli/init包,对于集团公司而言,每个团队init命令可能都各不相同,可能需要实现init命令动态化,如:

  • 团队A使用@code-robot-cli/init作为初始化模板
  • 团队B使用自己开发的@code-robot-cli/my-init作为初始化模板
  • 团队C使用自己开发的@code-robot-cli/your-init作为初始化模板

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

脚手架架构优化

jiaoshoujiayouhua.png

命令执行阶段

  1. const SETTINGS = {
  2. init: "@code-robot-cli/init",
  3. }
  4. const CACHE_DIR = 'dependencies/';
  5. async function exec() {
  6. let targetPath = process.env.CLI_TARGET_PATH;
  7. const homePath = process.env.CLI_HOME_PATH;
  8. let storeDir = '';
  9. let pkg;
  10. log.verbose('targetPath', targetPath);
  11. log.verbose('homePath', homePath);
  12. const cmdObj = arguments[arguments.length - 1];
  13. const cmdName = cmdObj.name();
  14. const packageName = SETTINGS[cmdName];
  15. const packageVersion = 'latest';
  16. if (!targetPath) {//是否执行本地代码
  17. //生成缓存路径
  18. targetPath = path.resolve(homePath, CACHE_DIR);
  19. storeDir = path.resolve(targetPath, 'node_modules');
  20. log.verbose(targetPath, storeDir);
  21. //初始化Package对象
  22. pkg = new Package({
  23. targetPath,
  24. storeDir,
  25. packageName,
  26. packageVersion
  27. });
  28. //判断Package是否存在
  29. if (await pkg.exists()) {
  30. //更新package
  31. await pkg.update()
  32. } else {
  33. //安装package
  34. await pkg.install();
  35. }
  36. } else {
  37. pkg = new Package({
  38. targetPath,
  39. packageName,
  40. packageVersion
  41. });
  42. }
  43. //获取入口文件
  44. const rootFile = pkg.getRootFile();
  45. if (rootFile) {//判断入口文件是否存在
  46. try {
  47. //在当前进程中调用
  48. // require(rootFile).call(null, Array.from(arguments));
  49. //在node子进程中调用
  50. const args = Array.from(arguments);
  51. const cmd = args[args.length - 1];
  52. const o = Object.create(null);
  53. Object.keys(cmd).forEach(key=>{
  54. if (cmd.hasOwnProperty(key) && !key.startsWith('_') && key !== 'parent') {
  55. o[key] = cmd[key];
  56. }
  57. })
  58. args[args.length - 1] = o;
  59. const code = `require('${rootFile}').call(null, ${JSON.stringify(args)})`;
  60. const child = spawn('node',['-e',code],{
  61. cwd:process.cwd(),
  62. stdio:'inherit'
  63. });
  64. //执行产生异常
  65. child.on('error',e=>{
  66. log.error(e.message);
  67. process.exit(1);
  68. });
  69. //执行完毕 正常退出
  70. child.on('exit',e=>{
  71. log.verbose('命令执行成功:'+e);
  72. process.exit(e);
  73. })
  74. } catch (e) {
  75. log.error(e.message);
  76. }
  77. }
  78. //1.targetPath -> modulePath
  79. //2.modulePath -> Package(npm模块)
  80. //3.Package.getRootFile(获取入口文件)
  81. //4.Package.update/Package.install
  82. }

脚手架项目创建功能设计

首先我们要思考下脚手架项目创建为了什么:

  • 可扩展性:能够快速复用到不同团队,适应不同团队之间的差异
  • 低成本:在不改动脚手架源码的情况下,能够新增模板,且新增模板的成本很低
  • 高性能:控制存储空间,安装时充分利用Node多进程提升安装性能

创建项目功能架构设计图

整体过程分为三个阶段:

  • 准备阶段

prepare.png

  • 下载模块

downloadTemplate.png

  • 安装模块

installTemplate.png

准备阶段

准备阶段的核心工作就是:

  • 确保项目的安装环境
  • 确认项目的基本信息

下载模块

下载模块是利用已经封装Package类快速实现相关功能

安装模块

安装模块分为标准模式和自定义模式:

  • 标准模式下,将通过ejs实现模块渲染,并自动安装依赖并启动项目
  • 自定义模式下,将允许用户主动去实现模块的安装过程和后续启动过程

核心代码如下:

  1. class InitCommand extends Command {
  2. init() {
  3. this.projectName = this._argv[0] || '';
  4. this.force = this._cmd.force;
  5. log.verbose(this._argv);
  6. log.verbose('projectName', this.projectName);
  7. log.verbose('force', this.force);
  8. }
  9. async exec() {
  10. try {
  11. //1.准备阶段
  12. const projectInfo = await this.prepare();
  13. if (projectInfo) {
  14. //2.下载模板
  15. log.verbose('projectInfo', projectInfo);
  16. this.projectInfo = projectInfo
  17. await this.downloadTemplate();
  18. //3.安装模板
  19. await this.installTemplate();
  20. }
  21. } catch (e) {
  22. log.error(e.message);
  23. if (process.env.LOG_LEVEL === 'verbose') {
  24. console.log(e);
  25. }
  26. }
  27. }
  28. async installTemplate() {
  29. log.verbose('templateInfo', this.templateInfo);
  30. if (this.templateInfo) {
  31. if (!this.templateInfo.type) {
  32. this.templateInfo.type = TEMPLATE_TYPE_NORMAL
  33. }
  34. if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
  35. //标准安装
  36. await this.installNormalTemplate();
  37. } else if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
  38. //自定义安装
  39. await this.installCustomTemplate();
  40. } else {
  41. throw new Error('无法失败项目模板类');
  42. }
  43. } else {
  44. throw new Error('项目模板信息不存在');
  45. }
  46. }
  47. checkCommand(cmd) {
  48. if (WHITE_COMMAND.includes(cmd)) {
  49. return cmd;
  50. }
  51. return null;
  52. }
  53. async execCommand(command, errMsg) {
  54. let ret;
  55. if (command) {
  56. const cmdArray = command.split(' ');
  57. const cmd = this.checkCommand(cmdArray[0]);
  58. if (!cmd) {
  59. throw new Error('命令不存在!命令:' + command);
  60. }
  61. const args = cmdArray.slice(1);
  62. ret = await execAsync(cmd, args, {
  63. stdio: 'inherit',
  64. cwd: process.cwd(),
  65. })
  66. }
  67. if (ret !== 0) {
  68. throw new Error(errMsg)
  69. }
  70. }
  71. async ejsRender(options) {
  72. const dir = process.cwd();
  73. const projectInfo = this.projectInfo;
  74. return new Promise((resolve, reject) => {
  75. glob('**', {
  76. cwd: dir,
  77. ignore: options.ignore || '',
  78. nodir: true,
  79. }, (err, files) => {
  80. if (err) {
  81. reject(err);
  82. }
  83. Promise.all(files.map(file => {
  84. const filePath = path.join(dir, file);
  85. return new Promise((resolve1, reject1) => {
  86. ejs.renderFile(filePath, projectInfo, {}, (err, result) => {
  87. console.log(result);
  88. if (err) {
  89. reject1(err);
  90. } else {
  91. fse.writeFileSync(filePath, result);
  92. resolve1(result);
  93. }
  94. })
  95. });
  96. })).then(() => {
  97. resolve();
  98. }).catch(err => {
  99. reject(err);
  100. });
  101. })
  102. })
  103. }
  104. async installNormalTemplate() {
  105. //拷贝模板代码直当前目录
  106. let spinner = spinnerStart('正在安装模板');
  107. log.verbose('templateNpm', this.templateNpm)
  108. try {
  109. const templatePath = path.resolve(this.templateNpm.cachFilePath, 'template');
  110. const targetPath = process.cwd();
  111. fse.ensureDirSync(templatePath);//确保当前文件存不存在,不存在会创建
  112. fse.ensureDirSync(targetPath);
  113. fse.copySync(templatePath, targetPath);//把缓存目录下的模板拷贝到当前目录
  114. } catch (e) {
  115. throw e;
  116. } finally {
  117. spinner.stop(true);
  118. log.success('模板安装成功');
  119. }
  120. const templateIgnore = this.templateInfo.ignore || [];
  121. const ignore = ['**/node_modules/**', ...templateIgnore];
  122. await this.ejsRender({ ignore });
  123. //依赖安装
  124. const { installCommand, startCommand } = this.templateInfo
  125. await this.execCommand(installCommand, '依赖安装过程中失败');
  126. //启动命令执行
  127. await this.execCommand(startCommand, '启动执行命令失败');
  128. }
  129. async installCustomTemplate() {
  130. //查询自定义模板的入口文件
  131. if (await this.templateNpm.exists()) {
  132. const rootFile = this.templateNpm.getRootFile();
  133. if (fs.existsSync(rootFile)) {
  134. log.notice('开始执行自定义模板');
  135. const options = {
  136. ...this.options,
  137. cwd:process.cwd(),
  138. }
  139. const code = `require('${rootFile}')(${JSON.stringify(options)})`;
  140. log.verbose('code',code);
  141. await execAsync('node',['-e', code], { stdio: 'inherit', cwd: process.cwd()});
  142. log.success('自定义模板安装成功');
  143. } else {
  144. throw new Error('自定义模板入口文件不存在');
  145. }
  146. }
  147. }
  148. async downloadTemplate() {
  149. //1. 通过项目模板API获取项目模板信息
  150. //1.1 通过egg.js搭建一套后端系统
  151. //1.2 通过npm存储项目模板
  152. //1.3 将项目模板信息存储到mongodb数据库中
  153. //1.4 通过egg.js获取mongodb中的数据并且通过API返回
  154. const { projectTemplate } = this.projectInfo;
  155. const templateInfo = this.template.find(item => item.npmName === projectTemplate);
  156. const targetPath = path.resolve(userHome, '.code-robot-cli', 'template');
  157. const storeDir = path.resolve(userHome, '.code-robot-cli', 'template', 'node_modules');
  158. const { npmName, version } = templateInfo;
  159. this.templateInfo = templateInfo;
  160. const templateNpm = new Package({
  161. targetPath,
  162. storeDir,
  163. packageName: npmName,
  164. packageVersion: version
  165. })
  166. if (! await templateNpm.exists()) {
  167. const spinner = spinnerStart('正在下载模板...');
  168. await sleep();
  169. try {
  170. await templateNpm.install();
  171. } catch (e) {
  172. throw e;
  173. } finally {
  174. spinner.stop(true);
  175. if (templateNpm.exists()) {
  176. log.success('下载模板成功');
  177. this.templateNpm = templateNpm;
  178. }
  179. }
  180. } else {
  181. const spinner = spinnerStart('正在更新模板...');
  182. await sleep();
  183. try {
  184. await templateNpm.update();
  185. } catch (e) {
  186. throw e;
  187. } finally {
  188. spinner.stop(true);
  189. if (templateNpm.exists()) {
  190. log.success('更新模板成功');
  191. this.templateNpm = templateNpm;
  192. }
  193. }
  194. }
  195. }
  196. async prepare() {
  197. // 判断项目模板是否存在
  198. const template = await getProjectTemplate();
  199. if (!template || template.length === 0) {
  200. throw new Error('项目模板不存在');
  201. }
  202. this.template = template;
  203. //1.判断当前目录是否为空
  204. const localPath = process.cwd();
  205. if (!this.isDirEmpty(localPath)) {
  206. let ifContinue = false;
  207. if (!this.force) {
  208. //询问是否继续创建
  209. ifContinue = (await inquirer.prompt({
  210. type: 'confirm',
  211. name: 'ifContinue',
  212. default: false,
  213. message: '当前文件夹不为空,是否继续创建项目?'
  214. })).ifContinue;
  215. if (!ifContinue) {
  216. return;
  217. }
  218. }
  219. //2.是否启动强制更新
  220. if (ifContinue || this.force) {
  221. //给用户二次确认
  222. const { confirmDelete } = await inquirer.prompt({
  223. type: 'confirm',
  224. name: 'confirmDelete',
  225. default: false,
  226. message: '是否确认清空当前目录下的文件?',
  227. })
  228. if (confirmDelete) {
  229. //清空当前目录
  230. fse.emptyDirSync(localPath)
  231. }
  232. }
  233. }
  234. return this.getProjectInfo();
  235. //3.选择创建项目或组件
  236. //4.获取项目得基本信息
  237. }
  238. async getProjectInfo() {
  239. function isValidName(v) {
  240. return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v);
  241. }
  242. let projectInfo = {};
  243. let isProjectInfoValid = false;
  244. if (isValidName(this.projectName)) {
  245. isProjectInfoValid = true;
  246. projectInfo.projectName = this.projectName;
  247. }
  248. //1.选择创建项目或组件
  249. const { type } = await inquirer.prompt({
  250. type: 'list',
  251. name: 'type',
  252. message: '请选择初始化类型',
  253. default: TYPE_PROJECT,
  254. choices: [{
  255. name: '项目',
  256. value: TYPE_PROJECT
  257. }, {
  258. name: '组件',
  259. value: TYPE_COMPONENT
  260. }]
  261. });
  262. log.verbose('type', type);
  263. this.template = this.template.filter(template => {
  264. return template.tag.includes(type);
  265. })
  266. const title = type === TYPE_PROJECT ? '项目' : '组件';
  267. //2.获取项目的基本信息
  268. const projectNamePrompt = {
  269. type: 'input',
  270. name: 'projectName',
  271. message: `请输入${title}的名称`,
  272. default: '',
  273. validate: function (v) {
  274. const done = this.async();
  275. setTimeout(function () {
  276. //1.输入的首字符必须为英文字符
  277. //2.尾字符必须为英文或数字,不能为字符
  278. //3.字符仅运行"-_"
  279. //\w = a-zA-Z0-9 *表示0个或多个
  280. if (!isValidName(v)) {
  281. done(`请输入合法的${title}名称`);
  282. return;
  283. }
  284. done(null, true);
  285. }, 0);
  286. },
  287. filter: function (v) {
  288. return v;
  289. }
  290. }
  291. let projectPrompt = [];
  292. if (!isProjectInfoValid) {
  293. projectPrompt.push(projectNamePrompt);
  294. }
  295. projectPrompt.push({
  296. input: 'input',
  297. name: 'projectVersion',
  298. message: `请输入${title}版本号`,
  299. default: '1.0.0',
  300. validate: function (v) {
  301. const done = this.async();
  302. setTimeout(function () {
  303. //1.输入的首字符必须为英文字符
  304. //2.尾字符必须为英文或数字,不能为字符
  305. //3.字符仅运行"-_"
  306. //\w = a-zA-Z0-9 *表示0个或多个
  307. if (!(!!semver.valid(v))) {
  308. done('请输入合法的版本号');
  309. return;
  310. }
  311. done(null, true);
  312. }, 0);
  313. },
  314. filter: function (v) {
  315. if (!!semver.valid(v)) {
  316. return semver.valid(v);
  317. } else {
  318. return v;
  319. }
  320. }
  321. }, {
  322. type: 'list',
  323. name: 'projectTemplate',
  324. message: `请选择${title}模板`,
  325. choices: this.createTemplateChoices()
  326. });
  327. if (type === TYPE_PROJECT) {
  328. const project = await inquirer.prompt(projectPrompt);
  329. projectInfo = {
  330. ...projectInfo,
  331. type,
  332. ...project
  333. }
  334. } else if (type === TYPE_COMPONENT) {
  335. const descriptionPrompt = {
  336. input: 'input',
  337. name: 'componentDescription',
  338. message: '请输入组件描述信息',
  339. default: '',
  340. validate: function (v) {
  341. const done = this.async();
  342. setTimeout(function () {
  343. //1.输入的首字符必须为英文字符
  344. //2.尾字符必须为英文或数字,不能为字符
  345. //3.字符仅运行"-_"
  346. //\w = a-zA-Z0-9 *表示0个或多个
  347. if (!v) {
  348. done('请输入组件描述信息');
  349. return;
  350. }
  351. done(null, true);
  352. }, 0);
  353. }
  354. }
  355. projectPrompt.push(descriptionPrompt);
  356. const component = await inquirer.prompt(projectPrompt);
  357. projectInfo = {
  358. ...projectInfo,
  359. type,
  360. ...component
  361. }
  362. }
  363. //return 项目的基本信息(object)
  364. if (projectInfo.projectName) {
  365. projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/, '');
  366. }
  367. if (projectInfo.projectVersion) {
  368. projectInfo.version = projectInfo.projectVersion;
  369. }
  370. if (projectInfo.componentDescription) {
  371. projectInfo.description = projectInfo.componentDescription;
  372. }
  373. return projectInfo;
  374. }
  375. isDirEmpty(localPath) {
  376. let fileList = fs.readdirSync(localPath);
  377. //文件过滤的逻辑
  378. fileList = fileList.filter(file => (
  379. !file.startsWith('.') && ['node_modules'].indexOf(file) < 0
  380. ));
  381. return !fileList || fileList.length <= 0;
  382. }
  383. createTemplateChoices() {
  384. return this.template.map(item => ({
  385. value: item.npmName,
  386. name: item.name
  387. }))
  388. }
  389. }
  390. function init(argv) {
  391. // console.log('init',projectName,cmdObj.force,process.env.CLI_TARGET_PATH);
  392. return new InitCommand(argv);
  393. }
  394. module.exports = init;
  395. module.exports.InitCommand = InitCommand;

至此我们完成了脚手架开发以及通过脚手架创建项目。

如何通过Yargs来开发脚手架?

  • 脚手架分为三部分构成(vue create vuex)
    • bin:主命令在package.json中配置bin属性,npm link本地安装
    • command:命令
    • options:参数(boolean/string/number)
    • 文件顶部增加#!/usr/bin/env node,这行命令的用途时告诉操作系统要在环境变量当中查询到node命令,通过node命令来执行文件
  • 脚手架初始化流程
    • 构造函数:Yargs() (通过Yargs构造函数的调用去生成一个脚手架)
    • 常用方法:
      • Yargs.options (注册脚手架的属性)
      • Yargs.option
      • Yargs.group (将脚手架属性进行分组)
      • Yargs.demandCommand (规定最少传几个command)
      • Yargs.recommendCommands (在输入错误command以后可以给你推荐最接近的正确的command)
      • Yargs.strict (开启以后可以报错提示)
      • Yargs.fail (监听脚手架的异常)
      • Yargs.alias (起别名)
      • Yargs.wrapper (命令行工具的宽度)
      • Yargs.epilogus (命令行工具底部的提示)
  • 脚手架参数解析方法
    • hideBin(process.argv)
    • Yargs.parse(argv, options)
  • 命令注册方法
    • Yargs.command(command,describe, builder, handler)
    • Yargs.command({command,describe, builder, handler})

Node.js模块路径解析流程

  • Node.js项目模块路径解析是通过require.resolve方法来实现的
  • require.resolve就是通过Module._resolveFileName方法实现的
  • require.resolve实现原理:
    • Module._resolveFileName方法核心流程有3点:
      • 判断是否为内置模块
      • 通过Module._resolveLookupPaths方法生成node_modules可能存在的路径
      • 通过Module._findPath查询模块的真实路径
    • Module._findPath核心流程有4点:
      • 查询缓存(将request和paths通过\x00(空格)合并成cacheKey)
      • 遍历paths,将path与request组成文件路径basePath
      • 如果basePath存在则调用fs.realPathSync获取文件真实路径
      • 将文件真实路径缓存到Module._pathCache(key就是前面生成的cacheKey)
    • fs.realPathSync核心流程有3点:
      • 查询缓存(缓存的key为p,即Module._findPath中生成的文件路径)
      • 从左往右遍历路径字符串,查询到/时,拆分路径,判断该路径是否为软连接,如果是软连接则查询真实链接,并生成新路径p,然后继续往后遍历,这里有1个细节需要注意:
        • 遍历过程中生成的子路径base会缓存在knownHard和cache中,避免重复查询
      • 遍历完成得到模块对应的真实路径,此时会将原路径original作为key,真实路径作为value,保存到缓存中
  • require.resolve.paths等价于Module._resolveLoopupPaths,该方法用于获取所有的node_modules可能存在的路径
  • require.resolve.paths实现原理: