1. 项目整体设计与技术选型剖析
1.1 t3code到底在解决什么问题
这段时间我一直在打磨一个项目,代号叫t3code。它不是一个商业产品,而是一套基于 T3 Stack 的参考工程和起步模板,目的是解决一个特别实际的问题:当你想从零启动一个 TypeScript 全栈项目时,为什么总要花掉一整天去纠结技术选型、目录结构、API 约定和鉴权方案?
很多团队在搭建新项目时会陷入“脚手架地狱”——选了 Next.js 之后要纠结 API 怎么暴露,是写 REST 还是 GraphQL;数据库 ORM 选 TypeORM 还是 Prisma;样式方案是 Tailwind、CSS Modules 还是 styled-components;鉴权又该接哪家服务。每个决定单独看都有道理,但组合在一起就容易形成一团乱麻。t3code 的核心立场是给出一个强约定的组合:Next.js + Tailwind CSS + tRPC + Prisma + NextAuth,用一套被社区反复验证过的默认值,把做决定的时间省下来,直接进入业务编码。
我在做这个项目时给自己定了几条原则:第一,所有依赖必须能顺畅地协同工作,不能出现“A 框架配 B 库需要一堆补丁”的情况;第二,默认配置要带生产可用的能力,比如鉴权、数据库迁移、环境变量校验、严格的 TypeScript 覆盖;第三,文档和代码要一一对应,看文档能跑通,跑通之后能理解为什么要这么设计。
如果你是刚接触全栈开发的人,t3code 可以当学习地图,沿着这个项目就能搞清楚现代 TypeScript 全栈的完整链路。如果你已经有一定经验,它可以当做一个骨架,把里面默认的表单、路由、API 调用改成自己的业务即可。这套东西最核心的价值是:你不是在拼积木,而是拿到了一套事先验证过的积木组合方案。
1.2 技术栈的取舍逻辑
先过我最终选定的技术栈清单,然后逐个解释为什么选它们。
- Next.js 14 + App Router:作为前端框架和运行时,负责服务端渲染、路由、API Route、中间件。
- tRPC:负责前后端类型安全的数据调用,替代 REST 或 GraphQL。
- Prisma:作为数据库访问层,负责模型定义、迁移、类型生成。
- Tailwind CSS:负责样式方案,配合 dark mode 和组件化设计。
- NextAuth.js:负责认证会话,对接 GitHub、Google 等 OAuth 提供商,也支持邮箱登录。
- Zod:负责运行时数据校验,既校验客户端输入,也校验服务端入参。
这套组合最容易被质疑的是“为什么用 tRPC 而不是 REST”和“为什么用 Prisma 而不是直接写 SQL”。我会在后面用专门的小节说明这两个关键决策。
选择 Next.js 是最没有争议的决定:它同时能覆盖前端渲染和后端 API,部署到 Vercel 或者自建服务器都方便。我在 t3code 里用了 App Router,并且在路由组件层面明确了标注哪些是服务端组件、哪些是客户端组件。这一点很重要,因为 tRPC 的调用通常发生客户端组件中,而页面初始数据的预取可以由服务端组件完成,两者必须清楚分离,否则会在 Next.js 的 hydration 环节踩坑。
Prisma 解决的是模型定义和数据库访问的繁琐问题。用它的 schema 文件声明 User、Post 这类数据模型,跑一条迁移命令就能生成 SQL 表和对应的 TypeScript 类型。开发时我不需要单独维护一份数据库表结构文档,单看 schema 文件就能理解整个数据模型。Prisma 的 client 在类型推断上做得非常好,查出什么字段,返回对象上就能拿到什么字段,多查或少查的情况在编译期就暴露了。
NextAuth.js 是 T3 Stack 默认推荐的认证方案,因为它和 tRPC 的配合很顺:把 session 数据注入到请求上下文中,tRPC procedure 里可以直接判断当前用户是否已登录,然后决定是否允许执行操作。我在 t3code 里还做了扩展,支持 session token 更新,这样可以处理用户改昵称或换头像后,页面 header 能立即同步显示新信息。
Zod 是被很多人忽略但绝对不能少的拼图。tRPC 的入参校验、Prisma 的输入净化、NextAuth 的 callback 参数校验,都可以用 Zod 统一描述 schema。我在 t3code 里把 Zod schema 放在一个公共目录中,前端做表单验证时用同一份 schema,后端接受请求时又用同一份 schema 做服务端校验。这样做的好处是前后端验证规则永远不会漂移,在多人协作时尤其重要。
1.3 为什么不选 REST 或 GraphQL
每次分享 t3code 的技术方案时,总有人问我:REST 已经够用了,干嘛要折腾 tRPC?我理解这种质疑。REST 本身不是错的,但它在你没有明确 API 文档、没有接口规范、没有版本管理的情况下,很容易变成一坨散落的 endpoint。
我画过一张熟悉的对比图,放在心里就足够了:
| 对比维度 | REST 传统方案 | GraphQL | tRPC |
|---|---|---|---|
| 类型安全 | 前后端类型需手动维护 | 需要生成 codegen | 类型直接从函数推导 |
| 学习成本 | 低,但规范随团队经验浮动 | 中高,需要理解 resolver 和数据加载 | 低,就是调用普通函数 |
| 调试成本 | 需要 Postman 或 Swagger | 需要 GraphQL playground | 浏览器直接看 network,类型自带 |
| 适用场景 | 需要公开 API 给第三方 | 复杂数据聚合、多端消费 | 前后端都是 TypeScript 的内部服务 |
| 请求体积 | 可能出现过获取 | 可精确控制 | 可精确控制,且天然按需暴露 |
我实际体验下来,tRPC 最爽的一点是:改一个后端函数的参数或返回类型,前端编译立刻报错。无需 codegen,无需手动同步类型定义,因为整个过程都是 TypeScript 的 function call。在 t3code 里新增一个功能时,流程是写一个 procedure,然后在页面里 import 那个调用钩子,自动就有类型提示,开发体验非常接近本地函数调用。
GraphQL 也有它的位置,比如你需要把一个 API 暴露给多个不同技术栈的客户端,或者需要非常灵活的查询能力。但就 t3code 这种以内置业务为主的项目来说,GraphQL 相当于杀鸡用了牛刀,还额外带来 resolver 层级、缓存策略、codegen 维护这些负担。tRPC 更像是“TypeScript 全栈应用的本地函数代理”,它不试图替代所有 API 方案,而是解决内部前后端通信这类高频场景。如果未来某个接口确实要开放给第三方,我仍然会在项目里补充一个 REST route,两者完全可以共存。
2. 核心实操:从零初始化 t3code 工程
2.1 环境准备与创建项目
在动手之前,先确认基础环境:Node.js 18.17 以上版本,npm、pnpm 或 yarn 任选其一。我自己长期用 pnpm,因为它安装速度快,磁盘占用低,关键是锁文件能保持依赖版本的一致,团队协作时不会出现“我这边跑得好好的你那边就是装不上”的问题。接下来执行:
pnpm dlx create-t3-app@latest t3code命令执行后,CLI 会询问需要哪些模块,我建议全部勾选:Next.js、Tailwind CSS、tRPC、Prisma、NextAuth.js,以及 eslint 配置。这里要特别提醒一下:不要觉得 prisma 和 next-auth 暂时用不到就不装,t3code 的定位就是全功能骨架,这些模块的默认集成是否完整,直接影响后续业务开发的速度。
初始化完毕后,先运行一次开发服务器确认模板正常:
cd t3code pnpm install pnpm dev浏览器打开http://localhost:3000,如果能看到默认 Starter 页面,说明整套基础管线已经跑通。到这里只完成了骨架,接下来要做的是理解目录结构,然后改造它。
2.2 目录结构解读与关键文件改造
T3 App 默认生成的目录结构很清爽,但很多人拿到手之后不知道该改哪里。“我该在哪写业务代码?哪边是后端?哪边是前端?”这些问题实际上只要理解三个目录就能解决:
src/ ├── pages/ # 页面文件,包含 API Routes 的额外定义 ├── server/ # 服务端代码:db 客户端、auth 配置、tRPC 路由 └── styles/ # 全局样式新版 create-t3-app 已经迁移到 App Router,所以pages目录换成了app。src/server是 t3code 的后端核心,包含所有 tRPC router 和 prisma 实例;src/app是前端页面和布局。这种结构天生就把“服务端逻辑”和“客户端展示”分开了,写之前先想好:这层数据要放进哪个目录?如果是在服务端读数据库,就放到server/api/routers里;如果是用于页面布局展示,就写到app下对应的布局组件里。
我在 t3code 里做的第一件改造是调整 src/server 下的模块组织方式。默认的root.ts会聚合所有子路由,我直接把业务相关路由拆分到routers目录,例如postRouter、userRouter,每个 router 独立维护自己的 procedure。这样当业务逐渐变大时,路由文件不会膨胀成一个几千行的怪物文件。
另一处关键改造是环境变量的校验。T3 默认引入了env.mjs配合 Zod 校验环境变量,我非常喜欢这个设计,因为环境变量在应用启动阶段就应该被强制验证,而不是运行时才发现某个 key 缺失。我在env.mjs里新增了DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL这些变量的验证规则,并为它们提供默认值或明确抛错信息。只要某个变量缺失,应用启动时就会直接给出清晰提示,连排查时间都省了。
2.3 数据模型设计:以实际业务场景为例
为了让 t3code 有示范价值而不是空壳,我给它塞了一个社区场景:用户可以注册登录,发帖子,给帖子点赞,关注其他用户。数据模型围绕这个场景来设计,用 Prisma schema 定义大概是这样:
model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] likes Like[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(cuid()) title String content String authorId String author User @relation(fields: [authorId], references: [id]) likes Like[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Like { id String @id @default(cuid()) userId String postId String user User @relation(fields: [userId], references: [id]) post Post @relation(fields: [postId], references: [id]) @@unique([userId, postId]) }这是很常见的用户-帖子-点赞模型,但我在设计时特别考虑了几点:Like表用联合唯一索引@@unique([userId, postId])防止重复点赞;User的email和accounts与 NextAuth 的默认表结构对应,后续实现登录时可以直接用官方 adapter;所有主键都用cuid()而不是自增 id,这样分布式环境下生成 ID 更安全,也不会暴露数据量。
写完 schema 后执行迁移命令:
pnpm prisma migrate dev --name init这条命令会生成 SQL 迁移文件、创建数据库表,并且重新生成 Prisma Client 的类型。如果之前已经在env.mjs里配置了DATABASE_URL,这里直接用本地 SQLite 也能跑通。我用的是 PostgreSQL,连接串形如postgresql://user:password@localhost:5432/t3code。
2.4 样式体系与 UI 配置
T3 默认的 Tailwind 配置已经能直接用,但我在 t3code 里做了一些定制。首先是暗色模式。Tailwind 的暗色模式有多种实现策略,项目默认的方案是通过添加 class 来控制,我建议维持这种设定,因为它在切换主题时更可控。在tailwind.config.ts中把darkMode设为'class',然后在根布局里加一个主题切换按钮,点击时切换<html>节点的 class 即可。
其次是组件设计。我把 UI 组件按通用程度拆成两层:通用基础组件放在src/components/ui下,比如 Button、Input、Card;业务组件则直接放在页面目录的相邻位置。我不喜欢把所有组件塞进一个components大杂烩,因为项目一大会变得无法索引。通用组件必须做到语义化、可复用;业务组件则可以更自由地依赖具体数据形态。
Tailwind 本身是一个原子类名的集合,但纯靠原子类名写复杂页面会导致 JSX 里 class 字样很长。我的解决策略是少量使用@layer components抽取复用类,但绝不全部抽。比如按钮会有btn-primary、btn-ghost这样的基础类,而页面里具体的布局类还是留在组件内。这样的权衡让代码既有复用性,又不至于把 Tailwind 的灵活性改成记忆负担。
3. 端到端链路打通:数据库、tRPC 与页面渲染
3.1 如何用 tRPC router 暴露业务能力
数据库模型有了,界面样式有了,现在到了 t3code 最有魅力的部分:把数据库能力通过 tRPC 暴露给前端。我写业务逻辑的步骤是这样的:先考虑前端需要什么能力,再把这些能力映射成 tRPC 的 procedure,最后在页面中调用。
拿“发帖子”这个功能举例,在src/server/api/routers/post.ts中定义:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const postRouter = createTRPCRouter({ create: protectedProcedure .input(z.object({ title: z.string().min(1), content: z.string().min(1) })) .mutation(async ({ ctx, input }) => { return ctx.db.post.create({ data: { title: input.title, content: input.content, authorId: ctx.session.user.id, }, }); }), getAll: publicProcedure.query(async ({ ctx }) => { return ctx.db.post.findMany({ orderBy: { createdAt: "desc" }, include: { author: true, likes: true }, }); }), });这里有两个 procedure 类型:publicProcedure允许任何人访问,protectedProcedure要求登录后才能访问。t3code 里大量使用 protectedProcedure 来保证写操作必须基于有效 session。不要在公共 procedure 里直接操作需要登录的资源,这是安全管理红线。每次写新 procedure 前,先判断这个操作是否需要当前用户信息,如果是,就用 protectedProcedure 并把用户 id 从ctx.session.user.id里取出,客户端传来的参数里根本不应该包含 user id。这样设计可以防止用户伪造请求去修改别人的数据。
3.2 页面中调用 tRPC 并处理加载态
前端页面调用 tRPC 的方式非常直接。比如帖子列表页,在app/posts/page.tsx或任一客户端组件里引入:
"use client"; import { api } from "~/utils/api"; export function PostList() { const { data: posts, isLoading, error } = api.post.getAll.useQuery(); if (isLoading) return <div>加载中...</div>; if (error) return <div>出错了:{error.message}</div>; return ( <ul> {posts?.map((post) => ( <li key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> <span>作者:{post.author.name}</span> </li> ))} </ul> ); }注意这里用了useQuery这个自动生成的 React Query 钩子。它自带缓存、重新获取、loading/error 状态,不需要额外写 fetch 和 useEffect。这种做法的体验是“像在调本地函数”,但底层已经在走网络请求,并且自动处理了序列化和反序列化。
还有一点需要强调:post.author 是通过 include 直接查出来的。Prisma 因为类型推断的存在,这个author对象在前端是被明确标注类型User | null的,你要是直接渲染post.author.name,TypeScript 会提醒你可能为 null,编译期就能规避空指针这类问题。这和传统 REST + 手拼 JSON 的体验完全不一样。
3.3 乐观更新与失效重取的实践
帖子列表页写完之后,我开始处理点赞功能。点赞是高频操作,如果每次点击都要等网络回包,再把新列表重新加载一遍,体验会很差。t3code 里我采用了 React Query 的乐观更新模式。
具体做法:用api.post.toggleLike.useMutation()触发 mutation,在onMutate阶段先取消相关查询的缓存,然后手动更新缓存中的点赞数据,这样 UI 立即响应用户操作。如果请求失败,则在onError里回滚缓存,并触发一次重新获取,保证数据最终一致。
这里的重点是理解 React Query 的缓存 key 机制。tRPC 钩子生成的 key 是过程路径加参数的组合,比如["post.getAll", { page: 1 }]。在乐观更新时,我需要用api.useUtils().post.getAll.setData去指定要更新哪一份缓存数据。如果不指定 key,很可能更新错了页面范围。我在实际开发中因为缓存 key 没对上还出过 bug,这个细节值得所有用 tRPC 的人留意。
乐观更新不是所有场景都适用。涉及金额、库存等强一致性业务,我强烈建议不要用乐观更新,而是走“mutate 之后先显示 loading,成功后自动失效重取”的模式:
const utils = api.useUtils(); const likeMutation = api.post.toggleLike.useMutation({ onSuccess: () => utils.post.getAll.invalidate(), });这里invalidate()的作用是让所有跟post.getAll相关的查询标为过期,然后页面会自动重新请求。React Query 对失效重取的处理非常成熟,哪怕你只是简单地在 mutation 成功后调用 invalidate,也能做到数据新鲜。
3.4 鉴权边界与中间件保护
再往后,我把整套应用的鉴权逻辑梳理了一遍,加了几道安全防线。
第一道防线是 NextAuth 的 session 策略。在auth.ts里设置session: { strategy: "jwt" },并把callbacks里返回的用户信息包括 id。很多教程默认只返回 name 和 email,这样后面写ctx.session.user.id时会拿不到 id,数据关系就断掉了。记住,JWT 的 session 本身只是拿凭据,你需要把数据库里的用户 id 合并进 session 对象。
第二道防线是 Next.js 中间件。t3code 里有一些整页都需要登录才能访问的,比如/admin、/settings。我在src/middleware.ts里配置:
import { withAuth } from "next-auth/middleware"; export default withAuth({ pages: { signIn: "/auth/signin" }, }); export const config = { matcher: ["/admin/:path*", "/settings/:path*"], };这层保护发生在路由解析阶段,比在页面组件里写useSession判断更早,所以体验更顺滑,未登录用户会被直接重定向到登录页。
第三道防线才是 tRPC 的 protectedProcedure。前面两道防线保证页面级访问安全,这第三道保证的是 API 级访问安全。三者叠加才能形成完整的防护链。我在日常开发中见过太多人只做了页面级判断,结果用户可以直接调接口改数据库,这种漏洞必须堵上。
4. 部署、性能与常见问题实录
4.1 自部署:Docker 镜像构建与容器编排
t3code 从一开始就要支持自部署,因为在某些业务场景中你必须把应用跑在自己的服务器上,而不是依赖某个托管平台。我把 Dockerfile 放在项目根目录,构建流程分成两阶段:编译阶段用 node 20 镜像安装依赖并执行next build,运行阶段用 node 20 slim 镜像只保留.next、public、node_modules中必要内容。
Dockerfile 的核心配置大概长这样:
FROM node:20-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm prisma generate RUN pnpm build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 CMD ["node", "server.js"]这里有几个关键点。第一,next.config.js里必须开启output: "standalone",否则.next目录里不会生成可独立运行的server.js,Docker 容器里就没法用 node 直接启动。第二,Prisma 的 client 需要在构建时执行 generate,否则运行时找不到 client。第三,COPY --from=builder /app/public ./public是为了把静态资源复制进来,如果项目里没有 public 目录可以删除这一行,但 t3code 保留了 favicon 和默认占位图,所以需要。
docker-compose 里我用了一个 PostgreSQL 服务加一个应用服务。数据库和应用都是容器,通过 compose 网络互通。首次启动时,应用启动命令需要先执行迁移,可以通过CMD里的npx prisma migrate deploy && node server.js实现。
4.2 实际性能数据与优化调整
我把 t3code 部署到一台 2 核 4G 的云服务器上,跑了一次压力摸底。在没有接入任何 CDN 和缓存时,首页首屏 TTFP 大概在 1.2 秒左右,主要耗时在数据库读取和页面服务端渲染。开发者模式下更慢,但那是正常的,生产构建会好些。
之后我做了几个针对性优化:
第一,减少客户端组件数量。t3code 的首页布局全部使用服务端组件,只有需要交互的区块进入客户端组件。这样初始页面的大部分 HTML 可以直接从服务端生成,JavaScript 体积明显变小。
第二,对帖子列表启用 React Query 的staleTime。因为帖子列表在一分钟之内的变化用户通常可以接受,我在查询配置里设置了staleTime: 60 * 1000,这样切回页面时不会频繁触发重新请求,API 的压力小很多。
第三,静态资源走 CDN。Tailwind 产物、图片、字体这类内容几乎不变化,我在 Docker 部署时把它们挂到 CDN,实际效果是 TTFB 降低了约 30%。如果你用的是 Vercel 或 Cloudflare 这类平台,这一步会更简单。
第四,数据库连接池。如果是自建 PostgreSQL,我用 PgBouncer 做连接池,避免每个 Next.js 实例都持有大量数据库连接。Prisma 官方支持directUrl和连接池配置,在环境变量里把DATABASE_URL指向 PgBouncer 就能完成切换。
优化完之后同一台 2C4G 服务器上,首页动态内容接口的平均响应时间从 180ms 降到 100ms 左右,并发 50 时没有出现明显排队。
4.3 常见问题速查表
在开发 t3code 和给朋友内测的过程中,我收集了不少典型问题,这里整理成速查表,基本覆盖了 T3 Stack 新手会踩到的绝大多数坑。
| 问题现象 | 产生原因 | 解决方案 |
|---|---|---|
PrismaClientInitializationError | schema 变更后 client 未重新生成 | 执行pnpm prisma generate |
P1001: Can't reach database server | DATABASE_URL 指向错误或数据库未启动 | 检查连接串、确认 PostgreSQL 容器健康 |
tRPC 报UNAUTHORIZED | 使用 protectedProcedure 但当前无 session | 先确保完成了登录,或改用 publicProcedure |
| NextAuth 登录后 session 里没有 user.id | callbacks 里未合并数据库用户 id | 在 jwt/session callback 里返回 id 字段 |
| Tailwind 样式不生效 | 内容目录未加入 content 配置 | 检查tailwind.config.ts中content是否包含所有 src 路径 |
| 页面出现 hydration 不匹配 | 客户端和服务端渲染结果不一致 | 避免强制使用随机数或当前时间直出,用 useEffect 延后执行 |
| Docker 构建后容器启动即退出 | 未开启 standalone 模式 | 在 next.config 加入output: "standalone" |
| Prisma 多行事务报错 | 用错了交互式事务写法 | 使用$transaction(async (tx) => ...)或数组批处理 |
4.4 踩坑清单与避坑建议
最后分享一些不容易在网上看到但我实操中真实踩过的坑,这些教训很有价值。
先说 Prisma 的迁移问题。prisma migrate dev是开发环境用的,部署环境一定要用prisma migrate deploy。dev 命令会尝试重置数据库、生成迁移文件、执行 seed,这些操作在生产环境很危险;deploy 只是按顺序把未应用的迁移执行一遍,安全得多。我第一次部署时直接在服务器上跑了 migrate dev,差点把线上数据清空,这个教训非常深刻。
再说 Next.js 中间件和 Edge Runtime 的兼容性问题。NextAuth 的 middleware 很好用,但你要注意它运行在 Edge Runtime 上,里面不能高兴写 Node.js 的 API,比如fs、path。所以中间件里不要做数据库查询,只做 session 判断和重定向即可。复杂的权限判断应该放在 tRPC 层。
关于 tRPC 的错误处理,我有一个原则:不要把数据库原始错误直接抛给客户端。用户在注册时如果邮箱重复,你拿到的是一个唯一的约束错误,但前端显示这个英文错误很吓人。我习惯在 procedure 内部捕获已知错误,转成业务错误返回给前端。在 t3code 里,我为所有 mutation 写了一个统一错误处理 wrapper,能对 Zod 验证错误、数据库唯一约束、权限校验这三种常见错误做归一处理,前端拿到的是结构统一的错误对象。
最后关于项目结构,这是一条软性建议,但非常重要:如果 t3code 这类骨架项目的业务模块逐渐变多,一定要在 app 层做功能模块目录拆分,而不是无限地把所有页面平铺在 app 根目录下。比如app/dashboard、app/posts、app/settings各自拥有独立的布局和页面,跨模块共享的组件再提升到公共位置。这样当项目的某个模块需要单独迁移到 monorepo 包时,可以几乎无缝操作,不会因为目录纠缠而痛苦。
我在用 t3code 搭建下一个业务项目时,最大的体会是:选择一个强约定的全栈模板,不是你放弃了思考,而是把有限的心力聚焦在真正有业务价值的地方。与其每次新项目都重新纠结一遍技术选型和接口规范,不如把 t3code 这种经过验证的骨架直接作为起点。等踩过几次坑,再根据自己的团队习惯缓缓调整配置,整个过程比从零搭建省下的时间非常明显。如果你想把 t3code 做成自己的长期脚手架,还有两个地方可以继续投入:一是把测试环境搭起来,用 Vitest 写覆盖 tRPC router 的集成测试;二是往里加 OpenTelemetry 可观测性埋点,这样到生产环境排查问题时能快速定位到某个 procedure 的执行耗时。这些扩展都是我下一步打算做的,也希望你能在动手实践后找到自己的节奏。