4-6、RN 入门与实战

期望这篇 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 知识体系

结论:
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 ,会生成一个二维码

然后下载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
2
3
4
5
// 1. 安装
npm install @ant-design/react-native --save

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

asyncLoadFont.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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. 效果如下:

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

2、安装路由

本次选择的是 react-navigation

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

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

export default Navigation;
  1. App.js 导入该组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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. 新建文件,作为首页展示
1
2
mkdir -p src/pages/home
touch src/pages/home/index.jsx
  1. 更改 src/pages/home/index.jsx 文件
1
2
3
4
5
6
7
8
9
10
11
12
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 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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. 页面效果如下

  2. 新建文件,作为详情页展示

1
2
mkdir -p src/pages/details
touch src/pages/details/index.jsx
  1. 更改 src/pages/details/index.jsx 文件
1
2
3
4
5
6
7
8
9
10
11
12
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 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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. 效果如下

更多路由操作看官方文档

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

  1. 更改 src/navigation/index.jsx 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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. 效果如下:

补充知识

WebView 是什么?

一种在 APP 内嵌入浏览器引擎的组件
Android 与 IOS 都提供了 WebView 组件

JSBridge 是什么?

一种 JS 与原生通信的技术,包括调用原生方法传递数据、获取返回结果等操作。
WebView 组件自带实现了一些 JSBridge。
各个跨端框架都有自己的 JSBridge。

npm dedupe

作用:简化依赖树,解决幽灵依赖
描述:搜索本地包树并尝试通过将依赖关系向上移动树来简化整体结构,在那里它们可以被多个依赖包更有效地共享。
场景:A 包依赖 B 包,C 包也依赖 B 包,于是存在安装了两个 B 包的情况。而当 A、C 两个包依赖的 B 包版本是同一版本时,实际只需要安装 1 个 B 包。则可以运行npm dedupe来简化依赖
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
原始依赖图如下
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 的更新
    • 路由:地址的变化后加载对应组件
    • 状态管理:如何设计好观察者或发布订阅模式
  • 团队影响力
    • 做的东西,可成标准
    • 跨团队领导力
  • 一杆到底
    • 精通某一个领域,领域内不存在问题

个人能力图谱

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

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

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

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

JS 代码本质是字符串,需要翻译。
谁来翻译?JS 引擎来翻译,解析(词法分析生成 tokens,再生成 AST 树) + 编译(翻译成中间代码或直接转换为机器代码)
谁来执行?宿主环境来执行,翻译成可执行的形式后,宿主环境来执行,并提供额外功能(DOM 操作、文件访问等)

所以 Web 开发本质是用 JS 去调用 DOM、BOM…
Node 开发本质是用 JS 去调用 磁盘、网卡…

mac 下载配置 Android Emulator

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

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

// 提示这个,表明安装成功
Performing Streamed Install
Success
  1. 找到应用

手动安装 adb

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

// 将解压后的文件夹移到刚创建的文件夹下(也可以自己鼠标拖动)
mv platform-tools/ ~/android-sdk-macosx/platform-tools
  1. 添加 path 到环境变量中,命令行运行下面的命令
1
echo 'export PATH=$PATH:~/android-sdk-macosx/platform-tools/' >> ~/.bash_profile
  1. 重载 *_profile 文件,命令行运行下面的命令
1
source ~/.bash_profile
  1. 测试 adb 命令,命令行运行下面的命令
1
adb version
  1. 打印如下结果,则安装成功
1
2
3
4
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:https://github.com/fangwei716/30-days-of-react-native

一个比较大的 RN 实际项目:https://github.com/MarnoDev/react-native-eyepetizer

比较好的 RN 学习笔记:https://github.com/crazycodeboy/RNstudyNotes


4-6、RN 入门与实战
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/4-6、RN 入门与实战/
作者
黄智强
发布于
2024年1月13日
许可协议