T3 Stack 作弊表

593 阅读5分钟

App router or Page router

T3 默认选项是Page Router,App router 还不稳定

正如在 T3-原则第二条 里提到的,我们热爱新技术,但是也看重稳定性,项目的整个路由不是很容易迁移,而你却在此使用风险较高的新技术,这不是一个明智的选择↗

App Router 目前只是 对未来特性的一瞥而已↗,它还未做好准备被用于生产环境;这项 API 还处于 beta 阶段,可以预见接下来还会有破坏性的改变。

google登录

需要获取客户端id和密钥,在google控制台获取

还需要填写重定向路径

后端服务需要代理都则会出现登录超时 防止超时所以设置时间长一点

providers: [
    GoogleProvider({
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      // 超时时间
      httpOptions: {
        timeout: 100000,
      },
      // // 下面的配置是为了让用户每次登录都能选择账号
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code",
        },
      },
    }),

trpc类型获取

export type GroupProps =
  RouterOutputs["group"]["getInfiniteGroup"]["groups"][number];

trpc数据库使用

使用zod进行入参校验,查询使用query,修改使用mutation即可 查询:

import { type Prisma } from "@prisma/client";
import { z } from "zod";

import {
  type createTRPCContext,
  createTRPCRouter,
  protectedProcedure,
  publicProcedure,
} from "~/server/api/trpc";

export const groupRouter = createTRPCRouter({
  getAll: publicProcedure
    .input(
      z
        .object({
          name: z.string().optional(),
        })
        .default({
          name: "",
        }),
    )
    .query(async ({ ctx, input }) => {
      const groups = await ctx.db.group.findMany({
        where: {
          ...(input.name && { name: { contains: input.name } }),
          deletedAt: null,
        },
        orderBy: {
          createdAt: "desc",
        },
      });

      return groups.map((group) => ({
        ...group,
        rawPrice: convertCentsToDollars(group.rawPrice),
      }));
    }),
   )}
  const { data: groups } = api.group.getAll.useQuery(); 

创建:

 create: protectedProcedure
    .input(
      z.object({
        name: z.string().min(1),
        description: z.string().min(1),
        rawPrice: z.number().min(0),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const newGroup = await ctx.db.group.create({
        data: {
          name: input.name,
          description: input.description,
          rawPrice: convertDollarsToCents(input.rawPrice),
          createdBy: { connect: { id: ctx.session.user.id } },
        },
      });
      return newGroup;
    }),
  const createGroup = api.group.create.useMutation({
    onSuccess: () => {
      void router.push("/group/mine");
    },
    onError,
  });

数据库连接命令

npx prisma migrate dev 数据库迁移同步数据库

prisma migrate deploy 在服务器部署数据库

prisma generate 生成数据库

无限加载实现

import { type Prisma } from "@prisma/client";
import { z } from "zod";

import {
  type createTRPCContext,
  createTRPCRouter,
  protectedProcedure,
  publicProcedure,
} from "~/server/api/trpc";
import { convertCentsToDollars, convertDollarsToCents } from "~/utils/mount";

export const groupRouter = createTRPCRouter({ 
  getInfiniteGroup: publicProcedure
    .input(
      z
        .object({
          name: z.string().optional(),
          limit: z.number().optional(),
          cursor: z.object({ id: z.string(), createdAt: z.date() }).optional(),
        })
        .default({
          name: "",
        }),
    )
    .query(async ({ input: { name, limit = 4, cursor }, ctx }) => {
      return await getInfiniteGroups({
        ctx,
        whereClause: {
          name: {
            contains: name,
            mode: "insensitive",
          },
          deletedAt: null,
        },
        limit,
        cursor,
      });
    }),
  
    }), 
});

