# Rust语言实现微信小程序渲染层类似Skyline **Repository Path**: io123/mini-program-core ## Basic Information - **Project Name**: Rust语言实现微信小程序渲染层类似Skyline - **Description**: 源码100%可用,单元测试完善,这不是例子是生产级应用,适配微信小程序源码渲染引擎对齐skyline渲染引擎,可将该跨平台应用集成到你想集成的系统中实现原生渲染,同样体验 包括andriod、ios、macos、windows、linux系统、鸿蒙系统等等中去 仅供学习参考!!! - **Primary Language**: Rust - **License**: AGPL-3.0 - **Default Branch**: main - **Homepage**: https://365code365.github.io/mini-program-core - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-12-28 - **Last Updated**: 2026-07-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # mini-render **自绘的小程序渲染引擎。用 Rust 从零实现 —— 不基于 WebView、不映射系统原生控件、不依赖 Skia。** 给它一份微信小程序源码(WXML / WXSS / JS / app.json)和一块可写的像素缓冲,它把界面画出来、 把交互跑起来。对齐目标是微信的 **Skyline**(同样是原生渲染,同样没有 DOM)。 下面四张是引擎自己渲染的输出(纯 WXML + WXSS + 数据,375×667 @2x,无浏览器参与): | | | | | |:---:|:---:|:---:|:---:| |
电商首页 |
资讯信息流 |
外卖点餐 |
表单控件 | 全部 65 张见 [`doc/场景画廊.md`](doc/场景画廊.md) —— 它们同时是逐字节可复现的回归基线。 --- ## 目录 - [核心介绍](#核心介绍) - [它是什么、不是什么](#它是什么不是什么) - [一帧是怎么画出来的](#一帧是怎么画出来的) - [八个关键设计取舍](#八个关键设计取舍) - [现在到什么程度了(实测)](#现在到什么程度了实测) - [能力边界:能做什么、明确不做什么](#能力边界能做什么明确不做什么) - [适合 / 不适合的场景](#适合--不适合的场景) - [技术栈与依赖](#技术栈与依赖) - [30 秒跑起来(桌面)](#30-秒跑起来桌面) - [集成进 App(SDK)](#集成进-appsdk) - [支持范围](#支持范围) - [测试与回归](#测试与回归) - [仓库结构](#仓库结构) - [文档](#文档) --- ## 核心介绍 ### 它是什么、不是什么 一句话:**小程序源码进,像素出**。中间没有浏览器内核、没有系统控件、没有第三方图形库。 ``` 输入 引擎(纯 Rust 库) 输出 ──────────────────────────────────────────────────────────────────────────── my-mini-app/ ┌──────────┐ ├── app.json ─┐ │ RGBA │ ├── app.wxss │ 解析 → 逻辑层 → 模板求值 → 样式计算 │ 像素 │ ├── pages/*.wxml ├─▶ → Flexbox 布局 → 自研 2D 光栅器 ──────▶ │ 缓冲 │ ├── pages/*.wxss │ → 分层合成(页面 / tabBar / fixed) └──────────┘ ├── pages/*.js ─┘ │ └── 图片等资源 ▼ 窗口 / UIView / SurfaceView / PNG ``` 产物是一个普通的 Rust 库(`cdylib` / `staticlib` / `rlib`),落地条件只有一条: **给我一块可写的像素缓冲**。所以同一份代码能跑在 Android、iOS、Windows、macOS、Linux 上, 也能在没有屏幕的 CI 里直接出 PNG。 它和常见方案的区别: | 方案 | 做法 | 本引擎 | |---|---|---| | 微信小程序的 WebView 渲染层 | WXML → DOM,交给浏览器内核排版绘制 | ❌ 不用 DOM、不用浏览器内核 | | React Native / Weex | JS 描述 → 映射成系统原生控件(`UIView` / `android.view`) | ❌ 不映射系统控件(不受各端控件差异摆布) | | Flutter | Dart + Skia 自绘 | ✅ 思路最接近,但这里是 **Rust + 自研光栅器**,无 Skia 依赖 | | Electron / Tauri | 打包/复用一个浏览器 | ❌ 不打包浏览器(产物 34MB 级,不是 100MB 级) | | 微信 **Skyline** | 原生渲染 + 自绘组件 | ✅ 对齐目标:语义、手感、事件模型都按它对 | **为什么值得自绘。** WebView 方案的痛点不在「能不能渲染」,而在你**控制不了**:内核版本随 系统走、同一份 CSS 在不同 Android 上排版不同、首屏要等内核起来、滚动与手势的手感由内核决定、 想插一层原生能力就要架桥。自绘把这些变成自己的代码:**布局与绘制逻辑各端只有一份**,帧调度、手势仲裁、内存上限 全都在手里 —— 各端的剩余差异只来自系统字体(要彻底消掉就把字体一起打包, `assets/` 里放了 Noto Sans SC,`TextRenderer::from_file` 直接加载)。代价也很实:CSS 覆盖面要自己一条一条补 (见[能力边界](#能力边界能做什么明确不做什么)),性能靠 CPU 省着花。 ### 一帧是怎么画出来的 八步,每步都能单独打开诊断日志(`MINI_*` 开关见[架构文档](doc/架构与实现原理.md#帧成本是怎么压下来的)): | # | 阶段 | 做什么 | 关键实现 | |---|---|---|---| | ① | **解析** | WXML → 节点树;WXSS → 样式表 | 手写解析器;CSS 选择器引擎(标签/类/#id/`*`/属性/后代/子代 + 特异性排序),选择器在**解析期就编译**好 | | ② | **逻辑层** | 跑 `App` / `Page` / `Component`,`setData` 产出数据快照 | QuickJS(rquickjs);CommonJS 模块、`Promise`/`async`、定时器、微任务每帧 pump | | ③ | **模板求值** | `{{ }}` 表达式 + `wx:if` / `wx:for` 展开 → 渲染节点树 | 自研表达式引擎;`class`/`style` 绑定到数组/对象时按 CSS 语义拼接 | | ④ | **样式计算** | 命中的 CSS + 内联 `style` 合成计算样式 | 继承语义(`color` / `font-size` / `font-weight` / `line-height` / `letter-spacing` …);简写与细项按固定档位排序落地 | | ⑤ | **布局** | Flexbox 求解盒模型 | taffy 0.12;**文本节点挂自定义度量函数**,换行与 min/max-content 由真实字形宽度决定;布局后有第二遍修正(`reflow`)收拾只有算完才知道的溢出 | | ⑥ | **绘制** | 逐层画进 RGBA 画布 | 自研 2D 光栅器:扫描线 + even-odd 填充、4× 超采样抗锯齿、圆角三次贝塞尔逼近(K=0.5523)、fontdue 字形 + 多字体回退、Apple `sbix` 彩色 emoji、图片双线性 + 面积两级过滤、线性/径向渐变、掩膜化 `box-shadow`、裁剪栈 | | ⑥' | **动画** | `@keyframes` / `transition` / `wx.createAnimation` | 在**绘制期**按全局时钟求值:`transform`/`opacity`/颜色只影响绘制,不触发重排 → 动画帧成本≈静态帧 | | ⑦ | **分层合成** | 正常流 → tabBar → `position:fixed` 覆盖层 | 与浏览器层叠顺序一致(全屏遮罩能压暗 tabBar);覆盖层用视口坐标、独立画布、按需重绘 | | ⑧ | **上屏** | 贴到窗口 / 交给宿主 / 存 PNG | 桌面走 softbuffer;移动端把 RGBA 交给宿主 View;无头模式直接 `save_png` | 逻辑层与渲染层之间只有**数据快照**这一条通道(`setData` → `Arc`),没有 DOM、没有 虚拟节点 diff、没有 GC 压力。 ### 八个关键设计取舍 这些是「为什么这么写」的核心,踩过的坑都写在 [`doc/踩坑记录.md`](doc/踩坑记录.md)。 1. **文本度量驱动布局,不估算宽度。** `TextMeasure` 上下文挂到 taffy 叶子上,由真实字形参与 布局。这样「定宽容器内按容器宽换行」和「收缩容器被内容撑开」两种 CSS 语义能同时成立。 反面教材:曾给每个文本盒 +4px「保险余量」,结果所有按内容定宽的徽标/标签/胶囊都比浏览器宽一圈。 2. **动画只重绘不重排。** `@keyframes` 里改 `width`/`height` 这类会引发重排的属性不生效, 改 `transform`/`opacity`/颜色都生效。换来的是「动画帧和静态帧一样便宜」。 3. **`setData` 只重绘变化的那一块。** 新旧渲染树并行走一遍,收集「文本/属性变了」或「几何变了」 的节点包围盒当裁剪矩形。首页秒杀倒计时的失效范围是 **45×33 像素**,不是整屏。 拿不准就退回整帧 —— 少画一块留下脏像素比多画一次严重得多。 4. **页面滚动不重绘。** 页面画布是整页高的、用内容坐标,滚动只是取不同切片上屏(每帧 2~3ms)。 只有滚出「已绘制条带」那一刻才补画一次。 5. **滚动手感按 iOS/微信那套做。** 橡皮筋衰减 `1 - 1/(x·0.55/d + 1)`、回弹是带初速度的 **临界阻尼弹簧**(不是固定时长缓动)、惯性撞边界不当场停死而是把动量交给弹簧。 6. **手势归属不在按下时决定。** 等第一次明显位移(4px)按主方向锁定,再按「到边界仍在推」 交棒给外层。横向卡片列表里竖着划该滚页面,纵向列表里横着划谁也不动。 7. **宿主层沉进 lib,桌面与移动端共用一份。** 页面栈、覆盖层、触摸状态机、手势仲裁、 picker 面板、像素合成都在 `src/host/`,只有帧调度各自实现。两条链路由 `tools/sdk-parity.sh`(逐像素)+ `src/tests/engine_input_tests.rs`(输入行为)双重守着。 8. **不引入 GPU、不做元素级位图缓存。** 定位就是「给我一块像素缓冲我就能画」;位图缓存与 动画/裁剪栈的交互复杂度不值当。抗锯齿也没换成子采样 —— 实测与 Chrome 的差异反而从 4.7% 涨到 5.3%。 ### 现在到什么程度了(实测) **性能**(375×667 @2x,即 750×1334 物理像素,CPU 逐像素写出): | 场景 | 优化前 | 现在 | |---|---|---| | 商城首页整帧(轮播 + 每秒倒计时 + 骨架动画) | 49.6 ms | **6.1 ms** | | 商城首页稳态帧间隔(144Hz 屏,节拍 6.94ms) | 6.9~25 ms 抖动 | **6.7~7.2 ms** | | 首页最慢一帧 / 帧间隔峰值 | 17.9 / 24.9 ms | **11.4 / 11.4 ms** | | 一次 `setData` 的重建成本 | 32 ms | **4 ms** | | 拖动单帧(tea-app 首页,60 帧平均) | 7.17 ms | **3.74 ms** | | 远程图片二次打开(磁盘缓存) | 6132 ms | **11 ms** | | 首帧「建树+样式」(三条中文字体栈的应用) | 3093 ms | **955 ms** | **双端一致性**(同一份源码:一边原生渲染,一边编译成 HTML 用 Chrome 截图,逐像素比): | 示例 | 页数 | 与 Chrome 的变化像素比 | |---|---|---| | `sample-app`(商城) | 15 | **4.55%** | | `news-app`(资讯) | 6 | **6.33%** | 剩余差异集中在粗体字形与亚像素文本位置。这个数字是**回归判据**,不是宣传语 —— 任何渲染改动都要先看它有没有变坏。 **跑得起来的真实工程**(不是 demo 级的自造样板): | 小程序 | 规模 | 说明 | |---|---|---| | `sample/tea-app` | 36 页 | **uni-app 编译到 mp-weixin 的产物**:259KB Vue 3 运行时 + 60 个 CommonJS 模块,用 `Component()` 构造器定义页面 | | `sample/real-sample` | 8 页 | 微信官方 demo | | `sample/sample-app` | 15 页 | 商城闭环:购物车 → 确认订单 → 下单 → 订单/物流,跨页状态走 `wx.storage` | | `sample/news-app` | 6 页 | 资讯闭环:频道横滑、正文字号即时生效、评论、收藏持久化 | **回归规模**(每次改动都要全绿,命令见[测试与回归](#测试与回归)): | 判据 | 规模 | |---|---| | 单元测试 | lib **471** + bin **9**;其中宿主纯逻辑 41 条(手势仲裁 13 / 侧滑返回 12 / picker 面板 10 / 触摸状态机 6)、SDK 指针链路 7 条 | | 场景画廊 | **65** 张,固定动画时钟下逐字节可复现 | | 局部重绘校验 | 增量重绘 vs 强制整帧,5 个场景**逐字节相同** | | SDK 与桌面一致性 | sample 15/15、news 6/6 **逐像素相同** | | 指针层与覆盖层 | 6 张确定性交互快照 + 2 条手势断言 + **空转守卫** | | 编译警告 | **0**(`cargo check --all-targets`) | ### 能力边界:能做什么、明确不做什么 **已实现**(详表见[支持范围](#支持范围)): - **24 个组件标签**(容器 / 文本媒体 / 表单三类,含 `swiper`、`scroll-view`、`picker-view`); - **事件**:六种绑定前缀(`bind` / `catch` / `capture-bind` / `capture-catch` / `mut-bind` / `bind:`) 与完整的捕获-冒泡链,触摸序列按微信语义产出(slop 取消 tap、350ms longpress、被滚动接管补 `touchcancel`); - **模板**:`wx:if` / `wx:elif` / `wx:else` / `wx:for`、`block`、表达式引擎、`class`/`style` 绑定、 页面 json 的 `usingComponents`(自定义组件含样式隔离与独立数据作用域); - **样式**:选择器与特异性层叠、继承、`@import`、CSS 变量、`rpx`、`calc()`、渐变、阴影、 `transform` / `transition` / `@keyframes`; - **API**:39 个 `wx.*`(`request` 含 `abort`、storage 全套且跨启动持久化、Toast/Loading/Modal、 五个路由 API、设备信息全套、`createAnimation`、`createCanvasContext`); - **生命周期**:三级共 28 个钩子(App 7 / Page 13 / Component 5 + `pageLifetimes` 3), 页面 `onShow/onHide/onResize` 会联动页面内组件; - **交互**:惯性滚动与临界阻尼回弹、手势仲裁、侧滑返回、下拉刷新、picker 底部面板、局部重绘; - **媒体**:Canvas 2D(完整 2D 上下文 + 设备分辨率后备缓冲)、`