使用Next.js、TypeORM和Shadcn-UI从零搭建现代化SaaS系统(第二部分-状态管理和接口)

34
2024-06-27 18:52
3 天前

整理架构分析

     首先我们页面应该分为登录前的页面和登录后的页面,登录前的页面是跟用户无关的推广页与一些SEO倒流页,这种页面一般不会做千人千面的展示,所以我们在这不需要得到用户信息。另一种页面是登陆后操作的页面,我们创建好用户之后,进入到我们的工作台,工作台相关的页面还有主要的Sass提供页面是需要登录的,我们先整体设计,我们所有的页面进去都是没有用户信息的,则一开始都是没有登录状态,然后进到页面之后,我们请求前端接口,如果有cookie或者localstorage里面存有token,我们则需要请求用户信息接口,如果获取得到用户信息,则表示用户登录成功,首页的login按钮则会变成跳转到工作台的按钮。

分析系统的状态管理

   从上面的描述我们可以得出我们的appStore会有这些接口需要设计,是否登录和用户信息,这个也是贯穿整个系统的,接下来我们就想使用什么框架做状态管理了。React相关的状态管理有什么库,有Redux,Redux-Toolkit,Mbox,Recoil,Zustand,我对比了一圈之后,发现比较喜欢用Zustand,因为这个库比较灵活,也可以不依赖React使用,相当于可以在入口初始化的时候先预取接口信息,设置应用状态,这个特别在Admin之类的应用特别好用。可以把逻辑封装到Zustand里面。那我们后面就会使用Zustand做整个应用的数据管理。

分析接口如何方便调用(tRPC+React Query)

   因为我们在Nextjs中使用Server Conpoment里面运行服务端的代码,意味着我们开启了Node服务,我们可以在Next的App Api Router里面写一些服务端的逻辑,但是经过我的实验,发现在那里写接口没问题,但是接口调用的是接口地址,调用的时候没有类型提示,这个就会变成很有意思了,要不我们在启动一个后端的项目,我一般会用NestJs写,然后再写一个type包利用declare namespace把接口信息统一管理,这样就不需要在各个系统引入,因为这个文章我们直接使用NextJs编写,我发现用tRPC这个写的时候会挺省心,直接返回了类型提示,并且还用上了ReactQuery,那我们的请求库和接口编写就用tRPC这个库来编写。

编码实现整个过程

引入Zustand做为状态管理

我们在src目录创建stores目录,这里存放所有的Zustand管理的状态。在这个目录我们首先创建一个appStpre.ts

cd client/src
mkdir stores

## 添加依赖包
pnpm add zustand immer
// client/src/stores/appStore.ts
import { createStore } from 'zustand/vanilla';
import { produce } from 'immer';
export type AppState = {
    userProfile: {
        email: string;
        nickName:string
    };
    isLogin: boolean | undefined;
};

export type AppActions = {
    setLogin: (login: boolean) => void;
    setUserProfile: (login: boolean | undefined, userInfo?: Partial<AppState['userProfile']>) => void;
};

export type AppStore = AppState & AppActions;
// 初始化状态,默认isLoing都是undefined,这个等我们客户端接口返回之后才去设置他是true或者false
export const initAppStore = (): AppState => {
    return {
        userProfile: {
            email: '',
            nickName:""
        },
        isLogin: undefined
    };
};

export const defaultInitState: AppState = {
    isLogin: undefined,
    userProfile: {
        email: '',
        nickName:""
    }
};

export const createAppStore = (initState: AppState = defaultInitState) => {
    return createStore<AppStore>()(set => {
        return {
            ...initState,
            setLogin: login => {
                set(
                    produce((draft: AppState) => {
                        draft.isLogin = login;
                    })
                );
            },
            setUserProfile: (login, userInfo = {}) => {
                console.log('setUserProfile', login, userInfo);
                set(
                    produce((draft: AppState) => {
                        draft.userProfile = {
                            ...initAppStore().userProfile,
                            ...userInfo
                        };

                        draft.isLogin = login;
                    })
                );
            }
        };
    });
};

添加Provider引入Zustand

// src/components/providers/appStoreProvider.tsx
'use client';

import { type ReactNode, createContext, useRef, useContext } from 'react';
import { useStore } from 'zustand';

import { type AppStore, createAppStore, initAppStore } from '@/stores/appStore';

export type AppStoreApi = ReturnType<typeof createAppStore>;

export const AppStoreContext = createContext<AppStoreApi | undefined>(undefined);

