1. 项目概述:为什么在 React Native 里做落地页,还得死磕 Flexbox?

“Building a Landing Page in React Native Using Flexbox”——这个标题乍看有点反直觉。React Native 做 App 是强项,但落地页(Landing Page)?第一反应是:那不是 Web 的地盘吗?用 Next.js、Vite 或纯 HTML/CSS 就够了,干嘛非得搬 React Native 过来?别急,这背后藏着三个真实且越来越普遍的业务场景: 跨端一致性交付、离线优先型营销活动、以及原生能力深度集成的高转化入口 。我去年帮一家东南亚跨境教育平台重构其暑期课程推广页,客户明确要求:iOS/Android/App内嵌WebView/微信小程序/H5 四端视觉与交互完全一致,且必须支持离线加载首屏、点击即唤起摄像头扫码报名、长按图片保存海报——这些需求,用纯 Web 方案要么性能打折扣,要么权限受限,要么开发成本翻倍。最终我们用 React Native + Flexbox 搭建了整套落地页体系,复用率超 85%,首屏渲染从 Web 的 1.8s 降到 0.42s(实测 iOS 15+),关键转化按钮点击延迟低于 30ms。Flexbox 在这里不是炫技,而是唯一能同时满足 响应式布局精度、跨平台渲染一致性、以及原生视图层级控制力 的底层支撑。它不像 CSS Grid 那样在 React Native 中支持不全(截至 RN 0.74, grid 属性仍为实验性且无 Android 兼容),也不像绝对定位那样难以维护。你看到的每一行居中文字、每一张自适应卡片、每一个随屏幕宽度收缩的 CTA 按钮,底层都是 Flexbox 的 flexDirection justifyContent alignItems 在精准调度。这不是“用 React Native 写个网页”,而是把落地页当作一个轻量级原生模块来构建——它有原生导航栈、能调用设备传感器、可无缝接入推送系统,而 Flexbox 就是让这个模块在不同尺寸屏幕上“站得稳、排得齐、缩得准”的骨架。

2. 核心设计思路:Flexbox 不是 CSS 的平移,而是 RN 布局哲学的具象化

2.1 为什么放弃 WebView + HTML/CSS?三重硬伤无法绕过

很多人第一反应是:“直接塞个 WebView 进去不就完了?”我试过,也踩过坑。去年给一个健身 SaaS 做会员续费落地页,初期用 WebView 加载 Vue 打包的 H5,结果上线三天收到 27 条用户投诉:iOS 上滑动卡顿、Android 上字体模糊、微信内嵌时分享按钮失灵。根本原因在于三层隔离:WebView 容器 → Web 渲染引擎 → JS 执行环境。而 React Native 的 Flexbox 是直接驱动原生 UIView (iOS)和 ViewGroup (Android)的布局计算引擎。举个具体例子:一个常见的“图标+标题+描述”横向卡片,在 WebView 中你需要写:

.card {
  display: flex;
  align-items: center;
  padding: 16px;
}
.icon { width: 24px; height: 24px; }
.title { font-size: 16px; margin-left: 12px; }

但在 React Native 中,同样的结构变成:

<View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}>
  <Image source={icon} style={{ width: 24, height: 24 }} />
  <Text style={{ fontSize: 16, marginLeft: 12 }}>会员续费</Text>
</View>

表面看只是语法差异,实则本质不同:Web 的 flex 是浏览器渲染管线中的一个布局阶段,受制于 DOM 构建、样式计算、布局、绘制多道工序;而 RN 的 flexDirection 是直接映射到原生视图的 NSLayoutConstraint (iOS)或 ConstraintLayout (Android)参数,跳过了整个 Web 渲染栈。这意味着什么? 布局计算发生在原生线程,不受 JS 主线程阻塞影响 。我们做过压测:当页面包含 50 个同类卡片时,WebView 方案在低端 Android 设备上布局耗时达 120ms,而 RN Flexbox 方案稳定在 8ms 以内。这不是优化能解决的差距,而是架构层级的降维打击。

2.2 Flexbox 在 RN 中的三大不可替代性:安全区、动态尺寸、嵌套可控性

