欢迎光临
我们一直在努力

Taro 源码揭秘:1. 揭开整个架构的入口 CLI => taro init 初始化项目的秘密

1. 前言

大家好,我是若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。

截至目前(2024-07-17),taro 正式版是 3.6.34Taro 4.0 Beta 发布:支持开发鸿蒙应用、小程序编译模式、Vite 编译等。文章提到将于 2024 年第二季度,发布 4.x。目前已经发布 4.x。所以我们直接学习 main 分支最新版本是 4.0.2

多编译内核生态下的极速研发体验 官方博客有如下图。

多编译内核架构

计划写一个 Taro 源码揭秘系列,博客地址:https://ruochuan12.github.io/taro 可以加入书签,持续关注若川

学完本文,你将学到:

1. 学会通过两种方式调试 taro 源码
2. 学会入口 taro-cli 具体实现方式
3. 学会 cli init 命令实现原理,读取用户项目配置文件和用户全局配置文件
4. 学会 taro-service kernal (内核)解耦实现
5. 初步学会 taro 插件架构,学会如何编写一个 taro 插件

2. 准备工作

# 克隆项目
git clone https://github.com/NervJS/taro.git
# 切换到分支 main
git checkout main
# 写文章时,项目当前 hash
git checkout f53250b68f007310bf098e77c6113e2012983e82
# Merge branch 'main' into 4.x
# 写文章时,当前版本
# 4.0.2

看一个开源项目,第一步应该是先看 README.md 再看 贡献文档package.json

环境准备

需要安装 Node.js 16(建议安装 16.20.0 及以上版本)及 pnpm 7

我使用的环境:mac,当然 Windows 一样可以。

一般用 nvm 管理 node 版本。

nvm install 18
nvm use 18
# 可以把 node 默认版本设置为 18,调试时会使用默认版本
nvm alias default 18

pnpm -v
# 9.1.1
node -v
# v18.20.2

cd taro
# 安装依赖
pnpm i
# 如果网络不好,一直安装不上可以指定国内镜像站,速度比较快
pnpm i --registry=https://registry.npmmirror.com
# 编译构建
pnpm build
# 删除根目录的 node_modules 和所有 workspace 里的 node_modules
$ pnpm run clear-all
# 对应的是:rimraf **/node_modules
# mac 下可以用 rm -rf **/node_modules

安装依赖可能会报错。

pnpm-i-error.png

Failed to set up Chromium r1108766! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.

通过谷歌等搜索引擎可以找到解决方法。

stackoverflow

Mac : export PUPPETEER_SKIP_DOWNLOAD='true'
Windows: SET PUPPETEER_SKIP_DOWNLOAD='true'

pnpm build 完成,如下图所示:

pnpm-build.png

3. 调试

package.json

// packages/taro-cli/package.json
{
    "name": "@tarojs/cli",
    "version": "4.0.0",
    "description": "cli tool for taro",
    "main": "index.js",
    "types": "dist/index.d.ts",
    "bin": {
        "taro": "bin/taro"
    }
}

3.1 入口文件 packages/taro-cli/bin/taro

// packages/taro-cli/bin/taro

#! /usr/bin/env node

require("../dist/util").printPkgVersion();

const CLI = require("../dist/cli").default;

new CLI().run();

3.2 调试方法 1 JavaScript Debug Terminal

可参考我的文章新手向:前端程序员必学基本技能——调试 JS 代码,或者据说 90%的人不知道可以用测试用例(Vitest)调试开源项目(Vue3) 源码

简而言之就是以下步骤:

