一个全栈工程师的业余项目:我用 Taro 做了一个马拉松备赛小程序

作为一个全栈工程师,我最得意的业余项目不是某个技术 Demo,而是一个给我自己和身边跑者用的微信小程序——赛迹攻略。本文记录从痛点到产品的全过程。

为什么要做这个

我跑崩过一次全马和两次半马。

2023 年合肥全程马拉松,前半程按 330 配速跑,自我感觉良好,30km 后开始掉速,35km 彻底崩了,最后 3:51:42 完赛。

同年池州半程马拉松,前半程按 135 配速跑,后半程一路掉速,最后 1:40:30 完赛。

2024 年又跑了一场半马,比赛前两周训练量骤降、状态没调过来,前 10km 还能跟上配速组,20km 后心率直接飙到 185+,最后也是咬着牙走跑结合完赛。

三次比赛说明同一个问题:赛前训练不系统,跑量和训练结构都不合理。之前就是瞎跑,看心情出门,没有 VDOT 概念,没有配速区间,不知道自己该跑 E 轻松还是 T 阈值。

雪上加霜的是 2025 年因为工作原因停跑了整整一年。

2026 年 4 月重新开始恢复跑,加入了乐跑运动俱乐部跟着系统训练。这时候我发现市面上的跑步 APP 都有一个共同问题:它们擅长记录,不擅长指导。

Keep、悦跑圈、咕咚能帮我记录每次跑步的配速、心率、路线。但我真正需要的是:

  • 我的 VDOT 是多少? 这个能力指数应该决定我该用什么配速区间训练。
  • 按 Daniels 训练法,16 周全马备赛计划应该怎么排?
  • 目标赛事的赛道有多少上坡?35km 那个连续爬坡应该放慢多少秒?
  • 配速、步频、步幅之间是什么数学关系?我的步频是不是太低了?
  • 跟乐跑的跑友想约周末长距离,怎么组织报名?

这些问题没有一个 APP 能同时解决。所以我跟着乐跑系统训练的时候,先给自己写了这个工具——帮我算 VDOT、生成 16 周 Daniels 计划、算配速策略。跑团里其他朋友觉得好用,就陆续提了跑团管理、赛道攻略、步态换算等需求。

于是就有了赛迹攻略。

2026 年的目标很简单:半马小幅刷新 PB 到 1:38 左右,全马小幅刷新 PB 到 3:30 左右。不追求大跨步,科学训练,稳步提升。

为什么选 Taro

技术选型的核心考量是:不想被微信生态绑死。

1
技术栈:Taro 4 + React 18 + TypeScript + 微信云开发

Taro 的设计哲学是「一处代码,多端运行」。写 React,编译出来能跑:

  • 微信小程序 ✅(现在的主力)
  • H5 网页 ✅(已支持)
  • React Native(iOS / Android)✅(已支持)
  • 鸿蒙 HarmonyOS NEXT ✅(Taro 4.x 原生支持)
  • 支付宝 / 抖音小程序 ✅(已支持)

这个选择让我从第一天起就可以安心写业务逻辑,不用纠结以后要不要重写。微信云开发选的是纯 Node.js 环境——和我平时写后端的技术栈一致,没什么学习成本。

功能一:步态工具

这是我自己每天都在用的功能。

1
配速(秒/km)= 60000 ÷(步频 × 单步步长)

任意两个变量就能算出第三个。输入 5’00” 配速 + 180 步/分钟 → 步幅自动算出来是 1.11 米。输入配速 + 步幅 → 算出建议步频。

更进一步,我加了「保持配速,调整步频」的模拟:

1
2
配速 5'00",步频 160 → 步幅 1.25m
同配速,步频 +10%(176)→ 步幅自动缩短到 1.14m

这直接回答了「我想提高步频但不想变慢怎么办」—— 答案就是缩短步幅,这是数学决定的,不是玄学。

还有触地时间和腾空时间的估算模型。输入步频和触地时间 → 算出每次腾空多久、触地占步态周期的比例。公式参考 Garmin Running Dynamics Pod 说明书和一篇 2021 年的研究(PMID 20581720)。

功能二:Daniels VDOT 训练计划

Jack Daniels 博士的 VDOT 体系是跑步科学的基石。VDOT(V˙O₂ max derived running pace)把你的耐力能力量化成一个数字(30~80),每个 VDOT 对应五个配速区间:

