JavaScript基础课程三十一、React Native 跨端 App 入门
JavaScript 前端核心进阶:React Native 跨端 App 课程
本课是前端开发者迈向原生App开发的关键一课,聚焦React Native跨端框架入门,依托已学的React知识,实现从Web开发到原生App开发的平滑过渡。RN凭借原生渲染的性能优势、React语法的低门槛,成为前端开发者开发高性能跨端App的首选方案。课程从环境搭建、核心组件、样式系统,到路由导航、列表渲染、交互逻辑,用单词App案例贯穿全程,拆解跨端开发的核心逻辑与避坑要点。掌握本课内容,你将具备双端原生App的独立开发能力,打破前端与客户端的技术壁垒,拓宽技术边界与就业方向。RN与React的语法高度统一,学习成本极低,是前端开发者拓展技术栈的最优选择之一。
一、课程学习目的
-
理解 React Native(简称RN)的核心定位、底层原理,掌握“一套代码双端运行”的跨端开发逻辑。
-
完成RN开发环境搭建,学会使用Expo快速启动项目,降低原生环境配置门槛。
-
掌握RN核心组件、样式系统、布局规则,衔接已学的React知识,实现从Web到原生App的平滑过渡。
-
学会使用React Navigation实现App页面路由、跳转与参数传递,搭建多页面App结构。
-
掌握RN列表渲染、用户交互、调试方法,规避跨端开发常见坑。
-
独立开发可运行在iOS/Android双端的简易App,建立原生App开发思维,为企业级跨端项目开发奠定基础。
二、核心知识点讲解
1. React Native 基础认知
React Native 是Meta(原Facebook)推出的跨平台原生App开发框架,基于React语法,一套代码可同时编译为iOS、Android双端原生应用。
核心优势:
-
原生渲染:非WebView套壳,代码通过JS桥接渲染为原生组件,性能接近原生App
-
语法复用:完全兼容React Hooks、组件化开发模式,前端开发者上手成本极低
-
热更新:支持热重载,修改代码实时预览,无需重新编译原生包
-
生态完善:拥有丰富的第三方原生插件,覆盖绝大多数App开发场景
与uni-app的核心差异:RN主打原生渲染性能,适合中大型高性能App;uni-app主打多端全覆盖(小程序/H5/App),适合轻量化全平台项目。
2. 开发环境搭建
RN开发分为原生环境和Expo快速开发环境,入门阶段优先使用Expo,无需配置Android Studio/Xcode,开箱即用。
必备环境:
-
Node.js(18.0及以上版本)
-
手机端安装「Expo Go」App(用于真机预览)
-
代码编辑器:VS Code(推荐安装React Native相关插件)
采用项目创建命令:
# 全局安装Expo脚手架
npm install -g create-expo-app
# 创建RN项目
npx create-expo-app rn-word-app
# 进入项目,启动开发服务
cd rn-word-app
npm start
3. RN 核心组件(替代HTML标签)
RN无HTML标签,所有视图均使用官方提供的原生组件,核心常用组件如下:
| RN组件 | 对应Web标签 | 核心作用 |
|---|---|---|
| View | div | 视图容器,用于布局、包裹子元素 |
| Text | span/p | 文本展示,所有文字必须放在Text组件内 |
| Image | img | 图片展示,支持本地与网络图片 |
| TextInput | input | 输入框,处理用户文本输入 |
| Button | button | 按钮组件,处理点击交互 |
| ScrollView | div(overflow:scroll) | 滚动容器,用于少量内容滚动 |
| FlatList | ul/li + 虚拟列表 | 长列表渲染,自带复用优化,适合长列表场景 |
4. RN 样式系统与布局规则
RN样式完全基于JavaScript编写,无CSS文件,使用StyleSheet.create统一管理样式,核心规则:
-
布局默认使用Flex弹性布局,默认主轴为垂直方向(flexDirection: column),与Web默认水平方向相反
-
无单位:样式数值为无单位的密度无关像素,自动适配不同屏幕密度
-
样式无继承:除Text组件外,父组件样式不会传递给子组件
-
仅支持Web CSS的子集,无后代选择器、伪类、浮动等特性
5. 路由与页面跳转
RN官方无内置路由,主流使用React Navigation库实现页面导航,核心分为栈导航(Stack Navigation)、标签导航(Tab Navigation)。
-
栈导航:实现页面的推入、弹出、返回,类似小程序的navigateTo/navigateBack
-
标签导航:实现底部TabBar切换,对应小程序的tabBar配置
6. 生命周期与Hooks
RN完全兼容React的所有Hooks,useState、useEffect、useCallback等均可直接使用,生命周期逻辑与React完全一致,无需额外学习成本。
三、示例程序(带详细注释)
示例1:基础页面与样式(单词首页)
// App.js 项目入口文件
import { StyleSheet, Text, View, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import WordList from './src/pages/WordList';
import WordDetail from './src/pages/WordDetail';
// 创建栈导航
const Stack = createNativeStackNavigator();
export default function App() {
return (
// 导航容器,必须包裹所有路由
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
{/* 首页路由 */}
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: '单词学习App' }}
/>
{/* 单词列表页 */}
<Stack.Screen
name="WordList"
component={WordList}
options={{ title: '单词列表' }}
/>
{/* 单词详情页 */}
<Stack.Screen
name="WordDetail"
component={WordDetail}
options={{ title: '单词详情' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
// 首页组件
function HomeScreen({ navigation }) {
return (
<View style={styles.container}>
<Text style={styles.title}>欢迎使用单词学习App</Text>
<Text style={styles.desc}>基于React Native开发的跨端单词工具</Text>
{/* 跳转到单词列表页 */}
<Button
title="进入单词列表"
onPress={() => navigation.navigate('WordList')}
/>
</View>
);
}
// 样式定义
const styles = StyleSheet.create({
container: {
flex: 1, // 占满全屏
backgroundColor: '#f5f5f5',
alignItems: 'center', // 水平居中
justifyContent: 'center', // 垂直居中
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 12,
},
desc: {
fontSize: 16,
color: '#666',
marginBottom: 30,
},
});
示例2:单词列表页(FlatList长列表渲染)
// src/pages/WordList.js
import { StyleSheet, View, Text, FlatList, Pressable } from 'react-native';
import { useEffect, useState } from 'react';
export default function WordList({ navigation }) {
// 响应式单词数据
const [wordList, setWordList] = useState([]);
// 模拟请求数据,useEffect用法与React完全一致
useEffect(() => {
const mockData = [
{ id: '1', en: 'apple', cn: '苹果', phonetic: '/ˈæpl/' },
{ id: '2', en: 'banana', cn: '香蕉', phonetic: '/bəˈnɑːnə/' },
{ id: '3', en: 'orange', cn: '橙子', phonetic: '/ˈɒrɪndʒ/' },
{ id: '4', en: 'react', cn: '前端框架', phonetic: '/riˈækt/' },
{ id: '5', en: 'native', cn: '原生的', phonetic: '/ˈneɪtɪv/' },
];
setWordList(mockData);
}, []);
// 列表项渲染
const renderItem = ({ item }) => (
<Pressable
style={styles.item}
// 跳转到详情页,传递单词参数
onPress={() => navigation.navigate('WordDetail', { word: item })}
>
<Text style={styles.en}>{item.en}</Text>
<Text style={styles.cn}>{item.cn}</Text>
</Pressable>
);
return (
<View style={styles.container}>
<FlatList
data={wordList}
renderItem={renderItem}
keyExtractor={item => item.id} // 唯一key,对应小程序的wx:key
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
flexDirection: 'row',
justifyContent: 'space-between',
},
en: {
fontSize: 18,
fontWeight: '500',
color: '#333',
},
cn: {
fontSize: 16,
color: '#666',
},
});
示例3:单词详情页(路由参数接收)
// src/pages/WordDetail.js
import { StyleSheet, View, Text } from 'react-native';
export default function WordDetail({ route }) {
// 接收路由传递的单词参数
const { word } = route.params;
return (
<View style={styles.container}>
<View style={styles.card}>
<Text style={styles.en}>{word.en}</Text>
<Text style={styles.phonetic}>{word.phonetic}</Text>
<Text style={styles.cn}>{word.cn}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
card: {
backgroundColor: '#fff',
padding: 30,
borderRadius: 12,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
en: {
fontSize: 32,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
phonetic: {
fontSize: 18,
color: '#666',
marginBottom: 16,
},
cn: {
fontSize: 24,
color: '#42b983',
},
});
示例4:路由依赖安装命令
# 安装React Navigation核心依赖
npm install @react-navigation/native
npm install @react-navigation/native-stack
# Expo环境安装配套依赖
npx expo install react-native-screens react-native-safe-area-context
四、掌握技巧与方法
-
入门优先使用Expo环境,无需配置复杂的原生开发环境,快速验证代码效果。
-
牢记Flex布局主轴差异:RN默认垂直排列,Web默认水平排列,避免布局错乱。
-
所有文本必须放在Text组件内,禁止直接在View中写文字,否则会直接报错。
-
长列表必须使用FlatList,不要用ScrollView循环渲染,避免性能问题和内存占用过高。
-
样式使用StyleSheet.create统一管理,不要直接写在行内,提升性能和可维护性。
-
调试使用Expo Go真机预览,配合VS Code插件、React DevTools排查问题。
-
页面跳转前必须先在导航器中注册路由,否则会报路由不存在错误。
-
兼容双端差异,避免使用平台专属API,如需使用可通过Platform.OS判断系统类型。
五、课后作业
基础作业
-
安装Node.js与Expo脚手架,创建RN项目,成功启动开发服务,用Expo Go真机预览Hello World页面。
-
使用View、Text、StyleSheet编写基础页面,实现垂直居中的标题与描述文本,掌握Flex布局基础用法。
-
安装React Navigation,配置2个页面,实现页面之间的跳转与返回。
进阶作业
-
使用FlatList渲染单词列表,实现点击列表项跳转到详情页,并传递单词数据。
-
在详情页接收路由参数,完整展示单词的英文、音标、中文释义。
-
使用useEffect模拟异步请求数据,实现页面加载时的初始化数据渲染。
实战作业
- 开发完整的RN单词学习App,包含首页、单词列表页、单词详情页,实现路由跳转、参数传递、列表渲染、样式美化,可正常在iOS/Android双端预览运行,代码规范、注释完整,符合RN开发标准。
上一课:微信小程序实战 实战作业代码
代码功能说明
本实战作业基于微信小程序原生语法开发完整的单词学习小程序,覆盖课程全部核心知识点。项目包含首页、单词列表页、单词详情页3个核心页面,配置底部TabBar导航;实现网络请求获取单词数据、本地缓存离线存储、页面跳转与参数传递、列表渲染、加载状态与异常处理全流程。代码严格遵循小程序开发规范,适配微信平台规则,包含空数据提示、错误Toast、加载动画等用户体验优化,可直接在微信开发者工具中运行,完整演示小程序从项目搭建到功能实现的全流程,帮助巩固小程序开发核心技能。
注意事项
-
必须使用微信开发者工具打开项目,使用测试号或已注册的小程序AppID创建项目。
-
所有页面必须在app.json的pages数组中注册,否则无法访问。
-
正式发布前必须在微信公众平台配置request合法域名,测试阶段可在开发者工具中关闭「不校验合法域名」选项。
-
页面数据更新必须使用this.setData(),直接修改this.data无法触发视图刷新。
-
列表渲染wx:for必须搭配wx:key,推荐使用唯一id,避免仅用index作为key。
-
页面跳转TabBar页面必须使用wx.switchTab,不可使用wx.navigateTo。
-
小程序对包体积有严格限制,静态资源需压缩,避免超过2M主包限制。
-
发布前需完成用户隐私协议配置,否则无法正常使用网络、存储等接口。
完整实战代码
项目结构
wechat-word-miniprogram/
├── app.js
├── app.json
├── app.wxss
├── sitemap.json
├── pages/
│ ├── index/ // 首页(TabBar页面)
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.wxml
│ │ └── index.wxss
│ ├── list/ // 单词列表页(TabBar页面)
│ │ ├── list.js
│ │ ├── list.json
│ │ ├── list.wxml
│ │ └── list.wxss
│ └── detail/ // 单词详情页
│ ├── detail.js
│ ├── detail.json
│ ├── detail.wxml
│ └── detail.wxss
└── utils/
└── request.js // 封装请求工具
app.json(全局配置)
{
"pages": [
"pages/index/index",
"pages/list/list",
"pages/detail/detail"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#42b983",
"navigationBarTitleText": "单词学习小程序",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#666",
"selectedColor": "#42b983",
"borderStyle": "black",
"backgroundColor": "#fff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "",
"selectedIconPath": ""
},
{
"pagePath": "pages/list/list",
"text": "单词列表",
"iconPath": "",
"selectedIconPath": ""
}
]
},
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
app.js(全局入口)
App({
onLaunch() {
console.log('小程序启动')
},
globalData: {
userInfo: null
}
})
app.wxss(全局样式)
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
padding: 30rpx;
}
.tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
utils/request.js(请求封装)
// 基础域名(上线替换为已备案的合法域名)
const baseUrl = 'https://xxx.com/api'
const request = (options) => {
wx.showLoading({
title: '加载中...',
mask: true
})
return new Promise((resolve, reject) => {
wx.request({
url: baseUrl + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
wx.showToast({
title: '请求失败',
icon: 'none'
})
reject(res)
}
},
fail: (err) => {
wx.showToast({
title: '网络异常',
icon: 'none'
})
reject(err)
},
complete: () => {
wx.hideLoading()
}
})
})
}
// 导出GET/POST方法
export const get = (url, data) => request({ url, method: 'GET', data })
export const post = (url, data) => request({ url, method: 'POST', data })
pages/index/index(首页)
index.wxml
<view class="container home">
<view class="logo-box">
<text class="title">单词学习小程序</text>
<text class="desc">每天积累一个单词,轻松提升词汇量</text>
</view>
<view class="today-word" wx:if="{{todayWord}}">
<text class="word-en">{{todayWord.en}}</text>
<text class="word-phonetic">{{todayWord.phonetic}}</text>
<text class="word-cn">{{todayWord.cn}}</text>
</view>
<button class="enter-btn" type="primary" bindtap="goToList">查看全部单词</button>
</view>
index.js
import { get } from '../../utils/request.js'
Page({
data: {
todayWord: null
},
onLoad() {
this.getTodayWord()
},
// 获取今日单词
getTodayWord() {
// 模拟请求,可替换为真实接口
const mockWord = {
en: 'native',
phonetic: '/ˈneɪtɪv/',
cn: '原生的;本地的'
}
this.setData({ todayWord: mockWord })
// 真实接口请求示例
// get('/word/today').then(res => {
// this.setData({ todayWord: res.data })
// })
},
// 跳转到单词列表
goToList() {
wx.switchTab({
url: '/pages/list/list'
})
}
})
index.wxss
.home {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 120rpx;
}
.logo-box {
text-align: center;
margin-bottom: 80rpx;
}
.title {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.desc {
font-size: 28rpx;
color: #666;
}
.today-word {
width: 90%;
background: #fff;
padding: 60rpx 40rpx;
border-radius: 16rpx;
text-align: center;
margin-bottom: 80rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
}
.word-en {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 12rpx;
}
.word-phonetic {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.word-cn {
font-size: 36rpx;
color: #42b983;
}
.enter-btn {
width: 80%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
}
pages/list/list(单词列表页)
list.wxml
<view class="container">
<!-- 加载状态 -->
<view class="tip" wx:if="{{loading}}">正在加载单词列表...</view>
<!-- 单词列表 -->
<view wx:else>
<view
wx:for="{{wordList}}"
wx:key="id"
class="word-item"
bindtap="goToDetail"
data-item="{{item}}"
>
<view class="word-info">
<text class="en">{{item.en}}</text>
<text class="phonetic">{{item.phonetic}}</text>
</view>
<text class="cn">{{item.cn}}</text>
</view>
<view class="tip" wx:if="{{wordList.length === 0}}">暂无单词数据</view>
</view>
</view>
list.js
import { get } from '../../utils/request.js'
Page({
data: {
wordList: [],
loading: false
},
onShow() {
this.getWordList()
},
// 获取单词列表
getWordList() {
this.setData({ loading: true })
// 模拟数据,可替换为真实接口
const mockList = [
{ id: 1, en: 'apple', phonetic: '/ˈæpl/', cn: '苹果' },
{ id: 2, en: 'banana', phonetic: '/bəˈnɑːnə/', cn: '香蕉' },
{ id: 3, en: 'orange', phonetic: '/ˈɒrɪndʒ/', cn: '橙子' },
{ id: 4, en: 'react', phonetic: '/riˈækt/', cn: '反应;前端框架' },
{ id: 5, en: 'native', phonetic: '/ˈneɪtɪv/', cn: '原生的' },
{ id: 6, en: 'javascript', phonetic: '/ˈdʒɑːvəskrɪpt/', cn: 'JavaScript脚本语言' }
]
setTimeout(() => {
this.setData({
wordList: mockList,
loading: false
})
// 缓存单词列表
wx.setStorageSync('wordList', mockList)
}, 800)
// 真实接口请求示例
// get('/word/list').then(res => {
// this.setData({ wordList: res.data, loading: false })
// wx.setStorageSync('wordList', res.data)
// }).catch(() => {
// // 读取缓存兜底
// const cache = wx.getStorageSync('wordList')
// if (cache) this.setData({ wordList: cache })
// this.setData({ loading: false })
// })
},
// 跳转到详情页
goToDetail(e) {
const item = e.currentTarget.dataset.item
wx.navigateTo({
url: `/pages/detail/detail?item=${encodeURIComponent(JSON.stringify(item))}`
})
}
})
list.wxss
.word-item {
background: #fff;
padding: 30rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.word-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.en {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.phonetic {
font-size: 24rpx;
color: #999;
}
.cn {
font-size: 28rpx;
color: #42b983;
}
pages/detail/detail(单词详情页)
detail.wxml
<view class="container">
<view class="detail-card">
<text class="en">{{wordInfo.en}}</text>
<text class="phonetic">{{wordInfo.phonetic}}</text>
<view class="divider"></view>
<text class="cn">{{wordInfo.cn}}</text>
</view>
<button bindtap="goBack" class="back-btn">返回列表</button>
</view>
detail.js
Page({
data: {
wordInfo: {}
},
onLoad(options) {
// 接收并解析路由参数
if (options.item) {
const wordInfo = JSON.parse(decodeURIComponent(options.item))
this.setData({ wordInfo })
// 设置导航栏标题
wx.setNavigationBarTitle({
title: wordInfo.en
})
}
},
// 返回上一页
goBack() {
wx.navigateBack()
}
})
detail.wxss
.container {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.detail-card {
width: 100%;
background: #fff;
padding: 80rpx 40rpx;
border-radius: 16rpx;
text-align: center;
margin-bottom: 60rpx;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
}
.en {
display: block;
font-size: 56rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.phonetic {
display: block;
font-size: 32rpx;
color: #666;
margin-bottom: 40rpx;
}
.divider {
width: 60rpx;
height: 4rpx;
background: #42b983;
margin: 0 auto 40rpx;
border-radius: 2rpx;
}
.cn {
font-size: 40rpx;
color: #42b983;
}
.back-btn {
width: 80%;
}
运行方式
-
打开微信开发者工具,选择「不使用云服务」,用测试号创建小程序项目。
-
将上述代码按项目结构复制到对应文件中。
-
点击「编译」按钮,即可在模拟器中预览运行效果,也可扫码在真机预览。
-
测试阶段,在开发者工具详情中勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。
作业验收标准
-
项目可正常编译运行,无控制台报错,页面正常显示。
-
TabBar切换流畅,页面跳转、参数传递、返回功能正常。
-
单词列表正常渲染,详情页可正确展示对应单词信息。
-
加载状态、空数据提示、错误提示正常展示,用户体验完整。
-
代码规范,注释清晰,符合微信小程序开发标准。
-
网络请求、本地缓存功能正常,离线可读取缓存数据。
更多推荐



所有评论(0)