likes
comments
collection
share

4-6 RN 入门与实战

作者站长头像
站长
· 阅读数 4

期望这篇 RN 文章,能给你的 React、跨端、底层带来一些提升

移动端演进

第一阶段:

浏览器 APP,直接打开 HTML

第二阶段:hybrid 方案

原生 APP,使用 WebView(嵌入式浏览器) 打开 HTML,采用 JSBridge 与 APP 通信。

本质还是写 HTML+CSS+JS,只是在 APP 的内嵌浏览器中打开而已,借用的浏览器实现代码的执行与页面渲染

WebView:性能差,启动慢

第三阶段:RN

本质还是写 HTML+CSS+JS(用 React 库写)

但没有 WebView 了,那写出来的 JS 如何执行呢?页面如何渲染呢?

RN 提供了 JS 引擎:JSCore(Safari 浏览器),去解析执行所写的 JS 代码。

RN 提供了 渲染 引擎:根据宿主环境生成原生的 UI

tips:在 Web 端,React 是通过 react-dom 库调用document.createElement(),生成浏览器所能识别的 DOM

所以 RN 本质也是这样的,还是通过 react-dom 库,调用 JS 方法UIManager.createView(),生成了面向宿主环境的渲染代码,最终实现渲染。

优点:复用 React 的 diff、reconcile,只需要改最后的原生代码的生成

缺点:在运行时跟 native 通信,采用的异步消息,那连续的手势操作可能会卡顿,并且消息本身还需要序列化则耗时,而且消息多了会阻塞

RN 知识体系

4-6 RN 入门与实战

结论:

RN 还是借用了 React 的 diff、reconcile 处理更新逻辑(RN 源码仓库里面直接 CV 了一份 React 相关代码)

但 RN 的核心是用另一套逻辑去生成原生可渲染代码(这也是跟 React 源码上的区别)

RN Demo 实战

环境搭建

原生 metro 环境

Facebook 出品的打包工具,类似于 Webpack

需要启动对应的项目进行开发,比如:xcode、Android studio 等

沙箱环境 expo

社区提供了 expo-cli,expo 需要注册一下的,用它能简化开发流程

安装:npm i expo-cli -g

初始化项目:

旧命令expo init yourProjectName

新命令expo createexpo-app yourProjectName

选择第一个:创建空项目

启动:cd yourProjectName && npm i && npm start ,会生成一个二维码

4-6 RN 入门与实战

然后下载Expoapp,可以在 GooglePlay(开启魔法)、iOS Store 内下载

下载后,安卓手机打开该 App,先登录注册下,然后点击扫码,扫描生成的二维码,就能看到页面

若想在浏览器查看,还需运行npx expo install react-native-web react-dom @expo/metro-runtime

然后在命令行按w,就会用电脑默认浏览器打开项目,就跟开发 PC 端项目一样了

注意事项:我 mac 电脑启动项目时,竟然要开启魔法~,但Expoapp 扫描时手机不需要魔法

RN 常见的特性与坑点

  1. RN 没有<div />只有自己的标签,常见的为:<View />、<Text />可理解为<div />、<span />,但 RN 里面文本必须用<Text />包裹,否则会报错
  2. 所有的布局默认为flex,所以不用显式声明display: flex,但 RN 里面flex-direction默认为: column
  3. 需要滚动则要用<ScrollView />包裹
  4. 像素 - RN 里面的 CSS 像素是根据物理像素与dpr 计算的,比如 dpr = 2.75,物理像素宽为 1080,则 window.width = 1080 / 2.75 = 392.72727
  5. transform 写法有变:transform: [{ rotate: "45deg" }, { scale: 1.5 }]
  6. 表单使用 e.nativeEvent.text,不再是 e.target.value

开始实战

先安装 VSCode 插件:

1、安装 UI 组件

本次选择的是 RN antd

  1. 安装对应依赖
// 1. 安装
npm install @ant-design/react-native --save

// 2. 安装字体图标
npm install @ant-design/icons-react-native --save
  1. 如果你用的是 expo 请确保字体已经加载完成再初始化 app

asyncLoadFont.js

import { loadAsync } from "expo-font";

// 处理字体加载
export const asyncLoadFont = () => {
  return Promise.all([
    loadAsync(
      "antoutline",
      // eslint-disable-next-line
      require("@ant-design/icons-react-native/fonts/antoutline.ttf")
    ),
    loadAsync(
      "antfill",
      // eslint-disable-next-line
      require("@ant-design/icons-react-native/fonts/antfill.ttf")
    ),
  ]);
};

