主题
跨端开发
为什么需要跨端
在移动互联网时代,一个产品往往需要同时覆盖多个平台:iOS App、Android App、微信小程序、支付宝小程序、抖音小程序、H5 网页、PC 桌面应用……如果每个平台都用原生技术独立开发,成本会成倍增长。跨端开发的核心目标就是——一套代码(或尽量少的适配),运行在多个平台上。
跨端开发的理想目标
┌──────────────────────────────────────────────────┐
│ │
│ 一套核心业务代码 │
│ │
└─────┬────────┬────────┬────────┬────────┬────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ iOS │ │ 安卓 │ │ 小程序│ │ H5 │ │ 桌面 │
│ App │ │ App │ │ │ │ │ │ App │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘但现实中,"Write Once, Run Anywhere" 只是理想。各端在渲染能力、系统 API、性能特征上存在根本差异,真正的跨端开发需要在开发效率和用户体验之间寻找平衡点。
一、跨端方案概览
四端差异全景
| 维度 | Web (H5) | 小程序 | 原生 App | 桌面应用 |
|---|---|---|---|---|
| 渲染引擎 | 浏览器内核 (Blink/WebKit) | WebView + 原生组件混合 | 系统原生 UI / 自绘引擎 | Chromium / 系统 WebView / 自绘 |
| 编程语言 | JS/TS + HTML + CSS | JS/TS (受限子集) | Swift/Kotlin/Dart/JS | JS/TS + Rust + 原生语言 |
| API 能力 | 浏览器 API (受沙箱限制) | 宿主 App 提供的 API | 系统 API 全量访问 | 系统 API + Node.js API |
| 性能上限 | 受浏览器引擎限制 | 受 WebView 限制 | 接近系统极限 | 取决于方案 |
| 分发方式 | URL 即分发 | 平台内搜索/扫码 | 应用商店 | 安装包/应用商店 |
| 审核机制 | 无审核 | 平台审核 | 应用商店审核 | 较宽松 |
| 热更新 | 天然支持 | 天然支持 | 受限 (需要特殊方案) | 天然支持 |
跨端技术方案分类
根据实现原理,跨端方案可以分为四大类:
┌─────────────────────────────────────────────────────────────────┐
│ 跨端技术方案分类 │
├─────────────┬──────────────┬──────────────┬─────────────────────┤
│ │ │ │ │
│ Hybrid │ 编译时转换 │ 运行时抹平 │ 自绘引擎 │
│ (WebView) │ │ │ │
│ │ │ │ │
│ Cordova │ Taro 1/2 │ Taro 3 │ Flutter │
│ Ionic │ uni-app │ React Native│ Qt │
│ 微信 H5 │ (编译模式) │ Weex │ Compose │
│ │ │ │ Multiplatform │
│ │ │ │ │
│ 原理: │ 原理: │ 原理: │ 原理: │
│ WebView │ AST 转换 │ JS 驱动原生 │ 自带渲染引擎 │
│ 加载 H5 │ 源码→目标 │ 组件渲染 │ 直接绘制像素 │
│ 页面 │ 平台代码 │ │ │
└─────────────┴──────────────┴──────────────┴─────────────────────┘1. Hybrid (WebView 方案)
最朴素的跨端方案——用 WebView 嵌套 H5 页面,通过 JSBridge 调用原生能力。
┌─────────────────────────────────────┐
│ 原生 App 容器 │
│ ┌───────────────────────────────┐ │
│ │ WebView │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ H5 页面 │ │ │
│ │ │ │ │ │
│ │ │ JS ←──JSBridge──→ │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ 原生 API 层 (相机/定位/支付) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘JSBridge 的实现原理:
javascript
const JSBridge = {
callbackMap: new Map(),
callbackId: 0,
call(method, params) {
return new Promise((resolve, reject) => {
const id = ++this.callbackId;
this.callbackMap.set(id, { resolve, reject });
const message = JSON.stringify({ method, params, callbackId: id });
if (window.webkit?.messageHandlers?.nativeBridge) {
window.webkit.messageHandlers.nativeBridge.postMessage(message);
} else if (window.NativeBridge) {
window.NativeBridge.postMessage(message);
} else {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = `jsbridge://${method}?params=${encodeURIComponent(message)}`;
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 0);
}
});
},
onCallback(callbackId, result) {
const callback = this.callbackMap.get(callbackId);
if (callback) {
callback.resolve(result);
this.callbackMap.delete(callbackId);
}
}
};2. 编译时转换
通过 AST 解析源代码,将一种 DSL(如 React JSX / Vue Template)编译为各平台的目标代码。
编译时转换流程
┌──────────────┐ ┌──────────┐ ┌──────────────────┐
│ React JSX │ │ │ │ 微信小程序 WXML │
│ 或 │───▶│ 编译器 │───▶│ 支付宝 AXML │
│ Vue Template │ │ (AST │ │ 抖音 TTML │
│ │ │ 转换) │ │ H5 HTML │
└──────────────┘ └──────────┘ └──────────────────┘
具体过程:
Source Code → Parse → AST → Transform → Target AST → Generate → Target Code3. 运行时抹平
在各端实现一套统一的运行时,JS 代码在运行时通过这套运行时与原生交互。
运行时抹平架构
┌──────────────────────────────────────────┐
│ JavaScript 业务代码 │
└─────────────────────┬────────────────────┘
│
┌─────────────────────▼────────────────────┐
│ 运行时适配层 │
│ (统一的组件 / API / 生命周期 抽象) │
└─────┬──────────┬──────────┬──────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌──────────┐
│ 微信小程序│ │ H5 │ │ RN │
│ 运行时 │ │ 运行时 │ │ 运行时 │
└──────────┘ └────────┘ └──────────┘4. 自绘引擎
自带渲染引擎,跳过系统原生 UI 框架,直接操控 GPU 绘制像素。
自绘引擎架构 (以 Flutter 为例)
┌──────────────────────────────────────────┐
│ Dart 业务代码 │
└─────────────────────┬────────────────────┘
│
┌─────────────────────▼────────────────────┐
│ Flutter Framework │
│ (Widget / Rendering / Painting) │
└─────────────────────┬────────────────────┘
│
┌─────────────────────▼────────────────────┐
│ Skia / Impeller 渲染引擎 │
│ (C++ 实现, 直接操控 GPU) │
└─────────────────────┬────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌────────┐ ┌────────┐
│ iOS │ │ Android│
│ GPU │ │ GPU │
└────────┘ └────────┘二、React Native
核心思想
React Native 的核心理念是 "Learn Once, Write Anywhere"——使用 React 的编程模型,通过 JavaScript 驱动原生组件渲染。注意,它不是 "Write Once, Run Anywhere",而是承认各平台的差异,允许开发者在需要时写平台特定代码。
旧架构:三线程 + Bridge
React Native 旧架构由三个线程和一座 Bridge 组成:
React Native 旧架构
┌─────────────────────────────────────────────────────────┐
│ │
│ JS Thread Bridge UI Thread │
│ ┌──────────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ │ │ │ │ │ │
│ │ React 组件 │ │ JSON 序列化│ │ 原生视图 │ │
│ │ 状态管理 │───▶│ 异步消息 │───▶│ UIManager │ │
│ │ 业务逻辑 │ │ 队列传递 │ │ 布局计算 │ │
│ │ │◀───│ │◀───│ │ │
│ │ (Hermes/ │ │ │ │ (iOS: │ │
│ │ JavaSC) │ │ │ │ UIKit │ │
│ │ │ │ │ │ Android: │ │
│ └──────────────┘ └────────────┘ │ Android │ │
│ │ View) │ │
│ Shadow Thread └─────────────┘ │
│ ┌──────────────┐ │
│ │ Yoga 布局 │ │
│ │ 引擎 │ │
│ │ (Flexbox │ │
│ │ 计算) │ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘旧架构的数据流:
用户交互 (触摸)
│
▼
UI Thread 捕获事件
│
▼ (序列化为 JSON)
Bridge 传递到 JS Thread
│
▼
JS Thread 处理事件, React 重新渲染
│
▼ (序列化 UI 操作为 JSON)
Bridge 传递到 UI Thread
│
▼
UI Thread 更新原生视图
│
▼
Shadow Thread (Yoga) 重新计算布局
│
▼
屏幕更新旧架构的核心问题:
问题 1: Bridge 是异步的、序列化的瓶颈
JS Thread Bridge UI Thread
│ │ │
│── 消息1 (JSON) ──────▶ │ │
│── 消息2 (JSON) ──────▶ │── 消息1 ────────────▶ │
│── 消息3 (JSON) ──────▶ │── 消息2 ────────────▶ │
│ │── 消息3 ────────────▶ │
│ │ │
大量消息排队导致: │
- 动画卡顿 │
- 手势响应延迟 │
- 列表滚动掉帧 │
问题 2: 所有数据必须 JSON 序列化/反序列化
{ type: "View", props: { style: { flex: 1 } } }
每次都要 JSON.stringify + JSON.parse
高频操作时性能开销巨大新架构 (New Architecture)
React Native 新架构从根本上重新设计了 JS 与原生的通信方式,用四大核心模块替代了旧的 Bridge:
React Native 新架构
┌──────────────────────────────────────────────────────────────┐
│ │
│ JS Thread UI Thread │
│ ┌──────────────┐ ┌─────────────┐ │
│ │ React 组件 │ │ Fabric │ │
│ │ (React 18) │ │ Renderer │ │
│ │ │◀═══════ JSI ═══════════▶│ │ │
│ │ Hermes │ (同步调用, │ 原生视图 │ │
│ │ Engine │ 无需序列化, │ 树管理 │ │
│ │ │ C++ 共享内存) │ │ │
│ └──────────────┘ └─────────────┘ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ TurboModules │ │ │
│ └─▶│ (懒加载原生模块, │◀─────────┘ │
│ │ 按需初始化, │ │
│ │ 类型安全) │ │
│ └─────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Codegen │ │
│ │ (编译时生成类型安全的 JS ↔ Native 接口代码) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘JSI (JavaScript Interface)
JSI 是新架构的基石,它是一个轻量的 C++ 层,允许 JavaScript 直接调用 C++ 对象的方法,无需通过 Bridge 的 JSON 序列化。
旧架构 Bridge 通信:
JS ──JSON.stringify──▶ Bridge Queue ──JSON.parse──▶ Native
~2ms ~5ms ~2ms
总耗时: ~9ms (异步)
新架构 JSI 通信:
JS ──直接引用 C++ HostObject──▶ Native
~0.01ms (同步)JSI 的工作原理:
cpp
class NativeModule : public jsi::HostObject {
jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name) override {
if (name.utf8(rt) == "getDeviceName") {
return jsi::Function::createFromHostFunction(
rt, name, 0,
[](jsi::Runtime& rt, const jsi::Value& thisVal,
const jsi::Value* args, size_t count) -> jsi::Value {
std::string deviceName = getDeviceNameFromNative();
return jsi::String::createFromUtf8(rt, deviceName);
}
);
}
return jsi::Value::undefined();
}
};在 JS 侧的使用:
javascript
const deviceName = global.nativeModule.getDeviceName();Fabric (新渲染器)
Fabric 替代了旧的 UIManager,可以直接通过 JSI 在 C++ 层创建 Shadow Tree,支持同步渲染和优先级调度。
Fabric 渲染流程:
1. React 渲染阶段 (JS Thread)
React Element Tree
│
▼
2. 创建 Shadow Tree (C++ 层, 通过 JSI)
Shadow Node Tree ←── 这一步在 C++ 中完成
│ 不再需要 Bridge
▼
3. Yoga 布局计算 (C++ 层)
Layout Tree
│
▼
4. 挂载阶段 (UI Thread)
创建/更新原生视图
关键改进:
- Shadow Tree 在 C++ 中创建, JS 和 Native 共享同一内存
- 支持同步渲染 (React 18 Concurrent Features)
- 支持多优先级渲染TurboModules
TurboModules 替代了旧的 Native Modules,核心改进是懒加载和类型安全。
旧架构 Native Modules:
App 启动 → 一次性注册所有 Native Modules → 占用大量内存和启动时间
启动时加载:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│Camera│ GPS │Share │ Push │ Pay │ ... │
│ │ │ │ │ │ x50 │
└──────┴──────┴──────┴──────┴──────┴──────┘
即使用户只用了 Camera, 其余 49 个也加载了
新架构 TurboModules:
App 启动 → 只注册 Module 的引用 → 首次调用时才初始化
启动时:
┌──────────────────────────────────────────┐
│ Module Registry (仅存储引用, 极轻量) │
└──────────────────────────────────────────┘
首次调用 Camera:
┌──────┐
│Camera│ ← 此时才真正初始化
└──────┘Codegen
Codegen 在编译时根据 JS/TS 类型定义自动生成原生接口代码,确保 JS 和 Native 之间的类型安全。
typescript
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getConstants(): {
brand: string;
model: string;
osVersion: string;
};
getBatteryLevel(): Promise<number>;
vibrate(pattern: number[]): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');Codegen 会根据这个 Spec 自动生成:
- iOS 端的 Objective-C++ 接口代码
- Android 端的 Java 接口代码
- C++ 的类型绑定代码
旧架构 vs 新架构对比
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| JS ↔ Native 通信 | Bridge (异步, JSON 序列化) | JSI (同步, C++ 直接调用) |
| 渲染器 | UIManager (异步) | Fabric (同步, C++ Shadow Tree) |
| 原生模块 | Native Modules (全量加载) | TurboModules (懒加载) |
| 类型安全 | 无 (运行时 JSON) | Codegen (编译时生成) |
| 动画性能 | 依赖 Bridge, 容易掉帧 | JSI 直接驱动, 流畅 |
| 启动速度 | 慢 (加载所有模块) | 快 (按需加载) |
| 内存占用 | 高 | 低 |
| React 18 支持 | 不支持 | 完整支持 (Concurrent) |
| 手势处理 | 异步, 有延迟 | 同步, 即时响应 |
Hermes 引擎
Hermes 是 Meta 专门为 React Native 优化的 JavaScript 引擎,与 V8/JavaScriptCore 的核心差异在于预编译。
传统 JS 引擎 (V8 / JavaScriptCore):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS 源码 │───▶│ 解析 │───▶│ 生成字节码│───▶│ 执行 │
│ (.js) │ │ (Parse) │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
App 运行时 App 运行时 App 运行时
~~~~~~~~~ 这些都发生在用户设备上 ~~~~~~~~~
Hermes 引擎:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ JS 源码 │───▶│ 解析 │───▶│ 生成字节码│ │ 执行 │
│ (.js) │ │ (Parse) │ │ (.hbc) │────────▶│ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
构建时(CI) 构建时(CI) App 运行时
~~~~~~~~~ 这些在构建时完成 ~~~~~~~~~
Hermes 优势:
- 启动时间: 减少 ~50% (跳过解析和编译)
- 内存占用: 减少 ~30%
- 包体积: 字节码比源码更紧凑
- TTI: 首次交互时间大幅缩短React Native 代码示例
一个典型的 React Native 组件:
tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
FlatList,
StyleSheet,
Platform,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
interface User {
id: string;
name: string;
avatar: string;
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
}
return (
<FlatList
data={users}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.item}
onPress={() => console.log(item.id)}
>
<Text style={styles.name}>{item.name}</Text>
</TouchableOpacity>
)}
style={styles.list}
/>
);
}
const styles = StyleSheet.create({
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
flex: 1,
backgroundColor: '#fff',
},
item: {
padding: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
android: {
elevation: 2,
},
}),
},
name: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
});
export default UserList;平台特定代码的组织方式:
src/
├── components/
│ ├── Button.tsx # 共享逻辑
│ ├── Button.ios.tsx # iOS 特定实现
│ ├── Button.android.tsx # Android 特定实现
│ └── Header/
│ ├── index.tsx
│ └── styles.ts
├── utils/
│ ├── storage.ts
│ ├── storage.native.ts # RN 特定
│ └── storage.web.ts # Web 特定三、Flutter(概念了解)
Dart 语言与自绘引擎
Flutter 采用了与 React Native 完全不同的思路——自绘引擎。它不使用系统原生 UI 组件,而是通过 Skia(现在逐步迁移到 Impeller)图形引擎直接在 Canvas 上绘制每一个像素。
Flutter 架构分层
┌──────────────────────────────────────────┐
│ Dart 业务代码 │
│ (Widget / State / Logic) │
├──────────────────────────────────────────┤
│ Flutter Framework │
│ ┌────────┐ ┌──────────┐ ┌───────────┐ │
│ │Material│ │ Cupertino│ │ Widgets │ │
│ │ Design │ │ Style │ │ Library │ │
│ └────────┘ └──────────┘ └───────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ Rendering Layer │ │
│ │ (布局 / 绘制 / 合成 / 手势) │ │
│ └──────────────────────────────────┘ │
├──────────────────────────────────────────┤
│ Flutter Engine │
│ ┌──────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Skia / │ │ Dart │ │ Platform │ │
│ │ Impeller │ │ Runtime │ │ Channels │ │
│ │ (C++) │ │ │ │ │ │
│ └──────────┘ └─────────┘ └──────────┘ │
├──────────────────────────────────────────┤
│ Platform (OS) │
│ iOS / Android / Web / Desktop │
└──────────────────────────────────────────┘Widget 树的三棵树
Flutter 有三棵关键的树,理解它们是理解 Flutter 性能优化的基础:
Widget Tree Element Tree RenderObject Tree
(声明式配置) (中间管理层) (真正的布局和绘制)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ MyApp │ │ MyApp │ │ │
│ Widget │───────▶│ Element │────────▶│ RenderBox│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼─────┐ ┌────▼─────┐ ┌───▼──────┐
│ Scaffold │ │ Scaffold │ │ │
│ Widget │───────▶│ Element │────────▶│ RenderBox│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼─────┐ ┌────▼─────┐ ┌───▼──────┐
│ Column │ │ Column │ │ Render │
│ Widget │───────▶│ Element │────────▶│ Flex │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
▼ ▼ ▼ ▼ ▼ ▼
Text Button Text Button RenderPara RenderBox
Widget Widget Element Element graph
Widget: 不可变, 轻量, 每帧都可能重建
Element: 可复用, 管理 Widget 和 RenderObject 的关联
RenderObject: 真正执行布局(layout)和绘制(paint)Flutter 代码示例
dart
import 'package:flutter/material.dart';
class UserListPage extends StatefulWidget {
const UserListPage({super.key});
@override
State<UserListPage> createState() => _UserListPageState();
}
class _UserListPageState extends State<UserListPage> {
List<Map<String, String>> users = [];
bool loading = true;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
users = [
{'id': '1', 'name': 'Alice'},
{'id': '2', 'name': 'Bob'},
{'id': '3', 'name': 'Charlie'},
];
loading = false;
});
}
@override
Widget build(BuildContext context) {
if (loading) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user['name'] ?? ''),
onTap: () => debugPrint(user['id']),
);
},
);
}
}React Native vs Flutter 核心差异
| 维度 | React Native | Flutter |
|---|---|---|
| 语言 | JavaScript / TypeScript | Dart |
| 渲染方式 | 映射到原生组件 | 自绘引擎 (Skia/Impeller) |
| UI 一致性 | 各平台原生外观 (不完全一致) | 各平台完全一致 |
| 生态 | npm 生态, 前端友好 | pub.dev, 独立生态 |
| 热重载 | 支持 (Fast Refresh) | 支持 (Hot Reload, 更快) |
| 包体积 | 较小 (~7-15MB) | 较大 (~10-20MB) |
| 性能 | 接近原生 (新架构后) | 接近原生 |
| 学习曲线 | 前端开发者友好 | 需要学习 Dart |
| 动画性能 | 新架构后大幅提升 | 天然优秀 (自绘引擎) |
| 原生集成 | 成熟, 丰富的原生模块 | Platform Channel |
| Web 支持 | react-native-web | 官方支持但体验一般 |
| 典型用户 | Meta, Shopify, Discord | Google, BMW, 阿里闲鱼 |
四、Taro / Uni-app
Taro 架构演进
Taro 是由京东凹凸实验室推出的多端统一开发框架,经历了三个大版本的架构演进:
Taro 架构演进
Taro 1.x / 2.x: 编译时方案
┌──────────┐ ┌──────────┐ ┌───────────────┐
│ React │ │ 编译器 │ │ wxml + wxss │
│ JSX │───▶│ (AST │───▶│ + wxs + json │
│ │ │ 转换) │ │ (微信小程序) │
└──────────┘ └──────────┘ └───────────────┘
Taro 3.x: 运行时方案
┌──────────┐ ┌──────────┐ ┌───────────────┐
│ React │ │ taro │ │ 小程序渲染 │
│ 或 Vue │───▶│ runtime │───▶│ (通过模板递归 │
│ 组件 │ │ (运行时 │ │ 动态渲染) │
│ │ │ 适配层) │ │ │
└──────────┘ └──────────┘ └───────────────┘编译时方案原理 (Taro 1.x / 2.x)
编译时方案的核心是 AST 转换——将 React JSX 语法转换为各平台的模板语法:
源码 (React JSX):
function Index() {
const [count, setCount] = useState(0)
return (
<View className="container">
<Text>{count}</Text>
<Button onClick={() => setCount(count + 1)}>+1</Button>
</View>
)
}
│ Taro 编译器 (AST Transform)
▼
微信小程序 WXML:
<view class="container">
<text>{{count}}</text>
<button bindtap="handleClick">+1</button>
</view>
微信小程序 JS:
Page({
data: { count: 0 },
handleClick() {
this.setData({ count: this.data.count + 1 })
}
})编译时方案的局限:
问题: JSX 表达力远超模板语法, 无法完整映射
以下 JSX 写法无法编译:
1. 动态组件
const Comp = condition ? CompA : CompB
return <Comp />
2. 高阶组件
return components.map(Comp => <Comp key={Comp.name} />)
3. 复杂的条件渲染
return renderContent?.() || <FallbackView />
编译器只能处理有限的 JSX 模式, 开发者需要遵循严格的写法限制运行时方案原理 (Taro 3.x)
Taro 3.x 采用运行时方案,核心思路是在小程序端实现一套精简的 DOM/BOM API,让 React/Vue 的运行时可以直接工作:
Taro 3.x 运行时架构
┌─────────────────────────────────────────────────┐
│ React / Vue 运行时 │
│ (不做任何修改, 原汁原味的框架运行时) │
└────────────────────┬────────────────────────────┘
│ 操作 DOM API
▼
┌─────────────────────────────────────────────────┐
│ taro-runtime (核心) │
│ │
│ 模拟 DOM API: │
│ ┌───────────────────────────────────────────┐ │
│ │ document.createElement() │ │
│ │ element.appendChild() │ │
│ │ element.setAttribute() │ │
│ │ element.addEventListener() │ │
│ │ ... │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 维护一棵虚拟 DOM 树: │
│ ┌──────┐ │
│ │ root │ │
│ └──┬───┘ │
│ ├── view │
│ │ ├── text │
│ │ └── button │
│ └── view │
│ └── image │
│ │
│ 当 DOM 变更时, 序列化为 data 传给小程序: │
│ setData({ root: serializedTree }) │
│ │
└────────────────────┬────────────────────────────┘
│ setData
▼
┌─────────────────────────────────────────────────┐
│ 小程序递归模板渲染 │
│ │
│ <template name="taro_tmpl"> │
│ <block wx:for="{{root.cn}}"> │
│ <template is="tmpl_0" data="{{item}}"/> │
│ </block> │
│ </template> │
│ │
│ <template name="tmpl_0"> │
│ <view wx:if="{{i.nn === 'view'}}"> │
│ <block wx:for="{{i.cn}}"> │
│ <template is="tmpl_1" data="{{item}}"/> │
│ </block> │
│ </view> │
│ <text wx:elif="{{i.nn === 'text'}}"> │
│ {{i.v}} │
│ </text> │
│ </template> │
│ │
└─────────────────────────────────────────────────┘Taro 3.x 代码示例:
tsx
import { useState } from 'react';
import { View, Text, Button, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
function Index() {
const [count, setCount] = useState(0);
const [list, setList] = useState<string[]>([]);
const handleAdd = () => {
setCount(prev => prev + 1);
setList(prev => [...prev, `Item ${prev.length + 1}`]);
};
const handleNavigate = () => {
Taro.navigateTo({ url: '/pages/detail/index' });
};
const handleShare = () => {
Taro.showShareMenu({ withShareTicket: true });
};
return (
<View className="container">
<Text className="title">Count: {count}</Text>
<Button onClick={handleAdd}>Add Item</Button>
<Button onClick={handleNavigate}>Go to Detail</Button>
{list.map((item, index) => (
<View key={index} className="item">
<Text>{item}</Text>
</View>
))}
</View>
);
}
export default Index;编译时 vs 运行时方案对比
| 维度 | 编译时方案 (Taro 1/2) | 运行时方案 (Taro 3) |
|---|---|---|
| 原理 | AST 源码转换 | 运行时模拟 DOM API |
| 框架支持 | 仅类 React 语法 | React / Vue / Preact 等 |
| 语法限制 | 严格 (JSX 受限) | 几乎无限制 |
| 性能 | 更高 (静态编译优化) | 略低 (运行时开销 + setData) |
| 包体积 | 更小 | 略大 (需要运行时) |
| 生态兼容 | 差 (很多库不支持) | 好 (可用大部分 React/Vue 库) |
| 调试体验 | 差 (源码与产物差异大) | 好 (源码接近产物) |
| 维护成本 | 高 (每个端要维护编译逻辑) | 低 (统一运行时) |
Uni-app
Uni-app 是由 DCloud 推出的基于 Vue 语法的跨端框架,支持编译到 H5、各家小程序和 App(通过 weex 改造的原生渲染引擎)。
Uni-app 架构
┌──────────────────────────────────────────┐
│ Vue SFC (.vue 文件) │
│ <template> + <script> + <style> │
└─────────────────────┬────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌──────────┐
│ 小程序 │ │ H5 │ │ App │
│ 编译器 │ │ 编译器 │ │ 编译器 │
└─────┬────┘ └───┬────┘ └────┬─────┘
▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌──────────┐
│ 微信/ │ │ SPA / │ │ 原生渲染 │
│ 支付宝/ │ │ 静态 │ │ (基于 │
│ 抖音等 │ │ 页面 │ │ weex) │
└──────────┘ └────────┘ └──────────┘小程序跨端的难点
即使有了 Taro / Uni-app 这样的跨端框架,小程序跨端开发仍然面临三大核心难题:
难点 1: API 差异
功能 微信小程序 支付宝小程序 抖音小程序
支付 wx.requestPayment() my.tradePay() tt.pay()
分享 wx.shareAppMessage() my.shareAppMessage() tt.shareAppMessage()
获取位置 wx.getLocation() my.getLocation() tt.getLocation()
存储 wx.setStorage() my.setStorage() tt.setStorage()
参数格式也不同:
微信: wx.setStorage({ key, data, success, fail })
支付宝: my.setStorage({ key, data, success, fail })
但返回值结构可能不一样!
难点 2: 组件差异
微信: <scroll-view scroll-y="{{true}}">
支付宝: <scroll-view scroll-y="{{true}}"> (属性名相同, 但行为可能不同)
抖音: <scroll-view scroll-y="{{true}}">
微信: <picker mode="date">
支付宝: <date-picker> (完全不同的组件!)
难点 3: 样式差异
微信小程序: rpx 单位, 部分 CSS3 不支持
支付宝小程序: rpx 单位, 但渲染细节有差异
抖音小程序: rpx 单位, Flex 布局行为略有不同
H5: rem/vw/vh, 完整 CSS 支持跨端 API 适配层的实现思路:
typescript
type PlatformType = 'weapp' | 'alipay' | 'tt' | 'h5';
interface StorageOptions {
key: string;
data: unknown;
}
const platformAdapters: Record<PlatformType, {
setStorage: (options: StorageOptions) => Promise<void>;
getStorage: (key: string) => Promise<unknown>;
}> = {
weapp: {
setStorage: (options) => {
return new Promise((resolve, reject) => {
wx.setStorage({ ...options, success: resolve, fail: reject });
});
},
getStorage: (key) => {
return new Promise((resolve, reject) => {
wx.getStorage({
key,
success: (res) => resolve(res.data),
fail: reject,
});
});
},
},
alipay: {
setStorage: (options) => {
return new Promise((resolve, reject) => {
my.setStorage({ ...options, success: resolve, fail: reject });
});
},
getStorage: (key) => {
return new Promise((resolve, reject) => {
my.getStorage({
key,
success: (res) => resolve(res.data),
fail: reject,
});
});
},
},
tt: {
setStorage: (options) => {
return new Promise((resolve, reject) => {
tt.setStorage({ ...options, success: resolve, fail: reject });
});
},
getStorage: (key) => {
return new Promise((resolve, reject) => {
tt.getStorage({
key,
success: (res) => resolve(res.data),
fail: reject,
});
});
},
},
h5: {
setStorage: async (options) => {
localStorage.setItem(options.key, JSON.stringify(options.data));
},
getStorage: async (key) => {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
},
},
};
function createStorage(platform: PlatformType) {
return platformAdapters[platform];
}五、Electron / Tauri
Electron 架构
Electron 让前端开发者能用 Web 技术(HTML + CSS + JS)构建桌面应用。它的架构基于 Chromium + Node.js 的双引擎模型。
Electron 架构
┌─────────────────────────────────────────────────────────┐
│ Electron App │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Main Process (主进程) │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ │ │ Node.js │ │ Electron │ │ 系统 API │ │ │
│ │ │ Runtime │ │ API │ │ (文件/网络/ │ │ │
│ │ │ │ │ (Menu, │ │ 通知/托盘) │ │ │
│ │ │ fs/path │ │ Dialog, │ │ │ │ │
│ │ │ child │ │ Tray) │ │ │ │ │
│ │ │ process │ │ │ │ │ │ │
│ │ └─────────┘ └──────────┘ └───────────────┘ │ │
│ │ │ │
│ └────────────┬────────────────┬───────────────────┘ │
│ │ IPC │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ Renderer │ │ Renderer │ │
│ │ Process 1 │ │ Process 2 │ │
│ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │Chromium │ │ │ │Chromium │ │ │
│ │ │ 内核 │ │ │ │ 内核 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ React/ │ │ │ │ Vue/ │ │ │
│ │ │ Vue App │ │ │ │ 其他页面│ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘主进程与渲染进程的分工
Main Process (一个):
├── 应用生命周期管理 (app.on('ready'), app.on('quit'))
├── 创建和管理窗口 (BrowserWindow)
├── 系统级 API 调用 (菜单/对话框/托盘/快捷键)
├── Node.js 完整能力 (文件读写/网络请求/子进程)
└── 与所有渲染进程通信的中枢
Renderer Process (多个, 每个窗口一个):
├── 一个完整的 Chromium 渲染引擎
├── 运行 Web 应用 (HTML + CSS + JS)
├── 默认没有 Node.js 能力 (安全考虑)
├── 通过 IPC 与主进程通信
└── 通过 preload 脚本暴露安全的 APIIPC 通信
IPC 通信模式:
1. 渲染进程 → 主进程 (单向)
Renderer ──ipcRenderer.send('channel', data)──▶ Main
│
ipcMain.on('channel')
2. 渲染进程 → 主进程 → 渲染进程 (双向)
Renderer ──ipcRenderer.invoke('channel', data)──▶ Main
◀───────── Promise<result> ───────────────── │
ipcMain.handle('channel')
3. 主进程 → 渲染进程
Main ──win.webContents.send('channel', data)──▶ Renderer
│
ipcRenderer.on('channel')Electron IPC 代码示例:
typescript
// main.ts
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile('index.html');
}
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openFile'],
filters: [{ name: 'Text', extensions: ['txt', 'md'] }],
});
if (result.canceled) return null;
const filePath = result.filePaths[0];
const content = fs.readFileSync(filePath, 'utf-8');
return { filePath, content };
});
ipcMain.handle('fs:writeFile', async (_event, filePath: string, content: string) => {
fs.writeFileSync(filePath, content, 'utf-8');
return true;
});
app.whenReady().then(createWindow);typescript
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('fs:writeFile', filePath, content),
onMenuAction: (callback: (action: string) => void) => {
ipcRenderer.on('menu:action', (_event, action) => callback(action));
},
});typescript
// renderer.ts
declare global {
interface Window {
electronAPI: {
openFile: () => Promise<{ filePath: string; content: string } | null>;
writeFile: (filePath: string, content: string) => Promise<boolean>;
onMenuAction: (callback: (action: string) => void) => void;
};
}
}
async function handleOpen() {
const result = await window.electronAPI.openFile();
if (result) {
document.getElementById('editor')!.textContent = result.content;
document.title = result.filePath;
}
}
async function handleSave() {
const content = document.getElementById('editor')!.textContent || '';
const filePath = document.title;
await window.electronAPI.writeFile(filePath, content);
}
window.electronAPI.onMenuAction((action) => {
switch (action) {
case 'open': handleOpen(); break;
case 'save': handleSave(); break;
}
});Tauri 架构
Tauri 是 Electron 的新一代替代品,用 Rust 替代 Node.js 作为后端,用系统 WebView 替代 Chromium,带来了显著的体积和性能优势。
Tauri 架构
┌──────────────────────────────────────────────────────┐
│ Tauri App │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Rust Core (后端) │ │
│ │ │ │
│ │ ┌──────────┐ ┌───────────┐ ┌─────────────┐ │ │
│ │ │ Tauri │ │ 系统API │ │ 自定义 │ │ │
│ │ │ Runtime │ │ (文件/ │ │ Rust │ │ │
│ │ │ │ │ 网络/ │ │ Commands │ │ │
│ │ │ WRY │ │ 进程/ │ │ │ │ │
│ │ │ TAO │ │ 通知) │ │ │ │ │
│ │ └──────────┘ └───────────┘ └─────────────┘ │ │
│ │ │ │
│ └───────────────────┬────────────────────────────┘ │
│ │ invoke / events │
│ ┌───────────────────▼────────────────────────────┐ │
│ │ System WebView (前端) │ │
│ │ │ │
│ │ macOS: WKWebView │ │
│ │ Windows: WebView2 (Edge/Chromium) │ │
│ │ Linux: WebKitGTK │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ React / Vue / Svelte / 任意前端框架 │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘Tauri 代码示例:
rust
// src-tauri/src/main.rs
use tauri::command;
use std::fs;
#[command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[command]
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, &content).map_err(|e| e.to_string())
}
#[command]
fn get_system_info() -> Result<serde_json::Value, String> {
Ok(serde_json::json!({
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
}))
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
read_file,
write_file,
get_system_info
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}typescript
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
async function openFile(path: string): Promise<string> {
return await invoke('read_file', { path });
}
async function saveFile(path: string, content: string): Promise<void> {
await invoke('write_file', { path, content });
}
async function getSystemInfo() {
const info = await invoke('get_system_info');
console.log(info);
}Electron vs Tauri 对比
| 维度 | Electron | Tauri |
|---|---|---|
| 后端语言 | Node.js (JavaScript) | Rust |
| 渲染引擎 | 自带 Chromium | 系统 WebView |
| 安装包体积 | ~150MB+ | ~3-10MB |
| 内存占用 | ~100-300MB | ~30-80MB |
| 启动速度 | 较慢 | 较快 |
| 安全模型 | 需手动配置 CSP | 默认安全 (Rust + 权限系统) |
| 前端框架 | 任意 | 任意 |
| Node.js 生态 | 完整支持 | 不支持 (Rust 生态) |
| 跨平台一致性 | 极高 (自带 Chromium) | 依赖系统 WebView (略有差异) |
| 自动更新 | electron-updater | tauri-updater (内置) |
| 开发门槛 | 低 (纯前端) | 中 (需要基础 Rust) |
| 成熟度 | 非常成熟 (2013) | 较新但快速成长 (2022 v1) |
| 典型应用 | VS Code, Slack, Discord | 1Password, Camo |
安装包体积对比 (同一个 Hello World 应用):
Electron: ████████████████████████████████████████ ~150 MB
Tauri: ████ ~8 MB
内存占用对比 (同一个 Todo 应用):
Electron: ████████████████████████████████ ~200 MB
Tauri: ████████████ ~60 MB为什么体积差异这么大?
Electron 包含:
┌──────────────────────────────────────────┐
│ Chromium 浏览器引擎 (~120 MB) │
│ Node.js 运行时 (~20 MB) │
│ Electron 框架 (~5 MB) │
│ 应用代码 (~5 MB) │
│ 合计: ~150 MB │
└──────────────────────────────────────────┘
Tauri 包含:
┌──────────────────────────────────────────┐
│ Rust 编译的二进制 (~3-5 MB) │
│ 应用前端资源 (~2-5 MB) │
│ WebView: 使用系统自带 (0 MB) │
│ 合计: ~5-10 MB │
└──────────────────────────────────────────┘六、跨端方案综合对比
全方案对比表
| 维度 | React Native | Flutter | Taro 3 | Electron | Tauri | PWA |
|---|---|---|---|---|---|---|
| 目标平台 | iOS/Android/Web | iOS/Android/Web/Desktop | 小程序/H5/RN | Windows/macOS/Linux | Windows/macOS/Linux | Web (可安装) |
| 语言 | JS/TS | Dart | JS/TS | JS/TS | JS/TS + Rust | JS/TS |
| 渲染方式 | 原生组件 | 自绘引擎 | WebView/原生 | Chromium | 系统 WebView | 浏览器 |
| 性能 | ★★★★ | ★★★★★ | ★★★ | ★★★ | ★★★★ | ★★★ |
| 开发效率 | ★★★★ | ★★★ | ★★★★★ | ★★★★★ | ★★★★ | ★★★★★ |
| 生态丰富度 | ★★★★★ | ★★★★ | ★★★★ | ★★★★★ | ★★★ | ★★★★ |
| 包体积 | 中 | 大 | 小 | 很大 | 小 | 极小 |
| 学习成本 | 低 (前端) | 中 (Dart) | 低 (前端) | 低 (前端) | 中 (Rust) | 低 (前端) |
| 原生体验 | 好 | 好 | 中 | 中 | 中 | 差 |
| 热更新 | 支持 | 受限 | 支持 | 支持 | 支持 | 天然支持 |
| 离线能力 | 好 | 好 | 依赖平台 | 好 | 好 | Service Worker |
| 典型场景 | 移动 App | 高性能移动 App | 多端小程序 | 桌面工具 | 轻量桌面应用 | 轻量应用 |
选型决策树
你需要覆盖哪些平台?
│
├── 移动端 (iOS + Android)
│ │
│ ├── 团队是前端背景?
│ │ ├── 是 → React Native
│ │ └── 否 → Flutter
│ │
│ ├── 需要极致性能和动画?
│ │ └── Flutter
│ │
│ ├── 需要与大量原生模块交互?
│ │ └── React Native (生态更丰富)
│ │
│ └── 已有 React Web 项目, 想复用?
│ └── React Native
│
├── 多端小程序 + H5
│ │
│ ├── 团队用 React
│ │ └── Taro
│ │
│ ├── 团队用 Vue
│ │ ├── Taro (Vue 模式)
│ │ └── uni-app
│ │
│ └── 需要覆盖抖音/快手等新兴小程序?
│ └── Taro (平台支持更灵活)
│
├── 桌面应用
│ │
│ ├── 需要 Node.js 生态 / 复杂功能?
│ │ └── Electron
│ │
│ ├── 追求小体积和性能?
│ │ └── Tauri
│ │
│ └── 团队有 Rust 经验?
│ ├── 是 → Tauri
│ └── 否 → Electron (更容易上手)
│
└── 全平台 (移动 + 桌面 + Web)
│
├── Flutter (覆盖最广, 但 Web 体验一般)
│
└── React Native (移动) + Electron/Tauri (桌面) + Web
(组合方案, 代码复用需要架构设计)PWA 补充说明
PWA (Progressive Web App) 严格来说不是跨端框架,而是一组 Web 技术标准,让 Web 应用获得接近原生的体验:
PWA 核心技术栈
┌─────────────────────────────────────────────────┐
│ PWA Application │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ Web App (SPA/MPA) │ │
│ │ React / Vue / Angular │ │
│ └───────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Service │ │ Web App │ │ Push │ │
│ │ Worker │ │ Manifest │ │ API │ │
│ │ │ │ │ │ │ │
│ │ 离线缓存 │ │ 安装能力 │ │ 推送通知 │ │
│ │ 后台同步 │ │ 图标/名称 │ │ │ │
│ │ 请求拦截 │ │ 启动画面 │ │ │ │
│ └─────────────┘ └────────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────┘PWA 关键代码示例:
json
{
"name": "My PWA App",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007AFF",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}typescript
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
});
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
)
)
);
});七、面试高频问题
问题 1:React Native 旧架构的 Bridge 有什么问题?新架构是如何解决的?
回答思路:
旧架构 Bridge 的三大问题:
- 异步通信:所有 JS 与 Native 的交互都是异步的,无法同步获取 Native 数据,导致复杂场景(如手势跟随、同步测量布局)难以实现
- JSON 序列化:每次通信都需要 JSON.stringify 和 JSON.parse,高频操作(如动画每帧 60 次)时序列化开销巨大
- 单一瓶颈:所有通信都走同一个 Bridge 队列,某个模块的大量通信会阻塞其他模块
新架构的解法:
- JSI 替代 Bridge:C++ 层的直接调用,同步、无需序列化、零拷贝共享内存
- Fabric 替代 UIManager:渲染树在 C++ 层创建和管理,JS 和 Native 共享同一 Shadow Tree
- TurboModules 替代 Native Modules:懒加载、类型安全、直接通过 JSI 调用
问题 2:Flutter 和 React Native 的渲染机制有什么本质区别?
回答思路:
核心区别在于是否使用系统原生 UI 组件:
- React Native:JS 描述的组件最终映射为系统原生 UI 组件(iOS 的 UIView、Android 的 View),渲染由系统 UI 框架完成。优点是天然获得平台一致的外观和交互;缺点是受限于原生组件的能力和跨平台差异
- Flutter:完全跳过系统 UI 框架,自带 Skia/Impeller 渲染引擎,直接在 Canvas 上绘制每个像素。优点是各平台渲染结果完全一致、可自由控制每一个像素;缺点是无法直接使用系统原生组件的外观,需要自行实现 Material/Cupertino 风格
可以类比为:React Native 像是在各平台上"翻译"UI,Flutter 像是自带"画布"直接画。
问题 3:Taro 3.x 的运行时方案是如何工作的?相比编译时方案有什么优劣?
回答思路:
Taro 3.x 运行时方案的核心是在小程序端模拟了一套 DOM/BOM API:
- React/Vue 运行时操作 DOM(createElement、appendChild 等),这些操作被 taro-runtime 拦截
- taro-runtime 维护了一棵虚拟 DOM 树,记录所有 DOM 操作
- 当需要更新 UI 时,将虚拟 DOM 树序列化为 data,通过 setData 传给小程序
- 小程序端用一套递归模板根据 data 渲染出真实的小程序组件
优势:几乎不限制 JSX 的写法,React/Vue 生态库可直接使用,开发体验接近 Web。 劣势:运行时开销更大(多了一层虚拟 DOM + setData 序列化),包体积更大,首屏渲染速度不如编译时方案。
问题 4:Electron 应用为什么体积那么大?Tauri 是怎么解决的?
回答思路:
Electron 体积大的根本原因是它内置了一个完整的 Chromium 浏览器和 Node.js 运行时。每个 Electron 应用都打包了一个约 120MB 的 Chromium,这保证了跨平台渲染一致性,但代价是巨大的包体积。
Tauri 的解决方案是使用系统自带的 WebView(macOS 的 WKWebView、Windows 的 WebView2、Linux 的 WebKitGTK),后端用 Rust 编译为原生二进制。因为不需要打包浏览器引擎,体积可以从 150MB 降到 5-10MB。
但 Tauri 也有 trade-off:不同系统的 WebView 引擎不同,可能存在渲染差异;Windows 上需要用户安装 WebView2 Runtime(虽然 Windows 10+ 已内置)。
问题 5:JSBridge 的通信原理是什么?有哪些实现方式?
回答思路:
JSBridge 是 Web 页面(WebView)与原生代码之间的通信桥梁,有以下几种实现方式:
- URL Scheme 拦截:JS 通过创建 iframe 或修改 location.href 触发自定义 URL(如
jsbridge://method?params=xxx),Native 端拦截 WebView 的 URL 请求来获取调用信息。优点是兼容性好,缺点是 URL 长度有限制,且是单向通信 - 注入 API:Native 向 WebView 的 JS 上下文注入全局对象(如
window.NativeBridge),JS 直接调用该对象的方法。Android 用addJavascriptInterface,iOS 用WKScriptMessageHandler - postMessage:iOS WKWebView 提供
window.webkit.messageHandlers.xxx.postMessage()接口,是苹果推荐的方式
实际项目中通常组合使用:JS → Native 用注入 API 或 postMessage,Native → JS 通过 evaluateJavaScript 执行回调。
问题 6:小程序的双线程架构是什么?为什么要这样设计?
回答思路:
小程序双线程架构:
┌──────────────┐ ┌──────────────┐
│ 逻辑层 │ │ 渲染层 │
│ (JS 线程) │◀────────────▶│ (WebView) │
│ │ Native │ │
│ JS 引擎 │ 中转通信 │ WXML + WXSS │
│ (V8/JSC) │ │ → DOM 渲染 │
│ │ │ │
│ 无 DOM API │ │ 无 JS 能力 │
│ 无 BOM API │ │ (纯渲染) │
└──────────────┘ └──────────────┘这样设计的原因主要有两点:
- 安全性:逻辑层没有 DOM/BOM API,开发者无法操作 DOM、跳转页面、获取 Cookie 等。这防止了恶意小程序通过 JS 操控用户页面或窃取数据
- 性能管控:渲染和逻辑分离后,平台可以更好地管控小程序的资源使用。即使逻辑层 JS 出现死循环,也不会阻塞页面渲染
代价是:逻辑层和渲染层的通信需要经过 Native 中转(类似 React Native 的 Bridge),存在异步延迟。这就是为什么小程序的 setData 要尽量精简数据量——每次 setData 都要经过序列化、跨线程传输、反序列化。
问题 7:如何设计一个跨端组件库的架构?
回答思路:
核心思路是分层设计,将平台无关的逻辑和平台相关的实现分离:
跨端组件库分层架构:
┌──────────────────────────────────────────┐
│ Platform-agnostic Layer │
│ │
│ ┌────────────┐ ┌────────────────────┐ │
│ │ 组件逻辑 │ │ 状态管理 / Hooks │ │
│ │ (纯 JS) │ │ (纯 JS) │ │
│ └────────────┘ └────────────────────┘ │
├──────────────────────────────────────────┤
│ Adapter Layer │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌─────┐ │
│ │ Web │ │ RN │ │ 小程序│ │ ... │ │
│ │Render│ │Render│ │Render│ │ │ │
│ └──────┘ └──────┘ └──────┘ └─────┘ │
└──────────────────────────────────────────┘具体策略:
- 逻辑复用:将组件的状态管理、数据处理、事件逻辑抽取为平台无关的 Hooks / 纯函数
- 渲染适配:每个平台提供自己的渲染实现,调用共享的逻辑层
- 样式方案:定义 design token(颜色/间距/字号),各平台分别实现(Web 用 CSS、RN 用 StyleSheet、小程序用 rpx)
- 条件编译:利用构建工具的 alias / 文件后缀(
.web.tsx,.native.tsx)实现平台特定代码
问题 8:Hermes 引擎相比 V8/JSC 有什么优势?为什么 React Native 要自研引擎?
回答思路:
V8 和 JavaScriptCore 是通用 JS 引擎,它们针对浏览器场景优化——强调运行时的峰值性能(JIT 编译)。但在移动端场景,用户更在意的是启动速度和内存占用,而不是 JS 的极限运算性能。
Hermes 的核心优化是AOT(Ahead-of-Time)编译:在构建阶段就将 JS 源码编译为字节码(.hbc 文件),App 启动时直接执行字节码,跳过了解析和编译环节。这带来了:
- 启动时间减少约 50%(无需 parse/compile)
- 内存占用减少约 30%(无需存储源码和 AST)
- 包体积减少(字节码比源码紧凑)
此外 Hermes 不包含 JIT 编译器,这在 iOS 上是必要的(Apple 禁止第三方 App 使用 JIT),也进一步减少了内存占用。
八、延伸阅读
React Native:
Flutter:
Taro / 小程序:
Electron / Tauri:
综合: