Taro 是一个开放源代码的多端统一开发框架,旨在帮助开发者通过一次编码实现多端应用(如微信小程序、支付宝小程序、H5、React Native 等)的开发。为了更好地理解 Taro 的源码,这里将简要介绍其核心原理和主要组成部分。
核心原理
Taro 的核心原理是通过编译器将代码转换为不同平台的代码。开发者可以使用 React 语法编写代码,然后通过 Taro 编译工具将其转换为目标平台的代码。
项目分析
首先,我们需要clone
相应的仓库源代码,并查看贡献指南。在指南中详细说明了安装依赖的步骤,并提供了单点调试的操作方法。
接下来,让我们浏览一下目录结构,在packages
文件夹里有许多包,如何开始分析呢?
实际上,官方文档已经提供了对仓库的概述。
路径 | 描述 |
---|---|
@tarojs/cli | CLI 工具 |
@tarojs/service | 插件化内核 |
@tarojs/taro-loader | Webpack loaders |
@tarojs/helper | 工具库,主要供 CLI、编译时使用 |
@tarojs/runner-utils | 工具库,主要供小程序、H5 的编译工具使用 |
@tarojs/shared | 工具库,主要供运行时使用 |
@tarojs/taro | 暴露各端所需要的 Taro 对象 |
@tarojs/api | 和各端相关的 Taro API |
babel-preset-taro | Babel preset |
eslint-config-taro | ESLint 规则 |
postcss-pxtransform | PostCSS 插件,转换 px 为各端的自适应尺寸单位 |
postcss-html-transform | PostCSS 插件,用于 HTML、小程序标签的类名相互转换 |
断点调试
文档中有详细的断点调式方法,这里列出简要步骤
- 首先我们安装依赖并编译项目
pnpm i
pnpm build
- 打开
.vscode/launch.json
- 这里以编译微信小程序为例
examples/mini-program-example
- 修改命令
- 打上断点
- 因为有
sourceMap
,我们会调到源码中
- 因为有
// lauch.json
{
"configurations": [
{
// ...
"cwd": "这是设置成将要调试的项目 examples/mini-program-example",
"args": ["build", "--type", "weapp", "--watch"]
}
]
}
执行了这行命令是做什么,内部执行了什么操作?
读取环境变量
首先,创建了一个CLI
对象,这个对象里的run
函数只做了一件事,解析参数。
我们进入函数看看。
首先,会获取当前的环境变量,根据传入的参数,去读对应的配置文件。
创建内核
然后,创建一个Kernal
内核,那么这个内核是做什么的呢。我们等下再看。
创建完内核,将不同的插件
传递给kernal
。
这里有一个疑问,插件是什么,为什么要以插件的形式传递给内核?
我们在创建Kernal
的时候,打上断点,调到packages/taro-service/src/Kernel.ts
中。
export default class Kernel extends EventEmitter {
// ...
}
这个类继承自EventEmitter
,应该是用来注册和监听事件的功能。
回到cli.ts
,我们看下传递完参数,执行了kernal
的什么方法。
插件注册
- 执行了
kernel.run
// packages/taro-cli/src/commands/customCommand.ts
export default function customCommand (
command: string,
kernel: Kernel,
args: { _: string[], [key: string]: any }
) {
if (typeof command === 'string') {
// 省略代码...
// 执行了 run 函数
kernel.run({
name: command,
opts: {
_: args._,
options,
isHelp: args.h
}
})
}
}
接下来,看下run
函数的
async run(args) {
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);
// 初始化预设和插件
this.debugger('initPresetsAndPlugins');
this.initPresetsAndPlugins();
// 执行 'onReady' 插件
await this.applyPlugins('onReady');
// 记录调试信息并执行 'onStart' 插件
this.debugger('command:onStart');
await this.applyPlugins('onStart');
// 检查命令是否存在
if (!this.commands.has(name)) {
throw new Error(`${name} 命令不存在`);
}
// 如果选项中包含 isHelp 属性,则运行帮助命令
if (opts?.isHelp) {
return this.runHelp(name);
}
// 如果选项中包含平台信息,则处理平台选项
if (opts?.options?.platform) {
// 根据平台运行并获取配置
opts.config = this.runWithPlatform(opts.options.platform);
// 执行 'modifyRunnerOpts' 插件,并传入更新后的选项
await this.applyPlugins({
name: 'modifyRunnerOpts',
opts: {
opts: opts?.config
}
});
}
// 执行命令对应的插件
await this.applyPlugins({
name,
opts
});
}
我们重点关注的是这句代码:this.initPresetsAndPlugins();
这个函数主要做了以下功能
- 获取初始配置和全局配置。
- 合并命令行选项和项目配置中的插件和预设。
- 将全局配置中的插件和预设转换为对象。
- 在非测试环境中创建一个 swc 注册器,只包括合并后的插件和预设。
- 初始化 plugins 为一个空的 Map 对象。
- 初始化额外插件的配置对象。
- 解析合并后的插件和预设。
initPresetsAndPlugins () {
const initialConfig = this.initialConfig
const initialGlobalConfig = this.initialGlobalConfig
// 合并presets
const cliAndProjectConfigPresets = mergePlugins(this.optsPresets || [], initialConfig.presets || [])()
const cliAndProjectPlugins = mergePlugins(this.optsPlugins || [], initialConfig.plugins || [])()
// 将全局配置中的 plugins 转换为对象
const globalPlugins = convertPluginsToObject(initialGlobalConfig.plugins || [])()
// 将全局配置中的 presets 转换为对象
const globalPresets = convertPluginsToObject(initialGlobalConfig.presets || [])()
// 调试信息:输出初始化的 presets 和 plugins 配置
this.debugger('initPresetsAndPlugins', cliAndProjectConfigPresets, cliAndProjectPlugins)
this.debugger('globalPresetsAndPlugins', globalPlugins, globalPresets)
// 如果当前环境不是测试环境,则创建 swc 注册器
// https://www.npmjs.com/package/@swc/core
// rust 编写
// 编译速度更快 对应的性能测试
// https://docs.taro.zone/en/blog/2022/05/19/Taro-3.5-beta/#3-%E6%8F%90%E9%80%9F%E6%95%88%E6%9E%9C
// https://swc.rs/docs/benchmarks
process.env.NODE_ENV !== 'test' &&
helper.createSwcRegister({
only: [
// 仅包括所有合并后的 plugins 和 presets 的键
...Object.keys(cliAndProjectConfigPresets),
...Object.keys(cliAndProjectPlugins),
...Object.keys(globalPresets),
...Object.keys(globalPlugins)
]
})
// 初始化 plugins 为一个 Map 对象
this.plugins = new Map()
// 初始化额外的插件配置对象
this.extraPlugins = {}
this.globalExtraPlugins = {}
// 解析合并后的 presets
this.resolvePresets(cliAndProjectConfigPresets, globalPresets)
// 解析合并后的 plugins
this.resolvePlugins(cliAndProjectPlugins, globalPlugins)
}
这一步是将配置中的预设和插件收集起来,等待交给下一步操作
那这些插件的作用是什么? 怎么编写这些插件 (下一章节) docs.taro.zone/docs/plugin…
我们在文档中可以得知插件的固定写法
export default (ctx, options) => {
// plugin 主体
ctx.onBuildStart(() => {
console.log('编译开始!')
})
ctx.onBuildFinish(() => {
console.log('Webpack 编译结束!')
})
ctx.onBuildComplete(() => {
console.log('Taro 构建完成!')
})
}
插件在编译过程中注入需要执行的函数,以拓展编译器的功能,例如代码转换、代码检查、文件处理和服务器上传。
插件执行
接下来,我们看下怎么执行这些插件的
在这里,使用了AsyncSeriesWaterfallHook方法来创建异步流水线,该方法可以按顺序执行插件。
你可以在github.com/webpack/tap…找到这个库,它用于创建执行的流水线。
总结
通过上述调试,我们已了解Taro CLI
的功能及项目调试方法,并学会了如何配置launch.json
。
目前,我们已成功解决以下问题:
当执行 taro build --type weapp,Taro做了什么
- 读取环境配置,设置变量(dotenv)
- 创建 kernel,执行 kernel.run
- 通过创建一个 Map 来存储 Plugins 和 Presets
- 通过创建一个流水线来实现插件的顺序执行
Taro源码中怎么实现提高编译速度
- 使用了 swc 代替原来的 babel 来完成编译
Taro中的插件怎么实现按顺序执行
- 使用了tapable库来创建流水线,实现了按顺序执行插件