JavaScript 前端核心进阶:React Native 跨端 App 课程

本课是前端开发者迈向原生App开发的关键一课,聚焦React Native跨端框架入门,依托已学的React知识,实现从Web开发到原生App开发的平滑过渡。RN凭借原生渲染的性能优势、React语法的低门槛,成为前端开发者开发高性能跨端App的首选方案。课程从环境搭建、核心组件、样式系统,到路由导航、列表渲染、交互逻辑,用单词App案例贯穿全程,拆解跨端开发的核心逻辑与避坑要点。掌握本课内容,你将具备双端原生App的独立开发能力,打破前端与客户端的技术壁垒,拓宽技术边界与就业方向。RN与React的语法高度统一,学习成本极低,是前端开发者拓展技术栈的最优选择之一。

一、课程学习目的

  1. 理解 React Native(简称RN)的核心定位、底层原理,掌握“一套代码双端运行”的跨端开发逻辑。

  2. 完成RN开发环境搭建,学会使用Expo快速启动项目,降低原生环境配置门槛。

  3. 掌握RN核心组件、样式系统、布局规则,衔接已学的React知识,实现从Web到原生App的平滑过渡。

  4. 学会使用React Navigation实现App页面路由、跳转与参数传递,搭建多页面App结构。

  5. 掌握RN列表渲染、用户交互、调试方法,规避跨端开发常见坑。

  6. 独立开发可运行在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,useStateuseEffectuseCallback等均可直接使用,生命周期逻辑与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

四、掌握技巧与方法

  1. 入门优先使用Expo环境,无需配置复杂的原生开发环境,快速验证代码效果。

  2. 牢记Flex布局主轴差异:RN默认垂直排列,Web默认水平排列,避免布局错乱。

  3. 所有文本必须放在Text组件内,禁止直接在View中写文字,否则会直接报错。

  4. 长列表必须使用FlatList,不要用ScrollView循环渲染,避免性能问题和内存占用过高。

  5. 样式使用StyleSheet.create统一管理,不要直接写在行内,提升性能和可维护性。

  6. 调试使用Expo Go真机预览,配合VS Code插件、React DevTools排查问题。

  7. 页面跳转前必须先在导航器中注册路由,否则会报路由不存在错误。

  8. 兼容双端差异,避免使用平台专属API,如需使用可通过Platform.OS判断系统类型。

五、课后作业

基础作业

  1. 安装Node.js与Expo脚手架,创建RN项目,成功启动开发服务,用Expo Go真机预览Hello World页面。

  2. 使用View、Text、StyleSheet编写基础页面,实现垂直居中的标题与描述文本,掌握Flex布局基础用法。

  3. 安装React Navigation,配置2个页面,实现页面之间的跳转与返回。

进阶作业

  1. 使用FlatList渲染单词列表,实现点击列表项跳转到详情页,并传递单词数据。

  2. 在详情页接收路由参数,完整展示单词的英文、音标、中文释义。

  3. 使用useEffect模拟异步请求数据,实现页面加载时的初始化数据渲染。

实战作业

  1. 开发完整的RN单词学习App,包含首页、单词列表页、单词详情页,实现路由跳转、参数传递、列表渲染、样式美化,可正常在iOS/Android双端预览运行,代码规范、注释完整,符合RN开发标准。

上一课:微信小程序实战 实战作业代码

代码功能说明

本实战作业基于微信小程序原生语法开发完整的单词学习小程序,覆盖课程全部核心知识点。项目包含首页、单词列表页、单词详情页3个核心页面,配置底部TabBar导航;实现网络请求获取单词数据、本地缓存离线存储、页面跳转与参数传递、列表渲染、加载状态与异常处理全流程。代码严格遵循小程序开发规范,适配微信平台规则,包含空数据提示、错误Toast、加载动画等用户体验优化,可直接在微信开发者工具中运行,完整演示小程序从项目搭建到功能实现的全流程,帮助巩固小程序开发核心技能。

注意事项

  1. 必须使用微信开发者工具打开项目,使用测试号或已注册的小程序AppID创建项目。

  2. 所有页面必须在app.json的pages数组中注册,否则无法访问。

  3. 正式发布前必须在微信公众平台配置request合法域名,测试阶段可在开发者工具中关闭「不校验合法域名」选项。

  4. 页面数据更新必须使用this.setData(),直接修改this.data无法触发视图刷新。

  5. 列表渲染wx:for必须搭配wx:key,推荐使用唯一id,避免仅用index作为key。

  6. 页面跳转TabBar页面必须使用wx.switchTab,不可使用wx.navigateTo。

  7. 小程序对包体积有严格限制,静态资源需压缩,避免超过2M主包限制。

  8. 发布前需完成用户隐私协议配置,否则无法正常使用网络、存储等接口。

完整实战代码

项目结构


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%;
}

运行方式

  1. 打开微信开发者工具,选择「不使用云服务」,用测试号创建小程序项目。

  2. 将上述代码按项目结构复制到对应文件中。

  3. 点击「编译」按钮,即可在模拟器中预览运行效果,也可扫码在真机预览。

  4. 测试阶段,在开发者工具详情中勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。

作业验收标准

  1. 项目可正常编译运行,无控制台报错,页面正常显示。

  2. TabBar切换流畅,页面跳转、参数传递、返回功能正常。

  3. 单词列表正常渲染,详情页可正确展示对应单词信息。

  4. 加载状态、空数据提示、错误提示正常展示,用户体验完整。

  5. 代码规范,注释清晰,符合微信小程序开发标准。

  6. 网络请求、本地缓存功能正常,离线可读取缓存数据。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