区间 名称 用途
E 轻松跑 基础有氧、恢复
M 配速跑 模拟目标配速、提高经济性
T 阈值跑 提高乳酸阈值
I 间歇跑 提高 VO2 max
R 重复跑 提高速度

我的小程序做的事很简单:输入你的最近一场比赛成绩 → 用运动科学论文里的公式算出 VDOT → 自动生成 16 周训练计划。

训练计划每周自动排好:周一休息、周二 E 轻松、周三 I 间歇、周四休息、周五 T 阈值、周六 M 配速、周日休息。赛前两周 taper 自动减量到 40%。

功能三:配速策略

输入目标完赛时间和目标赛事 → 输出逐公里配速计划。

比普通配速计算器多两个关键修正:

坡度修正:如果赛道 35km 处有 1.2km 连续上坡 → 那一段配速自动加 10 秒。
天气修正:赛前 3 天自动拉取赛事当地天气。如果 30°C 高温 → 全程配速加 15 秒。
耐力衰减系数:根据你的历史完赛记录和近 30 天训练配速,算出后程掉速能力,自动调整 30km 以后的配速目标。

功能四:赛道可视化

用 Canvas 把海拔剖面和路线图画出来。

  • 海拔剖面:红色是上坡、绿色是下坡、灰色是平坦,拖动可以看每公里的海拔和均坡
  • 路线图:经纬度投影到 Canvas,标记起点、终点、补给站、方向箭头、公里标记
  • 赛道难度评分:基于总爬升 + 平均坡度 + 最陡段综合打分

功能五:跑团管理

小程序有完整的跑团套件:

  • 创建 / 加入跑团(邀请码)
  • 角色体系(团长 / 副团长 / 普通成员,副团长数量无上限)
  • 活动发布(配速分组:轻松 / 节奏 / 阈值)
  • 活动备注(”只跑前 10 公里”)
  • 可信度积分体系(每日登录 +1、完赛 +2、训练 +1、推荐 +2、点赞 +1,防刷)
  • 跑团排行榜、跑团地图、完赛自动汇总

架构上的几个决定

云函数模块化:一开始我把所有逻辑塞在一个 500 行的 user/index.js 里。后来拆成了 9 个子模块(group-service / group-logic / event-logic / finish-records 等)+ 7 个单元测试文件。

Services 统一调用层:services/cloud.ts 封装了云函数调用。微信环境走 Taro.cloud.callFunction,H5 环境自动 import '../data/${name}' 从本地 mock 数据导入 —— 前端开发不需要依赖真实云函数。

功能开关前置:config/features.ts 里把积分、会员、广告全部默认关闭。这样先上功能,按需开启。

分包:主包控制在 660KB(微信限制 1.5MB),其余 21 个页面分包按需下载。

踩过的坑

鸿蒙剪贴板权限:HarmonyOS NEXT 系统级管控了剪贴板权限,默认不允许微信小程序访问。用户需要手动去「设置 → 隐私 → 剪贴板」里允许微信。代码层面没什么好办法,只能提供长按选中文本手动复制的兜底路径。

getFuzzyLocation 未授权:这个坑我掉了两次。微信 2023 年起位置 API 需要三重配置:代码里写 permission、后台配隐私协议、申请接口权限并发布版本。缺了哪一层都报 -80424。

分包主包大小:比赛详情页逻辑多(海拔图 + 地图 + 推荐 + 跑团关联),单独就 600KB。必须分包,不然主包超 1.5MB 限制上传失败。

怎么用

微信小程序搜「赛迹攻略」。

开发者微信:LIFELIVELEARN

有任何功能建议、赛道数据错误、想合作推广,都欢迎加我微信聊。


本文首发于 Bit Garden — 一个全栈工程师的个人笔记。

移动端与桌面端跨平台开发

按「概念 → 原理 → 源码入口 → 示例 → 案例」串起来,覆盖移动端与桌面端常见方案,并包含以 Web/React 技术辐射多端(含 Taro 等小程序方案)与桌面端 Electron 等典型选型,方便对照。

一、基本概念

1.1 什么是跨平台开发?

