expo开启React Native之旅
mac环境
- node>=18
- watchman 检测本地代码文件改动,实现热更新
- IDE使用vscode等就行,安装ES7+ React/Redux/React-Native snippets插件,方便
rnfe快捷创建模板
1.我们推荐使用[Homebrew](http://brew.sh/)来安装Watchman
2.brew install watchman
// 检测是否安装成功
watchman --version
expo 创建 react native项目
expo文档 docs.expo.dev/
- npx create-expo-app@latest
- 创建一个空模板 npx create-expo-app@latest myApp --template
选择 Blank 或 Blank TypeScript
// jsconfig.json
"jsx": "react-jsx"
手机真机预览
- 运行expo创建的项目
npm run start
- 手机翻墙下载
expo go软件 - 安卓手机打开expo go软件,扫码步骤1中的二维码,等待编译即可看到效果
简化项目
npm run reset-project
npm run start -- --clear
react native基础
基础组件 + react语法
import {
Text,
View,
Button,
ScrollView,
Image,
TextInput,
StyleSheet,
} from "react-native";
import { Link } from "expo-router";
import { useState } from "react";
export default function Index() {
const [count, setCount] = useState(0);
const onAddCount = () => {
setCount((count) => ++count);
};
return (
<ScrollView contentContainerStyle={{ flex: 1 }}>
<View style={style.boxWrapper}>
<Text>Edit app/index.tsx to edit this screen.</Text>
<Text>count:{count}</Text>
<Button title="点击我" onPress={onAddCount}></Button>
<Link href="/about">link to About</Link>
<Image source={require("@/assets/images/react-logo.png")}></Image>
</View>
</ScrollView>
);
}
// 样式表
const style = StyleSheet.create({
boxWrapper: {
flex: 1,
justifyContent: "center",
},
});
基础组件
Image 图片
<Image
source={require("../../assets/images/react-logo.png")}
style={{
width: 150,
height: 150,
borderRadius: 150,
borderWidth: 5,
borderColor: "pink",
aspectRatio: 16 / 9 }}
></Image>
}}
></Image>
字体图标库 @expo/vector-icons
import { FontAwesome6 } from "@expo/vector-icons";
<View style={{ flexDirection: "row", gap: 10, marginVertical: 50 }}>
<FontAwesome6 name="github" size={24} color="pink"></FontAwesome6>
<FontAwesome6 name="at" size={24} color="red"></FontAwesome6>
</View>
react-native ScrollView 内容超长时滚动条
react-native SafeAreaView 子元素处于安全区域
// import { SafeAreaView } from "react-native"
import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
const insets = useSafeAreaInsets();
console.log("insets", insets.bottom, insets.top);
return (
<SafeAreaProvider>
{/* 允许只设置顶部或底部在安全区域内 edges={["bottom"]:仅底部在安全区域*/}
<SafeAreaView edges={["bottom"]}>
<View style={{ height: "100%" }}>
<Text>Title</Text>
<Text style={{ marginTop: "auto" }}>Footer</Text>
</View>
</SafeAreaView>
</SafeAreaProvider>
);
Button
- 可绑定事件 onPress
- 但是Button组件在安卓和ios上的样式有出入,一般项目会封装Button,封装时又由于View等无法绑定onPress点击事件,所以用Pressable代替
const onClick = () => {
console.log("click");
};
<Button title="click me" onPress={() => onClick()}></Button>
Pressable
- 可绑定点击事件 onPress + 长按事件 onLongPress
import {
Text,
StyleSheet,
Pressable,
} from "react-native";
const onNext = () => {
console.log("onNext");
};
const onLong = () => {
console.log("onLong");
};
<Pressable style={styles.button} onPress={onNext} onLongPress={onLong}>
<Text style={styles.buttonText}>next</Text>
</Pressable>
jsx语法
- 三元 或 && 实现条件渲染
export default function Index() {
const isLoading = false;
return (
<View style={{ flex: 1, justifyContent: "center" }}>
{/* {isLoading ? <ActivityIndicator /> : null} */}
{isLoading && <ActivityIndicator />}
</View>
);
}
- map等实现列表渲染
组件基础
props
- props可以传入js中所有数据,当然也可以传入组件等。这些属性会被子函数组件的参数接收
- props中的
函数实现子数据回传给父组件; - props中的
组件实现vue中的具名插槽效果 - 当父组件在子
闭合标签内传入标签组件等,会传入到子组件的props的children,类似vue的默认插槽
import Card from "./Card";
return (
<Card title={question.title} t={t} f={f()}>
<View style={{ gap: 10 }}>
{question.options.map((option) => (
<AnswerOption
key={option}
onOptionSelected={() => onOptionSelected(option)}
option={option}
isSelected={option === optionSelected}
/>
))}
</View>
</Card>
);
// Card.tsx
import { View, Text, StyleSheet } from "react-native";
import React from "react";
export type Card = {
title: string;
children: React.ReactNode;
t?: React.ReactNode;
f?: React.ReactNode;
};
const Card = ({ title, children, t, f }: Card) => {
return (
<View style={styles.questionCard}>
{t}
<Text style={styles.question}>{title}</Text>
{children}
{f}
</View>
);
};
const styles = StyleSheet.create({
questionCard: {
backgroundColor: "#fff",
padding: 20,
borderRadius: 20,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
gap: 20,
},
question: {
fontSize: 24,
fontWeight: 500,
},
});
export default Card;
组件的封装
import {
View,
Text,
Button,
StyleSheet,
SafeAreaView,
Pressable,
} from "react-native";
import React from "react";
import { FontAwesome6 } from "@expo/vector-icons";
import CustomButton from "../components/CustomButton";
<CustomButton
onPress={onPress}
title="Next"
rightIcon={
<FontAwesome6
name="arrow-right-long"
size={24}
color="white"
></FontAwesome6>
}
></CustomButton>
// CustomButton
import {
View,
Text,
Pressable,
StyleSheet,
PressableProps,
} from "react-native";
import React, { ComponentProps } from "react";
// 除了使用PressableProps联合类型外也可以使用ComponentProps; typeof可以是任何一个组件,包括自定义组件
export type CustomButton = {
title: string;
rightIcon?: React.ReactNode;
} & PressableProps;
// export type CustomButton = {
// title: string;
// rightIcon?: React.ReactNode;
// } & ComponentProps<typeof Pressable>;
// ...PressableProps 将父组件其他的属性都收集到PressableProps对象上
const CustomButton = ({
title,
rightIcon,
...PressableProps
}: CustomButton) => {
return (
// ...PressableProps将属性扩展开,相当于 onPress={PressableProps.onPress}
<Pressable style={styles.button} {...PressableProps}>
<Text style={styles.buttonText}>{title}</Text>
<View style={styles.rightIcon}>{rightIcon}</View>
</Pressable>
);
};
const styles = StyleSheet.create({
button: {
backgroundColor: "#055055",
padding: 10,
borderRadius: 30,
alignItems: "center",
justifyContent: "center",
},
buttonText: {
color: "#fff",
letterSpacing: 1.5,
fontSize: 20,
},
rightIcon: {
position: "absolute",
right: 20,
},
});
export default CustomButton;
hooks
- useState useEffect useContext使用方法同react一致
- 当然也可以自定义hooks
响应式状态 useState
import React, { useState } from "react";
const [index, setIndex] = useState(0);
const question = questions[index];
const onNext = () => {
console.log("onNext");
setIndex((current) => current + 1);
};
<QuestionCard question={question}></QuestionCard>
给子孙组件共享数据 createContext
// providers/QuizProvider.tsx
import { createContext, useContext } from "react";
const QuizContext = createContext({});
const QuizProvider = ({ children }) => {
const [index, setIndex] = useState(0);
const onNext = () => {
setIndex((current) => current + 1);
};
return (
<QuizContext.Provider
value={{
index,
onNext,
}}
>
{children}
</QuizContext.Provider>
);
};
export default QuizProvider;
export const useQuizContext = () => useContext(QuizContext);
// 在用这个providers包裹需要这些数据和方法的父容器,这样QuizScreen组件及其子孙组件都可以共享QuizProvider组件中value提供的方法和数据了
<QuizProvider>
<QuizScreen></QuizScreen>
</QuizProvider>
// 譬如QuizScreen.tsx组件中使用
import { useQuizContext } from "../providers/QuizProvider";
const QuizScreen = () => {
const { index, onNext } = useQuizContext();
return (
<SafeAreaView style={styles.page}>
<View style={styles.constainer}>
<View>
<Text style={styles.title}>
Question {index + 1}
</Text>
</View>
<CustomButton
onPress={onNext}
title="Next"
rightIcon={
<FontAwesome6
name="arrow-right-long"
size={24}
color="white"
></FontAwesome6>
}
></CustomButton>
</View>
</SafeAreaView>
);
};
useEffect
数据本地化
npm install @react-native-async-storage/async-storage
// expo
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from "@react-native-async-storage/async-storage";
// 设置和读取都是异步的
AsyncStorage.setItem("id", "111");
const getStorage = async () => {
const id = await AsyncStorage.getItem("id");
console.log("id", id);
};
expo Router
- 创建项目
npx create-expo-app@latest rn-camera --template
选择 Blank 或 Blank TypeScript
- 运行项目
npm run start
- 其他配置同文档链接
- 创建 src/app/index.tsx, 同时删除app.tsx index.tsx
路由自动生成和匹配
匹配规则:
- 根据app目录下文件名称和层级自动生成对应的路由
- 带有(xx)的目录:在生成路由时将跳过该目录
如 app/index.tsx 访问路径为 / 作为进入应用的第一个默认路由页面
如 app/login.tsx 访问路径为 /login
如 app/home/list.tsx 访问路径为 /home/list
如 app/detail/index.tsx 访问路径为 /detail 注意此时若通过/detail/index 将会404
如 app/(tabs)/setting.tsx 最终的路由为 /setting
Link
asChild属性:作用是将Link的所有属性和行为传递给它的第一个子元素,而不是默认将子元素包裹在一个<Text>组件中。这样可以让你更灵活地自定义Link的行为和样式,同时保留导航功能。默认情况下,Link会将它的子元素包裹在一个<Text>组件中。当你设置asChild属性时,会将它的所有属性(如href、onPress、role等)传递给它的第一个子元素。
import { Text, View, Button, ScrollView, Pressable } from "react-native";
import { Link } from "expo-router";
import { MaterialIcons } from "@expo/vector-icons";
export default function Index() {
return (
<ScrollView contentContainerStyle={{ flex: 1 }}>
{/* <Link href="/about">link to About</Link> */}
<Link href="/about" asChild>
<Pressable
style={{
padding: 15,
borderRadius: 50,
backgroundColor: "pink",
position: "absolute",
bottom: 10,
right: 10,
}}
>
<MaterialIcons name="photo-camera" size={30} color="red" />
</Pressable>
</Link>
</ScrollView>
);
}
replace:Removes the current route from the history and replace it with the specified URL
router
- navigate:
(href: Href, options: LinkToOptions) => void. Perform anavigateaction. - push:
(href: Href, options: LinkToOptions) => void. Perform apushaction. - replace:
(href: Href, options: LinkToOptions) => void. Perform areplaceaction. - back:
() => void. Navigate back to previous route. - canGoBack:
() => booleanReturnstrueif a valid history stack exists and theback()function can pop back. - setParams:
(params: Record<string, string>) => voidUpdate the query params for the currently selected route.
import { router } from 'expo-router';
export function logout() {
router.replace('/login');
}
_layout.tsx
- 它们允许你在多个页面之间共享相同的布局结构(如头部、底部导航栏、侧边栏等),而不需要重复代码。
// app/_layout.tsx
import { Stack, Tabs, Slot } from "expo-router";
import { Text, View, Button, ScrollView, Pressable } from "react-native";
export default function RootLayout() {
return (
<>
<Text>公用的头部</Text>
<Slot />
<Text>公用的尾部</Text>
</>
);
}
- _layout 布局容器中可以添加Stack, Tabs, Slot等
// app/_layout.tsx
import { Stack, Tabs, Slot } from "expo-router";
import React from "react";
const _layout = () => {
console.log("_layout");
return (
// 设置所有页面的公用样式
<Stack screenOptions={{ headerTintColor: "pink" }}>
{/* 设置某些页面的头部样式 */}
<Stack.Screen
name="index"
options={{ title: "home", headerTitleAlign: "center" }}
></Stack.Screen>
</Stack>
);
};
export default _layout;
// 当然也可以在某个视图页面添加Stack.Screen
const Camera = () => {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Stack.Screen
options={{ title: "camera", headerTitleAlign: "center" }}
></Stack.Screen>
<Text style={{ fontSize: 20, fontWeight: 600 }}>camera</Text>
</View>
);
};
export default Camera;
Camera File_System
- expo-camera
状态管理 zustand
zustand.docs.pmnd.rs/getting-sta…
- 安装插件
"zustand": "^5.0.6"
- 创建状态管理
// store/useCountStore.ts
import { create } from 'zustand'
export type UserType = {
id: string | undefined,
name: string | undefined
}
export type State = {
bears: number
user: UserType
}
export type Actions = {
increasePopulation: () => void,
removeAllBears: () => void,
updateBears: (bears: number) => void,
setUser: (user: UserType) => void,
}
// 状态 || 改变状态的方法
export default create<State & Actions>((set) => ({
bears: 10,
user: { id: "", name: "" },
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
updateBears: (newBears) => set({ bears: newBears }),
setUser: (user) => set({ user }),
}))
- 使用
import { View, Text, Pressable } from "react-native";
import React from "react";
import useCountStore from "../store/useCountStore";
const count = () => {
const bears = useCountStore((state) => state.bears);
const user = useCountStore((state) => state.user);
const increasePopulation = useCountStore((state) => state.increasePopulation);
const updateBears = useCountStore((state) => state.updateBears);
const setUser = useCountStore((state) => state.setUser);
return (
<View>
<Text>bears:{bears}</Text>
<Text>user:{user.name}</Text>
<Pressable onPress={increasePopulation}>
<Text>increasePopulation</Text>
</Pressable>
<Pressable onPress={() => updateBears(999)}>
<Text>increasePopulation</Text>
</Pressable>
<Pressable onPress={() => setUser({ name: "bwf", id: "111" })}>
<Text>setUser</Text>
</Pressable>
</View>
);
};
export default count;