1. 前言
大家好,我是若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。
截至目前(2024-07-17
),taro
正式版是 3.6.34
,Taro 4.0 Beta 发布:支持开发鸿蒙应用、小程序编译模式、Vite 编译等。文章提到将于 2024 年第二季度,发布 4.x
。目前已经发布 4.x
。所以我们直接学习 main
分支最新版本是 4.0.2
。
多编译内核生态下的极速研发体验 官方博客有如下图。
计划写一个 Taro 源码揭秘系列,博客地址:https://ruochuan12.github.io/taro 可以加入书签,持续关注若川。
- [x] 1. 揭开整个架构的入口 CLI => taro init 初始化项目的秘密
- [x] 2. 揭开整个架构的插件系统的秘密
- [x] 3. 每次创建新的 taro 项目(taro init)的背后原理是什么
- [x] 4. 每次 npm run dev:weapp 开发小程序,build 编译打包是如何实现的?
- [x] 5. 高手都在用的发布订阅机制 Events 在 Taro 中是如何实现的?
- [x] 6. 为什么通过 Taro.xxx 能调用各个小程序平台的 API,如何设计实现的?
- [x] 7. Taro.request 和请求响应拦截器是如何实现的
- [x] 8. Taro 是如何使用 webpack 打包构建小程序的?
- [x] 9. Taro 是如何生成 webpack 配置进行构建小程序的?
- [ ] 等等
学完本文,你将学到:
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
安装依赖可能会报错。
Failed to set up Chromium r1108766! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.
通过谷歌等搜索引擎可以找到解决方法。
Mac : export PUPPETEER_SKIP_DOWNLOAD='true'
Windows: SET PUPPETEER_SKIP_DOWNLOAD='true'
pnpm build 完成,如下图所示:
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
命令作为示例。
如下图所示:
也可以使用项目中提供的测试用例 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
。如下图所示:
运行等过程报错,不要慌。可能是我们遗漏了一些细节,贡献文档等应该会给出答案。所以再来看下 贡献文档-10-rust-部分
通过 rustup 找到安装命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装完成后,执行 pnpm run build:binding:debug
或 pnpm 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"
}
]
}
console- 启动程序的控制台(internalConsole,integratedTerminal,externalTerminal)。
// 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
工作目录),两个函数 run
和 parseArgs
。
// 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-cli
、webpack-cli
和 create-react-app
用的是这个。
cac,类似 Commander.js
但更轻巧、现代,支持插件。也有很多使用这个cac npm,比如vite
使用的是这个。
yargs,交互式命令行工具。功能强大的框架,但显得过于臃肿。
cli.run
函数最终调用的是 cli.parseArgs
函数。我们接着来看 parseArgs
函数。
6. cli parseArgs
6.1 presets 预设插件集合
presets
对应的目录结构如图所示:
6.2 Config
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
函数简单来说就是通过 dotenv 和 dotenv-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
构造函数有两个属性。appPath
是 taro
项目路径。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
使用了 @swc/register
来编译 ts
等转换成 commonjs
。可以直接用 require
。
使用 swc 的方法之一是通过
require
钩子。require
钩子会将自身绑定到node
的require
并自动动态编译文件。不过现在更推荐 @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
的方法。
文档中也有对这个全局参数的描述。
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"
为则调用的 npm
包 debug。
一个仿照
Node.js
核心调试技术的微型JavaScript
调试实用程序。适用于Node.js
和Web
浏览器。
我们接着看构造器函数里调用的几个初始化函数,基本都是顾名知义。
// 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中所示。
7.1 cli kernel.optsPlugins 等
我们接下来看,customCommand
函数。
7.2 cli customCommand 函数
我们可以看到最终调用的是 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
}
我们接着来看,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
注册了一个 name
为 init
的命令,会存入到内核 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
等可以有多个。
applyPlugins
根据执行的命令 init
从 hooks
取出,串起来,然后依次执行插件的 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
为例。输出结果如下图所示:
具体实现代码如下:
// 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())
}
根据 name
从 this.commands
Map
中获取到命令,输出对应的 optionsMap
和 synopsisList
。
13. 总结
我们主要学了
- 学会通过两种方式调试 taro 源码
- 学会入口 taro-cli 具体实现方式
- 学会 cli init 命令实现原理,读取用户项目配置文件和用户全局配置文件
- 学会 taro-service kernal (内核)解耦实现
- 初步学会 taro 插件架构,学会了如何编写一个 taro 插件
taro-cli 使用了minimist,命令行参数解析工具。
使用了 @swc/register
读取 config/index .js 或者 .ts 配置文件和用 fs-extra fs.readJSONSync 全局配置文件。
CLI 部分有各种预设插件集合 presets
。
taro 单独抽离了一个 tarojs/service
(packages/taro-service
) 模块,包含 Kernal
内核、Config
、Plugin
等。
taro 的基于 Tapable 的 AsyncSeriesWaterfallHook
(把函数组合在一起串行) 实现的插件机制。各个插件可以分开在各个地方,达到解耦效果。非常值得我们学习。
简单做了一个本文的总结图。
如果看完有收获,欢迎点赞、评论、分享、收藏支持。你的支持和肯定,是我写作的动力。
作者:常以若川为名混迹于江湖。所知甚少,唯善学。若川的博客,github blog,可以点个 star
鼓励下持续创作。
最后可以持续关注我@若川,欢迎关注我的公众号:若川视野。我倾力持续组织了 3 年多每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。