跨平台开发指用一套(或高度共享的)代码,同时支持多个操作系统或设备形态(如 iOS、Android、macOS、Windows、Linux、Web),从而降低开发与维护成本、加快迭代速度。

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────────────────┐
│ 跨平台开发的核心目标 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 一套 / 共享代码 ──► 多端运行 ──► 降低人力、统一体验、快速发布 │
│ │
│ 移动端:iOS + Android(+ 鸿蒙 / 其他) │
│ 桌面端:macOS + Windows + Linux │
│ 大前端:Web + 移动 + 桌面(部分方案可三端复用) │
│ │
└─────────────────────────────────────────────────────────────────────────┘

1.2 移动端跨平台 vs 桌面端跨平台

维度 移动端跨平台 桌面端跨平台
目标平台 iOS、Android、鸿蒙等 Windows、macOS、Linux
交互特点 触屏、手势、传感器、多分辨率 键鼠、窗口、菜单、多显示器
分发方式 App Store、应用市场、企业内部分发 安装包、商店、便携版、包管理器
性能关注 电量、内存、启动速度、流畅度 内存、CPU、GPU、安装体积
典型方案 Flutter、React Native、Kotlin Multiplatform Electron、Tauri、Flutter Desktop、Qt

1.3 为什么需要跨平台?

痛点 说明 跨平台带来的价值
多套原生实现 iOS (Swift/ObjC)、Android (Kotlin/Java) 各写一套 一套业务逻辑 + UI,多端复用
人力与节奏 双端/三端并行,需求同步、发版协调成本高 统一技术栈,一次开发多端发布
体验一致性 各端实现细节不同,交互与视觉易分裂 可控的 UI 与交互一致性(尤其自绘方案)
桌面端同样分裂 Win/Mac/Linux 三套原生或 Web 各搞一套 一套代码覆盖主流桌面系统

二、跨平台的核心原理

2.1 三种主流渲染思路

跨平台方案按「谁来画 UI」可分为三类:

1
2
3
4
5
6
7
8
9
10
11
12
┌────────────────────────────────────────────────────────────────────────────┐
│ 方式 │ 谁负责渲染? │ 代表技术 │
├────────────────────────────────────────────────────────────────────────────┤
│ 原生控件映射 │ 使用系统原生控件 │ React Native、Xamarin、Compose │
│ (Native Mapping) │ 通过桥接更新属性 │ Multiplatform │
├────────────────────────────────────────────────────────────────────────────┤
│ Web 容器 │ 内嵌 WebView/浏览器 │ Cordova、Capacitor、Electron │
│ (WebView/Chromium)│ 用 HTML/CSS/JS 画 │ (Electron 用 Chromium 做桌面壳) │
├────────────────────────────────────────────────────────────────────────────┤
│ 自绘 (Skia/Impeller)│ 自己画像素/矢量 │ Flutter、Qt、.NET MAUI 部分 │
│ (Self-draw) │ 不依赖系统控件 │ │
└────────────────────────────────────────────────────────────────────────────┘
  • 原生控件映射:体验最接近系统原生,但受限于各端控件能力与差异,需要处理平台差异。
  • Web 容器:开发效率高、生态成熟,桌面端 Electron 安装体积与内存占用较大。
  • 自绘:UI 一致性强、不依赖系统控件,可精细控制渲染;需要自己实现可访问性、输入法等。

2.2 架构共性:桥接与运行时

无论哪种方式,跨平台层都要和「宿主平台」通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────────────┐
│ 跨平台层(业务 + UI 描述) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Dart / JavaScript / C# / Kotlin 等 │ │
│ │ 组件树 / 虚拟 DOM / 声明式 UI │ │
│ └────────────────────────────┬────────────────────────────────────┘ │
│ │ 桥接层 (Bridge / Channel / FFI) │
├───────────────────────────────┼─────────────────────────────────────────┤
│ 宿主 / 原生层 ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Native (Swift/Kotlin/C++/Rust) + 系统 API + 原生控件/自绘引擎 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
  • 移动端:通常通过 Bridge / Channel(如 React Native 的 Bridge、Flutter 的 Platform Channel)做异步通信。
  • 桌面端:Electron 是 主进程 + 渲染进程 的 IPC;Tauri 是 Web 前端 + Rust 核心 的 IPC/FFI,体积更小。

2.3 技术选型简表

