首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
【架构师(第一篇)】整体需求分析和架构设计
2
【架构师(第二篇)】脚手架架构设计和框架搭建
3
【架构师(第三篇)】脚手架开发之掌握Lerna操作流程
4
【架构师(第四篇)】脚手架开发之Lerna源码分析
5
【架构师(第五篇)】脚手架之import-local执行流程及简历设计
6
【架构师(第六篇)】脚手架之需求分析和架构设计
7
【架构师(第七篇)】脚手架之准备阶段编写
8
【架构师(第八篇)】脚手架之 commander 框架使用方法
9
【架构师(第九篇)】如何让 Node 环境支持 ES Module
10
【架构师(第十篇)】脚手架之注册命令及架构优化
11
【架构师(第十一篇)】脚手架之命令注册和执行过程开发
12
【架构师(第十二篇)】脚手架之命令行交互工具 inquirer.js 使用方法
13
【架构师(第十三篇)】脚手架之创建项目准备阶段开发
14
【架构师(第十四篇)】脚手架之 egg.js 和 mongodb 的使用
15
【架构师(第十五篇)】脚手架之创建项目模板开发
16
【架构师(第十六篇)】脚手架之创建项目模板的下载与更新
17
【架构师(第十七篇)】脚手架之 ejs 和 glob 的使用
18
【架构师(第十八篇)】脚手架之项目模板的安装
19
【架构师(第十九篇)】脚手架之组件库模板开发
20
【架构师(第二十篇)】脚手架之自定义模板及第一阶段总结
21
【架构师(第二十一篇)】编辑器开发之需求分析和架构设计
22
【架构师(第二十二篇)】编辑器开发之项目整体搭建
23
【架构师(第二十三篇)】编辑器开发之画布区域组件的渲染
24
【架构师(第二十四篇)】编辑器开发之添加模版到画布
25
【架构师(第二十五篇)】编辑器开发之属性编辑区域表单渲染
26
【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染
27
【架构师(第二十七篇)】前端单元测试框架 Jest 基础知识入门
28
【架构师(第二十八篇)】 测试工具 Vue-Test-Utils 基础语法
29
【架构师(第二十九篇)】Vue-Test-Utils 触发事件和异步请求
30
【架构师(第三十篇)】Vue-Test-Utils 全局组件和第三方库 vuex | vue-router
31
【架构师(第三十一篇)】前端测试之 TDD 的开发方式
32
【架构师(第三十二篇)】 通用上传组件开发及测试用例
33
【架构师(第三十三篇)】 Vue 中的实例及本地图片预览
34
【架构师(第三十四篇)】 业务组件库开发之 vue3 的插件系统
35
【架构师(第三十五篇)】 业务组件库开发之使用 Rollup 进行打包
36
【架构师(第三十六篇)】 业务组件库开发之发布到 NPM
37
【架构师(第三十七篇)】 服务端开发之后端框架与数据库技术选型
38
【架构师(第三十八篇)】 服务端开发之本地安装最新版 MySQL 数据库
39
【架构师(第三十九篇)】 服务端开发之连接 MySQL 数据库
40
【架构师(第四十篇)】 服务端开发之连接 Mongodb 数据库
41
【架构师(第四十一篇)】 服务端开发之安装并连接 Redis数据库
42
【架构师(第四十二篇)】 服务端开发之常用的登录鉴权方式
43
【架构师(第四十三篇)】 服务端开发之单元测试和接口测试
44
【架构师(第四十四篇)】 服务端开发之 pm2 和 nginx 介绍
45
【架构师(第四十五篇)】 服务端开发之认识 Github actions
46
【架构师(第四十六篇)】 服务端开发之安装 Docker
47
【架构师(第四十七篇)】 服务端开发之认识 Docker
48
【架构师(第四十八篇)】 服务端开发之 Dockerfile
49
【架构师(第四十九篇)】 服务端开发之认识 Docker-compose
50
【架构师(第五十篇)】 服务端开发之自动发布到测试机
清单首页架构文章详情

【架构师(第四篇)】脚手架开发之Lerna源码分析


脚手架开发之 Lerna 源码分析

为什么要做源码分析

  • 自我成长,提升编码能力和技术深度的需要
  • 为我所用,应用到实际开发,实际产生效益
  • 学习借鉴,站在巨人肩膀上,登高望远

