不依赖宿主的内核
只依赖 manifest、bridge、glass-easel 和通用工具;入口暴露 host shell、service 线程、tree 协议/渲染器接口和宿主适配类型,不导出浏览器实现。
service 通过 loadTemplates、loadStyle、canvas、resolveAssetUrl 接收宿主能力;render 通过 TreeRenderer 接收渲染实现,Web 侧由 @tiny/host-web-components 提供。
架构
Tiny 采用微信式的双侧树:service 线程运行 glass-easel 逻辑树,宿主 render 线程维护真实渲染树,两者通过 component flow、虚拟节点 patch 和 tree 事件同步。编译器和 Bridge 只提供产物与传输,runtime 不依赖浏览器,devtool 负责观察和控制。
职责边界
先看职责,再看代码放在哪。几个角色之间的接口都是显式的。
| 角色 | 负责 | 不负责 | 代码位置 |
|---|---|---|---|
| compiler | 扫描源码、编译模板和样式、转译脚本、打包 service、生成 manifest 和诊断 | 页面生命周期、路由、wx API、DOM | tools/cli |
| manifest | 定义产物类型、schemaVersion 和解析后的命名配置 | 具体实现 | packages/manifest |
| runtime | App/Page/Component 语义、页面栈、setData、observer、relations、逻辑树、virtual node diff、tree 协议和 canvas 协议 | 真实 DOM、宿主内置组件实现、宿主 API、界面 | packages/runtime |
| bridge | 消息信封和帧、transport、调用与回复、批处理、超时和 JSON 校验 | wx API 语义、渲染指令、页面状态 | packages/bridge |
| host | 线程和 transport、启动流程、宿主 API、渲染树、内置组件、canvas、资源 URL、诊断收集 | 页面栈、业务 data、observer、relations 和事件回调 | hosts/browser · hosts/web-components · apps/devtool · tests/host · 未来原生宿主 |
页面栈、页面 data、组件实例、observer 和 relations 都归 service 线程管;render 线程只有宿主渲染树、DOM 和稳定 node id 的镜像。两棵树没有共享内存,所有同步都经过宿主转发的 tree 协议。
双侧树
这是当前运行时的核心设计。service 不创建 DOM,render 不执行小程序逻辑;两边通过稳定的 node id、component flow 和 virtual node patch 对齐。
| 层 | 在哪里 | 拥有 | 不负责 |
|---|---|---|---|
| 逻辑树 | service 线程 | glass-easel ComponentSpace、模板求值、virtual node、组件实例、observer、relations、带类型的 dataset、node id | DOM、布局、宿主内置组件实现 |
| 渲染树 | 宿主的 render 线程 | 真实内置组件、自定义组件容器、slot、shadow root、样式、布局、DOM | app methods、observer、relations、页面栈 |
| 同步协议 | 两棵树之间 | component flow 首屏同步、virtual node patch 增量更新、tree 事件、query/canvas 控制 | 业务语义 |
service thread render thread (one root per page)
+-------------------------------+ +-------------------------------+
| App / Page / Component | | host TreeRenderer |
| glass-easel logic ComponentSpace | tree | real built-in components |
| template -> virtual RenderNode | <------> | glass-easel render ComponentSpace |
| data / observers / relations | flow | DOM / layout / styles |
| node ids + patches | patch | no app methods |
+-------------------------------+ +-------------------------------+首屏由 tree.flow 发送一组 component flow:define、create-node、props、children、ready。后续模板变化由 tree.patch 发送 text、replace、props、insert、remove、reorder、extra,宿主只负责应用 patch。
如果让 render 负责模板求值、属性和 dataset,dataset 会经过 DOM 丢失类型,函数属性也无法跨线程;原生宿主还得重新实现一遍小程序语义。拆开之后,类型和语义留在 service,render 只负责“怎么画”,同一套 tree 协议可以接 Web、原生或测试渲染器。
内核约束
浏览器相关代码和内置组件实现都已经移到 hosts/ 下,并通过脚本检查依赖方向。
只依赖 manifest、bridge、glass-easel 和通用工具;入口暴露 host shell、service 线程、tree 协议/渲染器接口和宿主适配类型,不导出浏览器实现。
service 通过 loadTemplates、loadStyle、canvas、resolveAssetUrl 接收宿主能力;render 通过 TreeRenderer 接收渲染实现,Web 侧由 @tiny/host-web-components 提供。
@tiny/host-browser 负责 iframe、MessageChannel、DOM canvas handoff、资源 URL 和 DevtoolRuntimeClient。
它依赖 runtime 和 bridge,但 runtime 不会反向依赖它。tests/host 和 devtool 都从这里拿浏览器线程与 canvas 能力。
@tiny/host-web-components 负责 Web 渲染树、tiny-* 内置组件、行为和默认 CSS。
它是宿主包,不属于内核。render 入口把 createGlassRenderEntry 的渲染树工厂交给 bootBrowserRenderEntry,原生宿主可以实现同一个 TreeRenderer 契约。
# pnpm check:runtime-purity
packages/runtime/src/index.ts
-> component / service / tree (logic + render) / host / diagnostics
不允许出现在依赖图里的内容:
window · document · navigator · localStorage · MessageChannel
HTMLCanvasElement · Image · requestAnimationFrame · fetch · performance
名字里带 "browser" 的模块 · @tiny/host-* 导入
# hosts/browser
canvas-bridge.ts OffscreenCanvas handoff 与 canvas 测量
runtime-port.ts iframe 端口握手
service-entry.ts service 线程入口(含端口与 canvas 适配)
render-entry.ts render 线程入口(端口 + 渲染树工厂)
standalone-host.ts service + 每页 render 的连接
devtool-client.ts 调试器控制接口
# hosts/web-components
createGlassRenderTree Web 渲染树 + tiny-* 内置组件
createGlassRenderEntry 渲染树工厂(样式 + DOM 容器)原生宿主不需要模拟浏览器,只要实现 TreeRenderer、Transport、canvas handoff 和资源解析。有了这项检查,内核里如果误用了 document,CI 会直接失败,而不是等到换宿主时才发现。
运行时内核
glass-easel 不是完整的小程序 SDK。它负责组件、模板、数据绑定、事件和生命周期,App、路由、API 等由 Tiny 自己实现。
| 能力 | 由谁提供 | 在 Tiny 中如何使用 |
|---|---|---|
| 组件定义、properties、data、methods、模板 | glass-easel(service 逻辑树) | defineWithMethodCaller / defineComponent,模板在 service 求值 |
| WXML 运行时数据绑定与更新 | glass-easel | 编译器把 WXML 编成 ProcGen,运行时只传 groupList/content |
| 事件捕获与冒泡、slot、shadow 边界 | glass-easel(两侧各自) | 逻辑树分发事件,渲染树处理真实 DOM 事件 |
| created/attached/ready/moved/detached 生命周期 | glass-easel(service) | 组件实例和 relations 都在逻辑树 |
| 样式作用域和渲染树 | glass-easel + host | 逻辑树计算 scope;渲染树应用样式和 styleIsolation |
| Behaviors / observers / relations / selectComponent | Tiny runtime | component-definition 归一化 + ComponentTree + RelationManager |
| App 生命周期、页面栈、路由、onLoad/onShow/onReady | Tiny runtime | packages/runtime/src/service/thread.ts |
| 兼容输入配置、内部 Tiny 命名 | Tiny manifest + runtime | tiny.config.json → compatibility / runtimeCompatibility;内部名来自 TINY_ENGINE_NAMING |
| wx API、storage、system info、animation | Tiny runtime + host | service 侧 API host;设备信息由 extensions 注入 |
| canvas、资源 URL、DOM 节点 | 宿主 | canvas handoff / resolveAssetUrl / TreeRenderer |
| 跨线程模板更新、事件、页面切换 | Tiny runtime + bridge + host | tree.flow / tree.patch / tree.event / page.navigateTo |
| 元素树检查、生命周期控制、诊断投影 | Tiny runtime + devtool | DevtoolRuntimeClient + 渲染树 inspectTree |
官方 adapter 的注册接口更接近微信,但页面生命周期、宿主控制和原生能力都会被限制在 adapter 已支持的范围内。Tiny 直接使用 glass-easel 的核心 API,自己管理 App、页面栈、路由和 API host,后续接新宿主时也更自由。
依赖关系
这是当前工作区的真实依赖。核心包不能依赖 tools、apps、hosts 或 tests;宿主适配器反过来依赖内核。
| 包或应用 | 依赖 | 作用 |
|---|---|---|
@tiny/manifest | 无 | 产物与诊断类型 |
@tiny/bridge | 无 | 协议、transport、批处理和错误 |
@tiny/template-compiler | 无(纯 TS) | WXML → ProcGen |
@tiny/stylesheet-compiler | postcss | WXSS → 普通/低优先级 CSS |
@tiny/cli | manifest · template-compiler · stylesheet-compiler · esbuild · typescript | 负责生成全部产物 |
@tiny/runtime | manifest · bridge · glass-easel · es-toolkit | 双线程运行时内核,不依赖宿主 |
@tiny/host-browser | runtime · bridge | 浏览器宿主:iframe、MessageChannel、Canvas、devtool 覆盖层 |
@tiny/host-web-components | manifest · runtime · glass-easel;构建期 template-compiler | 24 个内置组件、AOT 模板和默认 CSS(宿主包) |
devtool | host-browser · host-web-components · glass-easel · React · Redux/rematch | 调试器宿主和控制面板 |
@tiny/website | React | 架构站点,不进入引擎链路 |
core packages/manifest
packages/bridge
packages/template-compiler
packages/stylesheet-compiler
packages/runtime -> manifest, bridge, glass-easel
hosts hosts/browser -> runtime, bridge (环境适配)
hosts/web-components -> runtime, manifest, glass-easel
(内置组件,注入 render)
tools tools/cli -> manifest, template-compiler, stylesheet-compiler
apps apps/devtool -> host-browser, host-web-components
tests tests/host -> host-browser, host-web-components
(不是 workspace 包)
fixtures tests/apps/* -> 只有源码,编译后交给宿主
规则:packages/* 不能导入 tools/*、apps/*、hosts/*、tests/*如果 runtime 为了类型去 import @tiny/cli,Node 的 fs 和 esbuild 就会被打进浏览器线程,打包时还会出现无法解析的 builtin。共享类型应该放在 @tiny/manifest,不要让工具反向提供类型。
启动流程
从编译产物到页面可交互,要经过 iframe、两次握手、两次 bootstrap 和一次 page.ready。render 可以共享,也可以每页一个 iframe。
读取 app.json,扫描入口,生成 templates.js、styles、scripts 和 service.js,最后写出 schema-v2 manifest.json。诊断信息随 manifest 一起返回。
compileMiniProgram({ sourceDir, outputDir })每个线程各一条 MessageChannel。service 和 render 不共享端口,由宿主分别持有两条 hostTransport。
createHostMessageChannelPair() × 2service iframe 被隐藏为 0×0,render iframe 填满容器;每页 render 模式下,每新建一个页面就创建一个 render iframe。宿主等待线程发回 tiny-runtime-port-ready,最多等 15 秒。
createStandaloneDualThreadHost(...)service 先拿到 manifest、templates 和 styles;render 先在 prepare 里装好内置样式,渲染树本身在端口到达后创建。service bundle 在 bootstrap 时通过 loadArtifacts 导入。线程要等 connection 安装完成,才调用 listenForRuntimePort,避免端口消息到达时丢帧。
bootBrowserServiceEntry(...) / bootBrowserRenderEntry(createGlassRenderEntry(...))宿主把 port2 作为 transferable 发给对应 iframe;线程再用 createMessagePortTransport 包装成 Transport。
postMessage({ type: 'tiny-runtime-port' }, '*', [port])宿主分别发起 runtime.hello,service/render 回复握手信息并发送 runtime.ready。协议版本不一致时连接会直接关闭。
host.service.handshake() · host.render.handshake()导入 service.js,App/Page/Component 在这里注册,App.onLaunch/onShow 也会立即执行。随后安装 runtimeCompatibility 兼容全局,构建组件 schema 和 glass-easel 逻辑树,创建首个页面并触发 onLoad/onShow,返回 pageId、initialData 和 componentSchemas。
service.control('runtime', 'bootstrap')render 创建 TreeRenderer(Web 侧 createGlassRenderTree),安装宿主内置组件,等待 service 发来的 tree.flow。首屏 flow 包含 define/create-node/props/children/ready,render 应用后挂到页面根。
render.control('runtime', 'bootstrap')render 挂载完成后宿主发送 tree.ready,service 用逻辑树快照发送 tree.flow;随后宿主发 page.ready,service 触发 onReady。此时逻辑树、组件实例和 relations 已经可用。
service.control('page', 'ready', { pageId })service 发送 RUNTIME_STATE,包含 pageStack、currentPage 和 transport 状态;宿主诊断仓库和 devtool 据此更新界面。
sendDiagnostic('state', { code: 'RUNTIME_STATE' })createStandaloneDualThreadHost 只负责创建线程、完成握手并返回 host,不会自动调用 bootstrap。测试宿主和调试器都需要自己执行 host.bootstrap(),否则页面不会被创建。
消息转发
所有跨线程消息都经过宿主的两条 BridgeConnection。宿主不是透明管道:它按 pageId 路由、在页面还没打开时排队、把请求代理到渲染树,还自己处理宿主 API 和诊断。
+------------------- host --------------------+
| createTinyRuntime(options) |
| |
| BridgeConnection(host <-> service) |
| BridgeConnection(host <-> render) × 每页 |
| pageId → render 连接 + 待发队列 |
| HostApiRegistry / HostDiagnosticStore |
+------+--------------------------+----------+
| |
MessageChannel MessageChannel
| |
+---------v---------+ +---------v---------+
| service iframe | | render iframe |
| 逻辑树 / 页面栈 | | 渲染树 / DOM |
+---------+---------+ +---------+---------+
^ ^
| window.postMessage |
+--- 端口交接 / canvas transferable ---+
(不承载业务协议)
service 和 render 之间没有直连通道宿主对消息有四种处理方式,决定了这条链路要等几次往返。
转发(event,host 重新封装后立即发给另一侧)
service ── tree.flow / tree.snapshot / tree.patch / tree.animate ──► host ──► render
service ◄── tree.event ───────────────────────────────────────────── host ◄── render
service ◄── page.scroll / reachBottom / pullDownRefresh ──────────── host ◄── render
代理(call/control,host 等渲染树回包后再回 service)
service ── query.select / query.intersection / canvas.measure ──► host ──► render
service ◄──────────────── reply ───────────────────────────────── host ◄── reply
终结(host 自己处理,不再转发)
service ── api.* ─────────────────────────────────► HostApiRegistry
render ── tree.hostAction ───────────────────────► navigate.* handler
service/render ── diagnostic.* ───────────────────► HostDiagnosticStore
发起(host 主动驱动两侧)
host ── runtime.bootstrap / page.ready / page.navigateTo ──► service
host ── runtime.bootstrap / runtime.inspectTree ──────────► render| 消息 | 路径 | 宿主处理 | 用途 |
|---|---|---|---|
tree.flow · tree.snapshot · tree.patch | service → host → render | 按 pageId 转发;目标页还没打开时先入队 | 首屏 component flow、完整快照、虚拟节点 patch |
tree.animate · tree.clearAnimate | service → host → render | 按 pageId 转发 | this.animate / clearAnimate 的关键帧 |
tree.event | render → host → service | 按 pageId 转发 | 事件带 node id、name、detail、native、touches |
tree.ready | host → service | render bootstrap 完成后由宿主发送 | 请求逻辑树发送首屏 flow |
query.select | service → host → render → host → service | 两次 RPC | selector query 在真实 DOM 中查节点,返回 rect/dataset/scroll |
query.intersection | service → host → render;entry 反向 render → host → service | 代理 observe/unobserve,再转发回调 | 页面/组件级 IntersectionObserver |
canvas.measure | service → host → render → host → service | 把 call 转给渲染树 | 测量 canvas 节点 |
canvas handoff | service → host → render → host → service | 只中继 transferable,不解析 | 把 OffscreenCanvas 交给 service;之后直接绘制 |
api.* | service → host | HostApiRegistry 查 handler;navigate.* 会再 control service | 导航、toast、storage、request 等宿主语义 |
page.scroll · page.reachBottom · page.pullDownRefresh | render → host → service | 按 pageId 转发 | 页面滚动、触底和下拉手势——render 文档才是页面的滚动容器 |
page.ready · page.navigateTo · page.navigateBack · page.switchTab · page.redirectTo · page.reLaunch · page.pullDownRefresh · page.share · page.saveExitState · page.hide · page.show | host → service | 宿主发起 | 页面栈和生命周期由 service 执行,宿主负责开/关 render 页面 |
runtime.bootstrap · runtime.resize · runtime.themeChange | host → service | 宿主发起 | 启动、窗口尺寸、主题 |
diagnostic.* | service/render → host | 写入 HostDiagnosticStore | 不回传另一侧,devtool 从宿主读取 |
service 在 runtime.bootstrap 里就发出首屏 tree.flow,而 render 页面要等 bootstrap 返回 pageId 之后才会被宿主创建。宿主把这段窗口期的消息放进待发队列(最多 16 页),页面打开后按顺序 flush;没有 pageId 或队列溢出会记 RENDER_SESSION_MISSING,而不是静默丢弃。
service 只知道页面栈,render 只知道组件树。一次 navigateTo 必须由宿主先让 service 创建页面并返回 pageId/initialData,再让 render 卸载旧页、挂载新页。只有宿主同时持有两侧的连接。
唯一的“点对点”是 OffscreenCanvas 对象移交:render 在 iframe 内执行 transferControlToOffscreen(),宿主把它作为 transferable 转给 service。移交完成后 service 直接持有控制面,绘制不再产生任何跨线程消息——这也是它和普通消息链路的区别。
状态归属
很多“页面不更新、事件不触发、返回后白屏”的问题,都是因为状态被复制到了错误的层。
| 状态 | 归属 | 副本或投影 | 同步方式 |
|---|---|---|---|
| manifest 和产物 | 编译器生成,运行时只读 | 各线程内存中的 manifest | 启动时加载,不做热更新 |
| App / Page / Component 定义 | service registry + 逻辑树 | 宿主不保存组件定义 | bootstrap 时构建 schema;flow 只带渲染所需元数据 |
| 页面栈、当前页、页面 data | service | devtool 的 pageStack 投影 | RUNTIME_STATE 诊断 |
| 组件实例、observer、relations | service 逻辑树 | render 只有组件容器和节点镜像 | component flow + node id;不跨线程执行逻辑 |
| 虚拟节点树和 node id | service 逻辑树 | render 的节点映射 | service 分配 id;tree.flow / tree.patch 同步 |
| 真实组件树 / DOM / 布局 | render 渲染树 | devtool 的 InspectNode | 渲染树 inspectTree |
| 样式和 scope id | service 计算 scope,render 应用 | 无 | 逻辑树生成带 role 的样式,flow 下发 |
| canvas 实现 | 宿主 canvas handoff | service 持有 OffscreenCanvas 控制面 | ServiceCanvasAdapter / 浏览器 OffscreenCanvas relay |
| DOM 查询 | 宿主渲染树 | selector query / IntersectionObserver | query.select / query.intersection |
| 资源 URL | 宿主 resolveAssetUrl | render 重写后的 img/src 和 CSS url() | 渲染树在应用 flow 时解析 |
| 宿主 transport / API 语义 | 宿主 | service 侧 API 快照 | api.* 调用 / systemInfo / storage / apiExtensions |
| 诊断 | 宿主诊断仓库 | devtool diagnostics | diagnostic.* 事件 |
如果让 render 保存页面栈或组件 data,逻辑树和渲染树很容易对不上。页面栈、组件实例和业务 data 只归 service;render 只保存渲染节点和稳定 id,宿主只负责转发。
设计约束
它们决定了引擎能否跨宿主、方便测试,也方便定位问题。
所有宿主只读 @tiny/manifest。一旦复制一份产物类型,编译器和运行时迟早会对不上。
Date、Map、Set、函数、undefined 和循环引用都会被 Bridge 拒绝。需要传递时,请先转成字符串或普通对象。
核心包不能依赖 tools、apps、hosts 或 tests;共享类型放在 manifest 里,不要从 CLI 反向导入。浏览器能力只能从 hosts/browser 进入宿主,内置组件只能从 hosts/web-components 进入宿主。
pnpm check:runtime-purity 会从 runtime 入口遍历依赖,禁止 window、document、iframe、MessageChannel、fetch 等宿主全局。
设备信息走 apiExtensions,canvas 走 ServiceCanvasAdapter / RenderCanvasAdapter,DOM 查询与相交观测走 RenderQueryAdapter,资源走 resolveAssetUrl,渲染后端和挂载点也由宿主提供。
输入侧模板指令、API、注册全局和 behavior scheme 来自 compatibility;Tiny 宿主属性、scope、marker 和内部 behavior 只来自 TINY_ENGINE_NAMING。不要在 runtime 里散落兼容字符串。
service 不创建真实 DOM、不加载 Web 内置组件;render 不执行 app methods、observer 或 relations。node id 由 service 分配,渲染树只做映射。
宿主产物、内置组件 ProcGen、devtool 静态文件和站点里的 devtool 都由构建生成;仓库只保留源码、约定和测试 fixture。