App.js

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { useEffect, useState } from "react";
import { asyncLoadFont } from "./asyncLoadFont";

export default function App() {
  const [isFontLoaded, setIsFontLoaded] = useState(false);

  useEffect(() => {
    asyncLoadFont().then(() => {
      // setTimeout 用来模拟加载,自己看效果的,可删除
      setTimeout(() => {
        setIsFontLoaded(true);
      }, 5000);
    });
  }, []);

  if (!isFontLoaded) {
    // 字体未加载完毕时,显示 loading
    return (
      <View style={styles.container}>
        <Text>Loading...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});
  1. 使用组件

App.js

import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { useEffect, useState } from "react";
import { asyncLoadFont } from "./asyncLoadFont";

// 手动引入 button 组件,可安装文档改为按需引入
import Button from "@ant-design/react-native/lib/button"; // +++

export default function App() {
  const [isFontLoaded, setIsFontLoaded] = useState(false);

  useEffect(() => {
    asyncLoadFont().then(() => {
      // setTimeout 用来模拟加载,自己看效果的,可删除
      setTimeout(() => {
        setIsFontLoaded(true);
      }, 5000);
    });
  }, []);

  if (!isFontLoaded) {
    // 字体未加载完毕时,显示 loading
    return (
      <View style={styles.container}>
        <Text>Loading...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>

      // 使用 Button 组件
      <Button type="primary" style={{ marginTop: 10 }}> // +++
        primary // +++
      </Button> // +++
          
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});
  1. 效果如下:

4-6 RN 入门与实战4-6 RN 入门与实战

补充:找 RN UI 组件的网站(不仅仅是 UI 组件哦)

2、安装路由

本次选择的是 react-navigation

  1. 安装依赖
npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context @react-navigation/bottom-tabs
  1. 根目录新建文件夹,直接运行下面命令
mkdir -p src/navigation/
touch src/navigation/index.jsx
  1. VSCode 编辑器打开刚创建的 .jsx,输入rnfe回车,然后函数命名为Navigation
import { View, Text } from "react-native";
import React from "react";

const Navigation = () => {
  return (
    <View>
      <Text>Navigation</Text>
    </View>
  );
};

export default Navigation;
  1. App.js 导入该组件
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { useEffect, useState } from "react";
import { asyncLoadFont } from "./asyncLoadFont";

import Button from "@ant-design/react-native/lib/button";
import Navigation from "./src/navigation"; // +++

export default function App() {
  const [isFontLoaded, setIsFontLoaded] = useState(false);

  useEffect(() => {
    asyncLoadFont().then(() => {
      setTimeout(() => {
        setIsFontLoaded(true);
      }, 1000);
    });
  }, []);

  if (!isFontLoaded) {
    return (
      <View style={styles.container}>
        <Text>Loading...</Text>
      </View>
    );
  }

  return <Navigation /> // +++,其他的全部去掉
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});
  1. 新建文件,作为首页展示
mkdir -p src/pages/home
touch src/pages/home/index.jsx
  1. 更改 src/pages/home/index.jsx 文件
import { View, Text } from "react-native";
import React from "react";

const Home = () => {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Home</Text>
    </View>
  );
};

export default Home;
  1. 更改 /src/navigation/index.jsx 文件
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Home from "../pages/home";

const Stack = createNativeStackNavigator();

const RootStackNavigation = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={Home} />
    </Stack.Navigator>
  );
};

const Navigation = () => {
  return (
    <NavigationContainer>
      <RootStackNavigation />
    </NavigationContainer>
  );
};

export default Navigation;
  1. 页面效果如下 4-6 RN 入门与实战
  2. 新建文件,作为详情页展示
mkdir -p src/pages/details
touch src/pages/details/index.jsx
  1. 更改 src/pages/details/index.jsx 文件
import { View, Text } from "react-native";
import React from "react";

const Details = () => {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Details</Text>
    </View>
  );
};

export default Details;
  1. 更改 src/pages/home/index.jsx 文件
import { View, Text } from "react-native";
import React from "react";

import Button from "@ant-design/react-native/lib/button"; // +++