方案 类型 移动端 桌面端 语言/技术 特点摘要
Flutter 自绘 ✅ iOS/Android/鸿蒙 ✅ Win/Mac/Linux Dart + Skia/Impeller 一致性强、性能好、桌面逐渐成熟
React Native 原生映射 ✅ iOS/Android ❌ 需另选桌面方案 JS/TS + 原生桥 生态大、热更新友好
Electron Web 容器 ❌ ✅ Win/Mac/Linux HTML/CSS/JS + Node + Chromium 桌面占主导、包体大
Tauri Web + 原生壳 实验性 ✅ Win/Mac/Linux 前端任意 + Rust 轻量、安全、系统集成好
Kotlin Multiplatform 原生映射/共享逻辑 ✅ 主打 可共享逻辑 Kotlin 逻辑共享、UI 仍多端各自实现
Capacitor / Cordova WebView ✅ 可做桌面壳 Web 技术 用 Web 开发,打包成 App

三、代表性框架原理与源码要点

3.1 Flutter:自绘引擎与 Dart 层

3.1.1 整体架构

Flutter 的 UI 不依赖系统控件,由 Skia(或 Impeller on iOS)在画布上自绘,因此各端视觉与行为高度一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────────────────────────────────────────────────┐
│ Dart 层 │
│ Widget 树 → Element 树 → RenderObject 树(布局与绘制) │
└────────────────────────────┬─────────────────────────────────────┘
│ Dart VM / AOT 编译
┌────────────────────────────▼─────────────────────────────────────┐
│ Flutter Engine (C++) │
│ Layer 树 → Scene → Skia/Impeller 绘制 → 显示 │
└────────────────────────────┬─────────────────────────────────────┘
│ 平台嵌入层 (Embedder)
┌────────────────────────────▼─────────────────────────────────────┐
│ Android (SurfaceView) / iOS (Metal) / Windows (ANGLE) / ... │
└──────────────────────────────────────────────────────────────────┘

3.1.2 关键源码位置(概念性)

  • Widget → Element → RenderObject:flutter/lib/src/widgets/framework.dart、rendering/object.dart。
    Element 持有 RenderObject,RenderObject 负责 layout、paint,最终生成 Layer 提交给引擎。
  • 绘制入口:RenderView 的 compositeFrame() 将 Layer 树提交给 Window.render(),引擎再交给 Skia/Impeller。
  • 平台通道:PlatformChannel 在 flutter/lib/src/services/platform_channel.dart,Dart 与原生通过 BinaryMessenger 收发序列化消息。

理解「Widget 不可变 → Element 挂载/更新 → RenderObject 布局绘制」这条链路,就抓住了 Flutter UI 的核心。

3.2 React Native:桥接与原生组件

3.2.1 新架构(Fabric + TurboModules)与旧桥

  • 旧架构:JS 与原生通过 Bridge 异步传 JSON,原生侧用 UIManager 把「虚拟节点」转成原生 View。
  • 新架构:
    • Fabric:C++的渲染器,Shadow 树在 C++ 中,减少跨桥次数,支持同步布局与优先级。
    • TurboModules:按需加载的 JSI 原生模块,可同步调用,不再全部在启动时塞进 Bridge。
1
2
3
4
5
6
7
8
9
┌─────────────┐     JSI (JavaScript Interface)      ┌─────────────────┐
│ JavaScript │ ◄──── 同步/异步调用 ─────────────► │ C++ Fabric / │
│ React 组件 │ (新架构) │ TurboModules │
└─────────────┘ └────────┬────────┘
│
┌─────────────┐ Bridge (旧) / 序列化消息 ┌───────▼───────┐
│ Metro 打包 │ ◄──────────────────────────────► │ Native Views │
│ JS Bundle │ │ (Android/iOS)│
└─────────────┘ └───────────────┘

3.2.2 源码可读入口(概念性)

  • React 组件到原生:新架构下 ReactFabric、FabricUIManager 等(C++),旧架构下 UIManagerModule(Java/ObjC)把「节点树」转成原生 View。
  • 通信:NativeModules、NativeEventEmitter 对应到原生模块与事件发送,新架构下通过 JSI 直接调 C++ 再调原生。

3.3 Electron:多进程与 IPC

3.3.1 主进程 + 渲染进程

