React Native

214 阅读6分钟

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
选择 BlankBlank TypeScript
// jsconfig.json
"jsx": "react-jsx"

手机真机预览

  1. 运行expo创建的项目
npm run start
  1. 手机翻墙下载expo go软件
  2. 安卓手机打开expo go软件,扫码步骤1中的二维码,等待编译即可看到效果

简化项目

npm run reset-project

npm run start -- --clear

react native基础

reactnative.cn/docs/compon…

基础组件 + 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

docs.expo.dev/router/inst…

  • 创建项目
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

image.png

Link

  • asChild属性:作用是将 Link 的所有属性和行为传递给它的第一个子元素,而不是默认将子元素包裹在一个 <Text> 组件中。这样可以让你更灵活地自定义 Link 的行为和样式,同时保留导航功能。默认情况下,Link 会将它的子元素包裹在一个 <Text> 组件中。当你设置 asChild 属性时,会将它的所有属性(如 hrefonPressrole 等)传递给它的第一个子元素。
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 a navigate action.
  • push: (href: Href, options: LinkToOptions) => void. Perform a push action.
  • replace: (href: Href, options: LinkToOptions) => void. Perform a replace action.
  • back: () => void. Navigate back to previous route.
  • canGoBack: () => boolean Returns true if a valid history stack exists and the back() function can pop back.
  • setParams: (params: Record<string, string>) => void Update 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

docs.expo.dev/versions/la…

  • 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;