(此篇文章的流程,就是在当前项目中创建一个功能模块的基本全流程,建表→创建tRPC路由→创建组件并使用API路由)
-
Create categories schema
- 添加表结构 categories =>
src/db/schema.ts
- 根据定义的schema同步数据库,执行
bunx drizzle-kit push
- 查看Neon Console或运行
bunx drizzle-kit studio 应该能看到数据库新增了一张 categories 表
- seeding脚本预设categories数据 =>
src/scripts/seed-categories.ts
- 执行脚本,将预设的数据写入数据库 =>
bun src/scripts/seed-categories.ts
-
Procedures
- 创建 categories tRPC 路由 =>
src/modules/categories/server/procedures.ts,获取全部数据且无需参数
import { db } from "@/db";
import { categories } from "@/db/schema";
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
export const categoriesRouter = createTRPCRouter({
getAll: baseProcedure.query(async () => {
const data = await db
.select()
.from(categories)
return data;
})
})
- 将 categories 路由添加进总路由 =>
src/trpc/routers/_app.ts
import { createTRPCRouter } from '../init';
import { categoriesRouter } from '@/modules/categories/server/procedures';
// tRPC总路由
export const appRouter = createTRPCRouter({
categories: categoriesRouter
});
export type AppRouter = typeof appRouter;
-
创建 Categories 组件,并使用API路由
- Next.js 在 App Router 下会默认尽量静态化页面,当数据不会频繁发生变化时,每一次进页面都尽量使用缓存数据
- 设置动态渲染:
export const dynamic = 'force-dynamic';,页面不能使用静态优化(Static Rendering),必须每次请求时都动态渲染,每次运行组件都会prefetch一次数据
-
searchParams 是当前url的查询参数(Query Params)的固定写法,从url中自动解析的一个异步参数
-
prefetch() 的数据会通过 <HydrateClient> 传递,在客户端渲染时用 useSuspenseQuery() 获取缓存中的数据
// src/app/(home)/page.tsx
import { HydrateClient, trpc } from '@/trpc/server';
...
export const dynamic = 'force-dynamic';
interface PageProps {
searchParams: Promise<{
categoryId?: string;
}>
}
const Page = async ({ searchParams }: PageProps) => {
const { categoryId } = await searchParams;
void trpc.categories.getAll.prefetch();
return (
<div>
<HydrateClient>
...
</HydrateClient>
</div>
);
}
export default Page
// src/modules/categories/ui/sections/categories-section.tsx
'use client'
import { trpc } from '@/trpc/client'
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
interface CategoriesSectionProps {
categoryId?: string;
}
export const CategoriesSection = ({ categoryId }: CategoriesSectionProps) => {
const [categories] = trpc.categories.getAll.useSuspenseQuery()
return (
// fallback 是加载时的占位符,类似loading,当前选择用骨架屏占位
// ErrorBoundary 是错误边界,捕获子组件的错误,防止整个应用崩溃
<Suspense fallback={<CategoriesSkeleton />}>
<ErrorBoundary fallback={<p>Error...</p>}>
{JSON.stringify(categories)}
</ErrorBoundary>
</Suspense>
)
}
-
-
CarouselApi 是能够控制整个组件的一个控制对象,包含很多方法和事件
- 通过
useEffect() 监听 api,api初始化完成后执行
export const FilterCarousel = ({
...
}: FilterCarouselProps) => {
const [api, setApi] = useState<CarouselApi>();
const [current, setCurrent] = useState(0); // 轮播图当前索引
const [count, setCount] = useState(0); // 轮播图总数
// 依赖数组为监听项,监听api变化,若依赖数组为空,相当于mounted()
useEffect(() => {
if(!api) return;
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
api.on('select', () => {
setCurrent(api.selectedScrollSnap() + 1);
})
}, [api]);
return (
<Carousel
setApi={setApi} // 把 setApi 传进去,这样 Carousel 内部会在初始化时把它的 API 对象传出来,赋值给上面的 api
opts={{
align: 'start',
dragFree: true,
}}
>
...
...
</Carosel>
)
})
-
Tips
- react与vue组件通信的核心差异
- react中没有
$emit + 事件监听的机制,react更强调单项数据流
- react中通常父组件把一个事件处理函数作为prop传给子组件,子组件调用这个函数向父组件“上报数据”
-
flex-basis
- 用于设置 子项(flex item)的“主轴方向上的初始尺寸”
- 默认主轴是水平方向,那
flex-basis 表示元素宽度;如果 flex-direction: column,那么flex-basis 表示元素高度
- 默认值
flex-basis: auto,根据主轴方向决定使用width还是height,如果都没有则由内容撑开
-
flex-basis: 200px 时,只是设置了一个初始尺寸,最终大小还是会受缩小flex-shrink、放大flex-grow的影响
- 与之对应的
width、height属性是固定尺寸,不会受flex伸缩规则影响