React Native 的 Flexbox 不是 CSS 的简单移植,它针对移动场景做了关键增强,其中三点最值得深挖:

第一,SafeArea 的原生级融合 。Web 端处理刘海屏/小黑条靠 env(safe-area-inset-top) 这类 CSS 环境变量,但兼容性差(尤其 iOS 14.5 以下)。RN 则通过 SafeAreaView 组件将 SafeArea 逻辑下沉到原生层。它的实现原理是:在 iOS 上, SafeAreaView 会自动读取 UIApplication.shared.keyWindow?.safeAreaInsets 并生成对应的 padding ;在 Android 上,则解析 WindowInsetsCompat 获取状态栏/导航栏高度。而这个 padding 是直接作用于 Flexbox 的 paddingTop / paddingBottom 计算中的。这意味着你写:

<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
  <View style={{ flex: 1, paddingHorizontal: 20 }}>
    <Text>主内容</Text>
  </View>
</SafeAreaView>

SafeAreaView flex: 1 会先占据全部可用空间,再减去 SafeArea insets,最后把剩余空间分配给内部 View 。这种链式计算是 Web 无法模拟的——CSS 的 env() 只能作为静态值参与计算,无法动态响应键盘弹出、横竖屏切换等运行时变化。

第二,动态尺寸的零损耗计算 。Web 中获取元素尺寸需 ref.current?.getBoundingClientRect() ,触发强制同步布局(Forced Synchronous Layout),是性能杀手。RN 的 Flexbox 布局引擎在每次 setState 后自动触发,且尺寸计算在原生层完成。更关键的是,RN 提供 useWindowDimensions Hook,它监听 Dimensions 原生模块的变更事件,返回实时宽高。我们曾用它实现一个“根据屏幕宽度自动切换单列/双列布局”的产品卡片区:

const { width } = useWindowDimensions();
const isLargeScreen = width > 768;

return (
  <View style={{ flexDirection: isLargeScreen ? 'row' : 'column' }}>
    {products.map((p) => (
      <ProductCard 
        key={p.id} 
        style={{ flex: isLargeScreen ? 1 : undefined, width: isLargeScreen ? '50%' : '100%' }} 
      />
    ))}
  </View>
);

注意这里 flex: 1 width: '50%' 的组合——在 row 方向下, flex: 1 让卡片均分剩余空间, width: '50%' 是兜底(防极端情况),两者叠加确保布局坚如磐石。这种动态响应能力,是纯 CSS 媒体查询无法企及的,因为媒体查询只能基于设备像素比(DPR)和固定断点,而 RN 能精确到像素级实时反馈。

第三,嵌套层级的绝对可控性 。Web 中 div 嵌套过深易引发层叠上下文(stacking context)混乱,导致 z-index 失效。RN 的 View 是扁平化原生视图, zIndex 属性直接映射到 UIView.zPosition (iOS)或 View.setZ() (Android)。更重要的是,Flexbox 的 flexWrap alignContent 在 RN 中行为高度可预测。例如,一个需要换行的标签云组件:

<View style={{ 
  flexDirection: 'row', 
  flexWrap: 'wrap', 
  justifyContent: 'flex-start',
  gap: 8 // RN 0.73+ 支持 gap,替代手动 marginLeft
}}>
  {tags.map(tag => (
    <Tag key={tag} text={tag} />
  ))}
</View>

flexWrap: 'wrap' 在 RN 中的换行算法是确定性的:按子元素 width 累加,超过父容器 width 即换行,且换行后新行的 justifyContent 独立计算。这避免了 Web 中因 inline-block 微小空格、字体渲染差异导致的换行错位问题。我们在金融类 App 的“热门搜索词”模块中验证过:100 个长度不一的标签,在 iPhone 12 和 Pixel 5 上布局完全一致,误差小于 0.5px。

2.3 为什么不用第三方布局库?原生 Flexbox 的“够用”哲学

看到这里可能有人问:“既然这么复杂,为啥不直接用 react-native-responsive-screen react-native-size-matters ?”答案很实在: 过度封装反而增加不可控性 。我曾在一个电商项目中引入 responsive-screen ,结果发现它内部用 Dimensions.get('window') 监听尺寸变化,但该 API 在 Android 上存在 200ms 延迟(因需跨线程通信),导致横屏切换时布局闪动。而原生 Flexbox 的 flex: 1 width: '100%' 是声明式、无副作用的,布局计算由原生引擎保证原子性。更关键的是,RN 的 Flexbox 已覆盖 95% 的落地页场景: flexDirection 处理主轴方向, justifyContent 控制主轴对齐, alignItems 管理交叉轴, flexWrap 应对换行, gap 解决间距, aspectRatio 保障比例——这些原生命令足够构建从极简单页到复杂多模块落地页的所有布局。所谓“够用”,是指它用最少的 API 表面,解决了移动布局最核心的矛盾: 如何在千变万化的屏幕尺寸上,让内容既保持视觉秩序,又不牺牲性能 。当你开始为一个按钮的 marginRight 是否该用 scale(1.2) 而纠结时,说明你已经偏离了落地页的本质——它不是 UI 实验场,而是转化漏斗的第一环。Flexbox 的价值,正在于用确定性对抗不确定性。

3. 核心细节解析:从零搭建一个生产级落地页的 7 个关键环节

3.1 环境初始化:避坑指南与最小可行依赖

新建 RN 项目时,切忌直接 npx react-native init MyApp 。官方 CLI 生成的模板包含大量冗余(如 Hermes 调试工具、旧版 Metro 配置),对落地页这类轻量模块是负担。我的标准流程是:

  1. 使用 react-native@0.74.5 (当前最稳 LTS 版本) :0.73+ 引入 gap 属性和 useWindowDimensions 的稳定支持,0.74 修复了 Android 上 flexWrap ScrollView 内的换行 bug(详见 RN GitHub #37211)。
  2. 禁用 Hermes(除非你真需要字节码优化) :落地页 JS 逻辑简单,Hermes 的启动优势不明显,反而增加调试难度。在 android/app/build.gradle 中设 enableHermes = false
  3. 精简依赖 :删除 @react-native-community/async-storage (落地页通常无需持久化)、 react-native-screens (单页无需导航栈管理)。只保留:
    • @react-navigation/native (仅用于 NavigationContainer 包裹,不启用实际导航)
    • react-native-safe-area-context SafeAreaView 必需)
    • @react-native-async-storage/async-storage (如果需缓存用户行为数据)

提示: react-native-safe-area-context 必须搭配 react-native-screens 使用,但后者只需安装,无需初始化。这是 RN 生态的常见耦合,不必强行解耦。

初始化命令应为:

npx react-native init LandingPage --version 0.74.5 --skip-install
cd LandingPage
npm install @react-navigation/native @react-navigation/stack react-native-safe-area-context @react-native-async-storage/async-storage
# iOS 需额外 pod install
cd ios && pod install && cd ..

3.2 基础布局骨架:SafeAreaView + ScrollView + Flexbox 的黄金三角

落地页的核心容器必须解决三个问题:适配异形屏、支持滚动、保持 Flexbox 布局能力。错误做法是直接 <View style={{ flex: 1 }}> ,这会导致内容被 SafeArea 截断。正确骨架如下:

// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ScrollView, View, StyleSheet } from 'react-native';

export default function App() {
  return (
    <NavigationContainer>
      <SafeAreaProvider>
        <ScrollView 
          contentContainerStyle={styles.scrollContainer}
          showsVerticalScrollIndicator={false}
        >
          <View style={styles.pageContainer}>
            {/* 页面内容 */}
            <HeroSection />
            <FeaturesSection />
            <TestimonialsSection />
            <CTASection />
          </View>
        </ScrollView>
      </SafeAreaProvider>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  scrollContainer: {
    paddingBottom: 40, // 为底部 CTA 留出呼吸空间
  },
  pageContainer: {
    paddingHorizontal: 20,
    paddingTop: 24,
  },
});

关键点解析:

  • SafeAreaProvider 是根 Provider,必须包裹整个应用,它为子组件提供 SafeArea 上下文。
  • ScrollView contentContainerStyle 设置 paddingBottom ,而非 View paddingBottom ,因为 ScrollView 的内容容器(content container)才是实际滚动区域, paddingBottom 在此处生效才能确保滚动到底部时 CTA 按钮不被遮挡。
  • pageContainer paddingHorizontal paddingTop 是内容内边距, paddingTop 的值(24)需大于 SafeAreaView 的默认 top inset(通常 44),确保状态栏下方有足够空白。

3.3 Hero 区域:Flexbox 实现响应式头图与文案组合

Hero 区是转化率的关键,需在小屏上突出文案,在大屏上展示视觉。传统方案用 ImageBackground ,但存在宽高比失真风险。我们的方案是 Image + View 绝对定位组合:

// HeroSection.tsx
import { Image, Text, View, StyleSheet, Dimensions } from 'react-native';

const { width } = Dimensions.get('window');

export function HeroSection() {
  return (
    <View style={styles.heroContainer}>
      {/* 背景图,铺满容器 */}
      <Image 
        source={require('../assets/hero-bg.png')} 
        style={styles.heroImage} 
        resizeMode="cover" // 关键:保持比例,裁剪多余部分
      />
      {/* 文案层,绝对定位 */}
      <View style={styles.heroContent}>
        <Text style={styles.heroTitle}>改变你的学习方式</Text>
        <Text style={styles.heroSubtitle}>AI 驱动的个性化课程,7 天见效</Text>
        <PrimaryButton title="立即体验" onPress={() => {}} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  heroContainer: {
    position: 'relative',
    height: width * 0.55, // 动态高度:宽度的 55%,保证在所有设备上比例协调
  },
  heroImage: {
    ...StyleSheet.absoluteFillObject, // 等同于 { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }
    width: undefined, // 重置 width,让 resizeMode 生效
    height: undefined,
  },
  heroContent: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 20,
  },
  heroTitle: {
    fontSize: width > 768 ? 32 : 24, // 大屏用大字号
    fontWeight: 'bold',
    color: '#fff',
    textAlign: 'center',
    marginBottom: 12,
  },
  heroSubtitle: {
    fontSize: width > 768 ? 18 : 16,
    color: 'rgba(255,255,255,0.9)',
    textAlign: 'center',
    marginBottom: 24,
  },
});

这里 height: width * 0.55 是精髓:用 Dimensions.get('window').width 计算动态高度,确保头图在 iPhone SE(375px 宽)上高 206px,在 iPad Pro(1024px 宽)上高 563px,视觉比例始终一致。 resizeMode="cover" 保证图片不拉伸, ...StyleSheet.absoluteFillObject 确保背景图完美贴合容器。文案层用 justifyContent: 'center' alignItems: 'center' 实现真正的垂直水平居中——这是 Flexbox 相对于 position: absolute + transform 的巨大优势:无需计算 top / left ,布局逻辑清晰且可维护。

3.4 功能模块区:用 flexWrap 构建自适应卡片网格

功能介绍区常需 3-4 个图标+文案卡片。目标是:小屏单列,中屏双列,大屏三列。 flexWrap 是最优解:

// FeaturesSection.tsx
import { View, Text, Image, StyleSheet, useWindowDimensions } from 'react-native';

export function FeaturesSection() {
  const { width } = useWindowDimensions();
  
  // 根据宽度动态计算每行卡片数
  const cardsPerRow = width > 1024 ? 3 : width > 768 ? 2 : 1;
  const cardWidth = (width - 40) / cardsPerRow; // 减去总 paddingHorizontal 40

  return (
    <View style={styles.featuresContainer}>
      <Text style={styles.sectionTitle}>核心功能</Text>
      <View style={[styles.cardsGrid, { flexDirection: 'row', flexWrap: 'wrap' }]}>
        {FEATURES.map((feature, index) => (
          <FeatureCard 
            key={index} 
            icon={feature.icon} 
            title={feature.title} 
            description={feature.description}
            style={{ 
              width: cardWidth, 
              margin: 10 // 卡片间留白
            }} 
          />
        ))}
      </View>
    </View>
  );
}

const FEATURES = [
  { icon: require('../assets/icon-1.png'), title: '智能诊断', description: '3 分钟定位知识盲区' },
  { icon: require('../assets/icon-2.png'), title: '动态路径', description: '实时调整学习计划' },
  { icon: require('../assets/icon-3.png'), title: 'AI 讲师', description: '24 小时答疑解惑' },
  { icon: require('../assets/icon-4.png'), title: '成果报告', description: '每周生成能力雷达图' },
];

const styles = StyleSheet.create({
  featuresContainer: {
    marginTop: 40,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 24,
  },
  cardsGrid: {
    // 注意:这里不设 height,让 flexWrap 自动撑开
  },
});

// FeatureCard.tsx
import { View, Text, Image, StyleSheet } from 'react-native';

export function FeatureCard({ icon, title, description, style }: any) {
  return (
    <View style={[styles.card, style]}>
      <Image source={icon} style={styles.icon} />
      <Text style={styles.cardTitle}>{title}</Text>
      <Text style={styles.cardDesc}>{description}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 8,
    elevation: 2, // Android 阴影
  },
  icon: {
    width: 48,
    height: 48,
    marginBottom: 12,
  },
  cardTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
  },
  cardDesc: {
    fontSize: 14,
    color: '#666',
  },
});

flexWrap: 'wrap' 的魔力在此刻显现:当 cardsPerRow=2 时,4 个卡片自动排成两行;当 cardsPerRow=1 时,全部堆成一列。 margin: 10 是卡片间距,由于 flexWrap 会为每行最后一个元素额外添加 marginRight ,我们用 margin: 10 统一设置四边距,再通过 paddingHorizontal: 20 cardsGrid 外层预留空间,避免卡片紧贴屏幕边缘。这种“弹性网格”比 CSS Grid 的 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) 更可靠,因为 RN 的 minmax() 支持度有限,而 flexWrap 是 100% 原生支持。

3.5 用户评价区:用 alignItems gap 控制头像矩阵

用户评价常需头像+姓名+评语的组合,且要应对评价数量动态变化。 alignItems: 'center' + gap 是最佳搭档:

// TestimonialsSection.tsx
import { View, Text, Image, StyleSheet, useWindowDimensions } from 'react-native';

export function TestimonialsSection() {
  const { width } = useWindowDimensions();
  const avatarSize = width > 768 ? 64 : 48;

  return (
    <View style={styles.testimonialsContainer}>
      <Text style={styles.sectionTitle}>用户怎么说</Text>
      <View style={styles.testimonialsList}>
        {TESTIMONIALS.map((item, index) => (
          <TestimonialItem 
            key={index} 
            avatar={item.avatar} 
            name={item.name} 
            role={item.role} 
            content={item.content}
            avatarSize={avatarSize}
          />
        ))}
      </View>
    </View>
  );
}

const TESTIMONIALS = [
  { 
    avatar: require('../assets/avatar-1.png'), 
    name: '李明', 
    role: '高三学生', 
    content: '数学成绩从 72 分提到 96 分,老师说我的解题逻辑变了!' 
  },
  // ... 更多数据
];

const styles = StyleSheet.create({
  testimonialsContainer: {
    marginTop: 40,
  },
  testimonialsList: {
    marginTop: 24,
    gap: 24, // RN 0.73+ 支持,替代繁琐的 marginBottom
  },
});

// TestimonialItem.tsx
import { View, Text, Image, StyleSheet } from 'react-native';

export function TestimonialItem({ avatar, name, role, content, avatarSize }: any) {
  return (
    <View style={styles.item}>
      <View style={styles.avatarContainer}>
        <Image source={avatar} style={[styles.avatar, { width: avatarSize, height: avatarSize }]} />
      </View>
      <View style={styles.textContainer}>
        <Text style={styles.name}>{name}</Text>
        <Text style={styles.role}>{role}</Text>
        <Text style={styles.content}>"{content}"</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  item: {
    flexDirection: 'row',
    alignItems: 'flex-start', // 关键:让头像顶部对齐,避免文字长时错位
  },
  avatarContainer: {
    marginRight: 16,
  },
  avatar: {
    borderRadius: 32,
  },
  textContainer: {
    flex: 1, // 占据剩余空间,防止文字溢出
  },
  name: {
    fontSize: 16,
    fontWeight: '600',
  },
  role: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  content: {
    fontSize: 15,
    color: '#333',
    marginTop: 12,
    lineHeight: 22,
  },
});

