跨端开发热门面试题
使用说明
本篇共 45 道题。
跨端面试重点是选型、运行时边界、性能和平台差异。不要把“复用率”当唯一目标,应说明用户体验、团队和长期维护成本。
Q1: 跨端方案应该如何选型?
答案:
先明确目标平台、体验上限、原生能力、团队栈、发布方式和性能,再比较 Web/PWA、React Native、Flutter、小程序、Electron/Tauri 等方案。
复用代码只是收益之一。需要高性能复杂交互、强原生生态或桌面系统能力时,技术边界和人才成本更关键。
Q2: JSBridge 的基本原理是什么?
答案:
JSBridge 是 Web 运行时与 Native 运行时之间的协议和适配层。Web 侧把“方法、参数、请求 ID”等消息通过宿主提供的通道发送给 Native;Native 校验并执行能力,再把结果或事件回传给对应页面。
具体通道因平台而异,例如 WebView message handler、注入对象、URL Scheme、MessagePort 或宿主提供的调用接口。Native 回到 JS 也不只有一种固定写法,可以通过消息通道、事件或执行受控脚本完成。
完整 Bridge 还需要处理序列化、并发回调、超时、取消、页面销毁、版本协商和权限校验。它不是简单的 evaluateJavaScript 封装,更不能把任意 Native 反射能力直接暴露给网页。
Q3: JSBridge 有哪些安全风险?
答案:
如果 WebView 可加载不可信页面,而 Bridge 又暴露文件、设备或账户能力,XSS/恶意页面可能升级成原生权限攻击。
使用页面来源允许列表、能力白名单、参数校验、用户确认和最小权限,并禁用危险调试/文件访问设置。
- Web 内容是不可信输入,Native 暴露的方法必须使用白名单、参数 Schema、登录态和资源级权限校验,不能提供任意类名/方法反射调用。
- 回调 ID、来源页面和会话要绑定,防重放并限制调用频率;支付、文件和系统设置等高风险能力还需要用户确认与审计。
Q4: PWA 的核心能力是什么?
答案:
Web App Manifest 提供安装与展示元数据,Service Worker 提供离线缓存、请求代理和后台能力,HTTPS 保证安全上下文。
PWA 适合希望低成本覆盖多平台和弱网体验的应用,但系统能力、商店分发和平台支持有差异。
Q5: Service Worker 生命周期为什么容易出问题?
答案:
Service Worker 独立于页面运行,更新后通常经历 install、waiting、activate,再接管后续导航或未被旧 Worker 控制的页面。只要仍有旧页面存活,新 Worker 就可能长时间停在 waiting,因此“文件已发布”不等于所有用户立刻运行新版本。
常见坑包括:
- 首次注册后的当前页面通常尚未被控制,需下次导航或显式处理。
skipWaiting()与clients.claim()能加快接管,但可能让旧 HTML 和新缓存/运行逻辑混用。- install/activate/fetch 中的异步工作必须交给
waitUntil()或respondWith(),否则 Worker 可能被提前终止。 - 缓存清理过早会破坏仍在运行的旧页面;过晚又会长期占用空间。
- 浏览器可随时终止空闲 Worker,不能依赖进程内全局变量长期存在。
正确做法是资源内容哈希、版本兼容、更新提示和可回滚策略配合,而不是无条件强制接管。
Q6: React Native 和 WebView 混合方案有什么区别?
答案:
React Native 组件映射或驱动原生视图,交互与原生生态更紧密;WebView 运行完整 Web 页面,复用高、隔离清楚,但复杂交互和桥接成本更高。
实际应用可混合:核心高频体验用原生/RN,低频活动页用 WebView,并统一导航、登录和监控。
Q7: Metro 在 React Native 工程中承担什么职责?
答案:
Metro 是 React Native 的 JavaScript 构建与开发服务器,负责模块解析、代码转换、依赖图、Bundle 生成和 Fast Refresh 等开发体验;Native 的 Gradle/Xcode 编译仍由各平台工具链完成。
它和 Web 打包器的目标不同:需要理解 React Native 平台文件解析、资源处理、Hermes 字节码链路和移动端 Bundle 加载。排查问题时要区分 Metro 转换错误、Native 编译错误和运行时模块不兼容。
Monorepo 中还要正确配置项目根、watch folders、包导出与单实例依赖,避免解析到两份 React;性能优化先看模块图、缓存和自定义 Transformer,不要直接清空所有缓存当长期方案。
Q8: Flutter 的 Platform Channel 和 FFI 应该怎么选?
答案:
Platform Channel 适合调用 Kotlin/Java、Swift/Objective-C 暴露的平台 API,参数通常经过消息编解码,适用于相机、通知、系统设置等面向平台 SDK 的能力。
Dart FFI 适合直接调用稳定的 C ABI 库,能减少高频数据通路的编码层,但需要自己处理指针、线程、内存和各平台二进制分发。
选择原则:
- 已有平台 SDK 或需要 UI/生命周期回调,优先插件与 Platform Channel。
- 已有 C/C++/Rust 计算库且调用边界可设计得较粗,评估 FFI。
- 高频逐元素跨边界都可能昂贵,应批量传输并实测。
- 两种方案都必须定义异常、取消、版本兼容和资源释放,不能只封装“调用成功”路径。
Q9: 小程序运行时有什么特点?
答案:
常见模型把逻辑层和视图层分离,通过序列化数据通信,并由宿主提供组件、路由和 API;不同平台规范和限制不完全相同。
频繁传大对象会有通信成本,应控制 setData 粒度,并处理包体、分包、权限和审核规则。
- 小程序通常把逻辑层与渲染层隔离,通过序列化通信更新视图,并由宿主管理页面栈、权限、网络和生命周期。
- 这意味着频繁传大对象、细粒度 setData 和复杂自定义组件会产生桥接成本;同时要遵循包体、分包、审核和平台 API 限制。
Q10: Electron 的主要安全原则是什么?
答案:
渲染进程默认按不可信 Web 内容处理,开启 context isolation,关闭不必要的 Node integration,通过 preload 暴露最小、明确的 IPC API。
校验 IPC 参数和来源,限制导航与新窗口,使用 CSP 和安全更新签名。不能把整个 Node API 挂到 window。
Q11: Tauri 相比 Electron 有哪些主要取舍?
答案:
Tauri 通常复用系统 WebView,并把本地能力放在 Rust 核心与插件中,因此安装体积和常驻资源有机会更小,权限可通过 Capability 做细粒度收敛;Electron 自带 Chromium 和 Node.js,跨平台渲染一致性、调试体验及 npm/原生模块生态通常更成熟。
主要取舍:
- 一致性:系统 WebView 会随操作系统版本变化,需要更广的兼容测试。
- 生态与能力:Electron 的桌面生态更成熟;Tauri 接入本地能力可能需要 Rust、插件或自行封装。
- 安全:两者都需最小权限、CSP、签名更新和 IPC 校验,不能仅凭框架名称判断安全。
- 性能与体积:必须用真实页面、插件和目标平台测量,不能只比较空项目。
- 团队成本:评估 Rust/Native 能力、构建链、调试和长期维护。
工具型桌面应用常值得评估 Tauri;高度依赖 Chromium 一致性或成熟 Electron 插件时,Electron 可能更稳妥。
Q12: 跨端项目如何组织共享代码?
答案:
优先共享领域模型、API、校验、状态逻辑和设计 Token,把导航、存储、权限和 UI 细节放到平台适配层。
通过接口和依赖注入隔离平台能力。为了追求 100% 复用塞入大量条件分支,通常比适度重复更难维护。
- 优先共享领域模型、校验、请求客户端、状态机和无平台依赖的 hooks;UI 通过设计 Token 和平台组件实现一致体验。
- 把文件、通知、权限、存储等能力定义成接口,由各平台适配。不要在共享层堆满
if (platform),否则公共代码会变成最难维护的部分。
Q13: 跨端路由和返回栈如何统一?
答案:
先定义统一导航意图和路由参数,再由 Web、原生和小程序适配各自栈。深链、登录拦截、Tab、物理返回和恢复都要明确。
不要让业务层直接拼每个平台 URL;导航事件还要与埋点和权限状态一致。
- 先定义统一的 route name、参数 Schema 和导航动作,再由 Web History、Native Navigation、小程序页面栈分别适配。
- 返回行为要考虑系统返回键、手势、Tab 内子栈和外部深链;跨端只统一业务语义,不强行让每个平台的物理栈完全一致。
Q14: 跨端应用如何做热更新?
答案:
Web 资源可以远程更新,原生脚本包是否可热更新受平台政策和技术方案限制。必须签名、灰度、版本兼容、完整性校验和快速回滚。
热更新不能绕过商店安全规则,也不能修复所有原生二进制问题。
- Web 资源更新相对灵活,但 Native Bundle、JS Bundle、Wasm 和配置分别受应用商店政策、签名与兼容约束。
- 热更新包必须签名、灰度、可回滚,并校验壳版本与资源版本兼容;不能用热更新绕过审核下发新的原生能力或重大业务变化。
Q15: 跨端项目如何测试?
答案:
跨端项目的测试金字塔需要覆盖 共享逻辑 + 平台特定逻辑 + 真机验证 三层。详见 前端测试策略。
// 1. 单元测试 — shared 包中的纯逻辑(复用率 100%)
describe('formatPrice', () => {
it('should format price with currency', () => {
expect(formatPrice(1999, 'CNY')).toBe('¥19.99');
expect(formatPrice(1999, 'USD')).toBe('$19.99');
});
});
// 2. 组件测试 — React Native Testing Library
import { render, fireEvent } from '@testing-library/react-native';
describe('UserCard', () => {
it('should call onPress with user id', () => {
const onPress = jest.fn();
const { getByText } = render(
<UserCard name="张三" avatar="https://..." bio="..." onPress={onPress} />
);
fireEvent.press(getByText('张三'));
expect(onPress).toHaveBeenCalled();
});
});
// 3. E2E 测试 — Detox (React Native)
describe('Login Flow', () => {
it('should login successfully', async () => {
await element(by.id('email-input')).typeText('user@test.com');
await element(by.id('password-input')).typeText('password123');
await element(by.id('login-button')).tap();
await expect(element(by.id('home-screen'))).toBeVisible();
});
});
Q16: JSBridge 的消息协议应该怎样设计?
答案:
Bridge 不应只约定一个方法名和任意 JSON。稳定协议至少包含版本、请求 ID、方法、参数、结果和结构化错误,并明确超时与取消:
interface BridgeRequest {
version: 1;
id: string;
method: string;
params: unknown;
}
Native 端只开放允许列表中的能力,并对参数做 Schema 校验和权限判断;Web 端要处理页面销毁、重复回调、乱序响应和版本不兼容。
高频事件还需节流、批量或背压,避免大量序列化和线程切换拖垮 UI。敏感能力不能因为调用来自内嵌 WebView 就默认可信。
Q17: Service Worker 和 Web Worker 有什么区别?
答案:
| 对比维度 | Service Worker | Web Worker |
|---|---|---|
| 生命周期 | 独立于页面,可在后台运行 | 随页面关闭而销毁 |
| 作用域 | 可拦截 scope 内所有页面的请求 | 只服务于创建它的页面 |
| 网络拦截 | 可拦截 fetch 请求 | 不能拦截网络请求 |
| 缓存控制 | 可操作 Cache API | 可操作但通常不用 |
| 推送通知 | 支持 push 事件 | 不支持 |
| HTTPS 要求 | 必须 HTTPS | 不要求 |
| 复用性 | 多个页面共享同一个 SW | 每个页面独立创建 |
核心区别是 Service Worker 是网络代理,位于浏览器和网络之间,可以拦截和缓存请求;Web Worker 是计算线程,用于将 CPU 密集任务移出主线程。
Q18: React Native 新架构和旧架构的核心区别是什么?
答案:
旧架构主要通过异步 Bridge 批量序列化消息连接 JavaScript 与 Native。新架构以 JSI 为底层互操作基础,并包含 Turbo Native Modules、Fabric Renderer、Codegen 及新的调度/事件循环能力。
关键变化:
- TurboModules 可按需加载,并通过生成的类型契约减少 JS/Native 接口漂移。
- Fabric 与现代 React 调度结合,支持并发更新和更灵活的跨线程渲染工作。
- 部分 Native 能力可以同步或异步直接通信,不再被“所有调用都必须经 JSON Bridge”限制。
- 新架构自 React Native 0.76 起默认启用,但应用和三方库仍要验证兼容层、Native Module 与组件迁移。
它减少了旧 Bridge 的限制,不代表所有调用都应同步,也不会自动解决低效 React 渲染、图片和业务计算问题。
Q19: Flutter 的三棵树(Widget、Element、RenderObject)分别是什么?
答案:
Flutter 渲染系统由三棵树协同工作:
| 树 | 职责 | 特点 | 类比 React |
|---|---|---|---|
| Widget Tree | UI 配置描述 | 不可变(immutable),轻量创建 | JSX 元素 |
| Element Tree | Widget 的实例化表示 | 管理生命周期,执行 Diff | Fiber 节点 |
| RenderObject Tree | 实际布局和绘制 | 执行 layout / paint | 真实 DOM |
// Widget(配置) —— 每次 rebuild 都会创建新实例
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const Text('Hello'); // Text 是 Widget
}
}
// Element(实例) —— Flutter 框架自动管理
// 当 Widget 类型不变时,Element 会被复用
// Element 持有对 Widget 和 RenderObject 的引用
// RenderObject(渲染) —— 执行布局和绘制
// RenderParagraph 负责文字的测量和绘制
// 只有当 Widget 属性变化时,RenderObject 才会更新
关键流程:setState() -> 标记 Element dirty -> 下一帧重新调用 build() -> Element 对比新旧 Widget -> 决定复用或重建 RenderObject -> 执行 layout 和 paint。
Q20: 小程序为什么常采用逻辑层和渲染层分离的架构?
答案:
平台通常把业务 JavaScript 与页面渲染放在受控的不同运行环境中,让宿主可以限制 DOM/系统能力、统一组件与 API,并管理页面和资源生命周期。
代价是数据更新要跨运行环境传递和序列化,因此大对象、高频更新和过深数据路径可能造成明显开销。不同小程序平台的进程、线程和页面承载方式并不完全相同,不应把“逻辑层一个线程、每页固定一个 WebView”当成统一规范。
优化重点是减少无关数据传输、合并更新、把高频交互放到平台支持的渲染侧能力,并在真机上观察通信与渲染耗时。
Q21: Electron 的主进程和渲染进程有什么区别?
答案:
Electron 采用多进程架构,有且仅有一个 主进程(Main Process)和零到多个 渲染进程(Renderer Process)。
核心区别:
| 维度 | 主进程 | 渲染进程 |
|---|---|---|
| 数量 | 有且仅有 1 个 | 可以有多个(每个窗口一个) |
| 入口 | package.json 的 main 字段 | BrowserWindow.loadFile() 加载的 HTML |
| Node.js | 完全可用 | 默认禁用(出于安全考虑) |
| Electron API | 完整访问 | 仅 ipcRenderer 等少量 API |
| 职责 | 窗口管理、系统集成、IPC 中心 | UI 渲染、用户交互 |
| 崩溃影响 | 整个应用崩溃 | 仅该窗口崩溃 |
主进程负责创建和管理 BrowserWindow、调用原生 API(菜单、托盘、通知等),渲染进程只负责页面展示和用户交互。两者通过 IPC 通信。
Q22: Tauri Command 调用怎样避免把本地高权限能力暴露给前端?
答案:
Tauri 前端运行在 WebView 中,本地命令却可能访问文件、网络和系统能力。安全边界应设计为最小化的业务命令,而不是提供“执行任意 Shell”“读任意路径”等通用后门。
实践要点:
- Command 参数在 Rust 边界做类型和业务校验。
- 文件、URL、进程和插件能力使用允许列表或 Capability 配置。
- 返回最小数据,错误信息避免泄露本地路径和 Secret。
- 不信任远程内容、第三方脚本和前端传入路径。
- 为敏感动作增加用户确认、权限与审计。
- WebView 的 CSP、更新签名和依赖供应链也要治理。
Q23: 跨端开发能完全替代原生开发吗?
答案:
不能。跨端方案是在开发效率、代码复用、体验上限和平台能力之间取舍,不存在固定比例能说明“多少应用都能替代”。
更适合跨端的情况是业务页面多、平台差异有限、需要快速覆盖多端且团队希望共享领域逻辑;更需要 Native 的情况包括深度系统集成、对平台最新能力依赖强、极致图形/音视频性能,或交互必须完全贴合单个平台。
实际项目常采用分层组合:共享领域模型和网络层,主要页面用 RN/Flutter/WebView,少数高要求模块用 Native。决策应通过核心场景 PoC、真机性能、包体、可访问性、发布规则和团队维护成本验证,而不是追求 100% 复用。
Q24: Android addJavascriptInterface 的安全漏洞是什么?
答案:
漏洞背景:Android 4.2(API 17)以前,被注入的 Java 对象的所有 public 方法都会暴露给 JS。攻击者可通过反射调用 getClass()、forName() 等方法,任意执行 Java 代码(CVE-2012-6636)。
攻击示例:
// 老版本下,这段 JS 可以在设备上执行任意命令
NativeBridge.getClass().forName('java.lang.Runtime')
.getMethod('getRuntime').invoke(null)
.exec(['rm', '-rf', '/sdcard/']);
修复方案:
minSdkVersion >= 17,且方法必须加@JavascriptInterface注解才会暴露- 老版本可改用
WebViewClient.shouldOverrideUrlLoading拦截 URL Scheme - 严格 URL 白名单,禁止任意页面访问 Bridge
Q25: Service Worker 更新时怎样避免新旧资源版本错配?
答案:
首先让静态资源使用内容哈希并长期缓存,HTML 与入口清单使用可再验证策略。新 Worker 安装时准备新版本资源,只有完整成功后才进入 waiting,不能边更新边覆盖同名不可变资源。
接管策略要按业务选择:
- 稳妥方案是提示用户“新版本可用”,由用户刷新后让新 Worker 接管。
- 必须快速接管时可使用
skipWaiting()和clients.claim(),但要保证新 Worker 能兼容仍运行旧代码的页面。 - activate 清缓存前确认不会让旧客户端请求不到资源,必要时保留最近若干版本或先迁移客户端。
- 更新失败继续使用上一完整版本,并记录安装、激活和资源加载错误。
核心是一次发布的 HTML、JS、Wasm、缓存 Schema 与 Worker 协议保持兼容,而不只是给缓存名加版本号。
Q26: Hermes 引擎相比 JavaScriptCore 的优势和边界是什么?
答案:
Hermes 面向 React Native 的启动、内存和应用体积场景设计,可在构建阶段生成字节码,并与当前 React Native 工具链和调试能力深度集成。它在许多应用中能改善启动或内存表现,但收益取决于代码、设备和 RN 版本。
不能背“固定快 50%”或“内存必降 30%”。应比较 Release 包的冷启动、交互、峰值与常驻内存、包体、崩溃和低端设备表现。
Hermes 也不会优化 Native 主线程、图片解码或低效组件树;切换引擎前还要验证原生库、Intl、调试和性能采样兼容性。
Q27: Flutter 为什么选择 Dart?
答案:
Dart 同时支持开发期 JIT 与发布期 AOT,配合 Stateful Hot Reload 提高迭代效率,发布时又能生成目标平台代码。它的命名参数、空安全、异步模型和工具链也适合声明式 UI。
Flutter 团队能协同演进框架、引擎、语言和 DevTools,这是工程优势,但不能推导出“Dart 没有 GC 卡顿”或“JavaScript 永远只能解释执行”。Dart 与 JavaScript 都有成熟运行时和垃圾回收,实际体验还取决于组件树、布局、绘制、着色器和 Native 插件。
面试回答应说明语言与框架共同优化的价值,同时承认生态规模、招聘和非 Flutter 场景复用是成本。
Q28: setData 为什么是性能瓶颈?如何优化?
答案:
setData 的过程:逻辑层数据 → JSON 序列化 → Native 桥接 → WebView 反序列化 → 虚拟 DOM diff → 真实 DOM 更新。整个流程涉及跨线程数据传输和序列化开销。
优化方法:
- 减少数据量:只传递变化部分,使用路径更新(
'list[0].name') - 降低频率:合并多次 setData,避免在滚动等高频事件中调用
- 避免后台更新:页面不可见时不 setData(
onHide中停止) - 使用 WXS:数据格式化和简单交互逻辑放 WXS,在渲染层直接执行
- 自定义组件隔离:组件内的 setData 只 diff 组件自身的渲染树
Q29: 如何在主进程和渲染进程之间通信?
答案:
Electron 提供了多种 IPC 通信方式:
1. invoke/handle(推荐,双向 Promise):
// preload.ts
contextBridge.exposeInMainWorld('api', {
readFile: (path: string) => ipcRenderer.invoke('fs:read', path),
});
// main.ts
ipcMain.handle('fs:read', async (_e, path: string) => {
return fs.readFile(path, 'utf-8');
});
// renderer.ts
const content = await window.api.readFile('/path/to/file');
2. send/on(渲染 -> 主,单向):
ipcRenderer.send('log', 'something happened');
ipcMain.on('log', (_e, msg) => console.log(msg));
3. webContents.send(主 -> 渲染):
mainWindow.webContents.send('notification', '更新可用');
4. MessagePort(渲染 ↔ 渲染直连):
const { port1, port2 } = new MessageChannelMain();
win1.webContents.postMessage('port', null, [port1]);
win2.webContents.postMessage('port', null, [port2]);
推荐使用 invoke/handle 模式,它基于 Promise、支持返回值、语义清晰。
Q30: Tauri 的前后端通信机制有哪些?怎样选择?
答案:
Tauri 前端通常通过 invoke 调用 Rust Command,形成异步请求—响应;后端主动通知、窗口间广播等场景可使用 Event。两者都跨越 WebView 与本地权限边界。
选择原则:
- 需要一个明确结果或结构化错误,用 Command/
invoke。 - 进度、状态变化和多次通知用 Event,并返回可靠的取消监听方式。
- 高频二进制数据不要逐条走 JSON 消息,应批量传输或使用更适合的数据通道。
- Command 参数在 Rust 端做 Schema、路径、权限和业务校验;不能因为 TypeScript 有类型就信任前端输入。
- 页面销毁、超时和重复调用要有清理与幂等策略。
invoke 返回 Promise,不应把它描述成阻塞式“同步调用”;具体 API 和权限配置还要以目标 Tauri 版本为准。
Q31: 跨端设计系统如何处理平台差异?
答案:
统一的应是品牌 Token、语义、状态和可访问性目标,不是强迫所有平台像素与交互完全一致。可把颜色、排版、间距和动效等语义 Token 共享,再由 Web、iOS、Android 和小程序映射到各自组件。
组件 API 先表达业务意图,例如 Button variant="primary",平台适配层决定触感反馈、焦点、返回手势和系统控件细节。确有差异时使用显式能力接口或平台组件,不在共享组件内部堆满散落条件分支。
治理上需要 Token 版本、视觉/交互回归、无障碍测试和设计—代码映射。复用率不是唯一指标;可维护的平台一致性比一套代码覆盖所有细节更重要。
Q32: 如何处理 JSBridge 的异步回调?为什么需要 callbackId?
答案:
JS 调用 Native 是异步的(拍照、定位都需时间)。如果只用一个全局回调函数,多个并发调用的回调会互相覆盖。
解决方案:每次调用生成唯一 callbackId,存入 Map,Native 回调时携带该 ID 找到对应函数:
const callbacks = new Map<string, Function>();
let seed = 0;
function call(method: string, params: any, cb: Function) {
const callbackId = `cb_${++seed}`;
callbacks.set(callbackId, cb);
postMessage({ method, params, callbackId });
}
// Native 回调时
window.invokeCallback = (id, data) => {
callbacks.get(id)?.(data);
callbacks.delete(id); // 清理一次性回调
};
注意点:
- 一次性回调用完即删,否则内存泄漏
- 必须配合超时机制,否则 Native 不响应时 Promise 永久挂起
- 事件订阅(如
onPush)属于多次回调,需另外管理
Q33: PWA 的离线方案如何设计?
答案:
分三层设计:
- App Shell 预缓存:在 install 事件中缓存 HTML 骨架、核心 CSS/JS
- 运行时缓存策略:
- 静态资源(图片、字体)→ Cache First
- API 数据 → Network First + 离线回退
- CDN 资源 → Stale While Revalidate
- 离线数据同步:
- IndexedDB 本地存储用户操作
- Background Sync 网络恢复时自动同步
- 冲突解决策略(最后写入胜出 / 服务端合并)
// 离线回退页面
self.addEventListener('fetch', (event: FetchEvent) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() =>
caches.match('/offline.html') as Promise<Response>
)
);
}
});
Q34: React Native 性能问题应该怎样定位和优化?
答案:
先区分瓶颈位于 JavaScript 执行、React 渲染、Native 主线程、图片/网络,还是 JS 与 Native 边界的数据传递。
常见措施:
- 用框架与平台 Profiler 定位长任务和掉帧,不先全局加 memo。
- 长列表使用虚拟化组件,稳定 key、item 尺寸和渲染函数。
- 减少大对象跨边界传递和高频事件,动画优先交给 UI/原生侧执行。
- 图片按显示尺寸解码并缓存,避免主线程大图处理。
- CPU 密集任务拆分或使用原生模块/独立线程。
- 在 Release 构建和真实设备上验证,开发模式数据不能代表线上。
新架构减少了旧 Bridge 的部分序列化与调度成本,但不会自动修复低效组件树和昂贵业务逻辑。
Q35: StatelessWidget 和 StatefulWidget 的区别?
答案:
| 维度 | StatelessWidget | StatefulWidget |
|---|---|---|
| 状态 | 无可变状态,一旦创建不变 | 有可变状态,State 对象管理 |
| 重建 | 只在父组件重建时重建 | setState() 主动触发重建 |
| 生命周期 | 只有 build() | initState、didUpdateWidget、dispose 等 |
| 性能 | 更轻量 | 需要维护 State 对象 |
| 使用场景 | 纯展示型组件 | 需要交互、动画、网络请求 |
// StatelessWidget —— 无状态,纯展示
class GreetingCard extends StatelessWidget {
final String name;
const GreetingCard({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('Hello, $name!');
}
}
// StatefulWidget —— 有状态,可交互
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool _isLiked = false;
@override
void initState() {
super.initState();
// 初始化逻辑(如网络请求)
}
@override
void dispose() {
// 清理资源(如取消订阅)
super.dispose();
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(_isLiked ? Icons.favorite : Icons.favorite_border),
color: _isLiked ? Colors.red : Colors.grey,
onPressed: () => setState(() => _isLiked = !_isLiked),
);
}
}
StatefulWidget 本身也是 不可变的,真正持有可变状态的是 State 对象。当 StatefulWidget 被重建时,Element 会复用已有的 State 对象(通过 didUpdateWidget 通知),避免状态丢失。这类似于 React 中 Hooks 的状态会在组件重渲染时保持。
Q36: 小程序的分包加载是什么?如何设计分包策略?
答案:
分包把非首屏页面和资源从主包拆出,用户进入对应业务时再下载,目标是减小首次下载与解析成本。通常按业务域拆分,主包只保留启动、Tab 和真正公共的能力;独立分包、分包预下载等特性要按目标平台支持使用。
包体上限不是跨平台固定值。以某个平台、某个基础库版本为例可能有主包和总包限制,但上线前必须查目标平台当前规则,并在 CI 对实际产物设置预算。
公共依赖提升到主包可能让首屏变大,重复打进多个分包又会增加总量,应结合用户路径、缓存和下载瀑布分析,不能只追求“分包数量多”。
Q37: Electron 如何限制页面导航、弹窗和外部链接?
答案:
Electron 页面不应拥有任意导航或打开任意 URL 的能力。常见治理方式是:
- 监听
will-navigate,只允许应用自己的可信来源,其余导航调用preventDefault()。 - 使用
webContents.setWindowOpenHandler()拦截window.open,默认拒绝新窗口,只对明确业务场景放行。 - 外部链接通过主进程调用
shell.openExternal(),但必须先校验协议和域名白名单,至少拒绝file:、javascript:等危险协议。 - 对登录回调、深链和重定向使用精确匹配,不能只做
startsWith之类容易绕过的字符串判断。
同时保持 nodeIntegration: false、contextIsolation: true 和沙箱开启。导航拦截是纵深防御的一层,不能替代 CSP、最小化 preload API 和 IPC 参数校验。
Q38: Tauri 2.0 相比 1.x 有哪些重要变化?
答案:
Tauri 2.0 的三大核心变化:
-
移动端支持:新增 iOS 和 Android 平台支持,使 Tauri 从桌面框架升级为全平台框架。移动端同样使用系统 WebView(iOS 的 WKWebView、Android 的 WebView)
-
插件系统重构:核心功能拆分为独立插件(fs、dialog、shell 等),按需引入。降低了核心包体积,也让第三方插件开发更规范
-
权限系统升级(ACL):从 1.x 的简单
allowlist布尔开关,升级为基于 Capabilities 的细粒度 ACL 权限系统。支持按窗口、按插件、按操作、按路径作用域授权
Q39: React Native 和 Flutter 的核心渲染区别?
答案:
这是最根本的区别,决定了两者在性能、一致性和原生体验上的差异:
| 对比维度 | React Native | Flutter |
|---|---|---|
| UI 组件 | 映射为平台原生组件 | 全部自绘,不使用原生组件 |
| 渲染引擎 | 平台原生渲染引擎 | Skia / Impeller (自带) |
| 一致性 | 跟随平台风格变化 | 像素级跨平台一致 |
| 平台感 | 天然的平台原生感 | 需手动实现(Cupertino / Material) |
| 字体/文本 | 使用平台字体渲染 | 自带文本排版引擎 |
| 可访问性 | 直接使用平台 a11y 系统 | 自建 Semantics 树映射 |
| 系统控件 | 原生(日期选择器等) | 需自绘或调用 PlatformView |
| 动画性能 | Reanimated 在 UI 线程运行 | 原生 60/120fps |
- React Native 的优势:平台原生感 — 使用系统原生的 ScrollView、TextInput 等组件,交互细节(惯性滚动、键盘弹出等)与原生一致
- Flutter 的优势:渲染一致性 — 在 iOS 和 Android 上每个像素都相同,设计还原度极高
- React Native 新架构 (JSI + Fabric) 显著缩小了与 Flutter 的性能差距
Q40: iOS WKWebView 的 JSBridge 有什么坑?
答案:
- 内存泄漏:
userContentController.add(self, name:)强引用 self,导致 ViewController 无法释放。需用弱引用代理或deinit前主动remove。 - 不能传 Function:
postMessage只能传 JSON 可序列化的数据,函数会被丢弃。 - Cookie / Storage 隔离:WKWebView 与 NSURLSession 默认不共享 Cookie,Cookie 同步主要依赖
WKHTTPCookieStore;持久化数据还受WKWebsiteDataStore.default()/.nonPersistent()影响;多 WebView 之间还要统一WKProcessPool,否则登录态和存储行为容易不一致。 evaluateJavaScript必须主线程调用,否则 crash。
Q41: Web App Manifest 中哪些字段会影响安装和启动体验?
答案:
name / short_name、icons、start_url、scope、display、theme_color 和 background_color 等字段共同描述安装后的名称、图标、启动范围与窗口形态。字段要求和安装提示策略会随浏览器与平台变化,不能只背一套固定尺寸清单。
测试时应分别验证:
- Manifest 能被正确获取,图标、MIME 和作用域有效。
- 从不同 URL 安装后,
start_url与登录/路由行为正确。 - 离线或弱网启动有可理解的回退。
- Android、桌面浏览器和 iOS 的安装与能力差异。
- 更新 Manifest、图标和 Service Worker 后旧用户的迁移。
Lighthouse 的 PWA 测试已被标记为弃用,不能再把“刷 PWA 分数”当成验收目标;应按目标浏览器的当前安装条件和真实用户旅程测试。
Q42: 跨平台应用做 OTA 更新有哪些边界?
答案:
OTA 适合更新 JavaScript、资源和可解释业务配置,但不能假设可以绕过应用商店规则修改所有 Native 能力。
需要控制:
- JS Bundle 与 Native 二进制的兼容版本,防止调用不存在的模块。
- 分批发布、崩溃监控、自动回滚和最低可运行版本。
- 更新包签名、TLS、完整性校验和防降级攻击。
- 数据库 Schema 与本地缓存迁移的前后兼容。
- 平台政策:涉及新增原生能力、权限和重大行为变化时仍需商店发版。
- 冷启动下载、弱网和磁盘不足时继续使用上一稳定版本。
Q43: Flutter 的渲染流程大致怎样工作?
答案:
Flutter 通常从 Widget 配置树更新 Element 树,再由 RenderObject 完成布局与绘制,生成 Layer/Scene 交给引擎栅格化并合成到平台表面。
三个概念要区分:
- Widget 是不可变配置,频繁重建本身不等于重建全部 Native View。
- Element 保存树中位置与状态,负责复用和更新。
- RenderObject 承担约束传递、尺寸计算、绘制和命中测试。
性能问题常来自过大的重建范围、昂贵布局/绘制、图片解码和着色器,而不是“Widget 创建太多”这一句话。应使用 DevTools 的帧图、Rebuild 和 Raster 指标定位 UI 线程或 Raster 线程瓶颈。
Q44: Taro 和 uni-app 应该怎样选型?
答案:
两者都覆盖多种小程序与 Web/App 目标,但实际能力、渲染路径、插件生态和平台支持会随版本变化。选型不要只归结为“React 选 Taro、Vue 选 uni-app”。
应针对目标版本做 PoC,重点比较:
- 团队主框架与现有组件/状态管理资产。
- 必须覆盖的平台及各端特有 API、审核与升级速度。
- 长列表、复杂交互、启动和包体等核心性能场景。
- Native/App 能力接入、插件质量和失败时自行维护的成本。
- 调试、测试、CI、Source Map、监控与版本升级体验。
选定后仍要把平台能力放入适配层,避免业务代码到处使用条件编译。最终结论应来自核心业务样例,而不是静态功能表。
Q45: Electron 的 preload 脚本和 contextBridge 应怎样设计?
答案:
开启 contextIsolation 后,preload 运行在隔离上下文中,可通过 contextBridge.exposeInMainWorld 向页面暴露一组窄 API。
安全设计原则:
- 不暴露完整
ipcRenderer、Node 模块或任意 channel 调用。 - 每个方法对应明确业务能力,参数和返回值做 Schema 校验。
- 主进程再次校验来源窗口、权限、文件路径和 URL。
- 订阅 API 返回取消函数,避免页面持有底层 EventEmitter。
- 不把高权限对象或回调引用直接跨上下文泄露。
preload 是权限适配层,不是把 Node API 换个名字挂到 window 上。