公司动态

React Native实战:从零构建记忆翻牌游戏,掌握跨平台开发核心

📅 2026/8/24 5:29:52
React Native实战:从零构建记忆翻牌游戏,掌握跨平台开发核心
你肯定见过那种经典的“记忆翻牌游戏”屏幕上整齐排列着若干张背面朝上的卡牌你需要翻开两张如果图案相同则消除不同则翻回。规则简单但想用代码把它实现出来尤其是跨平台在 iOS 和 Android 上运行对初学者来说可能第一反应是“这得分别学 Swift 和 Kotlin 吧”其实这正是 React Native 最擅长的场景之一。它让你能用熟悉的 JavaScript 和 React 的思维去构建真正的原生移动应用。但很多教程一上来就讲环境搭建、组件列表学完感觉懂了一动手还是不知道从哪里开始。今天我们就换一种方式不空谈概念直接通过构建这个《记忆翻牌游戏》把 React Native 开发的核心链路走一遍。你会发现这个项目麻雀虽小五脏俱全。它几乎涵盖了 React Native 初学者必须掌握的几大核心组件化设计、状态管理、样式布局、用户交互以及如何将 React 的声明式 UI 思维应用到移动端。更重要的是在实现过程中你会遇到并解决那些新手最常卡住的点比如列表渲染、随机算法、游戏状态逻辑以及如何让界面在不同尺寸屏幕上看起来都舒服。我们不止步于“把游戏做出来”。我会带你思考为什么 React Native 适合这类 UI 驱动、逻辑清晰的轻量级应用在实现翻牌动画、管理游戏状态时用 React Hooks 和不用有什么区别当你想扩展功能比如增加难度、记录分数时代码结构应该如何演进才能保持清晰这些问题的答案远比单纯复制一段代码更有价值。1. 为什么选择《记忆翻牌游戏》作为 React Native 的入门项目在开始写代码之前我们先明确一件事学习一个新框架选对第一个实战项目至关重要。它不能太简单比如一个静态页面否则学不到核心也不能太复杂比如一个完整的社交应用否则容易在细节里迷失。《记忆翻牌游戏》恰好踩在了那个黄金平衡点上。首先它的业务逻辑完整且自洽。从初始化牌组、洗牌、翻牌、匹配判断到游戏结束判定形成了一个清晰的闭环。这让你能完整地实践一个功能模块从数据到视图的全流程。其次它强烈依赖状态驱动视图。牌是正面还是背面哪些牌被临时翻开哪些牌已经匹配成功被移除游戏是进行中还是已结束所有这些都是应用状态的直接体现。在 React 和 React Native 的世界里“状态”是核心中的核心。通过这个游戏你会深刻体会到“状态改变UI 自动更新”这一声明式编程范式的威力。再者它天然需要组件化思维。一张卡牌显然可以抽象成一个可复用的Card组件。整个游戏面板则是多个Card组件的集合。这让你从一开始就练习如何设计组件的属性props和内部状态state这是构建任何复杂应用的基础。最后它涉及关键的 React Native 基础 API。你需要用View和Text构建基础 UI用TouchableOpacity或Pressable处理点击事件用StyleSheet定义样式用useState、useEffect等 Hooks 管理状态和副作用。完成这个项目相当于把 React Native 最常用、最核心的部分亲手操作了一遍。所以这个游戏项目不是一个随意的选择。它是一个精心设计的“教学样本”几乎每一个实现步骤都对应着一个你需要掌握的 React Native 核心概念。2. 环境准备与项目初始化避开第一个坑在动手之前确保你的开发环境已经就绪。对于 React Native 开发官方推荐的方式是使用 Expo 或 React Native CLI。对于初学者我更推荐使用 Expo因为它极大地简化了环境配置过程你不需要直接面对 Android Studio 或 Xcode 的复杂设置可以更专注于代码本身。不过根据网络上的讨论一个常见的问题是“React Native 启动白屏”。这通常不是代码逻辑问题而是环境或依赖问题。为了最大程度避免我们按以下步骤操作2.1 安装 Node.js 与 Expo CLI首先确保你的电脑上安装了Node.js (LTS 版本)。可以在终端运行node -v和npm -v来检查。然后安装 Expo 命令行工具npm install -g expo-cli2.2 创建新项目使用 Expo 初始化一个新项目。我们给它起名叫MemoryGamenpx create-expo-app MemoryGame cd MemoryGame初始化完成后你的项目目录结构大致如下MemoryGame/ ├── App.js ├── app.json ├── package.json └── ...App.js就是应用的入口文件我们将主要在这里编写代码。2.3 在设备上运行你可以在电脑上安装 iOS 模拟器或 Android 模拟器但更快捷的方式是在真实的手机上测试。在你的 iOS 或 Android 手机上下载官方Expo Go应用。在项目根目录运行npx expo start。终端会显示一个二维码和本地服务器地址。用手机上的 Expo Go 应用扫描这个二维码。几秒钟后你就能在手机上看到默认的 Expo 启动界面然后是我们的应用。这种方式能让你立刻在真机上获得反馈体验非常直观。注意确保你的手机和电脑在同一个局域网Wi-Fi下。如果遇到连接问题这是第一个要检查的点。环境准备好后我们暂时不需要关注复杂的原生模块或第三方库。这个游戏的所有功能用 React Native 的内置组件和 React 本身的能力就足够了。3. 构建游戏的核心数据模型与组件设计任何应用的起点都是数据。对于记忆翻牌游戏我们需要先想清楚数据如何组织再据此设计组件。3.1 设计卡牌的数据结构一张卡牌至少需要包含以下信息id: 唯一标识符用于区分每一张牌。value: 牌面的值比如是数字、字母或图案的标识用于匹配判断。isFlipped: 布尔值表示这张牌当前是正面朝上还是背面朝上。isMatched: 布尔值表示这张牌是否已经成功匹配。匹配成功的牌通常会被移除或标记为不可点击。我们可以用一个对象数组来表示整个牌组。例如一个包含 8 张牌4 对的初始数据可能长这样const initialCards [ { id: 1, value: A, isFlipped: false, isMatched: false }, { id: 2, value: A, isFlipped: false, isMatched: false }, { id: 3, value: B, isFlipped: false, isMatched: false }, { id: 4, value: B, isFlipped: false, isMatched: false }, // ... 更多牌 ];3.2 创建可复用的Card组件接下来我们创建Card组件。在项目根目录创建一个新文件components/Card.js。这个组件的职责很明确根据isFlipped属性显示正面或背面。根据isMatched属性决定是否可点击以及样式变化。在被点击时通知父组件游戏面板。// components/Card.js import React from react; import { View, Text, TouchableOpacity, StyleSheet } from react-native; const Card ({ card, onPress }) { // 如果牌已经匹配显示一个特殊状态比如半透明 if (card.isMatched) { return ( View style{[styles.card, styles.matchedCard]} Text style{styles.value}{card.value}/Text /View ); } // 主要交互部分可点击的卡牌 return ( TouchableOpacity style{[styles.card, card.isFlipped ? styles.cardFront : styles.cardBack]} onPress{() onPress(card.id)} // 点击时传递牌ID disabled{card.isFlipped || card.isMatched} // 已翻开或已匹配的牌不可点击 View {card.isFlipped ? ( Text style{styles.value}{card.value}/Text // 正面显示值 ) : ( Text style{styles.backText}?/Text // 背面显示问号或图案 )} /View /TouchableOpacity ); }; const styles StyleSheet.create({ card: { width: 80, height: 80, margin: 5, borderRadius: 8, justifyContent: center, alignItems: center, }, cardFront: { backgroundColor: #87CEEB, // 正面颜色浅蓝色 }, cardBack: { backgroundColor: #4A5568, // 背面颜色深灰色 }, matchedCard: { backgroundColor: #90EE90, // 匹配成功颜色浅绿色 opacity: 0.6, }, value: { fontSize: 24, fontWeight: bold, color: #2D3748, }, backText: { fontSize: 28, color: #E2E8F0, }, }); export default Card;关键点解析TouchableOpacity这是 React Native 中用于创建可点击区域的组件点击时有轻微的透明度反馈体验很好。也可以使用更新的Pressable组件它提供更细粒度的按压控制。条件渲染我们通过card.isFlipped和card.isMatched来控制显示内容和样式这是 React 条件渲染的典型应用。样式StyleSheet使用StyleSheet.create来定义样式它有助于性能优化和代码组织。注意样式的写法是 JavaScript 对象属性名是驼峰式如backgroundColor。组件通信Card组件自身不改变状态。当被点击时它通过onPress回调函数将自身的id传递给父组件。状态提升State Lifting是 React 中管理共享状态的常见模式游戏的核心状态应该由父组件游戏面板来管理。3.3 构建游戏面板与状态管理现在让我们回到主文件App.js在这里管理游戏的核心状态和逻辑。// App.js import React, { useState, useEffect } from react; import { SafeAreaView, View, Text, StyleSheet, Button, Alert } from react-native; import Card from ./components/Card; // 生成初始牌组的函数 const createDeck (pairCount) { let deck []; for (let i 0; i pairCount; i) { // 每对牌有相同的值这里用字母 A, B, C... 表示 let value String.fromCharCode(65 i); // 65 是 A 的 ASCII 码 // 一对牌两个对象id 不同 deck.push({ id: i * 2, value, isFlipped: false, isMatched: false }); deck.push({ id: i * 2 1, value, isFlipped: false, isMatched: false }); } // 洗牌打乱数组顺序 return deck.sort(() Math.random() - 0.5); }; export default function App() { // 核心状态牌组 const [cards, setCards] useState([]); // 状态当前翻开的牌最多两张的ID const [flippedCardIds, setFlippedCardIds] useState([]); // 状态游戏是否结束 const [gameOver, setGameOver] useState(false); // 状态尝试次数 const [moves, setMoves] useState(0); // 初始化游戏 const initializeGame () { const newDeck createDeck(6); // 使用6对牌12张 setCards(newDeck); setFlippedCardIds([]); setGameOver(false); setMoves(0); }; // 使用 useEffect 在组件首次加载时初始化游戏 useEffect(() { initializeGame(); }, []); // 处理卡牌点击事件 const handleCardPress (cardId) { // 规则1如果已经翻开了两张牌或者这张牌已经匹配/翻开则忽略点击 if (flippedCardIds.length 2 || cards.find(c c.id cardId)?.isMatched || cards.find(c c.id cardId)?.isFlipped) { return; } // 规则2更新被点击牌的状态为“翻开” const newCards cards.map(card { if (card.id cardId !card.isMatched) { return { ...card, isFlipped: true }; } return card; }); setCards(newCards); // 规则3将这张牌的ID加入“已翻开”列表 const newFlippedIds [...flippedCardIds, cardId]; setFlippedCardIds(newFlippedIds); // 规则4如果翻开了两张牌进行检查匹配的逻辑 if (newFlippedIds.length 2) { setMoves(prev prev 1); // 增加尝试次数 const [firstId, secondId] newFlippedIds; const firstCard newCards.find(c c.id firstId); const secondCard newCards.find(c c.id secondId); if (firstCard.value secondCard.value) { // 匹配成功 setTimeout(() { const updatedCards newCards.map(card { if (card.id firstId || card.id secondId) { return { ...card, isMatched: true }; } return card; }); setCards(updatedCards); setFlippedCardIds([]); // 清空已翻开列表 // 检查游戏是否结束所有牌都匹配了 if (updatedCards.every(card card.isMatched)) { setGameOver(true); Alert.alert(恭喜, 游戏结束你用了 ${moves 1} 步。); } }, 500); // 给用户一点时间看到匹配的牌 } else { // 匹配失败翻回去 setTimeout(() { const resetCards newCards.map(card { if (card.id firstId || card.id secondId) { return { ...card, isFlipped: false }; } return card; }); setCards(resetCards); setFlippedCardIds([]); // 清空已翻开列表 }, 1000); // 给用户一点时间看到不匹配的牌 } } }; return ( SafeAreaView style{styles.container} View style{styles.header} Text style{styles.title}记忆翻牌游戏/Text Text步数: {moves}/Text Button title重新开始 onPress{initializeGame} / /View View style{styles.board} {/* 使用 Flexbox 布局渲染卡牌网格 */} {cards.map(card ( Card key{card.id} card{card} onPress{handleCardPress} / ))} /View {gameOver ( View style{styles.gameOverOverlay} Text style{styles.gameOverText}太棒了你赢了/Text /View )} /SafeAreaView ); } const styles StyleSheet.create({ container: { flex: 1, backgroundColor: #F7FAFC, }, header: { alignItems: center, padding: 20, borderBottomWidth: 1, borderBottomColor: #E2E8F0, }, title: { fontSize: 28, fontWeight: bold, marginBottom: 10, color: #2D3748, }, board: { flex: 1, flexDirection: row, flexWrap: wrap, justifyContent: center, alignItems: center, padding: 10, }, gameOverOverlay: { ...StyleSheet.absoluteFillObject, // 覆盖整个屏幕 backgroundColor: rgba(0, 0, 0, 0.7), justifyContent: center, alignItems: center, }, gameOverText: { fontSize: 36, color: white, fontWeight: bold, }, });核心逻辑深度解析状态设计我们使用了四个useStateHook。cards核心数据源驱动所有卡牌渲染。flippedCardIds一个临时状态用于追踪当前翻开的两张牌。它的变化触发了匹配判断逻辑。moves和gameOver衍生状态用于 UI 展示和游戏流程控制。不可变数据更新这是 React 状态管理的黄金法则。在handleCardPress中我们总是通过map或展开运算符 (...) 创建新的数组或对象再调用setCards或setFlippedCardIds。直接修改原状态对象不会触发重新渲染。异步与定时器匹配成功或失败后我们使用setTimeout延迟了状态更新。这有两个目的一是给用户一个视觉反馈的时间看清牌面二是避免状态更新过于频繁导致界面卡顿。注意在组件卸载前要清除定时器这里因为是短暂延迟且逻辑简单暂未处理。在更复杂的应用中应使用useEffect的清理函数。游戏结束判断cards.every(card card.isMatched)是一个简洁的判断所有牌是否都已匹配的方法。布局游戏面板 (board) 使用了 Flexbox 布局flexDirection: row和flexWrap: wrap让卡牌水平排列并自动换行justifyContent: center和alignItems: center使其居中。这是 React Native 中实现网格布局的常用方式。至此一个功能完整的《记忆翻牌游戏》就实现了。保存文件Expo Go 应用会自动刷新你就能在手机上玩这个游戏了。4. 从“能运行”到“好用”优化、扩展与工程化思考让游戏跑起来只是第一步。接下来我们要思考如何让它更好并借此深入 React Native 的一些重要概念。4.1 添加动画提升体验目前的翻牌是瞬间切换缺乏趣味。我们可以使用 React Native 自带的AnimatedAPI 添加一个简单的翻转动画。修改components/Card.js// components/Card.js (部分修改) import React, { useRef, useEffect } from react; import { View, Text, TouchableOpacity, StyleSheet, Animated } from react-native; const Card ({ card, onPress }) { // 创建一个引用用于存储动画值 const flipAnimation useRef(new Animated.Value(0)).current; // 根据 isFlipped 状态触发动画 useEffect(() { Animated.timing(flipAnimation, { toValue: card.isFlipped ? 1 : 0, duration: 300, useNativeDriver: true, // 使用原生驱动以获得更好性能 }).start(); }, [card.isFlipped]); // 插值计算将 0-1 的值映射到 0-180 度 const rotateY flipAnimation.interpolate({ inputRange: [0, 1], outputRange: [0deg, 180deg], }); // 正面和背面的透明度 const frontOpacity flipAnimation.interpolate({ inputRange: [0.5, 1], outputRange: [0, 1], }); const backOpacity flipAnimation.interpolate({ inputRange: [0, 0.5], outputRange: [1, 0], }); if (card.isMatched) { return (/* 匹配状态视图同上 */); } return ( TouchableOpacity onPress{() onPress(card.id)} disabled{card.isFlipped || card.isMatched} View style{styles.cardContainer} {/* 背面 */} Animated.View style{[ styles.card, styles.cardBack, { opacity: backOpacity, transform: [{ rotateY }], position: absolute, // 绝对定位与正面重叠 }, ]} Text style{styles.backText}?/Text /Animated.View {/* 正面 */} Animated.View style{[ styles.card, styles.cardFront, { opacity: frontOpacity, transform: [{ rotateY }], }, ]} Text style{styles.value}{card.value}/Text /Animated.View /View /TouchableOpacity ); }; // 更新样式为容器添加透视效果使3D旋转更真实 const styles StyleSheet.create({ cardContainer: { width: 80, height: 80, margin: 5, }, card: { width: 100%, height: 100%, borderRadius: 8, justifyContent: center, alignItems: center, backfaceVisibility: hidden, // 关键隐藏背面 }, // ... 其他样式保持不变 });这个动画实现了经典的 3D 翻转效果。关键在于useNativeDriver: true和backfaceVisibility: hidden前者提升性能后者确保翻转时背面不可见。4.2 状态管理的进阶思考使用 useReducer当游戏逻辑变得更复杂时比如增加计时器、多种游戏模式、历史记录仅用多个useState可能会让handleCardPress函数变得庞大且难以维护。这时可以考虑使用useReducerHook。useReducer更适合管理包含多个子值的复杂状态逻辑。例如我们可以将游戏的所有状态聚合到一个对象中// 在 App.js 中 const initialState { cards: [], flippedCardIds: [], gameOver: false, moves: 0, timeElapsed: 0, }; function gameReducer(state, action) { switch (action.type) { case INIT_GAME: return { ...initialState, cards: createDeck(6) }; case FLIP_CARD: // ... 处理翻牌逻辑 case CHECK_MATCH: // ... 处理匹配检查逻辑 case INCREMENT_TIME: return { ...state, timeElapsed: state.timeElapsed 1 }; default: return state; } } // 在组件中使用 const [state, dispatch] useReducer(gameReducer, initialState); // 通过 dispatch({ type: FLIP_CARD, payload: cardId }) 来触发状态更新虽然对于当前游戏来说useState足够但了解useReducer是迈向更复杂状态管理如 Redux的重要一步。4.3 样式与布局的响应式考虑我们的游戏在固定尺寸下运行良好但在不同尺寸的手机上呢React Native 提供了DimensionsAPI 和Flexbox来应对。一种简单的响应式布局思路是根据屏幕宽度动态计算卡牌大小和间距import { Dimensions } from react-native; const { width } Dimensions.get(window); const cardSize (width - 10 * 2 - 5 * (COLUMNS - 1)) / COLUMNS; // 假设每行 COLUMNS 张牌更推荐的方式是充分利用 Flexbox。将游戏面板 (board) 的宽度设为100%或flex: 1并设置justifyContent: space-around或space-between让 Flexbox 自动分配空间。4.4 性能优化浅谈Key 与 列表渲染注意我们在渲染卡牌列表时使用了key{card.id}。为列表项提供稳定、唯一的key是 React 高效更新列表的关键。如果使用数组索引index作为 key当牌组顺序因洗牌而改变时会导致不必要的组件重新创建和 DOM 操作影响性能。对于这个游戏card.id是唯一且不变的是最佳的 key 选择。5. 项目复盘与 React Native 学习路径建议通过构建这个游戏我们实际上走完了一个小型 React Native 应用的核心开发流程。现在让我们跳出代码总结一下你刚刚实践了哪些关键技能以及接下来可以往哪里深入。你已经掌握的 React Native 核心组件思维将 UI 拆分为独立的、可复用的Card组件。状态管理使用useState和useReducer概念来驱动 UI 变化理解了“状态提升”。样式系统使用StyleSheet和 Flexbox 进行移动端布局。事件处理使用TouchableOpacity或Pressable响应用户交互。生命周期与副作用使用useEffect在组件加载时初始化游戏。列表渲染使用map渲染动态列表并理解了key的重要性。动画初步接触AnimatedAPI实现视觉反馈。这个项目揭示的 React Native 开发心法从数据开始先设计状态结构再设计组件。数据是 UI 的源头。状态最小化只把需要跨组件共享或随时间变化的数据放入状态。flippedCardIds就是一个典型的、必要的临时状态。副作用隔离像匹配检查后的延迟翻牌、计时器这类“动作”放在useEffect或事件处理函数中保持渲染逻辑纯净。样式是组件的一部分在 React Native 中样式通常与组件文件紧耦合这有助于维护。接下来的学习方向导航真实应用很少只有一个页面。学习React Navigation这个最流行的库了解栈导航、标签页导航、抽屉导航。数据持久化如何保存游戏最高分学习使用AsyncStorage或更强大的SQLite、Realm。网络请求从服务器获取卡牌图案或上传成绩。掌握fetch或axios在 React Native 中的使用。访问设备功能如果想为游戏添加震动反馈或播放音效就需要学习访问设备 API如Vibration、Audio。调试与性能熟练使用 React Native Debugger、Flipper以及学习使用PerformanceAPI 监控应用性能。打包发布最终你需要将应用打包成.ipa(iOS) 或.apk(Android) 文件提交到应用商店。Expo 提供了eas build等工具来简化这个过程。回到最初的问题为什么 React Native 适合这类应用因为它让你用一套思维React和一种语言JavaScript的绝大部分就能触及两个主要平台。你付出的学习成本主要在于理解其原生渲染机制和平台特定行为的边界而不是从头学习两套完全不同的技术栈。《记忆翻牌游戏》是一个完美的起点。它足够小让你能快速获得成就感它也足够完整包含了构建更复杂应用所需的核心模式。下一步试着为它添加一个“难度选择”调整牌对数量或者一个“历史最佳成绩”页面你会发现自己已经自然而然地走在了 React Native 开发者的道路上。