export interface AppStoreProviderProps {
    children: ReactNode;
}

export const AppStoreProvider = ({ children }: AppStoreProviderProps) => {
    // 只会初始化一次
    const storeRef = useRef<AppStoreApi>();
    if (!storeRef.current) {
        storeRef.current = createAppStore(initAppStore());
    }
    return <AppStoreContext.Provider value={storeRef.current}>{children}</AppStoreContext.Provider>;
};

export const useAppStore = <T,>(selector: (store: AppStore) => T): T => {
    const appStoreContext = useContext(AppStoreContext);
    if (!appStoreContext) {
        throw new Error(`useAppStore只能在AppStoreProvider运行`);
    }
    return useStore(appStoreContext, selector);
};

 Layout 代码引入Provider

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import {AppStoreProvider} from "@/components/providers/appStoreProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AppStoreProvider>
          {children}
        </AppStoreProvider>
      </body>
    </html>
  );
}

至此我们引入了Zustand在Nextjs了,下面我们开始改造大工程了,我们需要引入trpc来完成接口改造 了

引入tRPC改造接口

# 添加依赖包
pnpm add @trpc/client@next @trpc/react-query@next @trpc/server@next @trpc/next@next @tanstack/react-query@latest zod

创建tTPC路由器 

// client/src/trpc/trpc.ts
import { initTRPC } from '@trpc/server';



const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

添加tRPC测试接口 

import { publicProcedure, router } from './trpc';


export const appRouter = router({
    hello:publicProcedure.query(()=>{
        return 'hello sass Start'
    })
});

export type AppRouter = typeof appRouter;

添加tRPC的客户端 

import { AppRouter } from 'client/src/trpc';
import { createTRPCReact } from '@trpc/react-query';

export const trpc = createTRPCReact<AppRouter>();

添加tRPC的provider并在Layout引入

// client/src/components/provoders/trpcQueryProvider.tsx
'use client';
import { PropsWithChildren, useState } from 'react';
import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc } from '@/app/_trpc/index';
import { httpBatchLink, httpLink } from '@trpc/client';
import { absoluteUrl } from '@/lib/utils';
function makeQueryClient() {
    return new QueryClient({
        defaultOptions: {
            queries: {
                // With SSR, we usually want to set some default staleTime
                // above 0 to avoid refetching immediately on the client
                staleTime: 60 * 1000
            }
        }
    });
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
    if (isServer) {
        // Server: always make a new query client
        return makeQueryClient();
    } else {
        // Browser: make a new query client if we don't already have one
        // This is very important, so we don't re-make a new client if React
        // suspends during the initial render. This may not be needed if we
        // have a suspense boundary BELOW the creation of the query client
        if (!browserQueryClient) browserQueryClient = makeQueryClient();
        return browserQueryClient;
    }
}

export const TRPCQueryProviders = ({ children }: PropsWithChildren) => {
    const queryClient = getQueryClient();
    const [trpcClient] = useState(() =>
        trpc.createClient({
            links: [
                httpBatchLink({
                    url: absoluteUrl('/api/trpc'),
                    headers: () => {
                        const token = isServer ? '' : localStorage.getItem('accessToken') || '';
                        return {
                            authorization: token != '' ? `Bearer ${token}` : token
                        } as any;
                    }
                })
            ]
        })
    );
    return (
        <trpc.Provider client={trpcClient} queryClient={queryClient as any}>
            <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
        </trpc.Provider>
    );
};

 接下来我们需要添加trpc的api路由

//client/src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/trpc';

const handler = (req: Request) =>
    fetchRequestHandler({
        endpoint: '/api/trpc',
        req,
        router: appRouter,
        createContext: () => ({})
    });

export { handler as GET, handler as POST };

修改layout引入这个provider 

// client/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import {AppStoreProvider} from "@/components/providers/appStoreProvider";
import {TRPCQueryProviders} from "@/components/providers/trpcQueryProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next api",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AppStoreProvider>
          <TRPCQueryProviders>
          {children}
          </TRPCQueryProviders>
        </AppStoreProvider>
      </body>
    </html>
  );
}

至此我们的trpc也引入成功,这个教程就先到引入trpc吧,后面章节我们会加入邮箱验证登录,redis和整个仓库源码。

结语

这一个部分我先分析了一下如何做状态管理以及为什么需要引入trpc做为我们的接口。后面结束的章节我会使用nodemailer发送邮件验证做为注册登录,使用redis做验证码等功能,大家先有所期待吧