为什么要分析 Lerna 源码

  • 2w + star 的明星项目
  • Lerna 是脚手架,对我们开发脚手架有借鉴价值
  • Lerna 项目中蕴含大量的最佳实践,值得深入研究和学习

学习目标

  • Lerna 源码结构和执行流程分析
  • import-local 源码深度精读

学习收获

  • 如何将源码分析的收获写进简历
  • 学习明星项目的架构设计
  • 获得脚手架执行流程的一种实现思路
  • 脚手架调试本地源码的另一种方法
  • node.js 加载 node_modules 模块的流程
  • 各种文件操作算法和最佳实践

知识点: 本地库作为依赖的方法 file:路径 lerna 上线时会自动替换成线上的地址 "dependencies": { "@lerna/global-options": "file:../global-options", }

yargs 使用

安装

代码语言:javascript
复制
npm i yargs -S

最简单的 yargs 脚手架

代码语言:javascript
复制
// \bin\index.js

// 引入 yargs 构造函数
const yargs = require('yargs/yargs')

const { hideBin } = require('yargs/helpers')

// 解析参数
const arg = hideBin(process.argv)

// 调用 yargs 构造函数 传入一个参数进行解析  然后调用 argv  完成初始化过程
yargs(arg)
  .strict() // 开启严格模式 命令错误时 会出现 Unknown argument: xxx 的提示
  .argv // 可以解析参数

现在就可以在命令行运行了。

代码语言:javascript
复制
test-cli --help
test-cli --version
test-cli --h

输出如下

usage

打印在命令行最前面

代码语言:javascript
复制
yargs(arg)
  .usage("Usage:test-cli [command] <options>") // 打印在命令行最前面
  .strict() 
  .argv 

demandCommand

设置最少需要输入的 command 的数量

代码语言:javascript
复制
yargs(arg)
  .demandCommand(1, "A command is required. Pass --help to see all available commands and options.") 
  .argv

当你不输入 command 的时候,就会报错

alias

别名

代码语言:javascript
复制
yargs(arg)
  .alias("h", "help")
  .alias("v", "version")
  .argv

这样输入 h 和输入 help 的结果是一样的,vversion 的结果是一样的

wrap

cli 的宽度

代码语言:javascript
复制
yargs(arg)
  .wrap(100)
  .argv

可以看到 cli 在命令行中的宽度发生了变化

yargs.terminalWidth() 这个方法会返回命令行界面的宽度,这样cli就会全屏展示了

代码语言:javascript
复制
const cli = yargs(arg)
cli
  .wrap(cli.terminalWidth())
  .argv

epilogue

结尾的内容

代码语言:javascript
复制
cli
  .epilogue("this is footer")
  .argv

可以看到 cli 的最后输出了 this is footer

可以使用 dedent 这个库去去除缩进,使代码格式保持一致

代码语言:javascript
复制
cli
  .epilogue(dedent(`
      When a command fails, all logs are written to lerna-debug.log in the current working directory.

      For more information, find our manual at https://github.com/lerna/lerna
  `))
  .argv

options

增加一个全局的选项,对所有的 command 都有效

代码语言:javascript
复制
cli
  .options({
    debug: {
      type: 'boolean',
      describe: "bootstrap debug moe",
      alias: "d"
    }
  })
  .argv

option

options 可以定义多个选项,而 option 只可以定义一个,作用是一样的。

可以添加 hidden:true,来隐藏 option,供内部人员开发时使用。

代码语言:javascript
复制
  .option("registry", {
    type: 'string',
    describe: "define global registry",
    alias: "r",
    // hidden:true
  })

group

option 分组, options 是默认的组

代码语言:javascript
复制
cli
  .group(['debug'], 'Deb Options:')
  .group(['registry'], 'Publish Options:')
  .argv

command

定义一个 command,接收四个参数

  • 第一个:command 的格式,name [port]name 是命令的名称,port 表示一个自定义的 option
  • 第二个:对 command 的描述
  • 第三个:builder 函数,在执行命令之前做的一些事情
  • 第四个:handler 函数,执行 command 的行为

注意:定义脚手架的时候,任何地方的别名都不可以出现重复,不然会覆盖。