const Home = ({ navigation }) => {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Home</Text>

      // +++ 跳转按钮
      <Button type="primary" onPress={() => navigation.navigate("Details")}>
        Go to Details
      </Button>
    </View>
  );
};

export default Home;
  1. 更改 /src/navigation/index.jsx 文件
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Home from "../pages/home";
import Details from "../pages/details"; // +++

const Stack = createNativeStackNavigator();

const RootStackNavigation = () => {
  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen name="Home" component={Home} />
      <Stack.Screen name="Details" component={Details} /> // +++
    </Stack.Navigator>
  );
};

const Navigation = () => {
  return (
    <NavigationContainer>
      <RootStackNavigation />
    </NavigationContainer>
  );
};

export default Navigation;
  1. 效果如下 4-6 RN 入门与实战

更多路由操作看官方文档

3、下面讲一下完整的页面布局代码:

  1. 更改 src/navigation/index.jsx 文件
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

import Icon from "@ant-design/react-native/lib/icon";

import Home from "../pages/home";
import Search from "../pages/search";
import Details from "../pages/details";
import Setting from "../pages/setting";
import Profile from "../pages/profile";
import User from "../pages/user";

const SettingPageStack = createNativeStackNavigator();
const SettingPage = () => {
  return (
    <SettingPageStack.Navigator>
      <SettingPageStack.Screen
        name="Setting"
        component={Setting}
        options={{ title: "设置" }}
      />
      <SettingPageStack.Screen
        name="Profile"
        component={Profile}
        options={{ title: "个人信息" }}
      />
    </SettingPageStack.Navigator>
  );
};

const HomePageStack = createNativeStackNavigator();
const HomePage = () => {
  return (
    <HomePageStack.Navigator>
      <HomePageStack.Screen
        name="Home"
        component={Home}
        options={{ title: "首页" }}
      />
      <HomePageStack.Screen
        name="Details"
        component={Details}
        options={{ title: "详情" }}
      />
    </HomePageStack.Navigator>
  );
};

const TabStack = createBottomTabNavigator();
const Tab = () => {
  return (
    <TabStack.Navigator>
      <TabStack.Screen
        name="TabHome"
        component={HomePage}
        options={{
          title: "首页",
          headerShown: false,
          tabBarIcon: ({ color }) => <Icon name="home" color={color} />,
        }}
      />
      <TabStack.Screen
        name="TabSetting"
        component={SettingPage}
        options={{
          title: "设置",
          headerShown: false,
          tabBarIcon: ({ color }) => <Icon name="setting" color={color} />,
        }}
      />

      <TabStack.Screen
        name="TabUser"
        component={User}
        options={{
          title: "我的",
          tabBarIcon: ({ color }) => <Icon name="user" color={color} />,
        }}
      />
    </TabStack.Navigator>
  );
};

const RootStack = createNativeStackNavigator();

const RootStackNavigation = () => {
  return (
    <RootStack.Navigator>
      <RootStack.Screen
        name="Tab"
        component={Tab}
        options={{ headerShown: false }}
      />
      <RootStack.Screen name="Search" component={Search} />
    </RootStack.Navigator>
  );
};
const Navigation = () => {
  return (
    <NavigationContainer>
      <RootStackNavigation />
    </NavigationContainer>
  );
};

export default Navigation;
  1. 新增 src/pages/setting/index.jsx、src/pages/profile/index.jsx、src/pages/search/index.jsx、src/pages/user/index.jsx 文件,内容自己随便填
  2. 效果如下: 4-6 RN 入门与实战4-6 RN 入门与实战4-6 RN 入门与实战

补充知识

WebView 是什么?

一种在 APP 内嵌入浏览器引擎的组件

Android 与 IOS 都提供了 WebView 组件

JSBridge 是什么?

一种 JS 与原生通信的技术,包括调用原生方法传递数据、获取返回结果等操作。

WebView 组件自带实现了一些 JSBridge。

各个跨端框架都有自己的 JSBridge。

npm dedupe

作用:简化依赖树,解决幽灵依赖

描述:搜索本地包树并尝试通过将依赖关系向上移动树来简化整体结构,在那里它们可以被多个依赖包更有效地共享。

场景:A 包依赖 B 包,C 包也依赖 B 包,于是存在安装了两个B包的情况。而当 A、C 两个包依赖的 B 包版本是同一版本时,实际只需要安装 1 个 B 包。则可以运行npm dedupe来简化依赖

