一、技术选型决策
1.1 技术栈对比分析
核心差异对比表
| 维度 | Flutter | React Native | Electron |
|---|---|---|---|
| 开发语言 | Dart | JavaScript/TypeScript | JavaScript/TypeScript |
| 渲染方式 | 自绘引擎(Skia) | 原生组件桥接 | Chromium + Node.js |
| 性能 | 接近原生(60fps) | 良好(需优化) | 一般(内存占用高) |
| 包体积 | 15-30MB | 20-40MB | 100-200MB |
| 学习曲线 | 陡峭(新语言) | 平缓(JS生态) | 平缓(Web技术) |
| 热更新 | 不支持(App Store) | 支持(CodePush) | 支持(自动更新) |
| UI 一致性 | 完全一致 | 平台差异 | 完全一致 |
| 适用场景 | 高性能App | 快速迭代App | 桌面应用 |
| 代码复用 | 仅 Flutter | 可与 Web 共用 | 与 Web 共用 |
1.2 技术选型决策树
需求分析
├─ 平台需求?
│ ├─ 移动端(iOS/Android)
│ │ ├─ 高性能要求? → Flutter
│ │ ├─ 快速迭代? → React Native
│ │ └─ 团队技术栈? → 根据团队选择
│ └─ 桌面端(Windows/Mac/Linux)
│ └─ Electron
│
├─ 性能要求?
│ ├─ 极致性能 → Flutter / 原生
│ ├─ 良好性能 → React Native
│ └─ 可接受性能 → Electron
│
└─ 开发成本?
├─ 预算充足 → 原生开发
├─ 中等预算 → Flutter / React Native
└─ 紧张预算 → 选择团队熟悉的技术栈
1.3 实战选型案例
案例1: 电商 App 技术选型
需求分析:
- 平台: iOS + Android
- 性能: 需要流畅的列表滚动和动画
- 迭代: 快速迭代,频繁发版
- 团队: 前端团队,熟悉 React
选型结果: React Native
理由:
- 团队熟悉 React,学习成本低
- 支持热更新,满足快速迭代需求
- 性能满足电商场景(优化后)
- 生态丰富,第三方库多
案例2: 金融交易 App 技术选型
需求分析:
- 平台: iOS + Android
- 性能: 实时数据展示,复杂图表
- 安全: 高安全性要求
- UI: 需要高度定制化
选型结果: Flutter
理由:
- 高性能,适合实时数据展示
- 自绘引擎,UI 完全可控
- AOT 编译,不易被反编译
- 动画流畅,用户体验好
案例3: 办公协同软件技术选型
需求分析:
- 平台: Windows + Mac + Linux
- 功能: 文件管理,即时通讯
- 集成: 需要调用系统 API
- 团队: Web 前端团队
选型结果: Electron
理由:
- 跨三大桌面平台
- 可以复用 Web 代码
- Node.js 可以调用系统 API
- 团队熟悉,开发效率高
二、与 Web 代码复用
2.1 React Native 代码复用方案
实现架构
共享层 (Shared)
├─ 业务逻辑 (Hooks/Store)
├─ 工具函数 (Utils)
├─ API 请求 (Services)
└─ 类型定义 (Types)
平台适配层
├─ Web (React)
│ ├─ 路由 (React Router)
│ ├─ 组件 (HTML/CSS)
│ └─ 构建 (Webpack)
└─ Mobile (React Native)
├─ 导航 (React Navigation)
├─ 组件 (Native Components)
└─ 构建 (Metro)
代码复用示例
共享的业务逻辑 Hook:
// shared/hooks/useUserInfo.js
import { useState, useEffect } from 'react'
import { getUserInfo as getUserInfoApi } from '../services/user'
/**
* 获取用户信息 Hook
* 可在 Web 和 RN 中复用
*/
export function useUserInfo() {
const [userInfo, setUserInfo] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const getUserInfo = async () => {
setLoading(true)
setError(null)
try {
const data = await getUserInfoApi()
setUserInfo(data)
return data
} catch (err) {
setError(err)
throw err
} finally {
setLoading(false)
}
}
useEffect(() => {
getUserInfo()
}, [])
const updateUserInfo = async (updates) => {
setUserInfo(prev => ({ ...prev, ...updates }))
}
return {
userInfo,
loading,
error,
getUserInfo,
updateUserInfo
}
}
// shared/hooks/useRequest.js
import { useState, useCallback } from 'react'
/**
* 通用请求 Hook
* 支持 Web 和 RN
*/
export function useRequest(requestFn) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const run = useCallback(async (...args) => {
setLoading(true)
setError(null)
try {
const result = await requestFn(...args)
setData(result)
return result
} catch (err) {
setError(err)
throw err
} finally {
setLoading(false)
}
}, [requestFn])
const reset = useCallback(() => {
setData(null)
setError(null)
setLoading(false)
}, [])
return {
data,
loading,
error,
run,
reset
}
}
平台适配的组件:
// shared/components/Button/Button.web.js
import React from 'react'
import './Button.css'
export const Button = ({ children, onPress, disabled, style }) => {
return (
<button
className="custom-button"
onClick={onPress}
disabled={disabled}
style={style}
>
{children}
</button>
)
}
// shared/components/Button/Button.native.js
import React from 'react'
import { TouchableOpacity, Text, StyleSheet } from 'react-native'
export const Button = ({ children, onPress, disabled, style }) => {
return (
<TouchableOpacity
style={[styles.button, style, disabled && styles.disabled]}
onPress={onPress}
disabled={disabled}
>
<Text style={styles.text}>{children}</Text>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#007AFF',
padding: 12,
borderRadius: 8,
alignItems: 'center'
},
disabled: {
opacity: 0.5
},
text: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold'
}
})
// shared/components/Button/index.js
// Metro bundler 会根据平台自动选择文件
export { Button } from './Button'
使用示例:
// pages/Profile/index.js
// 这个文件可以在 Web 和 RN 中使用
import React from 'react'
import { useUserInfo } from '@/shared/hooks/useUserInfo'
import { Button } from '@/shared/components/Button'
import { Container, Title, Info } from './styles' // 平台特定样式
export const ProfilePage = () => {
const { userInfo, loading, updateUserInfo } = useUserInfo()
if (loading) {
return <Container><Title>加载中...</Title></Container>
}
if (!userInfo) {
return <Container><Title>暂无数据</Title></Container>
}
const handleEdit = () => {
// 业务逻辑,在两个平台都一样
updateUserInfo({ name: '新名字' })
}
return (
<Container>
<Title>个人信息</Title>
<Info>姓名: {userInfo.name}</Info>
<Info>邮箱: {userInfo.email}</Info>
<Button onPress={handleEdit}>编辑</Button>
</Container>
)
}
2.2 Electron 代码复用方案
实现架构
Electron 应用
├─ 主进程 (Main Process)
│ ├─ 窗口管理
│ ├─ 系统集成
│ └─ IPC 通信
│
└─ 渲染进程 (Renderer Process)
├─ Web 应用代码 (100% 复用)
├─ React/Vue 组件
├─ 业务逻辑
└─ 样式文件
代码复用示例
Electron 主进程:
// electron/main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
let mainWindow
// 创建窗口
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
// 加载 Web 应用
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:3000')
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
// 开发工具
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools()
}
}
// 应用启动
app.whenReady().then(createWindow)
// 窗口关闭
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
// IPC 通信 - 文件操作
ipcMain.handle('read-file', async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8')
return { success: true, data: content }
} catch (err) {
return { success: false, error: err.message }
}
})
ipcMain.handle('write-file', async (event, filePath, content) => {
try {
fs.writeFileSync(filePath, content, 'utf-8')
return { success: true }
} catch (err) {
return { success: false, error: err.message }
}
})
// IPC 通信 - 对话框
ipcMain.handle('show-open-dialog', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
})
return result
})
ipcMain.handle('show-save-dialog', async () => {
const result = await dialog.showSaveDialog(mainWindow, {
defaultPath: 'untitled.txt',
filters: [
{ name: 'Text Files', extensions: ['txt', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
})
return result
})
// IPC 通信 - 应用信息
ipcMain.handle('get-app-info', () => {
return {
version: app.getVersion(),
name: app.getName(),
path: app.getAppPath()
}
})
Preload 脚本:
// electron/preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
writeFile: (filePath, content) => ipcRenderer.invoke('write-file', filePath, content),
// 对话框
showOpenDialog: () => ipcRenderer.invoke('show-open-dialog'),
showSaveDialog: () => ipcRenderer.invoke('show-save-dialog'),
// 应用信息
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
// 监听事件
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', callback)
}
})
Web 应用代码(完全复用):
// src/hooks/useElectronAPI.js
import { useState, useEffect } from 'react'
/**
* Electron API Hook
* 在 Web 环境自动降级
*/
export function useElectronAPI() {
const [isElectron, setIsElectron] = useState(false)
const [appInfo, setAppInfo] = useState(null)
useEffect(() => {
// 检测是否在 Electron 环境
setIsElectron(!!window.electronAPI)
if (window.electronAPI) {
// 获取应用信息
window.electronAPI.getAppInfo().then(setAppInfo)
}
}, [])
const readFile = async (filePath) => {
if (!window.electronAPI) {
throw new Error('Not in Electron environment')
}
return window.electronAPI.readFile(filePath)
}
const writeFile = async (filePath, content) => {
if (!window.electronAPI) {
throw new Error('Not in Electron environment')
}
return window.electronAPI.writeFile(filePath, content)
}
const openFile = async () => {
if (!window.electronAPI) {
// Web 环境降级方案
return new Promise((resolve) => {
const input = document.createElement('input')
input.type = 'file'
input.onchange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (event) => {
resolve({
canceled: false,
filePaths: [file.name],
content: event.target.result
})
}
reader.readAsText(file)
} else {
resolve({ canceled: true })
}
}
input.click()
})
}
const result = await window.electronAPI.showOpenDialog()
if (result.canceled) {
return result
}
const content = await readFile(result.filePaths[0])
return { ...result, content: content.data }
}
const saveFile = async (content) => {
if (!window.electronAPI) {
// Web 环境降级方案
const blob = new Blob([content], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'file.txt'
a.click()
URL.revokeObjectURL(url)
return { canceled: false }
}
const result = await window.electronAPI.showSaveDialog()
if (result.canceled) {
return result
}
await writeFile(result.filePath, content)
return result
}
return {
isElectron,
appInfo,
readFile,
writeFile,
openFile,
saveFile
}
}
使用示例:
<!-- src/pages/Editor/index.vue -->
<template>
<div class="editor-page">
<div class="toolbar">
<button @click="handleOpen" class="btn">打开文件</button>
<button @click="handleSave" class="btn">保存文件</button>
<span v-if="isElectron" class="app-info">
{{ appInfo?.name }} v{{ appInfo?.version }}
</span>
</div>
<textarea
v-model="content"
class="editor"
placeholder="开始编辑..."
></textarea>
<div v-if="currentFile" class="status">
当前文件: {{ currentFile }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useElectronAPI } from '@/hooks/useElectronAPI'
const content = ref('')
const currentFile = ref('')
const { isElectron, appInfo, openFile, saveFile } = useElectronAPI()
const handleOpen = async () => {
try {
const result = await openFile()
if (!result.canceled) {
content.value = result.content
currentFile.value = result.filePaths[0]
}
} catch (err) {
alert('打开文件失败: ' + err.message)
}
}
const handleSave = async () => {
try {
const result = await saveFile(content.value)
if (!result.canceled) {
currentFile.value = result.filePath
alert('保存成功')
}
} catch (err) {
alert('保存文件失败: ' + err.message)
}
}
</script>
<style scoped>
.editor-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #1e1e1e;
}
.toolbar {
display: flex;
gap: 10px;
padding: 15px;
background: #2d2d2d;
border-bottom: 1px solid #3d3d3d;
}
.btn {
padding: 8px 16px;
background: #0066cc;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn:hover {
background: #0052a3;
}
.app-info {
margin-left: auto;
color: #999;
font-size: 14px;
}
.editor {
flex: 1;
padding: 20px;
background: #1e1e1e;
color: #d4d4d4;
border: none;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
}
.editor:focus {
outline: none;
}
.status {
padding: 10px 15px;
background: #2d2d2d;
border-top: 1px solid #3d3d3d;
color: #999;
font-size: 12px;
}
</style>
三、性能对比分析
3.1 性能测试方案
测试场景设计
// performance-test/scenarios.js
/**
* 性能测试场景
*/
export const testScenarios = [
{
name: '长列表滚动',
description: '渲染 1000 条数据的列表,测试滚动性能',
metrics: ['FPS', '内存占用', '首屏渲染时间']
},
{
name: '复杂动画',
description: '100 个元素同时执行动画',
metrics: ['FPS', 'CPU 占用', '动画流畅度']
},
{
name: '大量计算',
description: '执行复杂的数学计算',
metrics: ['执行时间', 'CPU 占用']
},
{
name: '网络请求',
description: '并发 20 个网络请求',
metrics: ['响应时间', '成功率']
},
{
name: '包体积',
description: '对比应用包大小',
metrics: ['APK/IPA 大小', '下载包大小']
}
]
/**
* 性能监控工具
*/
export class PerformanceMonitor {
constructor(platform) {
this.platform = platform
this.results = []
}
// 测试 FPS
measureFPS(duration = 5000) {
return new Promise((resolve) => {
let frameCount = 0
let lastTime = performance.now()
const measureFrame = () => {
frameCount++
if (performance.now() - lastTime >= duration) {
const fps = frameCount / (duration / 1000)
resolve(fps)
} else {
requestAnimationFrame(measureFrame)
}
}
requestAnimationFrame(measureFrame)
})
}
// 测试内存占用
measureMemory() {
if (performance.memory) {
return {
used: performance.memory.usedJSHeapSize / 1024 / 1024,
total: performance.memory.totalJSHeapSize / 1024 / 1024,
limit: performance.memory.jsHeapSizeLimit / 1024 / 1024
}
}
return null
}
// 测试渲染时间
measureRenderTime(fn) {
const start = performance.now()
fn()
const end = performance.now()
return end - start
}
// 生成报告
generateReport() {
return {
platform: this.platform,
timestamp: Date.now(),
results: this.results
}
}
}
3.2 实测数据对比
性能对比表(基于实际测试)
| 测试项 | Flutter | React Native | Electron |
|---|---|---|---|
| 长列表滚动 | |||
| FPS | 58-60 | 45-55 | 50-58 |
| 内存(MB) | 120 | 180 | 250 |
| 复杂动画 | |||
| FPS | 59-60 | 40-50 | 55-60 |
| CPU(%) | 35 | 55 | 45 |
| 首屏渲染 | |||
| 时间(ms) | 450 | 800 | 1200 |
| 包体积 | |||
| Android(MB) | 18 | 28 | - |
| iOS(MB) | 25 | 35 | - |
| Desktop(MB) | - | - | 150 |
| 开发效率 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
性能测试代码示例
React Native 性能测试:
// __tests__/performance.test.js
import { PerformanceMonitor } from './performance-test/scenarios'
import { render } from '@testing-library/react-native'
import { LongList } from '@/components/LongList'
describe('React Native 性能测试', () => {
const monitor = new PerformanceMonitor('React Native')
test('长列表渲染性能', async () => {
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
title: `Item ${i}`,
desc: `Description ${i}`
}))
const startTime = performance.now()
const { getAllByTestId } = render(<LongList data={data} />)
const endTime = performance.now()
const renderTime = endTime - startTime
const items = getAllByTestId('list-item')
expect(renderTime).toBeLessThan(1000) // 渲染时间小于 1s
expect(items.length).toBeGreaterThan(0) // 至少渲染了部分项
monitor.results.push({
scenario: '长列表渲染',
renderTime,
itemsRendered: items.length
})
})
test('滚动性能', async () => {
const fps = await monitor.measureFPS(5000)
expect(fps).toBeGreaterThan(50) // FPS 大于 50
monitor.results.push({
scenario: '滚动性能',
fps
})
})
test('内存占用', () => {
const memory = monitor.measureMemory()
if (memory) {
expect(memory.used).toBeLessThan(300) // 内存占用小于 300MB
monitor.results.push({
scenario: '内存占用',
memory
})
}
})
afterAll(() => {
const report = monitor.generateReport()
console.log('性能测试报告:', JSON.stringify(report, null, 2))
})
})
四、调试与发布流程
4.1 React Native 调试流程
开发环境调试
// debug/react-native-debug.js
/**
* React Native 调试配置
*/
// 1. 启用 React DevTools
import React from 'react'
import { AppRegistry } from 'react-native'
if (__DEV__) {
// 连接 React DevTools
require('react-devtools-core').connectToDevTools({
host: 'localhost',
port: 8097
})
// 启用性能监控
require('react-native').unstable_enableLogBox()
}
// 2. 网络请求调试
global.XMLHttpRequest = global.originalXMLHttpRequest || global.XMLHttpRequest
// 3. console 增强
const consoleLog = console.log
console.log = (...args) => {
consoleLog('[LOG]', new Date().toISOString(), ...args)
}
// 4. 错误边界
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo)
// 上报错误到监控平台
if (!__DEV__) {
reportError(error, errorInfo)
}
}
render() {
if (this.state.hasError) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>出错了: {this.state.error?.message}</Text>
</View>
)
}
return this.props.children
}
}
// 5. 性能监控
import { InteractionManager } from 'react-native'
const monitorPerformance = (screenName) => {
const start = Date.now()
InteractionManager.runAfterInteractions(() => {
const duration = Date.now() - start
console.log(`[Performance] ${screenName} 加载耗时: ${duration}ms`)
// 上报性能数据
if (!__DEV__) {
reportPerformance(screenName, duration)
}
})
}
// 使用示例
export const HomeScreen = () => {
useEffect(() => {
monitorPerformance('HomeScreen')
}, [])
return (
<ErrorBoundary>
{/* 页面内容 */}
</ErrorBoundary>
)
}
发布流程脚本
#!/bin/bash
# scripts/rn-release.sh
# React Native 发布脚本
set -e
echo "开始 React Native 发布流程..."
# 1. 检查环境
echo "检查环境..."
if ! command -v node &> /dev/null; then
echo "错误: 未安装 Node.js"
exit 1
fi
if ! command -v react-native &> /dev/null; then
echo "错误: 未安装 React Native CLI"
exit 1
fi
# 2. 清理缓存
echo "清理缓存..."
rm -rf node_modules
rm -rf ios/build
rm -rf android/app/build
npm cache clean --force
# 3. 安装依赖
echo "安装依赖..."
npm install
# 4. 代码检查
echo "代码检查..."
npm run lint
npm run type-check
# 5. 运行测试
echo "运行测试..."
npm test
# 6. 构建 Android
echo "构建 Android Release..."
cd android
./gradlew clean
./gradlew assembleRelease
cd ..
# 7. 构建 iOS
echo "构建 iOS Release..."
cd ios
pod install
xcodebuild -workspace YourApp.xcworkspace \
-scheme YourApp \
-configuration Release \
-archivePath build/YourApp.xcarchive \
archive
cd ..
# 8. 生成构建产物信息
BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S")
BUILD_VERSION=$(node -p "require('./package.json').version")
echo "{
\"version\": \"$BUILD_VERSION\",
\"buildDate\": \"$BUILD_DATE\",
\"platform\": \"React Native\",
\"android\": \"android/app/build/outputs/apk/release/app-release.apk\",
\"ios\": \"ios/build/YourApp.xcarchive\"
}" > build-info.json
echo "发布完成! 构建信息:"
cat build-info.json
4.2 Electron 调试与发布
开发环境配置
// electron/dev.js
/**
* Electron 开发环境配置
*/
const { app, BrowserWindow } = require('electron')
const path = require('path')
// 启用开发者工具
if (process.env.NODE_ENV === 'development') {
// 安装 Vue/React DevTools
app.whenReady().then(() => {
const { default: installExtension, REACT_DEVELOPER_TOOLS, VUEJS_DEVTOOLS } = require('electron-devtools-installer')
installExtension([REACT_DEVELOPER_TOOLS, VUEJS_DEVTOOLS])
.then((name) => console.log(`已安装扩展: ${name}`))
.catch((err) => console.log('安装扩展失败:', err))
})
// 热重载
require('electron-reload')(__dirname, {
electron: path.join(__dirname, '../node_modules', '.bin', 'electron'),
hardResetMethod: 'exit'
})
}
// 错误处理
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason)
})
打包配置
// electron-builder.config.js
/**
* Electron Builder 打包配置
*/
module.exports = {
appId: 'com.example.app',
productName: '应用名称',
copyright: 'Copyright © 2024',
// 打包目录
directories: {
output: 'release',
buildResources: 'build'
},
// 文件配置
files: [
'dist/**/*',
'electron/**/*',
'package.json'
],
// 排除文件
extraFiles: [
{
from: 'resources',
to: 'resources',
filter: ['**/*']
}
],
// Windows 配置
win: {
target: [
{
target: 'nsis',
arch: ['x64', 'ia32']
},
{
target: 'zip',
arch: ['x64']
}
],
icon: 'build/icon.ico',
artifactName: '${productName}-${version}-${arch}.${ext}'
},
// NSIS 安装程序配置
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: '应用名称'
},
// macOS 配置
mac: {
target: ['dmg', 'zip'],
icon: 'build/icon.icns',
category: 'public.app-category.productivity',
artifactName: '${productName}-${version}-${arch}.${ext}',
hardenedRuntime: true,
entitlements: 'build/entitlements.mac.plist',
entitlementsInherit: 'build/entitlements.mac.plist'
},
// DMG 配置
dmg: {
contents: [
{
x: 130,
y: 220
},
{
x: 410,
y: 220,
type: 'link',
path: '/Applications'
}
]
},
// Linux 配置
linux: {
target: ['AppImage', 'deb', 'rpm'],
icon: 'build/icons',
category: 'Utility',
artifactName: '${productName}-${version}-${arch}.${ext}'
},
// 自动更新配置
publish: [
{
provider: 'github',
owner: 'your-username',
repo: 'your-repo'
}
]
}
自动更新实现
// electron/updater.js
/**
* Electron 自动更新
*/
const { app, dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
class Updater {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.setupAutoUpdater()
}
setupAutoUpdater() {
// 配置更新服务器
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your-username',
repo: 'your-repo'
})
// 检查更新
autoUpdater.on('checking-for-update', () => {
console.log('正在检查更新...')
this.sendToRenderer('update-checking')
})
// 发现新版本
autoUpdater.on('update-available', (info) => {
console.log('发现新版本:', info.version)
this.sendToRenderer('update-available', info)
dialog.showMessageBox(this.mainWindow, {
type: 'info',
title: '发现新版本',
message: `发现新版本 ${info.version},是否现在更新?`,
buttons: ['更新', '稍后']
}).then(result => {
if (result.response === 0) {
autoUpdater.downloadUpdate()
}
})
})
// 没有新版本
autoUpdater.on('update-not-available', (info) => {
console.log('已是最新版本')
this.sendToRenderer('update-not-available')
})
// 下载进度
autoUpdater.on('download-progress', (progressObj) => {
console.log(`下载进度: ${progressObj.percent.toFixed(2)}%`)
this.sendToRenderer('update-download-progress', progressObj)
})
// 下载完成
autoUpdater.on('update-downloaded', (info) => {
console.log('更新下载完成')
this.sendToRenderer('update-downloaded')
dialog.showMessageBox(this.mainWindow, {
type: 'info',
title: '更新就绪',
message: '新版本已下载完成,应用将在重启后更新',
buttons: ['立即重启', '稍后重启']
}).then(result => {
if (result.response === 0) {
autoUpdater.quitAndInstall()
}
})
})
// 错误处理
autoUpdater.on('error', (err) => {
console.error('更新错误:', err)
this.sendToRenderer('update-error', err)
})
}
// 发送消息到渲染进程
sendToRenderer(channel, data) {
if (this.mainWindow) {
this.mainWindow.webContents.send(channel, data)
}
}
// 手动检查更新
checkForUpdates() {
autoUpdater.checkForUpdates()
}
}
module.exports = Updater
五、Electron 特色功能实现
5.1 系统托盘
// electron/tray.js
/**
* 系统托盘功能
*/
const { Tray, Menu, nativeImage } = require('electron')
const path = require('path')
class TrayManager {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.tray = null
this.createTray()
}
createTray() {
// 创建托盘图标
const icon = nativeImage.createFromPath(
path.join(__dirname, '../build/tray-icon.png')
)
this.tray = new Tray(icon)
// 设置提示文字
this.tray.setToolTip('应用名称')
// 创建右键菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
this.mainWindow.show()
}
},
{
label: '隐藏窗口',
click: () => {
this.mainWindow.hide()
}
},
{
type: 'separator'
},
{
label: '设置',
click: () => {
this.mainWindow.webContents.send('navigate-to', '/settings')
this.mainWindow.show()
}
},
{
type: 'separator'
},
{
label: '退出',
click: () => {
this.mainWindow.destroy()
}
}
])
this.tray.setContextMenu(contextMenu)
// 点击托盘图标
this.tray.on('click', () => {
if (this.mainWindow.isVisible()) {
this.mainWindow.hide()
} else {
this.mainWindow.show()
}
})
}
// 更新托盘图标
updateIcon(iconPath) {
const icon = nativeImage.createFromPath(iconPath)
this.tray.setImage(icon)
}
// 更新提示文字
updateTooltip(text) {
this.tray.setToolTip(text)
}
// 销毁托盘
destroy() {
if (this.tray) {
this.tray.destroy()
this.tray = null
}
}
}
module.exports = TrayManager
5.2 快捷键管理
// electron/shortcuts.js
/**
* 全局快捷键管理
*/
const { globalShortcut, dialog } = require('electron')
class ShortcutManager {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.shortcuts = new Map()
this.registerShortcuts()
}
registerShortcuts() {
// 显示/隐藏窗口
this.register('CommandOrControl+Shift+H', () => {
if (this.mainWindow.isVisible()) {
this.mainWindow.hide()
} else {
this.mainWindow.show()
}
})
// 刷新页面
this.register('CommandOrControl+R', () => {
if (process.env.NODE_ENV === 'development') {
this.mainWindow.webContents.reload()
}
})
// 打开开发者工具
this.register('CommandOrControl+Shift+I', () => {
this.mainWindow.webContents.toggleDevTools()
})
// 全屏切换
this.register('F11', () => {
const isFullScreen = this.mainWindow.isFullScreen()
this.mainWindow.setFullScreen(!isFullScreen)
})
// 截图功能
this.register('CommandOrControl+Shift+X', async () => {
const screenshot = await this.mainWindow.webContents.capturePage()
// 处理截图...
})
}
register(accelerator, callback) {
const success = globalShortcut.register(accelerator, callback)
if (success) {
this.shortcuts.set(accelerator, callback)
console.log(`快捷键注册成功: ${accelerator}`)
} else {
console.error(`快捷键注册失败: ${accelerator}`)
}
}
unregister(accelerator) {
globalShortcut.unregister(accelerator)
this.shortcuts.delete(accelerator)
}
unregisterAll() {
globalShortcut.unregisterAll()
this.shortcuts.clear()
}
isRegistered(accelerator) {
return globalShortcut.isRegistered(accelerator)
}
getRegistered() {
return Array.from(this.shortcuts.keys())
}
}
module.exports = ShortcutManager
5.3 原生菜单
// electron/menu.js
/**
* 应用菜单
*/
const { Menu, shell, app } = require('electron')
class MenuManager {
constructor(mainWindow) {
this.mainWindow = mainWindow
this.createMenu()
}
createMenu() {
const isMac = process.platform === 'darwin'
const template = [
// Mac 专用应用菜单
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about', label: '关于' },
{ type: 'separator' },
{ role: 'services', label: '服务' },
{ type: 'separator' },
{ role: 'hide', label: '隐藏' },
{ role: 'hideOthers', label: '隐藏其他' },
{ role: 'unhide', label: '显示全部' },
{ type: 'separator' },
{ role: 'quit', label: '退出' }
]
}] : []),
// 文件菜单
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CommandOrControl+N',
click: () => {
this.mainWindow.webContents.send('menu-new')
}
},
{
label: '打开',
accelerator: 'CommandOrControl+O',
click: () => {
this.mainWindow.webContents.send('menu-open')
}
},
{
label: '保存',
accelerator: 'CommandOrControl+S',
click: () => {
this.mainWindow.webContents.send('menu-save')
}
},
{ type: 'separator' },
isMac ?
{ role: 'close', label: '关闭窗口' } :
{ role: 'quit', label: '退出' }
]
},
// 编辑菜单
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
{ role: 'selectAll', label: '全选' }
]
},
// 查看菜单
{
label: '查看',
submenu: [
{ role: 'reload', label: '重新加载' },
{ role: 'forceReload', label: '强制重新加载' },
{ role: 'toggleDevTools', label: '开发者工具' },
{ type: 'separator' },
{ role: 'resetZoom', label: '实际大小' },
{ role: 'zoomIn', label: '放大' },
{ role: 'zoomOut', label: '缩小' },
{ type: 'separator' },
{ role: 'togglefullscreen', label: '全屏' }
]
},
// 窗口菜单
{
label: '窗口',
submenu: [
{ role: 'minimize', label: '最小化' },
{ role: 'zoom', label: '缩放' },
...(isMac ? [
{ type: 'separator' },
{ role: 'front', label: '全部置于顶层' }
] : [
{ role: 'close', label: '关闭' }
])
]
},
// 帮助菜单
{
label: '帮助',
submenu: [
{
label: '查看文档',
click: async () => {
await shell.openExternal('https://docs.example.com')
}
},
{
label: '反馈问题',
click: async () => {
await shell.openExternal('https://github.com/example/issues')
}
},
{ type: 'separator' },
{
label: '检查更新',
click: () => {
this.mainWindow.webContents.send('check-for-updates')
}
}
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
// 更新菜单项
updateMenuItem(id, options) {
const menu = Menu.getApplicationMenu()
const menuItem = menu.getMenuItemById(id)
if (menuItem) {
Object.assign(menuItem, options)
}
}
}
module.exports = MenuManager
六、简历描述模板
6.1 项目经验描述
项目名称: 企业级跨平台应用开发 技术栈: React Native / Electron + React + TypeScript 项目周期: 2023.09 - 2024.03 (6个月)
项目描述: 负责开发一款支持移动端(iOS/Android)和桌面端(Windows/Mac)的跨平台协同办公应用。移动端采用 React Native,桌面端采用 Electron,通过代码复用和统一的架构设计,大幅降低开发成本。
核心职责:
- 主导跨平台架构设计,实现 70% 的业务逻辑代码在移动端和桌面端复用
- 优化 React Native 性能,长列表滚动帧率从 45fps 提升到 58fps,内存占用降低 40%
- 开发 Electron 系统集成功能,包括托盘、快捷键、自动更新等,提升桌面端用户体验
- 建立完整的调试和发布流程,实现自动化打包和多平台发布,发版效率提升 80%
- 封装跨平台组件库,包含 30+ 个业务组件,支持移动端和桌面端统一调用
技术亮点:
- 代码复用架构: 设计三层架构(共享层/适配层/平台层),业务逻辑、状态管理、工具函数完全复用,仅 UI 层面做平台适配,开发效率提升 65%
- 性能优化: React Native 采用虚拟列表、图片懒加载、防抖节流等优化手段,核心页面性能提升 60%;Electron 通过进程隔离和资源预加载,启动时间缩短 50%
- 自动化流程: 建立 CI/CD 流水线,实现代码检查、自动化测试、多平台打包的全自动化,单次发版时间从 2 小时缩短到 20 分钟
- 系统集成: Electron 实现系统级功能,包括全局快捷键、系统托盘、文件关联、自动更新等,桌面端功能完整度达到原生应用水平
项目成果:
- 支持 4 个平台同步发布,代码复用率 70%
- 移动端性能优化后,用户留存率提升 30%
- 桌面端用户满意度 4.8/5.0,好评率 95%
- 开发和维护成本降低 60%
6.2 难点与亮点话术
难点1: React Native 性能优化
面试官: 你提到优化了 React Native 性能,具体做了哪些工作?
回答话术: "我们的应用有一个消息列表页面,数据量大概几千条,用户反馈滑动时明显卡顿。我用 React Native 的 Performance Monitor 分析后,发现主要问题:
第一,列表组件用的是 ScrollView,会一次性渲染所有数据,导致初始化很慢,内存占用高。
第二,每个列表项都包含头像图片,没有做懒加载,网络请求太多。
第三,滚动事件的回调函数里有复杂计算,没有做节流,导致主线程阻塞。
我的优化方案:
首先,把 ScrollView 换成 FlatList,利用虚拟列表只渲染可见区域。设置 initialNumToRender 为 20,windowSize 为 10,既保证了首屏渲染速度,又有足够的缓冲区。
其次,图片用 FastImage 库替代原生 Image,支持缓存和懒加载。还给图片加了占位符,改善了加载体验。
然后,滚动事件用了 lodash 的 throttle 函数,设置 100ms 的节流,避免频繁计算。
最后,用 React.memo 和 useMemo 优化了列表项组件,避免不必要的重渲染。
优化后,列表滚动帧率从 45fps 提升到 58fps,接近 60fps 的目标。内存占用从 280MB 降到 160MB。用户反馈卡顿问题基本解决。"
难点2: 移动端和桌面端代码复用
面试官: 如何实现移动端和桌面端的代码复用?遇到过什么问题?
回答话术: "这是我们项目最大的挑战。要在 React Native 和 Electron 之间复用代码,关键是做好分层设计。
我设计了三层架构:
第一层,共享层。包含业务逻辑、状态管理、工具函数、API 请求这些平台无关的代码。这部分代码 100% 复用,占整个应用代码的 70%。
第二层,适配层。封装平台差异 API,对外提供统一接口。比如文件操作,Electron 用 Node.js 的 fs 模块,RN 用 react-native-fs,但我封装了统一的 FileManager 类,业务代码调用同样的方法。
第三层,平台层。UI 组件各平台独立实现,但保持相同的 props 接口。比如 Button 组件,RN 用 TouchableOpacity,Electron 用 HTML button,但用法完全一样。
遇到的最大问题是路由。RN 用 React Navigation,Electron 用 React Router,API 完全不同。我的解决方案是,封装统一的 Navigator 类,内部根据平台调用不同的路由 API。业务代码只需调用 Navigator.push(),不用关心底层实现。
另一个问题是存储。RN 用 AsyncStorage,Electron 用 localStorage。我封装了 StorageManager,统一了读写接口,还加了加密功能,保证数据安全。
最终实现了 70% 的代码复用,大大降低了开发成本。而且因为业务逻辑是共享的,修 bug 只需要改一次,维护成本也降低了。"
难点3: Electron 系统集成
面试官: Electron 的系统集成功能你是怎么实现的?
回答话术: "我开发了几个关键的系统集成功能:
第一个是系统托盘。用户可以最小化应用到托盘,点击托盘图标可以快速显示/隐藏窗口。还做了右键菜单,包含常用功能的快捷入口。这个功能特别受用户欢迎,因为不占用任务栏空间。
实现上,用的是 Electron 的 Tray API。关键是要处理好窗口的显示/隐藏状态,还有托盘图标的更新。比如有新消息时,托盘图标会闪烁提醒。
第二个是全局快捷键。用户可以自定义快捷键来触发应用的功能,比如 Ctrl+Shift+H 显示/隐藏窗口,Ctrl+Shift+X 截图。
这个用的是 globalShortcut API。要注意的是,全局快捷键可能和系统快捷键冲突,所以我做了快捷键检测,如果注册失败会提示用户更换。
第三个是自动更新。应用启动时自动检查更新,有新版本会提示用户下载。支持静默更新和手动更新两种模式。
这个用的是 electron-updater 库。集成了 GitHub Releases 作为更新服务器,发版时会自动生成差量更新包,节省下载流量。还做了更新进度展示,用户体验很好。
第四个是文件关联。用户双击特定格式的文件,可以直接用我们的应用打开。这个在 Windows 上需要写注册表,Mac 上需要修改 Info.plist,比较复杂。
我用 electron-builder 的配置实现的,打包时会自动处理文件关联。还做了文件类型的图标关联,看起来更专业。
这些系统集成功能让我们的 Electron 应用达到了接近原生应用的体验,用户满意度很高。"
难点4: 跨平台打包发布
面试官: 多平台打包发布流程是怎么做的?
回答话术: "我们需要打包 4 个平台:iOS、Android、Windows、Mac,手动打包太慢,所以我建立了自动化流程。
第一步,统一构建脚本。写了一个 release.sh 脚本,可以一键打包所有平台。脚本会自动:
- 检查环境(Node.js、Xcode、Android SDK 等)
- 清理缓存
- 安装依赖
- 运行代码检查和测试
- 根据平台调用相应的构建命令
- 生成构建产物和版本信息
第二步,配置 CI/CD。用的是 GitHub Actions,每次推送到 release 分支就会自动触发打包流程。配置了并行构建,iOS 和 Android 同时打包,节省时间。
第三步,版本管理。用 semantic-release 自动生成版本号和 CHANGELOG。根据 commit 信息自动判断是 patch、minor 还是 major 更新。
第四步,产物管理。所有构建产物都上传到 GitHub Releases,自动生成下载链接。还集成了应用市场的自动提审,iOS 提交到 App Store Connect,Android 提交到各大应用市场。
第五步,灰度发布。支持分批发布,先给 10% 的用户推送更新,观察没问题再全量发布。这个用的是 CodePush(RN)和自定义更新服务(Electron)。
优化后,整个发版流程从 2 小时缩短到 20 分钟,而且全自动化,不需要人工介入。大大提高了迭代效率,我们现在可以做到一周发版 2-3 次。"
七、面试常见问题 SOP
Q1: Flutter、React Native、Electron 如何选择?
标准回答: "选择框架要考虑三个维度:
平台需求:
- 纯移动端 → Flutter 或 React Native
- 纯桌面端 → Electron
- 移动+桌面 → React Native + Electron(代码可复用)
性能要求:
- 极致性能(游戏、AR) → Flutter 或原生
- 一般应用 → React Native(优化后)
- 桌面工具 → Electron(性能可接受)
团队技术栈:
- 擅长 Dart → Flutter
- 擅长 JS/React → React Native / Electron
- 擅长 Vue → Weex(已不推荐)
我的建议:
- To C 高性能应用 → Flutter
- To B 快速迭代应用 → React Native
- 桌面办公软件 → Electron
- 跨移动+桌面 → React Native + Electron"
Q2: React Native 和原生开发的性能差距有多大?
标准回答: "React Native 的性能差距主要体现在:
列表滚动:
- 原生: 60fps 稳定
- RN 未优化: 40-50fps
- RN 优化后: 55-60fps
动画效果:
- 原生: 使用 GPU 加速,非常流畅
- RN: 复杂动画可能掉帧,需要用 Animated 或 Reanimated 优化
启动速度:
- 原生: 1-2 秒
- RN: 2-4 秒(需要加载 JS bundle)
内存占用:
- 原生: 较小
- RN: 稍大(因为 JS 引擎)
但通过优化,RN 可以达到接近原生的性能:
- 使用 FlatList 虚拟列表
- 图片懒加载和缓存
- 动画用 Animated API
- 避免不必要的 re-render
- 使用 Hermes 引擎
我们项目优化后,日常使用感知不到差距。除非是游戏或 AR 这种对性能要求极高的场景,否则 RN 完全够用。"
Q3: Electron 应用体积大,怎么优化?
标准回答: "Electron 应用确实偏大,主要是因为内置了 Chromium 和 Node.js。优化方案:
第一,代码层面:
- Tree Shaking 去除无用代码
- 代码分割,按需加载
- 图片压缩,使用 webp 格式
- 第三方库按需引入,不全量引入
第二,打包层面:
- 使用 electron-builder 压缩
- 配置 asar 打包,减小文件数量
- 使用 NSIS 安装程序,支持压缩
第三,资源层面:
- 资源文件放 CDN,不打包到应用里
- 字体按需加载,不全量引入
- 音视频用流式加载,不本地存储
第四,更新策略:
- 实现增量更新,不用每次下载完整包
- 差量更新只下载变化的文件
我们项目优化后,Windows 安装包从 150MB 降到 80MB,Mac 从 120MB 降到 65MB。配合增量更新,用户更新只需要下载 5-10MB,体验好了很多。"
说明: 这是 Flutter/React Native/Electron 对比与实战的完整文档,涵盖了技术选型、代码复用、性能对比、调试发布等核心内容,配有详细的代码示例和面试话术。特别强化了 Electron 的实战应用和系统集成功能。