async function getInfiniteGroups({
  whereClause,
  ctx,
  limit,
  cursor,
}: {
  whereClause?: Prisma.GroupWhereInput;
  limit: number;
  cursor: { id: string; createdAt: Date } | undefined;
  ctx: Awaited<ReturnType<typeof createTRPCContext>>;
}) {
  const userId = ctx.session?.user?.id;
  const loggedIn = !!userId;

  const groups = await ctx.db.group.findMany({
    take: limit + 1,
    cursor: cursor ? { id: cursor.id } : undefined,
    orderBy: [{ createdAt: "desc" }, { id: "desc" }],
    where: whereClause,
    select: {
      id: true,
      createdAt: true,
      name: true,
      description: true,
      rawPrice: true,
      members: loggedIn
        ? {
            where: {
              id: userId,
            },
            select: {
              id: true,
            },
          }
        : false,
      createdBy: {
        select: {
          id: true,
        },
      },
    },
  });

  let nextCursor: typeof cursor | undefined;
  if (groups.length > limit) {
    const nextItem = groups.pop();
    if (nextItem) {
      nextCursor = { id: nextItem.id, createdAt: nextItem.createdAt };
    }
  }

  return {
    groups: groups.map((group) => {
      const isMember = loggedIn
        ? group.members.some((member) => member.id === userId)
        : false;
      const isOwner = loggedIn ? group.createdBy?.id === userId : false;
      return {
        ...group,
        rawPrice: convertCentsToDollars(group.rawPrice),
        isMember,
        isOwner,
        memberCount: group.members?.length || 0,
      };
    }),
    nextCursor,
  };
}

客户端使用

import { useRouter } from "next/router";
import FloatingButton from "~/page_components/group/FloatingButton";
import SearchGroup from "~/page_components/group/SearchGroup";
import { Tabs } from "~/page_components/group/tabs";
import InfiniteArticleList from "~/shared/InfiniteArticleList";
import { api } from "~/utils/api";

const GroupsPage = () => {
  const router = useRouter();
  const groups = api.group.getInfiniteGroup.useInfiniteQuery(
    {
      name: router.query.q as string,
    },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    },
  );
  return (
    <div className="bg-nav h-full">
      <div className="bg-nav sticky top-[56px] z-20">
        <SearchGroup path="/group/all" />
        <Tabs active="all" />
      </div>
      <InfiniteArticleList
        groups={groups.data?.pages.flatMap((page) => page?.groups)}
        isError={groups.isError}
        isLoading={groups.isLoading}
        hasMore={groups.hasNextPage}
        fetchNewGroups={groups.fetchNextPage}
      />
      <FloatingButton path={`/group/new`} />
    </div>
  );
};

export default GroupsPage;

组件代码

import React from "react";
import GroupCard, { type GroupProps } from "./GroupCard";
import InfiniteScroll from "react-infinite-scroll-component";
import InfiniteLoading from "./InfiniteLoading";
import Loading from "./Loading";

type InfiniteTweetListProps = {
  isLoading: boolean;
  isError: boolean;
  hasMore: boolean | undefined;
  fetchNewGroups: () => Promise<unknown>;
  groups?: GroupProps[];
};
const InfiniteArticleList = ({
  groups,
  isError,
  isLoading,
  fetchNewGroups,
  hasMore = false,
}: InfiniteTweetListProps) => {
  if (isLoading) return <Loading />;
  if (isError) return <h1>Error...</h1>;
  if (groups == null || groups.length === 0) {
    return (
      <h2 className="my-4 text-center text-2xl text-gray-500">No Groups</h2>
    );
  }
  return (
    <div className=" bg-nav ">
      <ul>
        <InfiniteScroll
          className="min-h-groupList   bg-nav "
          dataLength={groups.length}
          next={fetchNewGroups}
          hasMore={hasMore}
          loader={<InfiniteLoading />}
          endMessage={
            <div className="pb-4 text-center">
              <span className=" text-xl font-bold">
                Yay! You have seen it all
              </span>
            </div>
          }
        >
          {groups?.map((group) => {
            return <GroupCard key={group.id} {...group} />;
          })}
        </InfiniteScroll>
      </ul>
    </div>
  );
};
export default InfiniteArticleList;

获取Trpc的类型

export type GroupProps =
  RouterOutputs["group"]["getInfiniteGroup"]["groups"][0];

env

如果使用google登录需要在这里加上对应配置

