Skip to content

跨端开发

为什么需要跨端

在移动互联网时代,一个产品往往需要同时覆盖多个平台: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 + CSSJS/TS (受限子集)Swift/Kotlin/Dart/JSJS/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 Code

3. 运行时抹平

在各端实现一套统一的运行时,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 NativeFlutter
语言JavaScript / TypeScriptDart
渲染方式映射到原生组件自绘引擎 (Skia/Impeller)
UI 一致性各平台原生外观 (不完全一致)各平台完全一致
生态npm 生态, 前端友好pub.dev, 独立生态
热重载支持 (Fast Refresh)支持 (Hot Reload, 更快)
包体积较小 (~7-15MB)较大 (~10-20MB)
性能接近原生 (新架构后)接近原生
学习曲线前端开发者友好需要学习 Dart
动画性能新架构后大幅提升天然优秀 (自绘引擎)
原生集成成熟, 丰富的原生模块Platform Channel
Web 支持react-native-web官方支持但体验一般
典型用户Meta, Shopify, DiscordGoogle, 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 脚本暴露安全的 API

IPC 通信

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 对比

维度ElectronTauri
后端语言Node.js (JavaScript)Rust
渲染引擎自带 Chromium系统 WebView
安装包体积~150MB+~3-10MB
内存占用~100-300MB~30-80MB
启动速度较慢较快
安全模型需手动配置 CSP默认安全 (Rust + 权限系统)
前端框架任意任意
Node.js 生态完整支持不支持 (Rust 生态)
跨平台一致性极高 (自带 Chromium)依赖系统 WebView (略有差异)
自动更新electron-updatertauri-updater (内置)
开发门槛低 (纯前端)中 (需要基础 Rust)
成熟度非常成熟 (2013)较新但快速成长 (2022 v1)
典型应用VS Code, Slack, Discord1Password, 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 NativeFlutterTaro 3ElectronTauriPWA
目标平台iOS/Android/WebiOS/Android/Web/Desktop小程序/H5/RNWindows/macOS/LinuxWindows/macOS/LinuxWeb (可安装)
语言JS/TSDartJS/TSJS/TSJS/TS + RustJS/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 的三大问题:

  1. 异步通信:所有 JS 与 Native 的交互都是异步的,无法同步获取 Native 数据,导致复杂场景(如手势跟随、同步测量布局)难以实现
  2. JSON 序列化:每次通信都需要 JSON.stringify 和 JSON.parse,高频操作(如动画每帧 60 次)时序列化开销巨大
  3. 单一瓶颈:所有通信都走同一个 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

  1. React/Vue 运行时操作 DOM(createElement、appendChild 等),这些操作被 taro-runtime 拦截
  2. taro-runtime 维护了一棵虚拟 DOM 树,记录所有 DOM 操作
  3. 当需要更新 UI 时,将虚拟 DOM 树序列化为 data,通过 setData 传给小程序
  4. 小程序端用一套递归模板根据 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)与原生代码之间的通信桥梁,有以下几种实现方式:

  1. URL Scheme 拦截:JS 通过创建 iframe 或修改 location.href 触发自定义 URL(如 jsbridge://method?params=xxx),Native 端拦截 WebView 的 URL 请求来获取调用信息。优点是兼容性好,缺点是 URL 长度有限制,且是单向通信
  2. 注入 API:Native 向 WebView 的 JS 上下文注入全局对象(如 window.NativeBridge),JS 直接调用该对象的方法。Android 用 addJavascriptInterface,iOS 用 WKScriptMessageHandler
  3. 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  │              │  (纯渲染)     │
└──────────────┘              └──────────────┘

这样设计的原因主要有两点:

  1. 安全性:逻辑层没有 DOM/BOM API,开发者无法操作 DOM、跳转页面、获取 Cookie 等。这防止了恶意小程序通过 JS 操控用户页面或窃取数据
  2. 性能管控:渲染和逻辑分离后,平台可以更好地管控小程序的资源使用。即使逻辑层 JS 出现死循环,也不会阻塞页面渲染

代价是:逻辑层和渲染层的通信需要经过 Native 中转(类似 React Native 的 Bridge),存在异步延迟。这就是为什么小程序的 setData 要尽量精简数据量——每次 setData 都要经过序列化、跨线程传输、反序列化。

问题 7:如何设计一个跨端组件库的架构?

回答思路:

核心思路是分层设计,将平台无关的逻辑和平台相关的实现分离:

跨端组件库分层架构:

┌──────────────────────────────────────────┐
│           Platform-agnostic Layer         │
│                                          │
│  ┌────────────┐  ┌────────────────────┐  │
│  │  组件逻辑   │  │  状态管理 / Hooks  │  │
│  │  (纯 JS)   │  │  (纯 JS)          │  │
│  └────────────┘  └────────────────────┘  │
├──────────────────────────────────────────┤
│           Adapter Layer                   │
│                                          │
│  ┌──────┐  ┌──────┐  ┌──────┐  ┌─────┐ │
│  │ Web  │  │  RN  │  │ 小程序│  │ ... │ │
│  │Render│  │Render│  │Render│  │     │ │
│  └──────┘  └──────┘  └──────┘  └─────┘ │
└──────────────────────────────────────────┘

具体策略:

  1. 逻辑复用:将组件的状态管理、数据处理、事件逻辑抽取为平台无关的 Hooks / 纯函数
  2. 渲染适配:每个平台提供自己的渲染实现,调用共享的逻辑层
  3. 样式方案:定义 design token(颜色/间距/字号),各平台分别实现(Web 用 CSS、RN 用 StyleSheet、小程序用 rpx)
  4. 条件编译:利用构建工具的 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:

综合:

最后更新于:

用心学习,用代码说话 💻