Electron 基于 Chromium,每个窗口是一个渲染进程,主进程负责生命周期、系统 API、原生菜单等。

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────────────────┐
│ Main Process (Node.js) │
│ BrowserWindow、app、Menu、系统 API、原生模块 (.node) │
└────────────────────────────┬────────────────────────────────────────────┘
│ IPC (ipcMain / ipcRenderer)
│ contextBridge.exposeInMainWorld 安全暴露 API
└────────────────────────────┬────────────────────────────────────────────┘
┌────────────────────────────▼─────────────────────────────────────────────┐
│ Renderer Process (Chromium) │
│ HTML / CSS / JS,类似前端 SPA;可禁用 Node 仅用 preload 暴露能力 │
└─────────────────────────────────────────────────────────────────────────┘

3.3.2 安全与 contextBridge

  • 不要在渲染进程直接开 nodeIntegration: true 并把整个 Node 暴露给页面。
  • 推荐:用 contextBridge.exposeInMainWorld 在 preload 里暴露有限 API,主进程通过 ipcMain 处理,渲染进程只调这些 API。

源码层面:lib/renderer/api/context-bridge.ts、lib/main/api/ipc-main.ts 等,理解「preload 注入 → contextBridge → ipcRenderer.invoke」这条链路即可。

3.4 Tauri:Rust 核心 + 前端

3.4.1 轻量从何而来

Tauri 不内嵌完整 Chromium,而是用 系统 WebView(Windows: WebView2,macOS: WKWebView,Linux: WebKitGTK),所以安装包小、内存占用低。

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────────────────────────────┐
│ Frontend (任意: React / Vue / Svelte / 纯 HTML) │
│ 运行在系统 WebView 中 │
└────────────────────────────┬────────────────────────────────────────────┘
│ Tauri API (invoke / event)
┌────────────────────────────▼─────────────────────────────────────────────┐
│ Tauri Core (Rust) │
│ 窗口、菜单、系统托盘、文件与 shell、插件;可调用任意 Rust 与系统 API │
└─────────────────────────────────────────────────────────────────────────┘

3.4.2 命令与安全

  • 前端通过 invoke('command_name', { ... }) 调用 Rust 端在 #[tauri::command] 里定义的函数。
  • 权限与能力在 tauri.conf.json 的 capabilities 中声明,避免前端随意调敏感 API。
    源码可看 tauri/src/manager.rs、tauri/src/app.rs 以及命令派发与 IPC 的实现。

四、示例代码

4.1 Flutter:一个跨移动端 + 桌面的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 同一套代码可跑在 iOS、Android、Windows、macOS、Linux
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cross-Platform Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter 跨平台')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点击次数: $_counter', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => setState(() => _counter++),
icon: const Icon(Icons.add),
label: const Text('增加'),
),
],
),
),
);
}
}
  • 移动端:flutter run 选 iOS/Android 设备即可。
  • 桌面端:flutter run -d windows / macos / linux(需先 flutter config --enable-*desktop)。

4.2 React Native:一个带原生能力的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 跨 iOS + Android,调用原生模块示例
import { NativeModules, Platform, StyleSheet, Text, View, Button } from 'react-native';

const { MyNativeModule } = NativeModules; // 需在原生侧实现 MyNativeModule

export default function App() {
const [result, setResult] = React.useState('');

const callNative = async () => {
try {
const value = await MyNativeModule?.getDeviceId?.() ?? '未实现';
setResult(value);
} catch (e) {
setResult(String(e));
}
};

return (
<View style={styles.container}>
<Text style={styles.text}>平台: {Platform.OS}</Text>
<Button title="调用原生 getDeviceId" onPress={callNative} />
<Text style={styles.result}>{result}</Text>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
text: { fontSize: 16, marginBottom: 12 },
result: { marginTop: 12, color: '#333' },
});

4.3 Electron:主进程与渲染进程通信

1
2
3
4
5
6
7
// preload.js(运行在隔离上下文中,通过 contextBridge 暴露)
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
openFolder: (path) => ipcRenderer.invoke('dialog:openFolder', path),
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.js(主进程)
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');

function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
},
});
win.loadFile('index.html');
}

app.whenReady().then(() => {
ipcMain.handle('get-app-version', () => app.getVersion());
ipcMain.handle('dialog:openFolder', (_, dir) =>
dialog.showOpenDialog({ properties: ['openDirectory'] })
);
createWindow();
});
1
2
3
4
5
6
7
8
9
<!-- 渲染进程 (index.html 里的脚本) -->
<button id="version">获取版本</button>
<div id="out"></div>
<script>
document.getElementById('version').onclick = async () => {
const v = await window.electronAPI.getAppVersion();
document.getElementById('out').textContent = 'Version: ' + v;
};
</script>

4.4 Tauri:前端调用 Rust 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! (from Rust)", name)
}

fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 前端 (如 React)
import { invoke } from '@tauri-apps/api/core';

function App() {
const [msg, setMsg] = useState('');

const sayHello = async () => {
const result = await invoke('greet', { name: 'Tauri' });
setMsg(result);
};

return (
<div>
<button onClick={sayHello}>Say Hello</button>
<p>{msg}</p>
</div>
);
}

五、实际项目中的应用案例

5.1 移动端跨平台

产品/项目 方案 说明
阿里巴巴闲鱼 Flutter 部分核心页面用 Flutter,与原生混编,统一商品与互动体验。
腾讯微信 多端自研 + 部分 RN 核心为原生,部分业务用自研或 RN 类方案做动态化与跨端。
字节抖音 / 飞书 Flutter / 自研 部分模块 Flutter,桌面端 Electron(如飞书)。
美团 React Native 部分 App 内页用 RN,热更新与双端复用。
Reflectly Flutter 日记/习惯类 App,全 Flutter,iOS/Android 一套 UI。
BMW 车载 Flutter 车载中控 UI 使用 Flutter 做多车型统一界面。

5.2 桌面端跨平台

产品/项目 方案 说明
VS Code Electron 编辑器 UI + 扩展用 Web 技术,主进程 Node,跨 Win/Mac/Linux。
Slack / Discord Electron 桌面客户端统一用 Web 栈,一套代码多端。
Figma Desktop Electron 设计工具桌面版,与 Web 共享核心逻辑与渲染。
1Password 8 Electron 密码管理桌面端,跨平台一致体验。
Clash Verge / 部分工具 Tauri 需要小体积、低内存的桌面工具,用 Tauri 替代 Electron。
Appflowy Flutter 桌面端用 Flutter,与移动端共享部分 UI 与逻辑。

5.3 移动 + 桌面 统一

产品/项目 移动端 桌面端 说明
Flutter 官方 Flutter (iOS/Android) Flutter (Win/Mac/Linux) 同一套 Dart 代码,多端运行,适合重 UI 一致性的产品。
飞书 原生 + 混合 Electron 移动端以原生为主,桌面端 Electron,部分逻辑与 UI 复用。
Notion 原生 / WebView Electron 桌面 Electron,移动端混合,内容与逻辑复用。

5.4 选型时可参考的维度

  • 团队技能:前端强可选 RN/Electron/Tauri;能接受 Dart 可选 Flutter 全平台。
  • 体验要求:要极致接近系统原生 → 原生映射(RN)或原生开发;要强一致性、可控渲染 → Flutter/自绘。
  • 桌面包体与内存:对体积和内存敏感 → Tauri 或 Flutter Desktop;优先生态与成熟度 → Electron。
  • 热更新与合规:移动端需热更新且符合商店政策时,需评估各方案的热更与审核风险。
  • 已有资产:已有 Web 或 React 技术栈,可优先 RN/Electron/Capacitor;已有 Rust/系统开发,可考虑 Tauri。

六、小结

  • 概念:跨平台开发用一套(或共享)代码覆盖多端,降低成本、统一体验;移动端与桌面端在平台特性、交互、分发上不同,但「桥接 + 运行时」的架构思想相通。
  • 原理:三种主要思路——原生控件映射、Web 容器、自绘;理解各方案的渲染模型与桥接方式,有助于选型和排坑。
  • 源码:Flutter 的 Widget/Element/RenderObject 与引擎、RN 的 Fabric/TurboModules、Electron 的 IPC 与 contextBridge、Tauri 的 Rust 命令与系统 WebView,是深入时的好入口。
  • 示例:同一套 Flutter 可跑移动+桌面;RN 通过 NativeModules 调原生;Electron 用 preload + contextBridge + ipcMain;Tauri 用 invoke 调 Rust 命令。
  • 实战:大量商业产品已在移动端(Flutter/RN)和桌面端(Electron/Tauri/Flutter)落地,选型时结合团队、体验、体积、热更新与既有技术栈综合权衡。

之后若往下挖,可以按各节给的入口去读引擎/桥接源码;选型时把团队栈、体验目标、包体与内存、热更新与审核几条摆桌上逐项对照即可。