alignItems: 'flex-start' 确保头像顶部与文字顶部对齐,这是 alignItems: 'center' 无法做到的(后者会让头像垂直居中,文字长时头像会下沉)。 gap: 24 testimonialsList 上统一设置间距,比为每个 TestimonialItem 单独加 marginBottom 更简洁、更不易出错。 flex: 1 textContainer 上保证文字区域自动伸缩,适应不同长度的评语。

3.6 CTA 区域:固定底部 + Flexbox 居中的终极方案

落地页的转化按钮必须“伸手可及”。 position: 'absolute' + bottom: 0 是陷阱(会被键盘顶起),正确方案是 ScrollView contentContainerStyle + flex

// CTASection.tsx
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

export function CTASection() {
  return (
    <View style={styles.ctaContainer}>
      <Text style={styles.ctaTitle}>准备好开启学习之旅了吗?</Text>
      <TouchableOpacity style={styles.ctaButton} onPress={() => {}}>
        <Text style={styles.ctaButtonText}>立即免费注册</Text>
      </TouchableOpacity>
      <Text style={styles.ctaDisclaimer}>无需信用卡,7 天无理由退款</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  ctaContainer: {
    marginTop: 40,
    marginBottom: 40, // 为 SafeArea 底部留白
    alignItems: 'center',
  },
  ctaTitle: {
    fontSize: 20,
    fontWeight: '600',
    textAlign: 'center',
    marginBottom: 24,
  },
  ctaButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 40,
    paddingVertical: 16,
    borderRadius: 12,
    minWidth: 200,
  },
  ctaButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  ctaDisclaimer: {
    fontSize: 12,
    color: '#999',
    marginTop: 16,
  },
});

marginTop / marginBottom 控制垂直间距, alignItems: 'center' 确保按钮水平居中。 minWidth: 200 防止小屏上按钮过窄。整个 CTA 区放在 ScrollView contentContainerStyle 内,自然获得滚动能力,且不会被键盘遮挡——因为 RN 的 KeyboardAvoidingView ScrollView 有原生支持,键盘弹出会自动调整 ScrollView contentInset

3.7 性能优化:Flexbox 布局的 3 个隐形加速器

Flexbox 本身不慢,但不当使用会拖垮性能。以下是经过真机测试的优化点:

1. 避免在 render 中创建内联样式对象
错误写法:

<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>

正确写法:

const rowStyle = useMemo(() => ({ flexDirection: 'row', justifyContent: 'space-between' }), []);
<View style={rowStyle}>

原因:JSX 中的 {} 每次渲染都创建新对象,导致 View 组件不必要的重绘。 useMemo 缓存样式对象,减少内存分配。

2. 用 aspectRatio 替代 height + width 计算
对于头图、卡片等需保持比例的元素,用 aspectRatio: 16/9 代替 height: width * 0.5625 aspectRatio 是原生属性,计算在原生层完成,无 JS 开销。

3. ScrollView removeClippedSubviews 启用
ScrollView 上添加 removeClippedSubviews={true} (RN 0.72+ 默认启用),它会卸载屏幕外的子组件,大幅降低内存占用。实测 50 个卡片列表,内存占用从 85MB 降至 32MB。

4. 实操全流程:从创建项目到真机部署的 12 步详解

4.1 创建项目与基础配置(步骤 1-3)

步骤 1:初始化项目(3 分钟)
打开终端,执行:

# 创建项目,指定版本
npx react-native init LandingPage --version 0.74.5 --skip-install

# 进入目录
cd LandingPage

# 安装核心依赖
npm install @react-navigation/native @react-navigation/stack react-native-safe-area-context @react-native-async-storage/async-storage

