Skip to content
目录

10 — CSS / JS 转化「引擎」的设计抽象(非流程清单)

本文回答:除了「先 A 再 B」的流程,Taro 在 设计层有没有一套可类比 编译器 / 浏览器引擎 的分层与用语。
结论:Taro 不是 V8、也不是 Blink 的缩小版——没有单一字节码 IR、没有统一布局树;但它有 可明确命名的阶段、中间表示、Lowering 与多后端,足以构成一套 内部设计语言
具体插件顺序仍以 0205 为准;本文给 心智模型


概念V8 / BlinkTaro
单一运行时 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」)。

mermaid
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 --> DATA

2.1 阶段 I — 语义改写(Babel 为前端)

输入:用户源码。
输出:仍属 JS/TS,但 语义已对准 Taro 契约(API 形态、动态 import 策略、RN 与小程序分支等)。

设计角色:相当于编译器的 Semantic Lowering(面向宿主的第一轮)

典型变换族(非穷尽):

  • 环境常量process.env.TARO_ENV / TARO_PLATFORM,使死分支可折叠(DefinePlugin / babel-plugin-global-define)。
  • API 形态babel-plugin-transform-taroapiTaro.xxx 转为 可按需引入 的 import,服务 Tree Shaking(设计目标:体积与静态可分析性)。
  • 小程序 + Reacttransform-taro-components 等对 热点组件模式 做 AST 级改写(设计目标:与模板/运行时假设对齐)。
  • 非 Webbabel-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 设计目标(为什么要有它)

  1. Tree Shaking / 显式依赖边
    业务常写 import Taro from '@tarojs/taro'Taro.getStorage(),bundler 难以证明只用了哪些 API。插件把 已声明支持的 API 变成 import { getStorage as _getStorage } from '@tarojs/taro' 这类 具名导入,让 模块图的边可见,利于摇树与按需打包。

  2. 未纳入「白名单」的 API 仍可用
    不在 definition.apis 集合中的具名 import,不把其从默认导出里拆出来,而是把引用 改写成 Taro.xxx,并保留 默认导入——兼顾 渐进支持不破坏未建模 API

  3. canIUse 编译期常量化
    构建时根据平台给出的 能力描述 JSONdefinition),把 Taro.canIUse('scheme') 折叠为布尔字面量,后续可由 minifier 做 死代码消除(等价于受限的 特性检测下沉到编译期)。

  4. H5 无障碍属性表面语法
    仅在 TARO_ENV === 'h5' 时,把 JSX 上的 ariaXxx 映射为 DOM 规范的 aria-*DEFAULT_ATTRIBUTE_MAP),属于 Lowering 到 Web 宿主 的细枝末节。

接入方式(示例):H5 平台在 modifyWebpackChain 里往 babel-loaderplugins 追加该插件,并传入 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 并标记需要默认导入
成员访问MemberExpressionTaro.xxxxxx 在白名单:在非赋值左值场景下 替换为已登记的局部标识符(多次使用共享同一 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 合并 apiscomponents,并对 canIUse 键置为 '*' 表示通配。
replaceCanIUse 把字符串 scheme 写成嵌套对象路径lodash/setWith),再用 isMatchWith 与 definition 做子树匹配——*true 作为通配,从而在 编译期得到 确定布尔值

37:54:packages/babel-plugin-transform-taroapi/src/index.ts
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-taroapiTaro 命名空间 / 具名 API 导入图canIUse 折叠、H5 aria模块边界与摇树
babel-preset-tarotransform-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 值得借鉴的设计点(通用工程)

  1. 数据驱动变换:用 definition JSON 描述「支持哪些 API / 组件能力」,插件只做 通用遍历与替换,新增能力往往 改数据不改插件核心逻辑
  2. 两遍法避免顺序坑:先在 enter~member 阶段 收集符号与需求,在 Program.exit 统一 生成 import,减少「先删 import 后找不到绑定」类问题。
  3. 白名单 + 降级路径:白名单内 精细化(具名 import);白名单外 降级为默认导出成员访问,避免 过度 transform 导致行为变化
  4. 编译期常量折叠的边界:只对 静态可证 的子集(如 canIUse('literal'))下手,不把任意表达式都求值——在 正确性收益 间划线。
  5. 平台条件分支JSXAttributeH5 执行,体现 同一 AST 管道、环境门控不同 Lowering——与 DefinePlugin / TARO_ENV 一致的思维。
  6. 可测试性packages/babel-plugin-transform-taroapi/tests/ 用 snapshot + 结构断言锁定 import 形态与 Taro.noop 等边界,适合作为 自定义 Babel 插件 的范例。