代码语言:javascript
复制
cli
  .command(
    "init [name]",
    "do init a project",
    (yargs) => {
      yargs.option("name", {
        type: "string",
        describe: 'name of a project',
        alias: "n"
      })
    },
    (argv) => {
      console.log('🚀🚀~ argv:', argv);
    }
  )
  .argv

所有内容和别名都会出现在 argv 这个参数中。

另外,command 也支持对象的写法

代码语言:javascript
复制
cli
  .command({
    command: "list",
    aliases: ["ls", "la", "ll"],
    describe: "List local packages",
    builder: (yargs) => { },
    handler: (yargs) => { }
  })
  .argv

recommendCommands

当你输入一个错误的 command 的时候,会自动的帮助你去寻找一个最接近的 command 来提示你

代码语言:javascript
复制
cli
  .recommendCommands()
  .argv    

当我们输入 test-cli lis ,输出 Did you mean list?

fail

command 不存在时的错误处理

当一个 command 不存在时,默认会输出 --help 的内容 ,如果我们不想看到,那么就可以在 fail 这个方法里进行定制

代码语言:javascript
复制
cli
  .fail((err, msg) => {
    console.log(err);
  })
  .argv    

这样就只有错误信息,而不会输出出其他东西了

parse

会把定义的内容注入到当前的项目中

代码语言:javascript
复制
// 定义一个内容
const context = {
  testVersion: pkg.version,
};
// 不用在这里解析参数了
const cli = yargs()
cli
  .parse(argv, context)

我们再次打印出 args , 发现之前定义的 testVersion 已经出现在 args 中了

Lerna 源码结构

代码语言:javascript
复制
D:\lerna-main
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commands
├── CONTRIBUTING.md
├── core
    ├── child-process
    ├── cli
    ├── command
    ├── conventional-commits
    ├── filter-options
    ├── global-options
    ├── lerna
    ├── otplease
    ├── package
    ├── package-graph
    ├── project
    ├── prompt
    └── validation-error    
├── doc
├── FAQ.md
├── helpers
├── integration
├── jest.config.js
├── jest.integration.js
├── lerna.json
├── LICENSE
├── node_modules
├── package-lock.json
├── package.json
├── README.md
├── scripts
├── setup-integration-timeout.js
├── setup-unit-test-timeout.js
├── utils
└── __fixtures__

入口文件

可以在根目录的 package.json 文件中发现脚手架的入口

代码语言:javascript
复制
 "bin": {
    "lerna": "core/lerna/cli.js"
  },

Lerna 初始化分析

根据入口文件,发现 Lerna 初始化的时候执行了 main 方法。

代码语言:javascript
复制
// core\lerna\cli.js

// 引入 import-local 这个库
const importLocal = require("import-local");

// import-local 的逻辑后面单独分析
if (importLocal(__filename)) {
  require("npmlog").info("cli", "using local version of lerna");
} else {
// 引入当前目录下的index.js模块 这个模块返回了一个 main 方法 并把 process.argv.slice(2) 作为参数执行
  require(".")(process.argv.slice(2)); //  相当于 main(process.argv.slice(2))
}

index.js 简略代码,这个模块只输出了一个 main 方法

代码语言:javascript
复制
// core\lerna\index.js
module.exports = main;
function main(argv) {
}

index.js 完整代码

代码语言:javascript
复制
// core\lerna\index.js
"use strict";

//
const cli = require("@lerna/cli");

// 引入若干指令
const addCmd = require("@lerna/add/command");
const bootstrapCmd = require("@lerna/bootstrap/command");
const changedCmd = require("@lerna/changed/command");
const cleanCmd = require("@lerna/clean/command");
const createCmd = require("@lerna/create/command");
const diffCmd = require("@lerna/diff/command");
const execCmd = require("@lerna/exec/command");
const importCmd = require("@lerna/import/command");
const infoCmd = require("@lerna/info/command");
const initCmd = require("@lerna/init/command");
const linkCmd = require("@lerna/link/command");
const listCmd = require("@lerna/list/command");
const publishCmd = require("@lerna/publish/command");
const runCmd = require("@lerna/run/command");
const versionCmd = require("@lerna/version/command");

// 引入 package.json 模块
const pkg = require("./package.json");

// 输出 main 方法
module.exports = main;