# iOS 依赖安装
cd ios && pod install && cd ..

注意: --skip-install 避免 npm 自动安装,让我们手动控制依赖版本。 pod install 必须在 ios 目录下执行,否则 CocoaPods 找不到 Podfile

步骤 2:配置 TypeScript(2 分钟)
RN 0.74 默认支持 TS,但需补全类型定义:

# 安装类型依赖
npm install --save-dev @types/react @types/react-native

# 创建 tsconfig.json(若不存在)
echo '{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "jsx": "react-native",
    "lib": ["es6", "dom"],
    "moduleResolution": "node",
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "strict": true,
    "target": "esnext"
  }
}' > tsconfig.json

步骤 3:设置 Metro 配置(1 分钟)
为支持图片资源,修改 metro.config.js

const { getDefaultConfig } = require('expo/metro-config'); // 如果用 Expo
// 或者用 RN 默认配置
// const { getDefaultConfig } = require('metro-config');

module.exports = (() => {
  const config = getDefaultConfig(__dirname);
  config.resolver.assetExts.push('ttf', 'woff', 'woff2');
  return config;
})();

4.2 编写核心组件(步骤 4-8)

步骤 4:创建 App.tsx 骨架(5 分钟)
替换 App.tsx 为前文所述的 SafeAreaProvider + ScrollView 结构。重点检查:

  • NavigationContainer 是否包裹最外层
  • SafeAreaProvider 是否在 NavigationContainer
  • ScrollView contentContainerStyle 是否含 paddingBottom

步骤 5:实现 HeroSection(8 分钟)
创建 src/components/HeroSection.tsx ,粘贴前文代码。关键验证点:

  • Dimensions.get('window').width 是否正确返回设备宽度
  • resizeMode="cover" 是否使图片无拉伸填充
  • position: 'relative' ...StyleSheet.absoluteFillObject 是否让文案层精准覆盖

步骤 6:构建 FeaturesSection(10 分钟)
创建 src/components/FeaturesSection.tsx ,重点测试 flexWrap

  • 在模拟器中切换 iPhone SE(375px)和 iPad Pro(1024px)尺寸
  • 观察卡片是否从单列→双列→三列自动切换
  • 检查 margin: 10 是否在所有设备上产生一致间距

步骤 7:集成 TestimonialsSection(6 分钟)
创建 src/components/TestimonialsSection.tsx ,验证 alignItems: 'flex-start'

  • 添加一条超长评语(> 3 行),确认头像未因文字增长而下移
  • 检查 gap: 24 是否替代了 marginBottom ,且无额外空白

步骤 8:完善 CTASection(3 分钟)
创建 src/components/CTASection.tsx ,测试键盘响应:

  • 在 iOS 模拟器中调出键盘(Cmd+K)
  • 确认 ScrollView 自动上推,CTA 按钮始终可见
  • 点击按钮,确认 onPress 事件正常触发

4.3 真机调试与性能分析(步骤 9-12)

步骤 9:iOS 真机部署(15 分钟)

  1. 在 Xcode 中打开 ios/LandingPage.xcworkspace
  2. 选择你的设备(非模拟器)
  3. 点击运行(▶️)
  4. 若报错 No bundle URL present ,在设备上摇动,选择 Dev Settings Debug server host & port for device ,输入电脑 IP + 8081 (如 192.168.1.100:8081

注意:电脑和手机必须在同一 WiFi 下。IP 地址在 Mac 的 系统设置 > 网络 中查看。

步骤 10:Android 真机调试(10 分钟)

  1. 在手机开发者选项中启用 USB 调试
  2. 用 USB 连接电脑
  3. 终端执行 adb reverse tcp:8081 tcp:8081 (将设备 8081 映射到电脑)
  4. 运行 npx react-native run-android

步骤 11:性能监控(5 分钟)
在应用中摇动,选择 Perf Monitor ,关注三项指标:

  • JS FPS :应稳定在 58-60(RN 目标帧率 60fps)
  • UI FPS :应稳定在 58-60(原生线程

更多推荐