在开发英雄联盟助手App的过程中,随着功能不断丰富,我们遇到了一个典型的产品设计问题:如何优雅地组织大量辅助功能?主导航栏(通常位于App底部)的空间有限,一般只能容纳4-5个核心入口,但我们有超过10个实用工具需要提供访问入口——从英雄数据筛选到装备对比,从符文配置到伤害计算。
这个实用工具页面本质上是一个"功能导航中心",它解决了三个核心问题:
从技术角度看,这个方案的价值在于:
在跨平台方案选型时,我们评估了几个主流框架:
最终选择React Native for OpenHarmony是因为:
实际测试数据显示,在搭载HarmonyOS 3.0的Mate 50 Pro上,这个工具页面的渲染时间仅18ms,与原生开发相差无几。
我们尝试了三种网格布局实现方式:
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Flex布局 | flexWrap + 百分比宽度 | 简单直接,兼容性好 | 需要手动计算间距 | 简单网格 |
| FlatList | numColumns=2 | 自动回收不可见项 | 需要处理item样式 | 大数据量 |
| 第三方库 | react-native-grid-view | 功能丰富 | 增加依赖 | 复杂网格 |
最终选择Flex布局方案是因为:
工具数据的结构设计考虑了扩展性和国际化需求:
typescript复制interface ToolItem {
key: string; // 唯一标识,用于路由跳转
icon: string; // Emoji或图标资源
name: string; // 显示名称(后续可替换为i18n key)
desc: string; // 功能描述
category?: string; // 分类标签(为分组功能预留)
minVersion?: string; // 最低App版本要求
}
字段设计考量:
key采用驼峰命名是为了与路由系统保持一致icon优先使用Emoji是因为:
category字段为后续的功能分组预留了扩展空间我们的导航系统核心是一个基于栈的状态管理方案:
typescript复制const NavigationProvider = ({children}) => {
const [state, setState] = useState({
history: [{name: 'Home', params: {}}], // 初始路由
params: {}, // 当前参数
});
const navigate = (name, params) => {
setState(prev => ({
history: [...prev.history, {name, params}],
params
}));
};
const goBack = () => {
setState(prev => ({
history: prev.history.length > 1
? prev.history.slice(0, -1)
: prev.history,
params: prev.history.length > 1
? prev.history[prev.history.length - 2].params
: prev.params
}));
};
return (
<NavigationContext.Provider value={{...state, navigate, goBack}}>
{children}
</NavigationContext.Provider>
);
};
性能优化点:
我们建立了可维护的样式系统:
typescript复制// colors.ts
export const colors = {
background: '#1E1E2D',
backgroundCard: '#2A2A3D',
textPrimary: '#E0E0FF',
textSecondary: '#A0A0C0',
border: '#3A3A4D',
accent: '#6C5CE7',
};
// typography.ts
export const typography = {
title: {
fontSize: 20,
fontWeight: 'bold',
lineHeight: 24,
},
toolName: {
fontSize: 16,
fontWeight: '600',
lineHeight: 19,
},
// ...其他文字样式
};
// spacing.ts
export const spacing = {
small: 8,
medium: 16,
large: 24,
};
样式管理的最佳实践:
通过React Native Performance Monitor监测,我们实施了以下优化:
避免内联样式:
typescript复制// 错误示范 - 每次渲染都创建新样式对象
<View style={{padding: 10}}>
// 正确做法 - 使用StyleSheet
const styles = StyleSheet.create({ pad: {padding: 10} });
<View style={styles.pad}>
优化重渲染:
typescript复制// 使用React.memo避免工具卡片不必要的重绘
const ToolCard = React.memo(({tool}) => (
<View style={styles.card}>
<Text>{tool.name}</Text>
</View>
));
图片预加载:
typescript复制// 在应用启动时预加载可能用到的图标
useEffect(() => {
Image.prefetch('https://example.com/icon.png');
}, []);
在不同设备上的渲染性能对比:
| 设备 | 系统 | 首次渲染(ms) | 交互延迟(ms) | 内存占用(MB) |
|---|---|---|---|---|
| Mate 50 Pro | HarmonyOS 3.0 | 18 | 8 | 42 |
| iPhone 13 | iOS 16 | 22 | 10 | 38 |
| Pixel 6 | Android 13 | 35 | 15 | 45 |
优化前后的关键指标对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 页面加载时间 | 320ms | 180ms | 43% |
| 交互响应时间 | 50ms | 12ms | 76% |
| 内存占用 | 68MB | 42MB | 38% |
基于用户行为数据实现智能排序:
typescript复制const [toolUsage, setToolUsage] = useState<Record<string, number>>({});
// 从本地存储加载使用记录
const loadUsageData = async () => {
const data = await AsyncStorage.getItem('toolUsage');
if (data) setToolUsage(JSON.parse(data));
};
// 更新使用计数
const recordToolUsage = (toolKey: string) => {
const newUsage = {
...toolUsage,
[toolKey]: (toolUsage[toolKey] || 0) + 1
};
AsyncStorage.setItem('toolUsage', JSON.stringify(newUsage));
setToolUsage(newUsage);
};
// 排序逻辑
const sortedTools = [...tools].sort((a, b) => {
const countA = toolUsage[a.key] || 0;
const countB = toolUsage[b.key] || 0;
// 优先按使用频率,其次按预设顺序
return countB - countA || a.key.localeCompare(b.key);
});
当工具数量超过15个时,建议启用分组:
typescript复制const groupTools = (tools: ToolItem[]) => {
const groups: Record<string, ToolItem[]> = {};
tools.forEach(tool => {
const category = tool.category || '其他';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(tool);
});
return Object.entries(groups).map(([name, items]) => ({
title: name,
tools: items,
}));
};
// 渲染分组
{groupedTools.map(group => (
<View key={group.title}>
<Text style={styles.groupTitle}>{group.title}</Text>
<View style={styles.grid}>
{group.tools.map(tool => (
<ToolCard key={tool.key} tool={tool} />
))}
</View>
</View>
))}
添加实时搜索过滤:
typescript复制const [searchQuery, setSearchQuery] = useState('');
const filteredTools = useMemo(() => {
if (!searchQuery) return tools;
const query = searchQuery.toLowerCase();
return tools.filter(tool =>
tool.name.toLowerCase().includes(query) ||
tool.desc.toLowerCase().includes(query) ||
tool.category?.toLowerCase().includes(query)
);
}, [tools, searchQuery]);
return (
<>
<TextInput
placeholder="搜索工具..."
value={searchQuery}
onChangeText={setSearchQuery}
style={styles.searchInput}
/>
<ToolGrid tools={filteredTools} />
</>
);
问题1:网格布局在Android上的对齐问题
typescript复制grid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginHorizontal: -spacing.small,
justifyContent: 'flex-start', // 显式指定对齐方式
},
card: {
width: '50% - 12px', // 使用calc替代百分比
margin: spacing.small,
}
问题2:Emoji在HarmonyOS上的显示异常
typescript复制// 使用备选文本方案
const getToolIcon = (icon: string) => {
return Platform.OS === 'harmony'
? icon.replace(/[⚠️⭐⚡]/g, '◉')
: icon;
};
图片加载优化:
内存管理技巧:
typescript复制// 在页面离开时清理大内存对象
useEffect(() => {
return () => {
imageCache.clear();
};
}, []);
避免过度渲染:
代码组织规范:
code复制/src
/features
/tools
/components
ToolCard.tsx
ToolGrid.tsx
/hooks
useTools.ts
index.ts
/navigation
context.ts
types.ts
测试策略:
文档规范:
typescript复制/**
* 工具卡片组件
* @param {ToolItem} tool - 工具数据对象
* @param {function} onPress - 点击回调
* @example
* <ToolCard
* tool={{key: 'filter', name: '筛选'}}
* onPress={() => {}}
* />
*/
const ToolCard = ({tool, onPress}) => (...);
未来计划实现后端控制的工具管理:
typescript复制interface RemoteToolConfig {
id: string;
visible: boolean;
order: number;
minAppVersion: string;
}
// 从服务器获取配置
const fetchToolConfig = async () => {
const response = await fetch('/api/tools/config');
return response.json() as Promise<RemoteToolConfig[]>;
};
// 合并本地与远程配置
const mergeTools = (local: ToolItem[], remote: RemoteToolConfig[]) => {
return local
.filter(tool => {
const config = remote.find(r => r.id === tool.key);
return config?.visible !== false;
})
.sort((a, b) => {
const configA = remote.find(r => r.id === a.key);
const configB = remote.find(r => r.id === b.key);
return (configA?.order || 0) - (configB?.order || 0);
});
};
计划增加的功能:
typescript复制// 用户偏好数据结构
interface UserToolPrefs {
favoriteTools: string[];
hiddenTools: string[];
customIcons: Record<string, string>;
layoutType: 'grid' | 'list';
}
// 保存到本地
const saveUserPrefs = async (prefs: UserToolPrefs) => {
await AsyncStorage.setItem('userToolPrefs', JSON.stringify(prefs));
};
针对视障用户的改进方案:
typescript复制<TouchableOpacity
accessible={true}
accessibilityLabel={`${tool.name}功能,${tool.desc}`}
accessibilityHint="双击打开此工具"
>
静态检查配置:
json复制// .eslintrc
{
"rules": {
"react-native/no-inline-styles": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
提交规范:
code复制feat(tools): 增加搜索功能
fix(grid): 修复Android布局错位问题
代码审查要点:
示例GitHub Actions配置:
yaml复制name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build-android
- run: npm run build-ios
关键监控指标:
实现方案:
typescript复制// 使用埋点SDK
const trackToolUsage = (toolKey: string) => {
analytics.logEvent('tool_click', {
tool: toolKey,
timestamp: Date.now(),
});
};
// 在点击处理中调用
const handlePress = (toolKey: string) => {
trackToolUsage(toolKey);
navigate(toolKey);
};
使用Platform模块处理差异:
typescript复制const styles = StyleSheet.create({
card: {
shadowColor: Platform.select({
ios: '#000',
android: 'transparent',
harmony: '#000',
}),
elevation: Platform.select({
android: 3,
default: 0,
}),
},
});
动态启用高级功能:
typescript复制const supportsHaptics = () => {
return Platform.OS === 'ios' ||
(Platform.OS === 'android' && Platform.Version >= 28);
};
const onToolPress = () => {
if (supportsHaptics()) {
ReactNativeHaptics.trigger('impactLight');
}
};
使用PixelRatio处理高清屏:
typescript复制const iconSize = Math.round(PixelRatio.roundToNearestPixel(24));
const borderWidth = 1 / PixelRatio.get();
从Context迁移到Zustand的方案:
typescript复制import create from 'zustand';
interface ToolState {
tools: ToolItem[];
favorites: string[];
addFavorite: (id: string) => void;
}
const useToolStore = create<ToolState>(set => ({
tools: [],
favorites: [],
addFavorite: (id) =>
set(state => ({
favorites: [...state.favorites, id]
})),
}));
// 在组件中使用
const { favorites } = useToolStore();
支持动态功能扩展:
typescript复制interface ToolPlugin {
id: string;
install: (context: PluginContext) => void;
}
const registerPlugin = (plugin: ToolPlugin) => {
plugin.install({
registerTool: (tool) => addTool(tool),
addRoute: (route) => addRoute(route),
});
};
与主应用的集成方案:
typescript复制// 导出微应用配置
export default {
name: 'tools-module',
routes: [
{ path: '/tools', component: ToolsPage },
],
sharedDeps: {
'react': require('react'),
'react-native': require('react-native'),
},
};