// main 方法
function main(argv) {
  // 定义一个对象,里面保存一个 lernaVersion 属性,值是 package.json 中的 version 属性的值
  const context = {
    lernaVersion: pkg.version,
  };

  return cli()
    .command(addCmd)    // 添加 addCmd 命令
    .command(bootstrapCmd) // 添加命令
    .command(changedCmd) // 添加命令
    .command(cleanCmd) // 添加命令
    .command(createCmd) // 添加命令
    .command(diffCmd) // 添加命令
    .command(execCmd) // 添加命令
    .command(importCmd) // 添加命令
    .command(infoCmd) // 添加命令
    .command(initCmd) // 添加命令
    .command(linkCmd) // 添加命令
    .command(listCmd) // 添加命令
    .command(publishCmd) // 添加命令
    .command(runCmd) // 添加命令
    .command(versionCmd) // 添加命令
    .parse(argv, context);// 合并参数 , 将 argv 和自定义的 context 中的属性合并到 argv 中
}

main 方法都做了那些事呢,首先是执行了 cli 这个方法。cli 这个模块输出的是 lernaCLI 方法。

代码语言:javascript
复制
// core\cli\index.js
module.exports = lernaCLI;
function lernaCLI(argv, cwd) {
}

接下里看看 lernaCLI 干了什么

代码语言:javascript
复制
// core\cli\index.js
// lernaCLI 方法
function lernaCLI(argv, cwd) {
  // 对 yargs 进行初始化  
  const cli = yargs(argv, cwd);
  
  // globalOptions 也是一个方法 把 yargs 作为参数传入 返回的还是这个 yargs 对象 
  // 然后基于 globalOptions() 的结果 又做了一些设置
  // 运用的是构造者模式,对一个对象调用方法,然后返回这个对象本身
  return globalOptions(cli)
    .usage("Usage: $0 <command> [options]") // 配置 cli 的开始内容  $0 表示再 argv 中寻找 $0 这个值进行替换
    .demandCommand(1, "A command is required. Pass --help to see all available commands and options.") // 配置输入的最小命令
    .recommendCommands() // 配置 command 的与错误最相近的 command 提示
    .strict() // 开启严格模式 命令不存在时 会报错
    .fail((msg, err) => { // 命令不存在时的错误定制
    })
    .alias("h", "help") // 别名
    .alias("v", "version") // 别名
    .wrap(cli.terminalWidth()) // 配置cli的宽度和命令行一样
    .epilogue(dedent` 
    `); // 配置 cli 结尾的内容
}

接下来看一下 globalOptions 这个东西都干了什么

代码语言:javascript
复制
// core\global-options\index.js
function globalOptions(yargs) {
  // 定义了一堆的 option
  const opts = {
    loglevel: {
      defaultDescription: "info",
      describe: "What level of logs to report.",
      type: "string",
    },
    concurrency: {
      defaultDescription: os.cpus().length,
      describe: "How many processes to use when lerna parallelizes tasks.",
      type: "number",
      requiresArg: true,
    },
    "reject-cycles": {
      describe: "Fail if a cycle is detected among dependencies.",
      type: "boolean",
    },
    "no-progress": {
      describe: "Disable progress bars. (Always off in CI)",
      type: "boolean",
    },
    progress: {
      // proxy for --no-progress
      hidden: true,
      type: "boolean",
    },
    "no-sort": {
      describe: "Do not sort packages topologically (dependencies before dependents).",
      type: "boolean",
    },
    sort: {
      // proxy for --no-sort
      hidden: true,
      type: "boolean",
    },
    "max-buffer": {
      describe: "Set max-buffer (in bytes) for subcommand execution",
      type: "number",
      requiresArg: true,
    },
  };

  // 拿到这些 option 的名称
  const globalKeys = Object.keys(opts).concat(["help", "version"]);

  
  return yargs
  .options(opts) // 给 yargs 添加 全局options 
  .group(globalKeys, "Global Options:") // 对 options 进行分组
  .option("ci", { // 添加了一个隐藏的 option 
    hidden: true,
    type: "boolean",
  });
}

Command 执行过程

前面提到 main 方法当中添加了很多 command,再来看看 Command 执行过程是什么样的。

listCmd 为例

代码语言:javascript
复制
// commands\list\command.js

const { filterOptions } = require("@lerna/filter-options");
const listable = require("@lerna/listable");

exports.command = "list"; // 配置命令的名称