3. CSS 转化:独立的「样式引擎」管道

CSS 在 Taro 里 不与 JS 共用一条 IR,而是 平行管道,最后在 data 与 wxss 两处 汇合

mermaid
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 --> VIEW

3.1 编译期:PostCSS 为「样式前端」

输入:样式源文件。
输出宿主样式文件(或可被 mini-css-extract 挂到 chunk 的 CSS 串)。

阶段语义(设计层,非插件枚举):

  1. 聚合postcss-import(模块化样式图)。
  2. 规范化:autoprefixer 等(面向宿主能力子集 的降级,而非 W3C 全集)。
  3. Lowering(单位与尺寸)postcss-pxtransform —— 从设计稿空间 → 宿主长度单位空间(rpx/rem/…)。
  4. 资源 Loweringpostcss-url、alias —— URL → 可被打包引用的路径
  5. 可选 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)。

LoweringStyle._valuecssText…st 路径setData

与编译期 wxss 汇合cl 字符串 选 wxss 规则;st 字符串 覆盖内联——语义上接近浏览器的 级联的一小段(但 无完整层叠计算)。


4. 设计语言:可背的几条「原则」

  1. 双轨 IRJS 模块图(Webpack)与 模板+数据协议(shared + runtime)正交;拼接点在 构建插件hydrate
  2. 逻辑 / 视图分离:Reconciler 只认 Taro DOM IR;宿主只认 模板 + MiniData
  3. Lowering 分后端:同构业务语法,不同宿主不同 emit(wxml vs DOM vs RN)。
  4. 协议稳定优先Shortcuts、组件 alias、Adapter 替换的是 「宿主语法表面」,底层 data 形状尽量稳定。
  5. 平台 = 策略注入plugin-platform-*策略包(Template 子类、hostConfig),不是第三套完整编译器。

5. 与「只有流程文档」的差别

流程文档设计文档(本文)
先 postcss 再 webpackPostCSS 是 样式 Lowering 前端,产物进入 资源 IR(文件)
babel-loader 配置Babel 是 JS 语义 Lowering,输出 Bundler 可链接的模块 IR
hydrate 调用链hydrate 是 视图序列化后端,输出 setData 协议 IR

6. 源码锚点(按层)

JS 相关CSS 相关
语义babel-preset-tarobabel-plugin-transform-taroapi(§2.6)、transform-taro-componentspostcss.mini.ts / postcss.h5.ts
模块图MiniPluginMiniSplitChunksPluginwebpack5-prebundleMiniWebpackModule 中 css rule
逻辑 IRtaro-runtime TaroElementtaro-react reconcilerStyleClassList
视图 IRhydrate.tsShortcutstemplate.tsstylesst/cl
后端策略platform-*/template.tshostConfig各端 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-runnerreact-native bundle,按 deviceType: ios | android 产出 JS bundle图片等 assets 路径(packages/taro-rn-runner/src/index.ts)。
  • Metro@tarojs/rn-supportertaroTransformertaro-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-runtime dom/root.ts)。
  • 上行:模板事件汇到 eheventHandler,在 逻辑层还原成 TaroEventdispatchpackages/taro-runtime/src/dom/event.tsdsl/common.ts)。
  • eh / setData 这一套只适用于小程序形态,不要和 RN App 混谈。
63:65:packages/shared/src/template.ts
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. 延伸阅读

MIT License.