「3」expo-shopping:状态管理、数据持久化、样式配置、部署在vercel上

  • 状态管理我们选择的是redux
  • 持久化是@react-native-async-storage
  • 样式我们用tailwind

状态管理

安装依赖:

shell 复制代码
pnpm i @reduxjs/toolkit react-redux

新建store/slices/user.slice.ts

ts 复制代码
import { createSlice } from "@reduxjs/toolkit";

const token = "";
const initialState = { token };

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    userLogout: (state) => {
      state.token = "";
    },

    userLogin: (state, action) => {
      state.token = action.payload;
    },
  },
});

export const { userLogout, userLogin } = userSlice.actions;

export default userSlice.reducer;

新建store/index.ts,因为我们用的是ts所以需要提前导出一些type以供后续页面使用

ts 复制代码
import { configureStore } from "@reduxjs/toolkit";

import userReducer from "./slices/user.slice";

export * from "./slices/user.slice";

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

我们来封装一下hooks,新建hooks/useRedux.ts

ts 复制代码
import { useDispatch, useSelector } from "react-redux";

export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;

hooks/index.ts中导出

ts 复制代码
export * from "./useRedux";

app/_layout.tsx中引入状态管理

tsx 复制代码
import { Stack } from "expo-router";
import { useState } from "react";
import { Provider } from "react-redux";

import { AuthProvider } from "../context/auth";
import { store } from "../store";

export default function Layout() {
  const [loadedUser, setLoadedUser] = useState(null);

  return (
    <Provider store={store}>
      <AuthProvider userCredentials={loadedUser}>
        <Stack>
          <Stack.Screen
            name="(main)/(tabs)"
            options={{
              headerShown: false,
            }}></Stack.Screen>
        </Stack>
      </AuthProvider>
    </Provider>
  );
}

我们在app/(main)/(tabs)/profile.tsx中使用一下试试:

tsx 复制代码
import { View, Text, Pressable } from "react-native";

import { userLogin, userLogout } from "../../../store";
import { useAppDispatch, useAppSelector } from "../../../hooks";

export default function Profile() {
  const dispatch = useAppDispatch();
  const { token } = useAppSelector((state) => state.user);

  const onLogIn = async () => {
    dispatch(userLogin("123456"));
  };

  const onLogout = async () => {
    dispatch(userLogout());
  };

  return (
    <View>
      <Text>Profile screen</Text>
      <Pressable onPress={onLogIn}>
        <Text>Log in</Text>
      </Pressable>
      <Pressable onPress={onLogout}>
        <Text>Log out</Text>
      </Pressable>
    </View>
  );
}

页面如下,可以看到点击登录过后user.token从''变为了123456,退出登录又变为空了,可以证明状态管理已经成功接入

持久化存储

安装依赖:

shell 复制代码
pnpm i @react-native-async-storage/async-storage

我们改变一下store/slices/user.slice.ts

ts 复制代码
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

const token = "";
const initialState = { token, status: "idle" };

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    setToken: (state, action) => {
      state.token = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(setTokenAsync.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(setTokenAsync.rejected, (state, action) => {
        state.status = "failed";
      })
      .addCase(setTokenAsync.fulfilled, (state, action) => {
        state.status = "succeeded";
      });
  },
});

export const setTokenAsync = createAsyncThunk(
  "user/setTokenAsync",
  async (token: string, { dispatch, getState }) => {
    try {
      await AsyncStorage.setItem("token", token);
      dispatch(setToken(token));
      return true;
    } catch (err) {
      console.error(err);
      return false;
    }
  },
);

export const { setToken } = userSlice.actions;

export default userSlice.reducer;

这里用到了异步reducer,会根据请求状态,返回不同的status

然后我们在根布局中,每次进入页面获取本地token没有token的话进行登录操作

修改app/_layout.tsx

tsx 复制代码
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Stack } from "expo-router";
import { useEffect, useState } from "react";
import { Text, View, StyleSheet } from "react-native";
import { Provider } from "react-redux";

import { setToken, store } from "../store";