1. 找到入口文件设置断点
2. ctrl + `/`` (反引号) 打开终端,配置`JavaScript调试终端`
3. 在终端输入 `node` 相关命令,这里用 `init` 举例
4. 尽情调试源码
node ./packages/taro-cli/bin/taro init taro-init-debug

本文将都是使用 init 命令作为示例。

如下图所示:

vscode 调试源码

也可以使用项目中提供的测试用例 packages/taro-cli/src/__tests__/cli.spec.ts 提前打断点调试源码。贡献文档-单元测试中有提到:

package.json 中设置了 test:ci 命令的子包都配备了单元测试。
开发者在修改这些包后,请运行 pnpm --filter [package-name] run test:ci,检查测试用例是否都能通过。

# JavaScript Debug Terminal
pnpm --filter @tarojs/cli run test:ci

调试和上图类似,就不截调试图了。

调试时应该会报错 binding taro.[os-platform].node。如下图所示:

binding-error.png

运行等过程报错,不要慌。可能是我们遗漏了一些细节,贡献文档等应该会给出答案。所以再来看下 贡献文档-10-rust-部分

binding-rust.png

通过 rustup 找到安装命令:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装完成后,执行 pnpm run build:binding:debugpnpm run binding:release 编译出文件:crates/native_binding/taro.darwin-arm64.node

就完美解决了,调试时不会报错了。

3.3 调试方式 2 配置 .vscode/launch.json

taro 文档 – 单步调测配置
写的挺好的,通过配置 launch.json 来调试,在此就不再赘述了。

不过补充一条:launch.json 文件可以添加一条 "console": "integratedTerminal"(集成终端)配置,就可以在调试终端输入内容。args 参数添加 init 和指定要初始化项目的文件夹。当然调试其他的时候也可以修改为其他参数。比如args: ["build", "--type", "weapp", "--watch"]

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "CLI debug",
            "program": "${workspaceFolder}/packages/taro-cli/bin/taro",
            // "cwd": "${project absolute path}",
            "args": [
                "init",
                "taro-init-debug",
            ],
            "skipFiles": ["<node_internals>/**"],
            "console": "integratedTerminal"
        }
    ]
}

vscode nodejs 调试

console- 启动程序的控制台(internalConsole,integratedTerminal,externalTerminal)。

vscode-console.png

// packages/taro-cli/bin/taro

#! /usr/bin/env node

require("../dist/util").printPkgVersion();

const CLI = require("../dist/cli").default;

new CLI().run();

我们跟着断点进入,入口文件中的第一句require("../dist/util").printPkgVersion(); printPkgVersion 函数。

4. taro-cli/src/utils/index.ts

工具函数

// packages/taro-cli/src/util/index.ts
import * as path from "path";

export function getRootPath(): string {
    return path.resolve(__dirname, "../../");
}

export function getPkgVersion(): string {
    return require(path.join(getRootPath(), "package.json")).version;
}

export function printPkgVersion() {
    console.log(`👽 Taro v${getPkgVersion()}`);
    console.log();
}

可以看出这句输出的是 taro/packages/taro-cli/package.json 的版本号。

👽 Taro v4.0.0

我们继续跟着断点,进入第二第三句,可以进入到 packages/taro-cli/src/cli.ts 这个文件。

5. CLI 整体结构

taro-cli 对应的文件路径是:

packages/taro-cli/src/cli.ts

我们先来看下这个文件的整体结构。class CLI 一个 appPath 属性(一般指 taro 工作目录),两个函数 runparseArgs

// packages/taro-cli/src/cli.ts
export default class CLI {
    appPath: string;
    constructor(appPath) {
        this.appPath = appPath || process.cwd();
    }

    run() {
        return this.parseArgs();
    }

    async parseArgs() {
        const args = minimist(process.argv.slice(2), {
            alias: {
                // 省略一些别名设置 ...
            },
            boolean: ["version", "help", "disable-global-config"],
            default: {
                build: true,
            },
        });
        const _ = args._;
        // init、build 等
        const command = _[0];
        if (command) {
            // 省略若干代码
        } else {
            if (args.h) {
                // 输出帮助信息
                // 省略代码
            } else if (args.v) {
                // 输出版本号
                console.log(getPkgVersion());
            }
        }
    }
}

使用了minimist,参数解析工具。

同类工具还有:
commander,命令行工具。功能齐全的框架,提供类似 git 的子命令系统,自动生成帮助信息等。有很多知名的 cli 都是用的这个commander。比如:vue-cliwebpack-clicreate-react-app 用的是这个。

cac,类似 Commander.js 但更轻巧、现代,支持插件。也有很多使用这个cac npm,比如vite 使用的是这个。

yargs,交互式命令行工具。功能强大的框架,但显得过于臃肿。

cli.run 函数最终调用的是 cli.parseArgs 函数。我们接着来看 parseArgs 函数。

6. cli parseArgs

6.1 presets 预设插件集合

parseArgs-1.png

presets 对应的目录结构如图所示:

presets.png

6.2 Config

parseArgs-2.png

64-78 行代码,代码量相对较少,就截图同时顺便直接放代码了。

// packages/taro-cli/src/cli.ts
// 这里解析 dotenv 以便于 config 解析时能获取 dotenv 配置信息
const expandEnv = dotenvParse(appPath, args.envPrefix, mode);

const disableGlobalConfig = !!(
    args["disable-global-config"] ||
    DISABLE_GLOBAL_CONFIG_COMMANDS.includes(command)
);

const configEnv = {
    mode,
    command,
};
const config = new Config({
    appPath: this.appPath,
    disableGlobalConfig: disableGlobalConfig,
});
await config.init(configEnv);

dotenvParse 函数简单来说就是通过 dotenvdotenv-expand 解析 .env.env.development.env.production 等文件和变量的。

dotenv 是一个零依赖模块,可将 .env 文件中的环境变量加载到 process.env 中。

我之前写过一篇 面试官:项目中常用的 .env 文件原理是什么?如何实现?

接着我们来看 Config 类。

// packages/taro-service/src/Config.ts
export default class Config {
    appPath: string;
    configPath: string;
    initialConfig: IProjectConfig;
    initialGlobalConfig: IProjectConfig;
    isInitSuccess: boolean;
    disableGlobalConfig: boolean;

    constructor(opts: IConfigOptions) {
        this.appPath = opts.appPath;
        this.disableGlobalConfig = !!opts?.disableGlobalConfig;
    }
    async init(configEnv: { mode: string; command: string }) {
        // 代码省略
    }
    initGlobalConfig() {
        // 代码省略
    }
    getConfigWithNamed(platform, configName) {
        // 代码省略
    }
}

Config 构造函数有两个属性。
appPathtaro 项目路径。
disableGlobalConfig 是禁用全局配置。

接着我们来看 Config 类的实例上的 init 方法。

6.2.1 config.init 初始化配置

读取的是 config/index .ts 或者 .js 后缀。
判断是否禁用 disableGlobalConfig 全局配置。不禁用则读取全局配置 ~/.taro-global-config/index.json

async init (configEnv: {
    mode: string
    command: string
  }) {
    this.initialConfig = {}
    this.initialGlobalConfig = {}
    this.isInitSuccess = false
    this.configPath = resolveScriptPath(path.join(this.appPath, CONFIG_DIR_NAME, DEFAULT_CONFIG_FILE))
    if (!fs.existsSync(this.configPath)) {
      if (this.disableGlobalConfig) return
      this.initGlobalConfig()
    } else {
      createSwcRegister({
        only: [
          filePath => filePath.indexOf(path.join(this.appPath, CONFIG_DIR_NAME)) >= 0
        ]
      })
      try {
        const userExport = getModuleDefaultExport(require(this.configPath))
        this.initialConfig = typeof userExport === 'function' ? await userExport(merge, configEnv) : userExport
        this.isInitSuccess = true
      } catch (err) {
        console.log(err)
      }
    }
  }

值得一提的是:

createSwcRegister.png

createSwcRegister 使用了 @swc/register 来编译 ts 等转换成 commonjs。可以直接用 require

使用 swc 的方法之一是通过 require 钩子。require 钩子会将自身绑定到 noderequire 并自动动态编译文件。不过现在更推荐 @swc-node/register

export const getModuleDefaultExport = (exports) =>
    exports.__esModule ? exports.default : exports;

this.initialConfig = typeof userExport === 'function' ? await userExport(merge, configEnv) : userExport。这句就是 config/index.ts 支持函数也支持对象的实现。

接着我们来看 Config 类的实例上的 initGlobalConfig 方法。

6.2.2 config.initGlobalConfig 初始化全局配置

读取配置 ~/.taro-global-config/index.json

{
    "plugins": [],
    "presets": []
}
initGlobalConfig () {
    const homedir = getUserHomeDir()
    if (!homedir) return console.error('获取不到用户 home 路径')
    const globalPluginConfigPath = path.join(getUserHomeDir(), TARO_GLOBAL_CONFIG_DIR, TARO_GLOBAL_CONFIG_FILE)
    if (!fs.existsSync(globalPluginConfigPath)) return
    const spinner = ora(`开始获取 taro 全局配置文件: ${globalPluginConfigPath}`).start()
    try {
      this.initialGlobalConfig = fs.readJSONSync(globalPluginConfigPath) || {}
      spinner.succeed('获取 taro 全局配置成功')
    } catch (e) {
      spinner.stop()
      console.warn(`获取全局配置失败,如果需要启用全局插件请查看配置文件: ${globalPluginConfigPath} `)
    }
  }

getUserHomeDir 函数主要是获取用户的主页路径。比如 mac 中是 /Users/用户名/
如果支持 os.homedir() 直接获取返回,如果不支持则根据各种操作系统和环境变量判断获取。

ora 是控制台的 loading 小动画。

优雅的终端旋转器

这里的是 fs@tarojs/helper

Taro 编译时工具库,主要供 CLI、编译器插件使用。

导出的 fs-extra

fs-extra 添加本机模块中未包含的文件系统方法 fs,并为这些方法添加承诺支持 fs。它还用于 graceful-fs 防止 EMFILE 错误。它应该是 的替代品 fs。

使用 fs.readJSONSync 同步读取 json 的方法。

文档中也有对这个全局参数的描述。

全局插件或插件集配置

global-config.png

Config 部分我们基本分析完成,接下来我们学习 Kernel (内核)部分。

7. Kernel (内核)

// packages/taro-cli/src/cli.ts

// 省略若干代码
const kernel = new Kernel({
    appPath,
    presets: [path.resolve(__dirname, ".", "presets", "index.js")],
    config,
    plugins: [],
});
kernel.optsPlugins ||= [];

接着我们来看 Kernel 类, Kernel 类继承自 Nodejs 的事件模块EventEmitter

// packages/taro-service/src/Kernel.ts
export default class Kernel extends EventEmitter {
    constructor(options: IKernelOptions) {
        super();
        this.debugger =
            process.env.DEBUG === "Taro:Kernel"
                ? helper.createDebug("Taro:Kernel")
                : function () {};
        // taro 项目路径
        this.appPath = options.appPath || process.cwd();
        // 预设插件集合
        this.optsPresets = options.presets;
        // 插件
        this.optsPlugins = options.plugins;
        // 配置
        this.config = options.config;
        // 钩子,Map 存储
        this.hooks = new Map();
        // 存储方法
        this.methods = new Map();
        // 存储命令
        this.commands = new Map();
        // 存储平台
        this.platforms = new Map();
        this.initHelper();
        this.initConfig();
        this.initPaths();
        this.initRunnerUtils();
    }
}
// packages/taro-helper/src/index.ts
export const createDebug = (id: string) => require("debug")(id);

this.debugger 当没有配置 DEBUG 环境变量时,则 debugger 是空函数。配置了 process.env.DEBUG === "Taro:Kernel" 为则调用的 npmdebug

一个仿照 Node.js 核心调试技术的微型 JavaScript 调试实用程序。适用于 Node.jsWeb 浏览器。

我们接着看构造器函数里调用的几个初始化函数,基本都是顾名知义。

// packages/taro-service/src/Kernel.ts
initConfig () {
    this.initialConfig = this.config.initialConfig
    this.initialGlobalConfig = this.config.initialGlobalConfig
    this.debugger('initConfig', this.initialConfig)
}

initHelper () {
    this.helper = helper
    this.debugger('initHelper')
}

initRunnerUtils () {
    this.runnerUtils = runnerUtils
    this.debugger('initRunnerUtils')
}
// packages/taro-service/src/Kernel.ts
initPaths () {
    this.paths = {
        appPath: this.appPath,
        nodeModulesPath: helper.recursiveFindNodeModules(path.join(this.appPath, helper.NODE_MODULES))
    } as IPaths
    if (this.config.isInitSuccess) {
        Object.assign(this.paths, {
        configPath: this.config.configPath,
        sourcePath: path.join(this.appPath, this.initialConfig.sourceRoot as string),
        outputPath: path.resolve(this.appPath, this.initialConfig.outputRoot as string)
        })
    }
    this.debugger(`initPaths:${JSON.stringify(this.paths, null, 2)}`)
}

初始化后的参数,如 taro 官方文档 – 编写插件 api中所示。

initConfig.png

7.1 cli kernel.optsPlugins 等

parseArgs-3.png

我们接下来看,customCommand 函数。

7.2 cli customCommand 函数

parseArgs-4.png

我们可以看到最终调用的是 customCommand 函数

// packages/taro-cli/src/commands/customCommand.ts
import { Kernel } from "@tarojs/service";

export default function customCommand(
    command: string,
    kernel: Kernel,
    args: { _: string[]; [key: string]: any }
) {
    if (typeof command === "string") {
        const options: any = {};
        const excludeKeys = [
            "_",
            "version",
            "v",
            "help",
            "h",
            "disable-global-config",
        ];
        Object.keys(args).forEach((key) => {
            if (!excludeKeys.includes(key)) {
                options[key] = args[key];
            }
        });

        kernel.run({
            name: command,
            opts: {
                _: args._,
                options,
                isHelp: args.h,
            },
        });
    }
}

customCommand 函数移除一些 run 函数不需要的参数,最终调用的是 kernal.run 函数。

接下来,我们来看 kernal.run 函数的具体实现。

8. kernal.run 执行函数

// packages/taro-service/src/Kernel.ts
async run (args: string | { name: string, opts?: any }) {
    // 上半部分
    let name
    let opts
    if (typeof args === 'string') {
      name = args
    } else {
      name = args.name
      opts = args.opts
    }
    this.debugger('command:run')
    this.debugger(`command:run:name:${name}`)
    this.debugger('command:runOpts')
    this.debugger(`command:runOpts:${JSON.stringify(opts, null, 2)}`)
    this.setRunOpts(opts)
    // 拆解下半部分
}

run 函数中,开头主要是兼容两种参数传递。

9. kernal.setRunOpts

把参数先存起来。便于给插件使用。

// packages/taro-service/src/Kernel.ts
setRunOpts (opts) {
    this.runOpts = opts
}

Taro 文档 – 编写插件 – ctx.runOpts

ctx.runOpts.png

我们接着来看,run 函数的下半部分。

// packages/taro-service/src/Kernel.ts
async run (args: string | { name: string, opts?: any }) {
    // 下半部分
    this.debugger('initPresetsAndPlugins')
    this.initPresetsAndPlugins()

    await this.applyPlugins('onReady')

    this.debugger('command:onStart')
    await this.applyPlugins('onStart')

    if (!this.commands.has(name)) {
      throw new Error(`${name} 命令不存在`)
    }

    if (opts?.isHelp) {
      return this.runHelp(name)
    }

    if (opts?.options?.platform) {
      opts.config = this.runWithPlatform(opts.options.platform)
      await this.applyPlugins({
        name: 'modifyRunnerOpts',
        opts: {
          opts: opts?.config
        }
      })
    }

    await this.applyPlugins({
      name,
      opts
    })
}

run 函数下半部分主要有三个函数:

1. this.initPresetsAndPlugins() 函数,顾名知义。初始化预设插件集合和插件。
2. this.applyPlugins() 执行插件
3. this.runHelp() 执行 命令行的帮助信息,例:taro init --help

我们分开叙述

this.initPresetsAndPlugins()函数,因为此处涉及到的代码相对较多,容易影响主线流程。所以本文在此先不展开深入学习了。将放在下一篇文章中详细讲述。

执行 this.initPresetsAndPlugins() 函数之后。我们完全可以在调试时把 kernal 实例对象打印出来。

我们来看插件的注册。

10. kernal ctx.registerCommand 注册 init 命令

// packages/taro-cli/src/presets/commands/init.ts
import type { IPluginContext } from "@tarojs/service";

export default (ctx: IPluginContext) => {
    ctx.registerCommand({
        name: "init",
        optionsMap: {
            "--name [name]": "项目名称",
            "--description [description]": "项目介绍",
            "--typescript": "使用TypeScript",
            "--npm [npm]": "包管理工具",
            "--template-source [templateSource]": "项目模板源",
            "--clone [clone]": "拉取远程模板时使用git clone",
            "--template [template]": "项目模板",
            "--css [css]": "CSS预处理器(sass/less/stylus/none)",
            "-h, --help": "output usage information",
        },
        async fn(opts) {
            // init project
            const { appPath } = ctx.paths;
            const { options } = opts;
            const {
                // 省略若干参数
            } = options;
            const Project = require("../../create/project").default;
            console.log(Project, "Project");
            const project = new Project({
                projectName,
                projectDir: appPath,
                // 省略若干参数
            });

            project.create();
        },
    });
};

通过 ctx.registerCommand 注册了一个 nameinit 的命令,会存入到内核 Kernal 实例对象的 hooks 属性中,其中 ctx 就是 Kernal 的实例对象。具体实现是 fn 函数。

11. kernal.applyPlugins 触发插件

// packages/taro-service/src/Kernel.ts
async applyPlugins (args: string | { name: string, initialVal?: any, opts?: any }) {
    // 上半部分
    let name
    let initialVal
    let opts
    if (typeof args === 'string') {
      name = args
    } else {
      name = args.name
      initialVal = args.initialVal
      opts = args.opts
    }
    this.debugger('applyPlugins')
    this.debugger(`applyPlugins:name:${name}`)
    this.debugger(`applyPlugins:initialVal:${initialVal}`)
    this.debugger(`applyPlugins:opts:${opts}`)
    if (typeof name !== 'string') {
      throw new Error('调用失败,未传入正确的名称!')
    }
    // 拆解到下半部分
}

上半部分,主要是适配两种传参的方式。

// packages/taro-service/src/Kernel.ts
async applyPlugins (args: string | { name: string, initialVal?: any, opts?: any }) {
    // 下半部分
    const hooks = this.hooks.get(name) || []
    if (!hooks.length) {
      return await initialVal
    }
    const waterfall = new AsyncSeriesWaterfallHook(['arg'])
    if (hooks.length) {
      const resArr: any[] = []
      for (const hook of hooks) {
        waterfall.tapPromise({
          name: hook.plugin!,
          stage: hook.stage || 0,
          // @ts-ignore
          before: hook.before
        }, async arg => {
          const res = await hook.fn(opts, arg)
          if (IS_MODIFY_HOOK.test(name) && IS_EVENT_HOOK.test(name)) {
            return res
          }
          if (IS_ADD_HOOK.test(name)) {
            resArr.push(res)
            return resArr
          }
          return null
        })
      }
    }
    return await waterfall.promise(initialVal)
}

Taro 的插件架构基于 Tapable

这里使用了这个函数:AsyncSeriesWaterfallHook

The hook type is reflected in its class name. E.g., AsyncSeriesWaterfallHook allows asynchronous functions and runs them in series, passing each function’s return value into the next function.

简言之就是异步或者同步方法串联起来,上一个函数的结果作为下一个函数的参数依次执行。依次执行。

这时让我想起一句小虎队的爱的歌词。

喔,把你的心我的心串一串,串一株幸运草串一个同心圆…

举个例子用户写的插件中有多个钩子函数。比如 onReday 等可以有多个。

插件方法.png

插件 hooks

applyPlugins 根据执行的命令 inithooks 取出,串起来,然后依次执行插件的 fn 方法。

我们顺便来看一下,kernal.runHelp 的实现。

12. kernal.runHelp 命令帮助信息

kernal.run 函数中,有一个 opts.isHelp 的判断,执行 kernal.runHelp 方法。

// packages/taro-service/src/Kernel.ts
// run 函数
if (opts?.isHelp) {
    return this.runHelp(name);
}

taro init --help 为例。输出结果如下图所示:

命令行 help.png

具体实现代码如下:

// packages/taro-service/src/Kernel.ts
runHelp (name: string) {
    const command = this.commands.get(name)
    const defaultOptionsMap = new Map()
    defaultOptionsMap.set('-h, --help', 'output usage information')
    let customOptionsMap = new Map()
    if (command?.optionsMap) {
      customOptionsMap = new Map(Object.entries(command?.optionsMap))
    }
    const optionsMap = new Map([...customOptionsMap, ...defaultOptionsMap])
    printHelpLog(name, optionsMap, command?.synopsisList ? new Set(command?.synopsisList) : new Set())
}

根据 namethis.commands Map 中获取到命令,输出对应的 optionsMapsynopsisList

13. 总结

我们主要学了

  1. 学会通过两种方式调试 taro 源码
  2. 学会入口 taro-cli 具体实现方式
  3. 学会 cli init 命令实现原理,读取用户项目配置文件和用户全局配置文件
  4. 学会 taro-service kernal (内核)解耦实现
  5. 初步学会 taro 插件架构,学会了如何编写一个 taro 插件

taro-cli 使用了minimist,命令行参数解析工具。

使用了 @swc/register 读取 config/index .js 或者 .ts 配置文件和用 fs-extra fs.readJSONSync 全局配置文件。

CLI 部分有各种预设插件集合 presets

taro 单独抽离了一个 tarojs/service (packages/taro-service) 模块,包含 Kernal 内核、ConfigPlugin 等。

taro 的基于 TapableAsyncSeriesWaterfallHook (把函数组合在一起串行) 实现的插件机制。各个插件可以分开在各个地方,达到解耦效果。非常值得我们学习。

简单做了一个本文的总结图。

简单总结


如果看完有收获,欢迎点赞、评论、分享、收藏支持。你的支持和肯定,是我写作的动力

作者:常以若川为名混迹于江湖。所知甚少,唯善学。若川的博客github blog,可以点个 star 鼓励下持续创作。

最后可以持续关注我@若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02 参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。

https://segmentfault.com/a/1190000045912572

未经允许不得转载:IT极限技术分享汇 » Taro 源码揭秘:1. 揭开整个架构的入口 CLI => taro init 初始化项目的秘密

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址