exports.aliases = ["ls", "la", "ll"]; // 配置命令的别名

exports.describe = "List local packages"; // 配置命令的描述

exports.builder = (yargs) => { // 配置命令在执行之前做的事情
  listable.options(yargs);

  return filterOptions(yargs);
};

exports.handler = function handler(argv) { // 配置命令在执行过程做的事情
  return require(".")(argv); // 调用当前目录下 index.js 导出的 factory 方法
};

继续看看 handler 所执行的 factory 方法。

代码语言:javascript
复制
// commands\list\index.js

module.exports = factory;

function factory(argv) {
  return new ListCommand(argv); // 实例化一个 ListCommand
}

// ListCommand 的结构
class ListCommand extends Command {
  get requiresGit() {
  }

  initialize() {
  }

  execute() {
  }
}

module.exports.ListCommand = ListCommand;

可以看到 ListCommand 是通过继承来了,继续看看父类的内容

代码语言:javascript
复制
// core\command\index.js

class Command {
  constructor(_argv) {
    // 深拷贝 argv
    const argv = cloneDeep(_argv);
    // 添加 name 属性  FooCommand => foo
    this.name = this.constructor.name.replace(/Command$/, "").toLowerCase();
    // 添加 composed 属性, 是否使用复合指令
    this.composed = typeof argv.composed === "string" &amp;&amp; argv.composed !== this.name;
    // 如果不是复合指令
    if (!this.composed) {
      // composed commands have already logged the lerna version
      log.notice("cli", `v${argv.lernaVersion}`);
    }
    // 最终的执行过程
    let runner = new Promise((resolve, reject) => {
      // 定义一个微任务 chain.then 会被加入到微任务队列
      let chain = Promise.resolve();
      // 会行程队列,一个接一个执行
      chain = chain.then(() => {
        this.project = new Project(argv.cwd);
      });
      chain = chain.then(() => this.configureEnvironment());
      chain = chain.then(() => this.configureOptions());
      chain = chain.then(() => this.configureProperties());
      chain = chain.then(() => this.configureLogging());
      chain = chain.then(() => this.runValidations());
      chain = chain.then(() => this.runPreparations());
      // 核心内容
      chain = chain.then(() => this.runCommand());

      chain.then(
        (result) => {
        },
        (err) => {
        }
      );
    });
    // 向 argv 中定义 cwd 和 $0 两个参数
    for (const key of ["cwd", "$0"]) {
      Object.defineProperty(argv, key, { enumerable: false });
    }
    // 对 argv 属性做一些处理
    Object.defineProperty(this, "argv", {
      value: Object.freeze(argv),
    });
    // 对 runner 属性做一些处理
    Object.defineProperty(this, "runner", {
      value: runner,
    });
  }
  // 核心内容
  runCommand() {
    return Promise.resolve()
      .then(() => this.initialize()) // 调用 initialize 方法
      .then((proceed) => {
        if (proceed !== false) {
          return this.execute(); // 调用 execute方法
        }
        // early exits set their own exitCode (if non-zero)
      });
  }
  // initialize 和 execute 强制用户实现,否则会报错
  initialize() {
    throw new ValidationError(this.name, "initialize() needs to be implemented.");
  }

  execute() {
    throw new ValidationError(this.name, "execute() needs to be implemented.");
  }
}

initializeexecute 强制用户实现,否则会报错。

那么现在返回来再看看这两个方法的实现,不用关心 lerna 的源码,主要是看一下执行过程。

代码语言:javascript
复制
// commands\list\index.js
module.exports = factory;

function factory(argv) {
  return new ListCommand(argv);
}

class ListCommand extends Command {
  get requiresGit() {
    return false;
  }

  initialize() {
    // 也是通过 chain 微任务队列的方式
    let chain = Promise.resolve();

    chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
    chain = chain.then((filteredPackages) => {
      this.result = listable.format(filteredPackages, this.options);
    });

    return chain;
  }

  execute() {
    // piping to `wc -l` should not yield 1 when no packages matched
    if (this.result.text.length) {
      output(this.result.text);
    }
    
    // 打印log 执行完毕
    this.logger.success(
      "found",
      "%d %s",
      this.result.count,
      this.result.count === 1 ? "package" : "packages"
    );
  }
}

module.exports.ListCommand = ListCommand;
下一篇
举报
领券