10 — CSS / JS 转化「引擎」的设计抽象(非流程清单)
本文回答:除了「先 A 再 B」的流程,Taro 在 设计层有没有一套可类比 编译器 / 浏览器引擎 的分层与用语。
结论:Taro 不是 V8、也不是 Blink 的缩小版——没有单一字节码 IR、没有统一布局树;但它有 可明确命名的阶段、中间表示、Lowering 与多后端,足以构成一套 内部设计语言。
具体插件顺序仍以 02、05 为准;本文给 心智模型。
1. 定位:和 V8 / Blink 类比时的边界
| 概念 | V8 / Blink | Taro |
|---|---|---|
| 单一运行时 VM | 有(Ignition/TurboFan 等) | 无;宿主是 微信/支付宝/浏览器/RN |
| 统一 IR | 字节码、布局树等 | 无官方单一 IR 文档;实际是 多条 IR 叠在一起(见下) |
| 管道思维 | 词法→语法→…→执行 | 适用:可描述为 多阶段 Lowering |
| 后端 | 机器码 / GPU | 各端产物:wxml 模板协议、wxss、JS chunk、RN bundle |
设计断言:Taro 的核心设计不是「再造 VM」,而是 在宿主约束下做 Source →(若干中间层)→ Host Artifact 的翻译,并固定 逻辑层与视图层之间的数据协议(hydrate 形状、Shortcuts、模板 Adapter)。
2. JS 转化:分层模型(引擎视角)
把 JS/TS 从源码到小程序可跑 看成一条 纵向流水线,每层有 输入/输出形态(即「类 IR」)。
flowchart LR
subgraph p1 [阶段 I 语义改写]
SRC[源码 TS/JSX]
AST[Babel AST]
SRC --> AST
end
subgraph p2 [阶段 II 模块图]
MG[ModuleGraph / ChunkGraph]
end
subgraph p3 [阶段 III 产物Lowering]
OUT[JS Chunks + JSON 配置]
end
subgraph p4 [阶段 IV 逻辑层执行]
DOM[TaroElement 树]
end
subgraph p5 [阶段 V 视图数据发射]
DATA[MiniData / setData 载荷]
end
AST --> MG
MG --> OUT
OUT --> DOM
DOM --> DATA2.1 阶段 I — 语义改写(Babel 为前端)
输入:用户源码。
输出:仍属 JS/TS,但 语义已对准 Taro 契约(API 形态、动态 import 策略、RN 与小程序分支等)。
设计角色:相当于编译器的 Semantic Lowering(面向宿主的第一轮)。
典型变换族(非穷尽):
- 环境常量:
p/rocess.env.TARO_ENV TARO_PLATFORM,使死分支可折叠(DefinePlugin /babel-plugin-global-define)。 - API 形态:
babel-plugin-transform-taroapi将Taro.xxx转为 可按需引入 的 import,服务 Tree Shaking(设计目标:体积与静态可分析性)。 - 小程序 + React:
transform-taro-components等对 热点组件模式 做 AST 级改写(设计目标:与模板/运行时假设对齐)。 - 非 Web:
babel-plugin-dynamic-import-node(设计目标:与小程序加载模型一致)。 - RN:整包换用
babel-preset-taro/rn+ Metro preset +transform-imports-api(设计目标:RN 解析路径与包体)。
不变量(设计层):Babel 阶段 不生成 wxml;只保证 后续 Bundler + Runtime 看到的 JS 符合契约。
2.2 阶段 II — 模块图 IR(Webpack / Vite / Metro)
输入:ESM/CJS 模块节点。
输出:带依赖边的图 + chunk 划分策略(主包、分包、runtime、vendors、taro、独立子编译器等)。
设计角色:类比 链接期 IR——决定 符号解析、代码分割、副作用边界。
Taro 特有设计:
MiniPlugin在图上 注入 entry、子 Compiler 拆 独立分包图(与宿主「分包」概念同构)。- Prebundle 先 重写依赖图 再进主编译(设计目标:npm → 小程序可承受形态)。
不变量:此阶段 仍不直接产出视图模板;模板由 另一引擎(模板合成) 在资源阶段写出。
2.3 阶段 III — 产物 Lowering(Emit JS Artifact)
输入:chunk 图。
输出:.js 文件 + app.json / 页面 json 等(由插件扫描配置生成)。
设计角色:Codegen to host filesystem——与 V8 机器码 codegen 不同,这里是 宿主文件布局。
2.4 阶段 IV — 逻辑层「执行模型」(运行时 IR)
输入:已加载的 JS。
输出:TaroElement / TaroText 树(可视为 逻辑层 DOM IR)。
设计角色:React 的 Fiber 操作的是 这棵 IR;它不是浏览器 DOM,但 接口形状刻意对齐 DOM,以便 同一套 Reconciler 算法。
不变量:Reconciler 不调用宿主视图 API;只操作 Taro 抽象树。
2.5 阶段 V — 视图数据发射(View Serialization IR)
输入:TaroElement 树。
输出:MiniData(短键 nn/cn/st/cl/...)——可称为 setData 协议 IR。
实现:hydrate + enqueueUpdate 路径合并。
设计角色:类似 把布局/渲染树序列化为远程指令——小程序 视图层只认 data,故这是 强制的后端接口。
与模板的关系:模板是 静态「解释器」;MiniData 是 动态操作数。二者 协议锁定 在 shared 的模板生成与 Shortcuts 上。
2.6 babel-plugin-transform-taroapi:API 形态规范化 + 编译期折叠(阶段 I 的实例)
本节把 babel-plugin-transform-taroapi 从「又一个 Babel 插件」收束为 阶段 I 里可命名的一种变换策略,并说明 设计意图 与 和兄弟插件的分工。
2.6.1 设计目标(为什么要有它)
Tree Shaking / 显式依赖边
业务常写import Taro from '@tarojs/taro'再Taro.getStorage(),bundler 难以证明只用了哪些 API。插件把 已声明支持的 API 变成import { getStorage as _getStorage } from '@tarojs/taro'这类 具名导入,让 模块图的边可见,利于摇树与按需打包。未纳入「白名单」的 API 仍可用
对 不在definition.apis集合中的具名 import,不把其从默认导出里拆出来,而是把引用 改写成Taro.xxx,并保留 默认导入——兼顾 渐进支持 与 不破坏未建模 API。canIUse编译期常量化
在 构建时根据平台给出的 能力描述 JSON(definition),把Taro.canIUse('scheme')折叠为布尔字面量,后续可由 minifier 做 死代码消除(等价于受限的 特性检测下沉到编译期)。H5 无障碍属性表面语法
仅在TARO_ENV === 'h5'时,把 JSX 上的ariaXxx映射为 DOM 规范的aria-*(DEFAULT_ATTRIBUTE_MAP),属于 Lowering 到 Web 宿主 的细枝末节。
接入方式(示例):H5 平台在 modifyWebpackChain 里往 babel-loader 的 plugins 追加该插件,并传入 packageName: '@tarojs/taro'(或实际 alias 后的实现包)与 definition: require(libraryDefinition)(聚合了 apis/components 的描述)(packages/taro-platform-h5/src/program.ts)。
2.6.2 执行流程(两遍扫描 + 退出期收口)
可理解为 收集 → 改写 → 收口:
| 阶段 | 钩子 | 行为摘要 |
|---|---|---|
| 重置 | Program.enter | 清空 invokedApis、重置 needDefault / referTaro;若作用域已有 Taro 绑定则 生成 UID 避免冲突 |
| 收集/拆解 | ImportDeclaration | 仅处理 source === packageName。默认导入记下 taroName;具名导入若在 apis 白名单 内则 rename 本地绑定 并登记 invokedApis;否则把 所有引用 改为 taroName.xxx 并标记需要默认导入 |
| 成员访问 | MemberExpression | Taro.xxx 且 xxx 在白名单:在非赋值左值场景下 替换为已登记的局部标识符(多次使用共享同一 UID);否则标记 needDefault |
| 调用 | CallExpression | 识别 canIUse(含 Taro.canIUse 与 rename 后的直接调用),调用 replaceCanIUse |
| 收口 | Program.exit | 内层 traverse:合并/去重对 packageName 的 import,重写为 默认 + 具名 或 仅具名;再次扫 CallExpression 处理 rename 后的 canIUse |
核心实现见 packages/babel-plugin-transform-taroapi/src/index.ts。
2.6.3 canIUse 的「定义驱动」匹配
definition 合并 apis 与 components,并对 canIUse 键置为 '*' 表示通配。replaceCanIUse 把字符串 scheme 写成嵌套对象路径(lodash/setWith),再用 isMatchWith 与 definition 做子树匹配——* 与 true 作为通配,从而在 编译期得到 确定布尔值:
function canIUse (definition, scheme = '') {
if (!scheme) return false
const o = setWith({}, scheme, true, Object)
return isMatchWith(definition, o, (a, b) => {
if (a === '*' || b === true) return true
})
}
function replaceCanIUse (ast: BabelCore.NodePath<BabelCore.types.CallExpression>, definition) {
const args = ast.node.arguments
if (args.length < 1) return
// Note: 暂不考虑其他类型的参数映射
if (t.isStringLiteral(args[0])) {
const isSupported = canIUse(definition, args[0].value)
ast.replaceInline(t.booleanLiteral(isSupported))
}
}设计取舍:注释写明 暂只处理字符串字面量参数——避免在编译期做复杂求值,换 可预测、可测试 的行为。
2.6.4 与其它 Transform 的分工(同阶段 I 家族)
| 插件 | 关注点 | 与 taroapi 关系 |
|---|---|---|
babel-plugin-transform-taroapi | Taro 命名空间 / 具名 API 导入图、canIUse 折叠、H5 aria | 模块边界与摇树 |
babel-preset-taro 内 transform-taro-components | 小程序 + React 下 部分内置组件 AST 优化 | 组件用法模式,不改 Taro import 形态 |
babel-plugin-transform-taroapi(RN 链外的 preset) | RN 用 babel-preset-taro/rn + transform-imports-api 指向 taro-rn 路径 | 另一套按需解析策略,与 H5 的 taroapi 并行存在 |
同一「阶段 I」,可按 端 挂 不同插件组合,符合本文 多后端 表述。
2.6.5 值得借鉴的设计点(通用工程)
- 数据驱动变换:用
definitionJSON 描述「支持哪些 API / 组件能力」,插件只做 通用遍历与替换,新增能力往往 改数据不改插件核心逻辑。 - 两遍法避免顺序坑:先在 enter~member 阶段 收集符号与需求,在 Program.exit 统一 生成 import,减少「先删 import 后找不到绑定」类问题。
- 白名单 + 降级路径:白名单内 精细化(具名 import);白名单外 降级为默认导出成员访问,避免 过度 transform 导致行为变化。
- 编译期常量折叠的边界:只对 静态可证 的子集(如
canIUse('literal'))下手,不把任意表达式都求值——在 正确性 与 收益 间划线。 - 平台条件分支:
JSXAttribute仅 H5 执行,体现 同一 AST 管道、环境门控不同 Lowering——与 DefinePlugin /TARO_ENV一致的思维。 - 可测试性:
packages/babel-plugin-transform-taroapi/tests/用 snapshot + 结构断言锁定 import 形态与Taro.noop等边界,适合作为 自定义 Babel 插件 的范例。
3. CSS 转化:独立的「样式引擎」管道
CSS 在 Taro 里 不与 JS 共用一条 IR,而是 平行管道,最后在 data 与 wxss 两处 汇合。
flowchart TB
subgraph css_compile [编译期样式引擎]
CSS_SRC[scss/css/less]
PCSS[PostCSS AST 链]
WXSS[wxss / acss / ttss 等]
CSS_SRC --> PCSS --> WXSS
end
subgraph css_runtime [运行时内联样式]
STYLE[Style 对象]
CT[cssText 字符串]
ST[data.st]
STYLE --> CT --> ST
end
subgraph host [宿主]
VIEW[视图层]
end
WXSS --> VIEW
ST --> VIEW3.1 编译期:PostCSS 为「样式前端」
输入:样式源文件。
输出:宿主样式文件(或可被 mini-css-extract 挂到 chunk 的 CSS 串)。
阶段语义(设计层,非插件枚举):
- 聚合:
postcss-import(模块化样式图)。 - 规范化:autoprefixer 等(面向宿主能力子集 的降级,而非 W3C 全集)。
- Lowering(单位与尺寸):
postcss-pxtransform—— 从设计稿空间 → 宿主长度单位空间(rpx/rem/…)。 - 资源 Lowering:
postcss-url、alias —— URL → 可被打包引用的路径。 - 可选 HTML/模板侧规则:
postcss-html-transform(与 类名/选择器与模板生成假设 对齐时打开)。
不变量:PostCSS 链 不解析 JSX;类名字符串 由 JS 侧写到 className,选择器 在 wxss 里匹配——联结点是「字符串协议」,不是单一 AST。
3.2 RN 分支:第二套样式后端
设计决策:RN 没有 wxss,样式必须进入 JS 值域。
管道:rn-style-transformer + babel-plugin-transform-react-jsx-to-rn-stylesheet —— 把 类 CSS 声明 Lowering 为 StyleSheet / style 对象。
与小程序对比:同一业务 className,在小程序走 wxss + cl,在 RN 走 Babel 变换——属于 多后端(Multi-backend) 设计。
3.3 运行时:Style 作为「内联 CSSOM 子集 IR」
设计:不实现完整 CSSOM;只实现 CSSStyleDeclaration 行为子集(style_properties + initStyle)。
Lowering:Style._value → cssText 串 → …st 路径 → setData。
与编译期 wxss 汇合:cl 字符串 选 wxss 规则;st 字符串 覆盖内联——语义上接近浏览器的 级联的一小段(但 无完整层叠计算)。
4. 设计语言:可背的几条「原则」
- 双轨 IR:JS 模块图(Webpack)与 模板+数据协议(shared + runtime)正交;拼接点在 构建插件 与 hydrate。
- 逻辑 / 视图分离:Reconciler 只认 Taro DOM IR;宿主只认 模板 + MiniData。
- Lowering 分后端:同构业务语法,不同宿主不同 emit(wxml vs DOM vs RN)。
- 协议稳定优先:
Shortcuts、组件 alias、Adapter替换的是 「宿主语法表面」,底层 data 形状尽量稳定。 - 平台 = 策略注入:
plugin-platform-*是 策略包(Template 子类、hostConfig),不是第三套完整编译器。
5. 与「只有流程文档」的差别
| 流程文档 | 设计文档(本文) |
|---|---|
| 先 postcss 再 webpack | PostCSS 是 样式 Lowering 前端,产物进入 资源 IR(文件) |
| babel-loader 配置 | Babel 是 JS 语义 Lowering,输出 Bundler 可链接的模块 IR |
| hydrate 调用链 | hydrate 是 视图序列化后端,输出 setData 协议 IR |
6. 源码锚点(按层)
| 层 | JS 相关 | CSS 相关 |
|---|---|---|
| 语义 | babel-preset-taro、babel-plugin-transform-taroapi(§2.6)、transform-taro-components 等 | postcss.mini.ts / postcss.h5.ts |
| 模块图 | MiniPlugin、MiniSplitChunksPlugin、webpack5-prebundle | MiniWebpackModule 中 css rule |
| 逻辑 IR | taro-runtime TaroElement、taro-react reconciler | Style、ClassList |
| 视图 IR | hydrate.ts、Shortcuts | template.ts 中 styles(st/cl) |
| 后端策略 | platform-*/template.ts、hostConfig | 各端 fileType.style |
7. 「到 App / 原生」是什么、交互在哪里
若你说的 「原生」= 自己发版安装的 iOS / Android App(而不是微信里打开的小程序),在 Taro 语境下 主要指 React Native 这条线:业务仍是 JS/React,但 UI 与系统能力通过 RN 映射到真·原生层。下面 7.1 以 App 为主;小程序放在 7.2 作对照。
7.1 App(React Native):怎么「到原生」、交互在哪
编译 / 打包在干什么
- Taro 侧:
taro-rn-runner调react-native bundle,按deviceType: ios | android产出 JS bundle 与 图片等 assets 路径(packages/taro-rn-runner/src/index.ts)。 - Metro(
@tarojs/rn-supporter配taroTransformer、taro-rn-transformer)负责 变换与打包,不由 Taro 把你的页面编译成.m/.java业务源码。 - 真机上的「原生」部分:你工程里的
ios/、android/工程(Xcode / Gradle)——壳、启动、加载 bundle、链接 RN 原生依赖 都在这里;Taro 仓库 不包含 这份壳的完整业务实现。
运行时:谁在跟原生打交道
- 渲染:RN 渲染器把 React 组件树 创建/更新为原生 View(新架构 Fabric + JSI 或旧桥接,视 RN 版本而定)。触摸、布局等 在原生 UI 线程与 JS 线程之间由 RN 内部协调。
- 能力调用:
Taro.xxx等在 RN 端会落到@tarojs/taro-rn等实现,内部再通过 RN 的 NativeModules / TurboModules 调 原生模块(相机、存储等)。交互边界是 JSI / Bridge,不是小程序的setData/eh。
和 Taro 小程序模型的本质区别
- 没有 逻辑层/视图层分离 + 整棵虚拟树序列化进
setData的模型。 - 交互:用户点按 原生 View → RN 合成 触摸事件 → 进 JS 里 React 的事件系统;业务写的
onPress/onClick在这条链上响应。
一句话:App 路线里,「到原生」= RN 把 React 树接到原生 UI + 桥接系统 API;Taro 负责 Metro 链、按需 API 路径、样式到 RN 的变换,不替代 RN 与 OS 之间的那一层。
7.2 小程序(对照):跑在超级 App 里,不是独立 App
- 产物是 小程序包(js + 模板 + wxss + json),跑在 微信/支付宝等客户端内置的小程序运行时里。
- 下行:
setData把序列化后的 data 推到小程序视图层(taro-runtimedom/root.ts)。 - 上行:模板事件汇到
eh→eventHandler,在 逻辑层还原成TaroEvent并dispatch(packages/taro-runtime/src/dom/event.ts、dsl/common.ts)。 eh/setData这一套只适用于小程序形态,不要和 RN App 混谈。
export const events = {
bindtap: 'eh'
}7.3 H5
跑在 浏览器 WebView 或系统浏览器里:真实 DOM + 标准事件;无 setData / RN Bridge。
7.4 可借鉴的设计点(按形态)
- 小程序:单一
eh+sid/id路由、Hook 补丁链(modifyMpEventImpl等),在 双线程 + 模板 约束下模拟 DOM 事件。 - App(RN):业务侧尽量 对齐小程序 API 形状,底层 交给 RN 的原生映射与 Bridge;扩展能力用 原生模块 而非再造一套
setData。
8. 延伸阅读
- 02 — 编译管线与 Runner
- 03 — 小程序模板、hydrate 与 setData
- 05 — CSS 与各端样式转换
- 09 — 常见问题(含实现细节问答)
babel-plugin-transform-taroapi源码:packages/babel-plugin-transform-taroapi/src/index.ts,测试:tests/index.spec.ts- 外部类比阅读:V8 文档、Blink rendering(仅 borrow 词汇,勿等同架构)