被领导“忽悠”去写APP后,我用React Native两周上线了第一个项目
去年考研查分那天,我盯着屏幕看了整整十分钟——国家线都没过。那会儿整个人像被抽干了力气,连泡面都懒得煮。但生活总得继续不是?投了快两个月简历,终于在三月底拿到了现在的offer,一家做跨境电商业务的中型公司,前端岗。
入职第一周,CTO就把我叫进会议室:“小张啊,我们打算做个移动端APP,用来给海外仓工人扫码入库用的。你不是说对跨端技术感兴趣吗?来,这个活儿交给你了。”
我:???
我什么时候说过我对跨端感兴趣了?哦对,可能是上周团建喝多了吹的牛。
于是,在完全没碰过React Native的情况下,我硬着头皮接下了这个任务。今天这篇技术分享,就是想把这两个月踩过的坑、熬过的夜、掉过的头发,整理成一份还算靠谱的教程,帮后来人少走点弯路。
为什么选 React Native?
说实话,一开始我差点选了 Flutter。毕竟社区里“Flutter 性能吊打 RN”的声音太多了。但现实很骨感:
- 我们团队全是 React 技术栈,老前端们看到 Dart 就头疼
- 公司要求快速验证 MVP,老板原话是:“下个月初必须能演示”
- 我自己最熟的也是 JavaScript/TypeScript
综合下来,RN 成了唯一合理的选择。再加上 Expo 这个“神器”,开发体验确实香。
小插曲:第一次用 Expo Go 扫码预览时,我激动得差点把手机摔了——这不比原生开发爽多了?
环境搭建:别信官方文档的“简单几步”
官方文档永远写着“只需三步即可运行”,但现实往往是:
npx create-react-native-app my-warehouse-app
然后卡在 Installing dependencies... 半小时不动。
原因?国内网络 + npm 源太慢。解决办法很简单,但没人明说:
# 切换淘宝源(或者用 pnpm)
npm config set registry https://registry.npmmirror.com
# 或者直接用 Expo CLI(更推荐)
npx create-expo-app my-warehouse-app
我建议直接上 Expo,它帮你屏蔽了原生配置的大部分地狱。尤其是像我这种连 Android Studio 都没装全的新手,Expo 的 managed workflow 简直救命。
真实场景:上周五晚上九点,测试突然说 iOS 上扫码按钮点不动。我本地调试一切正常。最后发现是因为用了
TouchableOpacity而不是Pressable,在某些 iOS 版本上有点击穿透问题。这种坑,只有真机测才能暴露。
第一个组件:从“Hello World”到扫码入库
我们的核心功能其实就两个页面:
- 登录页(对接公司 SSO)
- 扫码入库页(调用摄像头 + 提交数据)
先看最简单的登录页结构:
// LoginScreen.tsx
import { View, Text, TextInput, Button } from 'react-native';
import { useState } from 'react';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
// 实际项目中这里会调用 auth API
console.log('Logging in with', email);
// 跳转逻辑后面再说
};
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
<Text style={{ fontSize: 24, marginBottom: 20 }}>仓库助手</Text>
<TextInput
placeholder="邮箱"
value={email}
onChangeText={setEmail}
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<TextInput
placeholder="密码"
secureTextEntry
value={password}
onChangeText={setPassword}
style={{ borderWidth: 1, padding: 10, marginBottom: 20 }}
/>
<Button title="登录" onPress={handleLogin} />
</View>
);
}
看起来平平无奇,但注意几个细节:
- 样式不能用 CSS!得用 JavaScript 对象,单位默认是 dp(设备无关像素)
TextInput在 Android 和 iOS 上默认行为不同(比如回车键处理)- 别忘了加
accessibilityLabel,否则测试同学会骂你(我们 QA 组有无障碍测试要求)
导航:别再手写路由了!
早期 RN 社区导航方案乱成一锅粥:react-navigation、wix/react-native-navigation、甚至有人直接用 Redux 控制页面栈……现在基本统一到 React Navigation v6+ 了。
安装:
npm install @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context
配置也很简单:
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import LoginScreen from './screens/LoginScreen';
import ScanScreen from './screens/ScanScreen';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Login">
<Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
<Stack.Screen name="Scan" component={ScanScreen} options={{ title: '扫码入库' }} />
</Stack.Navigator>
</NavigationContainer>
);
}
跳转页面?一行代码搞定:
navigation.navigate('Scan');
血泪教训:千万别在组件外直接 import navigation 对象!一定要通过
useNavigation()hook 获取。否则热更新时状态会错乱,我曾因此导致登录后反复跳回登录页,debug 到凌晨两点。
调用摄像头:expo-camera 真香
扫码功能依赖摄像头。原生方案要分别处理 iOS 权限(Info.plist)和 Android 权限(AndroidManifest.xml),但 Expo 全包了。
npx expo install expo-camera
使用示例:
// ScanScreen.tsx
import { Camera } from 'expo-camera';
import { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
export default function ScanScreen() {
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [scanned, setScanned] = useState(false);
useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
if (hasPermission === null) {
return <View><Text>请求摄像头权限中...</Text></View>;
}
if (hasPermission === false) {
return <View><Text>请在设置中开启摄像头权限</Text></View>;
}
const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => {
setScanned(true);
alert(`扫码成功!内容: ${data}`);
// 这里应该调用提交接口
};
return (
<View style={{ flex: 1 }}>
<Camera
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={{ flex: 1 }}
/>
{scanned && (
<TouchableOpacity onPress={() => setScanned(false)}>
<Text>重试</Text>
</TouchableOpacity>
)}
</View>
);
}
注意:
- iOS 必须在
app.json中声明NSCameraUsageDescription - Android 13+ 需要动态申请权限(Expo 已处理)
- 扫码区域默认是全屏,但你可以用
Camera的ratio属性控制比例
平台适配:Android 和 iOS 的“双标”现场
RN 最大的幻觉就是“一次编写,到处运行”。实际上,平台差异无处不在:
| 问题 | Android 表现 | iOS 表现 | 解决方案 |
|---|---|---|---|
| 字体默认大小 | 较小 | 较大 | 用 PixelRatio.getFontScale() 动态调整 |
| 按钮样式 | 扁平 | 圆角阴影 | 用 Platform.OS 判断 |
| 安全区域 | 几乎不用管 | iPhone X 以上需处理刘海 | 用 SafeAreaView 包裹 |
| 状态栏颜色 | 黑色 | 白色 | 用 StatusBar 组件统一设置 |
举个具体例子:我们的按钮在 iOS 上看着很舒服,但在安卓低端机上文字糊成一团。最后加了这段:
const fontSize = Platform.OS === 'ios' ? 16 : 14 * PixelRatio.getFontScale();
性能优化:别让用户觉得“卡成PPT”
RN 应用最容易被吐槽的就是“卡”。虽然我们这个内部工具用户不多,但 CTO 说了:“哪怕只有一个人用,也得丝滑。”
几个关键优化点:
避免 inline function
❌<Button onPress={() => doSomething()} />
✅ 提前定义函数或用useCallback列表用 FlatList,别用 ScrollView
数据量稍大(>20条)就必须用FlatList,否则内存爆炸图片懒加载 + 缓存
用expo-image替代默认Image,支持占位图和缓存减少 re-render
大量使用React.memo和useMemo
我们有个商品列表页,最初每次滚动都卡顿。加上 keyExtractor 和 initialNumToRender={10} 后,帧率从 25 提升到 58。
发布上线:从 Expo 到应用商店
开发完只是开始,发布才是噩梦。
Expo 方案(适合内部工具)
- 直接
eas build打包 - 生成 APK/IPA 文件
- 内部通过企业证书分发(我们用的是 TestFlight + 蒲公英)
原生方案(上架 App Store/Google Play)
- 需要
npx expo prebuild生成原生代码 - 配置签名、图标、启动页
- 苹果审核特别严格,记得处理隐私政策
我们这次走 Expo 托管,省去了大量原生配置。但下周就要上架正式版,估计又得熬夜改 Info.plist……
写在最后:考研失败不是终点,而是新起点
回头看,如果当初考研成功了,可能还在图书馆背政治,而不是在这里折腾 APP。虽然现在每天被产品经理催需求、被测试提 bug、被运维问日志,但至少——我在做真实的产品,解决真实的问题。
React Native 并不是银弹,但它让我这个前端菜鸟,在两个月内交付了一个可用的移动应用。如果你也在职业转型期,别怕从零开始。技术分享的意义,就是让后来者少踩一点我踩过的坑。
这篇教程写得比较啰嗦,但都是实战经验。欢迎留言交流,也求轻喷——毕竟我才工作俩月,说不定明天就被优化了 😅
彩蛋:昨天上线后,仓库大叔发消息说“这玩意比 Excel 好用多了”。那一刻,我觉得加班都值了。

评论 0