例子:

原始依赖图如下
a
+--b <-- depends on c@1.0.x
  -- c@1.0.3
+--d <-- depends on c@~1.0.9
  --c@1.0.10

b 依赖 c
d 依赖 c

运行 npm dedupe 后,依赖图如下

a
+-- b
+-- d
-- c@1.0.10

b 和 d 都将通过树的根级别的单个 c 包满足它们的依赖关系

大厂 P6、P7 的区别

  • 技术无关性
    • 框架:handler 后如何触发 UI 的更新
    • 路由:地址的变化后加载对应组件
    • 状态管理:如何设计好观察者或发布订阅模式
  • 团队影响力
    • 做的东西,可成标准
    • 跨团队领导力
  • 一杆到底
    • 精通某一个领域,领域内不存在问题

个人能力图谱

最好拥有一个自己的个人能力图谱,类似如下:

4-6 RN 入门与实战

平时我们写的 JS 代码是用来干嘛的?谁来识别的?

console.log('hello') // 在浏览器、node 都能执行

document.getElementById('app') // 只能在浏览器执行

JS 代码本质是字符串,需要翻译。

谁来翻译?JS 引擎来翻译,解析(词法分析生成 tokens,再生成 AST 树) + 编译(翻译成中间代码或直接转换为机器代码)

谁来执行?宿主环境来执行,翻译成可执行的形式后,宿主环境来执行,并提供额外功能(DOM 操作、文件访问等)

所以 Web 开发本质是用 JS 去调用 DOM、BOM...

Node 开发本质是用 JS 去调用 磁盘、网卡...

mac 下载配置 Android Emulator

  1. 这里去下载 dmg 文件4-6 RN 入门与实战
  2. 下载后,双击打开,然后拖入应用程序内4-6 RN 入门与实战
  3. 打开4-6 RN 入门与实战
  4. 打开会有个报错提示,大概意思是没有识别到 adb 程序。我们稍后就在 Settings 配置。4-6 RN 入门与实战
  5. 安装 adb,可看下面的《手动安装 adb》教程
  6. 在 Android Emulator 内配置下4-6 RN 入门与实战4-6 RN 入门与实战
  7. 然后退出重启,就不会在报错了
  8. 安装 apk
// 进入 platform-tools 目录
cd /Users/xx/platform-tools/

// 运行如下命令,install 后面的是你本地 apk 的完整存放路径
./adb install /Users/xx/xx.apk

// 提示这个,表明安装成功
Performing Streamed Install
Success
  1. 找到应用 4-6 RN 入门与实战

手动安装 adb

  1. 这里去下载对应平台的 Platform-Tools4-6 RN 入门与实战
  2. 下载后双击解压,生成platform-tools文件夹
  3. 打开命令行,运行下面的命令
// 在根目录创建文件夹
mkdir ~/android-sdk-macosx 

// 将解压后的文件夹移到刚创建的文件夹下(也可以自己鼠标拖动)
mv platform-tools/ ~/android-sdk-macosx/platform-tools
  1. 添加 path 到环境变量中,命令行运行下面的命令
echo 'export PATH=$PATH:~/android-sdk-macosx/platform-tools/' >> ~/.bash_profile
  1. 重载 *_profile 文件,命令行运行下面的命令
source ~/.bash_profile
  1. 测试 adb 命令,命令行运行下面的命令
adb version
  1. 打印如下结果,则安装成功
Android Debug Bridge version 1.0.41
Version 35.0.0-11411520
Installed as /Users/hzq/android-sdk-macosx/platform-tools//adb
Running on Darwin 23.3.0 (arm64)

像素

物理像素:设备屏幕的实际像素,不统一,跟设备本身有关

逻辑像素(CSS 像素):浏览器计算布局用的虚拟像素,统一的

dpr(屏幕像素比) = 物理像素 / 逻辑像素,代表一个逻辑像素需要多少个物理像素来显示,所以 dpr 越高显示效果越好越细腻(前提是资源本身要跟上,比如2倍图)

举例:1 个 dpr 为 3 的设备,若 CSS 设置 width:200px,则占用的物理像素为 600 px

学习资料

30天学 RN:github.com/fangwei716/…

一个比较大的 RN 实际项目:github.com/MarnoDev/re…

比较好的 RN 学习笔记:github.com/crazycodebo…