import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({ 
    GOOGLE_CLIENT_ID: z.string(),
    GOOGLE_CLIENT_SECRET: z.string(),
  }, 
  /**
   * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
   * middlewares) or client-side so we need to destruct manually.
   */
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    NODE_ENV: process.env.NODE_ENV,
    NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
    NEXTAUTH_URL: process.env.NEXTAUTH_URL,
    GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
    GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
  },
 
})

dynamic

在点击之后在加载,优化很重的第三方库

import dynamic from "next/dynamic";
import Loading from "~/shared/Loading";

const ArticleForm = dynamic(
  () => import("~/page_components/article/ArticleForm"),
  {
    ssr: false,
    loading: () => <Loading />,
  },
);

const NewArticlePage = () => {
  return <ArticleForm />;
};

export default NewArticlePage;

软删除

model Group {
  id          String   @id @default(cuid())
  createdAt   DateTime @default(now())
  deletedAt   DateTime? // 添加这个字段用于软删除
  updatedAt   DateTime @updatedAt
  createdById String
  description String
  image       String?
  rawPrice    Int
  name        String
  createdBy   User     @relation(fields: [createdById], references: [id])
  members     User[]   @relation("UserGroups")
  articles    Article[]  

  @@index([createdById])
  @@unique([createdAt,id])
}

服务端代码

  // 软删除
  deleteGroup: protectedProcedure
    .input(z.object({ groupId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.session.user.id;
      const groupId = input.groupId;
      const group = await ctx.db.group.findFirst({
        where: {
          id: groupId,
          createdBy: {
            id: userId,
          },
          deletedAt: null, // 确保要删除的群组尚未被软删除
        },
      });
      if (!group) {
        throw new Error("Group not found or already deleted");
      }

      // 软删除操作,将 deletedAt 设置为当前时间
      await ctx.db.group.update({
        where: {
          id: groupId,
        },
        data: {
          deletedAt: new Date(), // 设置为当前时间标记为已删除
        },
      });

      return group;
    }),

使用:

  const deleteGroup = api.group.deleteGroup.useMutation({
    onSuccess: () => {
      void router.push(`/group/mine`);
    },
    onError,
  });
  const handleDeleteGroup = () => {
    deleteGroup.mutate({ groupId: id as string });
  };

部署vercel

  • 增加build命令
  • 增加环境变量
  • 配置NEXTAUTH_SECRET
  • 在google控制台增加vercel域名地址

nginx 配置开通https

## Version 2024/03/06 - Changelog: https://github.com/linuxserver/docker-swag/commits/master/root/defaults/nginx/site-confs/default.conf.sample

# redirect all traffic to https
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

# main server block
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name getseldwdwdwai.top;

    # SSL证书路径(由SWAG管理)
    include /config/nginx/ssl.conf;

    # 访问日志和错误日志的路径
    access_log /config/log/nginx/access.log;
    error_log /config/log/nginx/error.log;

    location / {
        proxy_pass http://selected_ai_app:3000; # 指向你的Next.js应用服务
        include /config/nginx/proxy.conf;
        resolver 127.0.0.11 valid=30s;
        set $upstream_app selected_ai_app;
        set $upstream_port 3000;
        set $upstream_proto https;
        proxy_set_header X-Forwarded-Proto https;
    }
}
# enable subdomain method reverse proxy confs
include /config/nginx/proxy-confs/*.subdomain.conf;
# enable proxy cache for auth
proxy_cache_path cache/ keys_zone=auth_cache:10m;

docker 部署相关命令收集

  • docker exec -it selected_ai_app npx prisma migrate deploy
  • docker ps
  • docker ps -a
  • docker system prune
  • df -h
  • docker start containerId
  • docker logs selected_ai_app
  • watch docker logs demo-1
  • docker exec -it selected_ai_nginx bash
  • history | grep "docker run "
  • docker exec selected_ai_nginx nginx -s reload
  • docker network ls
  • docker exec -it psql_1 psql -U selected_ai -d selected_ai_dev
  • docker exec -it next-app ash