export default function Layout() {
  const [isReady, setIsReady] = useState(false);
  const [loadedUser, setLoadedUser] = useState<any>(null);

  const getUserFromStorage = async () => {
    const token = await AsyncStorage.getItem("token");

    if (token) {
      setLoadedUser({
        email: "admin@qq.com",
      });
      store.dispatch(setToken(token));
    }
    setIsReady(true);
  };

  useEffect(() => {
    getUserFromStorage();
  }, []);

  if (!isReady) {
    return (
      <View style={styles.loading}>
        <Text>Loading...</Text>
      </View>
    );
  }
  return (
    <Provider store={store}>
      <Stack>
        <Stack.Screen
          name="(main)/(tabs)"
          options={{
            headerShown: false,
          }}></Stack.Screen>
      </Stack>
    </Provider>
  );
}

const styles = StyleSheet.create({
  loading: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
});

修改app/(auth)/login.tsx

tsx 复制代码
import { useRouter } from "expo-router";
import { Pressable, Text, View } from "react-native";

import { useAppDispatch, useAppSelector } from "../../hooks";
import { RootState, setTokenAsync } from "../../store";

export default function Login() {
  const router = useRouter();
  const dispatch = useAppDispatch();

  const { status } = useAppSelector((state: RootState) => state.user);

  const onLogin = async () => {
    try {
      await dispatch(setTokenAsync("123456") as any).unwrap();
      router.back();
    } catch (error) {}
  };

  return (
    <View>
      <Pressable onPress={onLogin}>
        <Text>Login: {status}</Text>
      </Pressable>
    </View>
  );
}

在这里我们点击登录就能看到status的变化

样式

安装依赖:

shell 复制代码
pnpm i nativewind
pnpm i tailwindcss@3.3.2 -D

运行以下命令,自动在根目录初始化talwindw.config.js

shell 复制代码
npx tailwindcss init

添加需要样式的content地址,修改tailwind.config.js

js 复制代码
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

babel.config.js中引入plugin

js 复制代码
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: ["nativewind/babel"],
  };
};

现在修改app/(main)/(tabs)/profile.tsx

tsx 复制代码
import { View, Text, Pressable } from "react-native";

import { useAppDispatch, useAppSelector } from "../../../hooks";
import { RootState, setTokenAsync } from "../../../store";

export default function Profile() {
  const dispatch = useAppDispatch();
  const { token } = useAppSelector((state: RootState) => state.user);

  const onLogIn = async () => {
    dispatch(setTokenAsync("123456") as any).unwrap();
  };

  const onLogout = async () => {
    dispatch(setTokenAsync("") as any).unwrap();
  };

  return (
    <View className="flex-1 items-center justify-center bg-gray-500">
      <Text>Profile screen</Text>
      <Pressable onPress={onLogIn}>
        <Text>Log in</Text>
      </Pressable>
      <Pressable onPress={onLogout}>
        <Text>Log out</Text>
      </Pressable>
    </View>
  );
}

效果如下:

但是web端还不行,我们还需要改进一下

安装依赖:

shell 复制代码
pnpm i postcss autoprefixer -D

在根目录下面创建postcss.config.js

js 复制代码
module.exports = {  
plugins: {  
tailwindcss: {},  
autoprefixer: {},  
},  
};

为了获取className的建议,需要在根目录下面创建nativewind.d.ts

ts 复制代码
// eslint-disable-next-line @typescript-eslint/triple-slash-reference  
/// <reference types="nativewind/types" />

在根目录下面新建global.css

css 复制代码
@tailwind base;  
@tailwind components;  
@tailwind utilities;

并将它引入app/_layout.tsx

tsx 复制代码
import "../global.css";

暂时不清楚components下面是否还会需要引入,需要等到后期使用的时候看看了

目前web端的样式也出来了

代码地址:github.com/liyunfu1998...

在vercel上面部署

顺便我们也可以在vercel上部署了,

参考地址:docs.expo.dev/distributio...

我们也可以直接在vercel上面配置更简单:

项目地址如下:expo-shopping.vercel.app/

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95271 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大1 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师1 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学1 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端