期望这篇 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
,会生成一个二维码
然后下载Expo
app,可以在 GooglePlay(开启魔法)、iOS Store 内下载
下载后,安卓手机打开该 App,先登录注册下,然后点击扫码,扫描生成的二维码,就能看到页面
若想在浏览器查看,还需运行npx expo install react-native-web react-dom @expo/metro-runtime
然后在命令行按w
,就会用电脑默认浏览器打开项目,就跟开发 PC 端项目一样了
注意事项:我 mac 电脑启动项目时,竟然要开启魔法~,但Expo
app 扫描时手机不需要魔法
RN 常见的特性与坑点
- RN 没有
<div />
只有自己的标签,常见的为:<View />、<Text />
可理解为<div />、<span />
,但 RN 里面文本必须用<Text />
包裹,否则会报错 - 所有的布局默认为
flex
,所以不用显式声明display: flex
,但 RN 里面flex-direction
默认为: column
- 需要滚动则要用
<ScrollView />
包裹 - 像素 - RN 里面的 CSS 像素是根据物理像素与dpr 计算的,比如 dpr = 2.75,物理像素宽为 1080,则 window.width = 1080 / 2.75 = 392.72727
- transform 写法有变:
transform: [{ rotate: "45deg" }, { scale: 1.5 }]
- 表单使用 e.nativeEvent.text,不再是 e.target.value
开始实战
先安装 VSCode 插件:
1、安装 UI 组件
本次选择的是 RN antd
- 安装对应依赖
js
// 1. 安装
npm install @ant-design/react-native --save
// 2. 安装字体图标
npm install @ant-design/icons-react-native --save
- 如果你用的是 expo 请确保字体已经加载完成再初始化 app
asyncLoadFont.js
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
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",
},
});
- 使用组件
App.js
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",
},
});
- 效果如下:
2、安装路由
本次选择的是 react-navigation
- 安装依赖
js
npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context @react-navigation/bottom-tabs
- 根目录新建文件夹,直接运行下面命令
js
mkdir -p src/navigation/
touch src/navigation/index.jsx
- VSCode 编辑器打开刚创建的 .jsx,输入
rnfe
回车,然后函数命名为Navigation
js
import { View, Text } from "react-native";
import React from "react";
const Navigation = () => {
return (
<View>
<Text>Navigation</Text>
</View>
);
};
export default Navigation;
- App.js 导入该组件
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",
},
});
- 新建文件,作为首页展示
js
mkdir -p src/pages/home
touch src/pages/home/index.jsx
- 更改 src/pages/home/index.jsx 文件
js
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;
- 更改 /src/navigation/index.jsx 文件
js
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;
- 页面效果如下
- 新建文件,作为详情页展示
js
mkdir -p src/pages/details
touch src/pages/details/index.jsx
- 更改 src/pages/details/index.jsx 文件
js
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;
- 更改 src/pages/home/index.jsx 文件
js
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;
- 更改 /src/navigation/index.jsx 文件
js
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;
- 效果如下
更多路由操作看官方文档
3、下面讲一下完整的页面布局代码:
- 更改 src/navigation/index.jsx 文件
js
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;
- 新增 src/pages/setting/index.jsx、src/pages/profile/index.jsx、src/pages/search/index.jsx、src/pages/user/index.jsx 文件,内容自己随便填
- 效果如下:
补充知识
WebView 是什么?
一种在 APP 内嵌入浏览器引擎的组件
Android 与 IOS 都提供了 WebView 组件
JSBridge 是什么?
一种 JS 与原生通信的技术,包括调用原生方法传递数据、获取返回结果等操作。
WebView 组件自带实现了一些 JSBridge。
各个跨端框架都有自己的 JSBridge。
npm dedupe
作用:简化依赖树,解决幽灵依赖
描述:搜索本地包树并尝试通过将依赖关系向上移动树来简化整体结构,在那里它们可以被多个依赖包更有效地共享。
场景:A 包依赖 B 包,C 包也依赖 B 包,于是存在安装了两个B包的情况。而当 A、C 两个包依赖的 B 包版本是同一版本时,实际只需要安装 1 个 B 包。则可以运行npm dedupe
来简化依赖
例子:
js
原始依赖图如下
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 代码是用来干嘛的?谁来识别的?
js
console.log('hello') // 在浏览器、node 都能执行
document.getElementById('app') // 只能在浏览器执行
JS 代码本质是字符串,需要翻译。
谁来翻译?JS 引擎来翻译,解析(词法分析生成 tokens,再生成 AST 树) + 编译(翻译成中间代码或直接转换为机器代码)
谁来执行?宿主环境来执行,翻译成可执行的形式后,宿主环境来执行,并提供额外功能(DOM 操作、文件访问等)
所以 Web 开发本质是用 JS 去调用 DOM、BOM...
Node 开发本质是用 JS 去调用 磁盘、网卡...
mac 下载配置 Android Emulator
- 点这里去下载 dmg 文件
- 下载后,双击打开,然后拖入应用程序内
- 打开
- 打开会有个报错提示,大概意思是没有识别到 adb 程序。我们稍后就在 Settings 配置。
- 安装 adb,可看下面的《手动安装 adb》教程
- 在 Android Emulator 内配置下
- 然后退出重启,就不会在报错了
- 安装 apk
js
// 进入 platform-tools 目录
cd /Users/xx/platform-tools/
// 运行如下命令,install 后面的是你本地 apk 的完整存放路径
./adb install /Users/xx/xx.apk
// 提示这个,表明安装成功
Performing Streamed Install
Success
- 找到应用
手动安装 adb
- 点这里去下载对应平台的 Platform-Tools
- 下载后双击解压,生成
platform-tools
文件夹 - 打开命令行,运行下面的命令
js
// 在根目录创建文件夹
mkdir ~/android-sdk-macosx
// 将解压后的文件夹移到刚创建的文件夹下(也可以自己鼠标拖动)
mv platform-tools/ ~/android-sdk-macosx/platform-tools
- 添加 path 到环境变量中,命令行运行下面的命令
js
echo 'export PATH=$PATH:~/android-sdk-macosx/platform-tools/' >> ~/.bash_profile
- 重载 *_profile 文件,命令行运行下面的命令
js
source ~/.bash_profile
- 测试 adb 命令,命令行运行下面的命令
js
adb version
- 打印如下结果,则安装成功
js
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...