type
Post
status
Published
date
Feb 8, 2026
slug
nextjs-shopping-platform-003
summary
tags
Next.js
category
编程学习
icon
password
1. Section Intro
Now that we have our basic layout and some components including the product card display, we can start to implement a database.
现在我们已经有了基本布局和一些组件,包括产品卡片展示,我们可以开始实现数据库了。
We're going to be using a cloud PostgreSQL database that is offered through Vercel but is managed and is hosted by Neon, which is a great company and service. We're using the free tier so you don't need to pay anything or enter any credit card information to use this service.
我们将使用 Vercel 提供的云 PostgreSQL 数据库,但它由 Neon 管理并托管,Neon 是一家很棒的公司和服务。我们使用的是免费套餐,所以你不需要支付任何费用或输入任何信用卡信息来使用这项服务。
We'll be using Prisma as our ORM to interact with our database. Instead of writing raw SQL queries, Prisma offers easy to use methods to create, read, update, delete and more.
我们将使用 Prisma 作为我们的 ORM 来与数据库交互。Prisma 提供了易于使用的方法来进行创建、读取、更新、删除等操作,而无需编写原始 SQL 查询。
Prisma has models that we can setup in the schema file and these models pertain to the database tables. So we define the fields, types and other annotations like default values and primary keys.
Prisma 有可以在 schema 文件中设置的模型,这些模型与数据库表相关。因此我们定义字段、类型和其他注解,如默认值和主键。
Once we create these models, we can use them to create and run a migration, which will actually create our tables with all the fileds. So there's no having to go into something like PG Admin to create our fields and provision the database.
一旦我们创建了这些模型,我们可以用它们来创建和运行迁移,这将实际创建包含所有字段的表。所以不需要进入像 PG Admin 这样的工具来创建字段和配置数据库。
I want to have some sample data to work with so I'll show you how we can setup seeding, which is an easy, reusable way to populate our database with sample data.
我希望有一些示例数据可以使用,所以我将向你展示如何设置种子,这是一种简单、可重用的方式,用示例数据填充我们的数据库。
Another library we'll be using and setting up is
Zod which is a schema validation library. We'll be using it to validate our data to make sure that it is in the correct format. We'll get this setup in this section.我们将使用和设置的另一个库是Zod,它是一个模式验证库。我们将使用它来验证我们的数据,确保它处于正确的格式。我们将在本节中进行设置。
Once everything is setup and we run our migrations, we'll refactor our code to use the database and Prisma instead of the TypeScript file with the sample data. We're going to create the product details page as well as the product images component which will have a large image and then clickable thumbnails under it.
一旦一切设置完成并运行迁移后,我们将重构代码以使用数据库和 Prisma,而不是包含示例数据的 TypeScript 文件。我们将创建产品详情页以及产品图片组件,它将有一个大图,下面有可点击的缩略图。
Lastly, we're actually going to deploy our application to Vercel. Usually I wait until the end, but I want this course to be more realistic and to show you how to deploy your application and have continuous deployment. So that when you push to Github, your application will automatically be deployed to Vercel. So we'll have our development environment and our production environment throughout the course.
最后,我们实际上将把应用部署到 Vercel。通常我会等到最后,但我希望这门课程更真实,向你展示如何部署应用并实现持续部署。这样当你推送到 Github 时,你的应用将自动部署到 Vercel。所以我们在整个课程中都会有开发环境和生产环境。
2. PostgreSQL & Prisma Setup
We are going to setup our database. You have many options for this. Some good ones are Neon, Supabase, AWS, PlanetScale, and MongoDB. We will be using Vercel Postgres, which is a managed Postgres database that actually uses Neon under the hood. You can read more about it here.
我们将设置我们的数据库。你有很多选择。一些不错的选择包括 Neon、Supabase、AWS、PlanetScale 和 MongoDB。我们将使用 Vercel Postgres,它是一个托管的 Postgres 数据库,实际上底层使用 Neon。你可以在这里阅读更多关于它的信息。
You first need to log into your Vercel account. Then click on the "Storage" tab and select "Create" next to Postgres.
你首先需要登录你的 Vercel 账户。然后点击 "Storage" 标签页,在 Postgres 旁边选择 "Create"。
Give it a name and click "Create".
给它一个名称并点击 "Create"。
<img src="../images/vercel-postgres.png" alt="Vercel Postgres Create" />
Once you do that, you will see a database string. We will come back to this in a few minutes.
完成这些后,你会看到一个数据库连接字符串。我们几分钟后会回到这里。
Install Prisma 安装 Prisma
Prisma is an ORM that will help us interact with the database. It is a TypeScript-first ORM that makes it easy to work with databases. It is a great choice for TypeScript projects. I prefer it over the vercel-postgres package and many others.
Prisma 是一个 ORM,将帮助我们与数据库交互。它是一个 TypeScript 优先的 ORM,使处理数据库变得容易。它是 TypeScript 项目的绝佳选择。我更喜欢它而不是 vercel-postgres 包和许多其他选项。
Run the following command to install Prisma:
运行以下命令来安装 Prisma:
Now run the following command to initialize Prisma:
现在运行以下命令来初始化 Prisma:
This creates a
prisma folder with a schema.prisma file. This is where we will define our database schema. It also adds a DATABASE_URL environment variable to your .env file.这会创建一个包含schema.prisma文件的prisma文件夹。这是我们将定义数据库 schema 的地方。它还会向你的.env文件添加一个DATABASE_URL环境变量。
Go back to your Vercel Postgres dashboard and click on the "Settings" tab. Copy the
POSTGRES_PRISMA_URL value and paste it in in the DATABASE_URL environment variable in your .env file. It will look like this:回到你的 Vercel Postgres 仪表板,点击 "Settings" 标签页。复制POSTGRES_PRISMA_URL的值并粘贴到你的.env文件中的DATABASE_URL环境变量中。它看起来像这样:
Prisma VS Code Extension Prisma VS Code 扩展
We are going to use the Prisma VS Code extension to help us with our database schema. Install it by going to the Extensions tab in VS Code and searching for "Prisma".
我们将使用 Prisma VS Code 扩展来帮助我们处理数据库 schema。通过在 VS Code 的 Extensions 标签页中搜索 "Prisma" 来安装它。
We also want to make sure formatting is setup. Open your command pallete(Ctrl+Shift+P) and type "settings" and select "Preferences: Open User Settings (JSON)". Then add the following to the
settings.json file:我们还希望确保格式化已设置好。打开你的命令面板(Ctrl+Shift+P),输入 "settings" 并选择 "Preferences: Open User Settings (JSON)"。然后将以下内容添加到settings.json文件中:
This will make sure the Prisma extension is the default formatter for
.prisma files.这将确保 Prisma 扩展是.prisma文件的默认格式化工具。
In the next section, we will create our database schema.
在下一节中,我们将创建我们的数据库 schema。
3. Prisma Models & Migrations 模型与迁移
Now that we have our Vercel Postgres database setup and Prisma installed and configured, we can create our product model in our database schema.
现在我们已经设置好了 Vercel Postgres 数据库并安装配置了 Prisma,我们可以在数据库模式中创建我们的产品模型。
Models represent the entities of our application domain, They also map to the tables of our database and we create our migrations from them. When used with TypeScript, the Prisma Client provides generated type definitions for your models and any variations of them to make database access entirely type safe.
模型代表我们应用领域的实体,它们也映射到数据库的表,我们从中创建迁移。当与 TypeScript 一起使用时,Prisma 客户端为你的模型及其任何变体提供生成的类型定义,使数据库访问完全类型安全。
Open the
schema.prisma file in your project and add the following:打开项目中的
schema.prisma 文件并添加以下内容:This is the schema for our
Product model. It has the fields that we went over for the sample data. In addition, it will have a generated id field that will be a UUID. We also have a createdAt field that will be a timestamp.这是我们
Product 模型的模式。它包含我们在示例数据中讨论过的字段。此外,它将有一个生成的 id 字段,该字段将是 UUID。我们还有一个 createdAt 字段,它将是一个时间戳。Anything that starts with
@ are annotations that define specific constraints, defaults, and database-specific configurations for fields. For instance:任何以
@ 开头的内容都是注解,用于定义字段的特定约束、默认值和数据库特定配置。例如:@id- Marks the field as the primary key of the table in the database. cEach record must have a unique value for this field.
@id- 将字段标记为数据库中表的主键。每条记录必须为此字段具有唯一值。
@unique- Ensures that the field has a unique value across all rows in the table.
@unique- 确保该字段在表的所有行中具有唯一值。
@unique(map: "product_slug_idx")- Adds a unique constraint on the field and assigns a custom name (product_slug_idx) to the database index created for this constraint. Without map, it would generate a default name.
@unique(map: "product_slug_idx")- 在该字段上添加唯一约束,并为为此约束创建的数据库索引分配自定义名称(product_slug_idx)。如果没有 map,它将生成默认名称。
@default- Specifies a default value for the field when a new record is created and no value is explicitly provided.
@default- 指定创建新记录且未显式提供值时字段的默认值。
@db- Maps the field to a specific database type, useful for defining database-specific precision or constraints. For example,@db.Uuidmaps the id field to a UUID type in the database.@db.Decimal(12, 2)maps the price field to a decimal type with precision 12 (total digits) and scale 2 (digits after the decimal point).
@db- 将字段映射到特定的数据库类型,可用于定义数据库特定的精度或约束。例如,@db.Uuid将 id 字段映射到数据库中的 UUID 类型。@db.Decimal(12, 2)将 price 字段映射到小数类型,精度为 12(总位数),小数位数为 2(小数点后的位数)。
default(dbgenerated(...))- This uses a database function (gen_random_uuid()) to generate a UUID for the id field.
default(dbgenerated(...))- 这使用数据库函数(gen_random_uuid())为 id 字段生成 UUID。
In order to create this schema and table in our database, there are some commands we have to run. Before we do that though, there are a few things that I want to do. First, we need to add a postinstall script to our
package.json file. This will run the Prisma migration commands after we install our dependencies.为了在我们的数据库中创建这个模式和表,我们需要运行一些命令。但在那之前,有几件事我想做。首先,我们需要向我们的
package.json 文件添加一个 postinstall 脚本。这将在我们安装依赖项后运行 Prisma 迁移命令。Open the
package.json file and add the following:打开
package.json 文件并添加以下内容:Now let's run the generate command locally to generate the Prisma client and the Prisma schema.
现在让我们在本地运行 generate 命令来生成 Prisma 客户端和 Prisma 模式。
Now we should be able to use the Prisma client to interact with our database.
现在我们应该能够使用 Prisma 客户端与我们的数据库进行交互。
Create the Migration 创建迁移
Run the following command to create a migration:
运行以下命令创建迁移:
This will create a migration file in the
prisma/migrations directory. you can look at it if you want, it is just a CREATE TABLE sql statement.这将在
prisma/migrations 目录中创建一个迁移文件。如果你愿意,可以查看它,它只是一个 CREATE TABLE SQL 语句。Prisma Studio Prisma 工作室
Prisma comes with a built-in studio that we can use to view our database. We can run the following command to start the studio:
Prisma 带有一个内置的工作室,我们可以用它来查看我们的数据库。我们可以运行以下命令来启动工作室:
This will open a new browser window with the Prisma Studio. You can use this to view your database and make changes to it.
这将打开一个带有 Prisma Studio 的新浏览器窗口。你可以用它来查看你的数据库并对其进行更改。
In the next lesson, we will seed our sample data.
在下一课中,我们将填充我们的示例数据。
4. Seed Sample Data 填充示例数据
Let's add some products to our database. We can do this by creating a seed file and running it. We are going to use the same sample file that we are using now, except we are going to seed the database with it instead of using it in the code.
让我们向数据库中添加一些产品。我们可以通过创建一个种子文件并运行它来做到这一点。我们将使用与现在相同的示例文件,只是我们将用它来填充数据库,而不是在代码中使用它。
Create a new file in the
db directory called seed.ts and add the following:在
db 目录中创建一个名为 seed.ts 的新文件并添加以下内容:This will delete all the products in the database and then create the products from the sample data.
这将删除数据库中的所有产品,然后从示例数据中创建产品。
Open your terminal and run the following command to seed the database:
打开终端并运行以下命令来填充数据库:
Now, open up Prisma Studio and you should see the products in the database.
现在,打开 Prisma Studio,你应该能在数据库中看到产品。
5. Load Products From Database 从数据库加载产品
Now that we have our database setup and seeded with some sample data, we can load the products from the database.
现在我们已经设置好了数据库并填充了一些示例数据,我们可以从数据库中加载产品了。
Convert Prisma Result to JS Object 将 Prisma 结果转换为 JS 对象
Before we do any fetching with Prisma, I want to create a simple utility function that will convert the Prisma result to a plain JavaScript object. Prisma returns a
Prisma.Product object, but this object is tightly coupled to Prisma's internal data structures. We can create a simple utility function that will convert the Prisma result to a plain JavaScript object.
在使用 Prisma 获取数据之前,我想创建一个简单的工具函数,将 Prisma 的结果转换为普通的 JavaScript 对象。Prisma 返回的是 Prisma.Product 对象,但这个对象与 Prisma 的内部数据结构紧密耦合。我们可以创建一个简单的工具函数来将 Prisma 结果转换为普通的 JavaScript 对象。Open the
lib/utils.ts file and add the following:
打开 lib/utils.ts 文件并添加以下内容:- 泛型
<T>是 TypeScript 的 “类型占位符”,核心作用是关联输入 / 输出类型,保证代码的复用性和类型提示;
- 标注
(value: T): T能成立,是因为 TypeScript 是 “结构类型系统”—— 转换后的普通对象和 Prisma 类型的属性结构一致;
- 这个写法有隐藏风险:运行时值丢失 Prisma 方法、特殊类型(如 Date)失真,严谨场景建议用
PlainObject<T>重新定义返回类型。
This function will convert the Prisma result to a plain JavaScript object. We can use this function to convert the Prisma result to a plain JavaScript object.
这个函数会将 Prisma 结果转换为普通的 JavaScript 对象。我们可以使用这个函数来转换 Prisma 的结果。
The function is simple. We run the value through
JSON.stringify and then JSON.parse to convert the Prisma result to a plain JavaScript object.
这个函数很简单。我们通过 JSON.stringify 和 JSON.parse 来将 Prisma 结果转换为普通的 JavaScript 对象。The TypeScript Generic TypeScript 泛型
This is not a TypeScript course, but I feel like I should explain what is happening there.
这不是 TypeScript 课程,但我觉得我应该解释一下那里发生了什么。
The
<T> is a generic. We can use this to specify the type of the value that we are passing in. In this case, T represents a placeholder for any type that the function might accept when it is called. It could be a string, object, array, or even a more complex type like a Prisma model.
<T> 是一个泛型。我们可以用它来指定传入值的类型。在这种情况下,T 代表函数被调用时可能接受的任何类型的占位符。它可以是 string、object、array,甚至是像 Prisma 模型这样更复杂的类型。value: T means that the value parameter can be of any type T. TypeScript infers the type at the time of function invocation.
value: T 表示 value 参数可以是任何类型 T。TypeScript 在函数调用时推断类型。: T after the function signature indicates that the function will return the same type as the input type T. This ensures that the return value has the same type as the argument that was passed in.
函数签名后的 : T 表示函数将返回与输入类型 T 相同的类型。这确保了返回值与传入的参数具有相同的类型。If you call the function with a
Product object, TypeScript knows that the return value will also be of type Product.
如果你用 Product 对象调用该函数,TypeScript 知道返回值也将是 Product 类型。I know TypeScript can be a bit confusing and can seem pointless to a lot of people. I can see that point of view. But I can also see the benefits of using TypeScript. I think it is a great tool for catching errors and making your code more readable. It's also nice that it is optional. If you don't want to use this stuff, you don't have to.
我知道 TypeScript 可能有点令人困惑,对很多人来说似乎毫无意义。我能理解这种观点。但我也能看到使用 TypeScript 的好处。我认为它是一个很好的工具,可以捕获错误并使代码更具可读性。它也是可选的,这很好。如果你不想使用这些东西,你不必使用。
Actions 操作(Actions)
We will be using server actions throughout this course. It is one of the best features of Next.js 14+. We can run server-side code within an asynchrounous function. This allows us to run code on the server without having to create API routes or make a request to the server. This is great for performance and security and makes it easy to use Prisma with Next.js.
我们将在整个课程中使用服务器操作(server actions)。这是 Next.js 14+ 的最佳功能之一。我们可以在异步函数中运行服务器端代码。这使我们能够在服务器上运行代码,而无需创建 API 路由或向服务器发出请求。这对性能和安全性都很有好处,并且使在 Next.js 中使用 Prisma 变得容易。
Let's create our
actions folder in the lib directory. Create a new file at lib/actions/product.actions.ts and add the following:
让我们在 lib 目录中创建 actions 文件夹。在 lib/actions/product.actions.ts 创建一个新文件并添加以下内容:We are simply fetching the latest products from the database. We are using the
take and orderBy options to limit the number of products returned and order them by the createdAt field. We are also using the convertToPlainObject function to convert the Prisma result to a plain JavaScript object.
我们只是从数据库中获取最新的产品。我们使用 take 和 orderBy 选项来限制返回的产品数量,并按 createdAt 字段排序。我们还使用 convertToPlainObject 函数将 Prisma 结果转换为普通的 JavaScript 对象。Using the Action 使用 Action
Now, let's use this action. Open the homepage at
app/(root)/page.tsx and add the following:
现在,让我们使用这个 action。打开首页 app/(root)/page.tsx 并添加以下内容:We no longer need to bring in the sample data. We are now seeing the products from the database.
我们不再需要引入示例数据了。我们现在看到的是来自数据库的产品。
Using a Constant For Limit 使用常量来限制数量
Let's open the
lib/constants/index.ts file and add the following:
让我们打开 lib/constants/index.ts 文件并添加以下内容:Now in the
lib/actions/product.actions.ts file, import the constant and use it in the getLatestProducts function:
现在在 lib/actions/product.actions.ts 文件中,导入该常量并在 getLatestProducts 函数中使用它:6. Zod Validation & Type Inference Zod 类型验证与类型推断
So far, we've been using
.ts and .tsx files to work with TypeScript, but we haven't fully utilized TypeScript's potential. For example, in our product code, we used the any type, which bypasses TypeScript's benefits.
到目前为止,我们一直在使用 .ts 和 .tsx 文件来处理 TypeScript,但我们还没有完全利用 TypeScript 的潜力。例如,在我们的产品代码中,我们使用了 any 类型,这绕过了 TypeScript 的好处。Now, let's take a step up by introducing zod, a powerful library for runtime validation and type inference.
现在,让我们通过引入 zod 来提升一个层次,这是一个用于运行时验证和类型推断的强大库。
Why zod? 为什么选择 zod?
Let's talk about why we're using Zod. Creating regular TypeScript types and interfaces is great for development because they catch type错误 at compile-time. However, they don't exist at runtime, meaning they can't validate data coming from an API, form input, or other external sources.
让我们来谈谈为什么要使用 Zod。创建常规的 TypeScript 类型和接口对开发很有帮助,因为它们可以在编译时捕获类型错误。然而,它们在运行时并不存在,这意味着它们无法验证来自 API、表单输入或其他外部源的数据。
With zod, we:
使用 zod,我们可以:
- Validate data at runtime. 在运行时验证数据。
- Ensure that data matches the expected format before it's used. 确保数据在使用前符合预期的格式。
- Automatically infer TypeScript types from validation schemas. 从验证模式自动推断 TypeScript 类型。
In short: zod = validation + type inference.
简而言之:zod = 验证 + 类型推断。
Install Zod 安装 Zod
Open a terminal and run the following command:
打开终端并运行以下命令:
Let's create a new file at
lib/validator.ts. This will be where we will write our validation code.
让我们在 lib/validator.ts 创建一个新文件。这将是我们编写验证代码的地方。Each field in the schema corresponds to a property in our Product model. Fields like name, slug, and description must be at least 3 characters. Images must contain at least one image. Banner can be null because it's optional.
模式中的每个字段都对应我们 Product 模型中的一个属性。像 name、slug 和 description 这样的字段必须至少 3 个字符。Images 必须包含至少一张图片。Banner 可以为 null,因为它是可选的。
z.coerce.number() ensures that values (e.g., "10") are converted to numbers, as the stock field in our database expects a number.
z.coerce.number() 确保值(例如 "10")被转换为数字,因为我们数据库中的 stock 字段期望一个数字。
Handling The Price Field 处理价格字段
Handling the price field is a little more complex because prices need to be consistent and precise in the database. Imagine running an e-commerce platform where prices have inconsistent decimal places—this could lead to incorrect totals, taxes, or user confusion. By validating and formatting the price field, we ensure that every value stored in the database is accurate and predictable.
处理价格字段稍微复杂一些,因为价格需要在数据库中保持一致和精确。想象一下运行一个价格小数位不一致的电子商务平台——这可能导致不正确的总计、税费或用户困惑。通过验证和格式化价格字段,我们确保存储在数据库中的每个值都是准确和可预测的。
Remember, with the price field:
记住,对于价格字段:
- It's a Decimal in the database, which requires precise formatting. 它在数据库中是 Decimal 类型,需要精确的格式化。
- Form inputs typically provides price as a string (e.g., "49.9"), but we need to ensure it's valid and formatted properly before passing it to the database. 表单输入通常将价格作为字符串提供(例如 "49.9"),但我们需要确保它在传递给数据库之前是有效的并正确格式化。
Helper Function 辅助函数
First, we're going to create a helper function. Open the
lib/utils.ts file and add the following:
首先,我们要创建一个辅助函数。打开 lib/utils.ts 文件并添加以下内容:This function ensures numbers always have two decimal places. For example:
这个函数确保数字始终有两个小数位。例如:
- 49 becomes "49.00". 49 变成 "49.00"。
- 49.9 becomes "49.90". 49.9 变成 "49.90"。
This is important for monetary values where precision matters.
这对于精度很重要的货币值来说很重要。
Add Validation For Price 添加价格验证
Now, let's define a custom validation rule for the price field using zod's
.refine() method.
现在,让我们使用 zod 的 .refine() 方法为价格字段定义一个自定义验证规则。Add this above your schema in validator.ts:
在 validator.ts 中的模式上方添加这个:
Explanation:解释:
- Input as a String: 输入为字符串:
- The price field is received as a string (e.g., "49.9"). 价格字段作为字符串接收(例如 "49.9")。
- Convert to Number: 转换为数字:
- Number(value) converts the string to a number (49.9). Number(value) 将字符串转换为数字(49.9)。
- Format: 格式化:
formatNumberWithDecimalensures the number has two decimal places (e.g., 49.9 → "49.90").formatNumberWithDecimal确保数字有两个小数位(例如 49.9 → "49.90")。
- Validate: 验证:
- The regex /^\d+(\.\d{2})?$/ checks that the final value is a valid decimal (e.g., "49.90"). 正则表达式 /^\d+(\.\d{2})?$/ 检查最终值是否是有效的十进制数(例如 "49.90")。
Why do we convert the string to a number? Strings like "49.9" may visually look correct but lack precision. By converting to a number, we strip out any unnecessary formatting or errors (e.g., leading zeros) and then use our helper function to enforce two decimal places before validating the result.
为什么我们要将字符串转换为数字?像 "49.9" 这样的字符串看起来可能正确,但缺乏精度。通过转换为数字,我们去除了任何不必要的格式或错误(例如前导零),然后使用我们的辅助函数在验证结果之前强制保留两位小数。
Regex Pattern 正则表达式模式
We are checking for a string that matches the following regex:
我们正在检查与以下正则表达式匹配的字符串:
This regex matches a string that starts with one or more digits, followed by an optional decimal point and exactly two digits. This is how we expect the price to be formatted.
这个正则表达式匹配以一位或多位数字开头的字符串,后跟可选的小数点和恰好两位数字。这就是我们期望价格的格式。
Here is the breakdown:
以下是分解:
^: Start of the string.^:字符串的开始。
\\d+: Matches one or more digits (e.g., 49 in 49.99).\\d+:匹配一位或多位数字(例如 49.99 中的 49)。
(\\.\\d{2})?: Matches an optional decimal point followed by exactly two digits(\\d{2}). Example: .99.(\\.\\d{2})?:匹配可选的小数点后跟恰好两位数字(\\d{2})。示例:.99。 The?makes the decimal part optional (it's fine if there's no .99).?使小数部分可选(如果没有 .99 也可以)。
$: End of the string.$:字符串的结束。
Add Price To Schema 将价格添加到模式
Now we need to use that
currency variable for our price field in the schema
现在我们需要在模式中使用 currency 变量作为我们的价格字段Generate TypeScript Types 生成 TypeScript 类型
We have our validators, but we need to create the
Product type. Create a file at types/index.ts. This is where we define our types. Add the following imports:
我们有了验证器,但我们需要创建 Product 类型。在 types/index.ts 创建一个文件。这是我们定义类型的地方。添加以下导入:We can use
z.infer to create a product type and include all the fields from the validator. Add the following to the types file:
我们可以使用 z.infer 来创建产品类型,并包含验证器中的所有字段。将以下内容添加到类型文件中:So we are saying a product should have all the fields in the Zod schema plus and id, createdAt and rating.
所以我们说一个产品应该具有 Zod 模式中的所有字段,以及 id、createdAt 和 rating。
Using
z.infer ensures that our TypeScript types are always in sync with our validation schema. If we update the schema (e.g., add a required field), the inferred type will automatically reflect the change, reducing the risk of type mismatches in our codebase.
使用 z.infer 确保我们的 TypeScript 类型始终与验证模式保持同步。如果我们更新模式(例如添加一个必填字段),推断的类型将自动反映更改,减少代码库中类型不匹配的风险。Update Components 更新组件
Now we want to use that
Product type.
现在我们要使用 Product 类型。Open the
components/shared/product/product-card.tsx file and import the Product type:
打开 components/shared/product/product-card.tsx 文件并导入 Product 类型:Then replace the
any type with the Product type:
然后将 any 类型替换为 Product 类型:Open the
components/shared/product/product-list.tsx file and import the Product type:
打开 components/shared/product/product-list.tsx 文件并导入 Product 类型:Then replace the
any type with the Product type:
然后将 any 类型替换为 Product 类型:As well as within the
map function:
以及在 map 函数中:Now we have an insert product validator and a type for products.
现在我们有了一个插入产品验证器和产品类型。
7. Servlerless Environment Config 无服务器环境配置
We have Prisma working locally, but I want to do our initial deployment soon. We're going to do things a bit different in this course. Usually we build the entire app locally and then deploy at the end. I want to take a more real-world approach and deploy in increments. This will allow us to catch any issues early on and make sure everything is working as expected. Rather than having a bunch of issues thrown at us when we deploy, we'll catch them as we go.
我们已经在本地让 Prisma 工作了,但我想很快进行初始部署。在本课程中,我们将采用稍微不同的方式。通常我们在本地构建整个应用程序,然后在最后部署。我想采用更真实的方法,逐步部署。这将使我们能够尽早发现任何问题,并确保一切按预期工作。而不是在部署时遇到一堆问题,我们将在开发过程中发现它们。
Now we have something to address. Traditional databases maintain persistent TCP connections to handle requests. However, serverless environments (like Vercel) are designed to scale automatically and don't maintain persistent connections between invocations. If you try to connect directly to a database from a serverless function, you might run into issues like:
现在我们有需要解决的问题。传统数据库维护持久的 TCP 连接来处理请求。然而,无服务器环境(如 Vercel)设计为自动扩展,并且不会在调用之间维护持久连接。如果你尝试从无服务器函数直接连接到数据库,可能会遇到以下问题:
- Connection limits: Serverless environments can spawn many instances simultaneously, exceeding database connection limits. 连接限制:无服务器环境可以同时生成许多实例,超过数据库连接限制。
- Cold starts: Connections are slow to initialize in serverless environments. 冷启动:在无服务器环境中,连接初始化很慢。
- Incompatibility with WebSockets: Neon uses WebSockets for serverless compatibility, while Prisma assumes a traditional TCP setup. 与 WebSockets 不兼容:Neon 使用 WebSockets 实现无服务器兼容性,而 Prisma 假设使用传统的 TCP 设置。
The Neon adapter solves these problems by adapting Prisma's behavior to Neon's serverless architecture. It allows Prisma to manage connections using WebSockets and pooling, so that it works in a serverless context.
Neon 适配器通过将 Prisma 的行为适应到 Neon 的无服务器架构来解决这些问题。它允许 Prisma 使用 WebSockets 和连接池来管理连接,使其在无服务器环境中工作。
Needed Packages 需要的包
There are a few packages that we need to install to set this up:
我们需要安装一些包来设置这个:
@neondatabase/serverless: Provides a low-level connection interface to interact with the Neon serverless PostgreSQL database using WebSockets. That's why we're also installing the websockets package. This adapter allows us to connect directly to Neon in serverless environments, such as Vercel or Netlify, where maintaining persistent connections to a database can be challenging.@neondatabase/serverless:提供低级连接接口,使用 WebSockets 与 Neon 无服务器 PostgreSQL 数据库交互。这就是为什么我们还要安装 websockets 包。这个适配器允许我们在无服务器环境(如 Vercel 或 Netlify)中直接连接到 Neon,在这些环境中维护与数据库的持久连接可能具有挑战性。
@prisma/adapter-neon: This is an adapter specifically for Prisma to ensure Prisma can operate smoothly with Neon in serverless environments. Prisma by default assumes traditional database connections (over TCP), so this adapter adapts Prisma's behavior to Neon's serverless infrastructure, which uses WebSockets and connection pooling.@prisma/adapter-neon:这是专门为 Prisma 设计的适配器,确保 Prisma 可以在无服务器环境中与 Neon 顺利运行。Prisma 默认假设使用传统数据库连接(通过 TCP),因此这个适配器将 Prisma 的行为适应到 Neon 的无服务器基础设施,该基础设施使用 WebSockets 和连接池。
ws: This is a WebSocket library used by the Neon adapter to establish and manage connections to the Neon serverless database.ws:这是 Neon 适配器使用的 WebSocket 库,用于建立和管理与 Neon 无服务器数据库的连接。
Let's install the following packages:
让我们安装以下包:
There are a couple dev dependencies we need to install as well:
我们还需要安装一些开发依赖:
@types/ws: This is the TypeScript type definitions for the ws package.@types/ws:这是 ws 包的 TypeScript 类型定义。
bufferutil: This is a utility package for working with buffers in Node.js.bufferutil:这是用于在 Node.js 中处理缓冲区的实用包。
Now that we have the packages installed, we need to update our Prisma schema to use the Neon adapter. Open the
prisma/schema.prisma file and update the provider to use the Neon adapter:
现在我们已经安装了包,需要更新 Prisma 模式以使用 Neon 适配器。打开 prisma/schema.prisma 文件并更新提供程序以使用 Neon 适配器:Generate Prisma Client 生成 Prisma 客户端
When we make changes like this, we need to regenerate the Prisma Client. Run the following command to generate the Prisma Client:
当我们进行这样的更改时,需要重新生成 Prisma 客户端。运行以下命令生成 Prisma 客户端:
Use The Adapter & Extend Prisma Client 使用适配器并扩展 Prisma 客户端
Now we will create a new file that will extend the Prisma Client. This will allow us to use the Neon adapter in our Prisma Client and we're also going to automatically convert the Decimal type to strings when needed.
现在我们将创建一个新文件来扩展 Prisma 客户端。这将允许我们在 Prisma 客户端中使用 Neon 适配器,并且我们还将自动将 Decimal 类型转换为字符串。
Most of the code we are using here is from https://neon.tech/docs/serverless/serverless-driver. I am going to comment it so you know what is happening.
我们在这里使用的大部分代码来自 https://neon.tech/docs/serverless/serverless-driver。我会对其进行注释,让你知道发生了什么。
Create a new file at
db/prisma.ts and add the following:
在 db/prisma.ts 创建一个新文件并添加以下内容:问题: PrismaNeon 构造函数期望的是 PoolConfig 类型(配置对象),但你传递了一个 Pool 实例(已创建好的连接池对象)。这导致了 TypeScript 类型不兼容错误。
修改后的代码
const adapter = new PrismaNeon({ connectionString });
改进:
- 直接传递配置对象 - { connectionString } 符合 PoolConfig 类型要求
- 简化代码 - 移除了手动创建 Pool 的步骤
- 让 PrismaNeon 内部管理连接池 - @prisma/adapter-neon 会在内部自己创建和管理 Pool 实例
We are basically just initializing the Prisma Client with the adapter and then extending it to convert the price and rating fields to strings.
我们基本上只是用适配器初始化 Prisma 客户端,然后扩展它以将 price 和 rating 字段转换为字符串。
The
compute method is a way to transform the data before it hits our code. In this case, we are converting the price and rating fields to strings but we could do anything we want.
compute 方法是一种在数据到达我们的代码之前转换数据的方式。在这种情况下,我们将 price 和 rating 字段转换为字符串,但我们可以做任何我们想做的事情。There will be other fields that we need to convert before they hit our code as well. We'll get to that later.
还有其他字段需要在它们到达我们的代码之前进行转换。我们稍后会讲到。
Use the New Client 使用新客户端
Now when we use Prisma, we import it from here. Open the
lib/actions/product.actions.ts file and update the import statement to the following:
现在当我们使用 Prisma 时,我们从这里导入它。打开 lib/actions/product.actions.ts 文件并更新导入语句为以下内容:And DELETE the following line:
并删除以下行:
We already initialized it in the
prisma.ts file.
我们已经在 prisma.ts 文件中初始化了它。Now everything should be working as expected.
现在一切应该按预期工作。
8. Product Details Page 产品详情页
Now I want to add the single product details page.
现在我要添加单个产品详情页。
getProductBySlug Action 根据 Slug 获取产品操作
Open the
lib/actions/product.actions.ts and add the following:
打开 lib/actions/product.actions.ts 文件并添加以下内容:This is pretty simple. We are just using Prisma to find the first product that matches the slug.
这非常简单。我们只是使用 Prisma 查找与 slug 匹配的第一个产品。
Install Badge Component 安装徽章组件
We are going to install the badge component from shadcn. Open a terminal and run the following:
我们将从 shadcn 安装徽章组件。打开终端并运行以下命令:
Product Details Page 产品详情页
Create a new page at
app/(root)/product/[slug]/page.tsx and add the following:
在 app/(root)/product/[slug]/page.tsx 创建一个新页面并添加以下内容:Everything should be showing except for the images. We will add those next.
除了图片之外,所有内容都应该显示正常。我们将在下一步添加图片功能。
9. Product Images Component 产品图片组件
We have the product details page. We will now create a component to show the images. There will be a main image and a list of images to click on to show the current one.
我们已有产品详情页。现在我们将创建一个组件来显示图片。该组件包含一个主图片和一个可点击的图片列表,用于切换显示当前图片。
Create a file at
components/product/product-images.tsx and add the following:
在 components/product/product-images.tsx 创建文件并添加以下内容:This is a simple component that will display the product images. We are using the
useState hook to keep track of the current image. We are also using the cn function from the @/lib/utils file to conditionally apply the border-orange-500 class to the current image.
这是一个简单的组件,用于显示产品图片。我们使用 useState Hook 来跟踪当前显示的图片。我们还使用 @/lib/utils 文件中的 cn 函数来条件性地为当前图片应用 border-orange-500 样式类。Import it in the
app/(root)/product/[slug]/page.tsx file:
在 app/(root)/product/[slug]/page.tsx 文件中导入该组件:And add it to the page:
将其添加到页面中:
Now you should see the images and be able to select one.
现在你应该能够看到图片并能够选择其中一张。
10. Initial Deployment 初始部署
Now we are going to make our initial deployment to Vercel.
现在我们将进行首次部署到 Vercel。
Create a GitHub Repository 创建 GitHub 仓库
Before we deploy to Vercel, we need to create a Github repo if you don't already have one. Make sure that you have Git installed on your machine with
git --version. If you don't have Git installed, you can download it from here or install it with your package manager.
在部署到 Vercel 之前,如果还没有 GitHub 仓库,我们需要创建一个。请确保你的机器上已安装 Git,可以通过运行 git --version 来验证。如果尚未安装 Git,可以从这里下载,或使用你的包管理器进行安装。Once you have Git installed, you can create a new repository by running the following command in your terminal:
安装 Git 后,你可以在终端中运行以下命令来创建一个新的仓库:
In your
/gitignore file, make sure that you have the .env file listed. You don't want to commit this file to your repository.
在你的 .gitignore 文件中,确保已列出 .env 文件。你不希望将此文件提交到你的仓库中。This will create a new Git repository in the current directory. You can then add all of the files in the directory to the repository by running the following command:
这将在当前目录中创建一个新的 Git 仓库。然后,你可以通过运行以下命令将目录中的所有文件添加到仓库:
You can then commit the changes to the repository by running the following command:
然后,你可以通过运行以下命令将更改提交到仓库:
Now go to GitHub and create a new repository. You can name it whatever you want. Once you have created the repository, you can add the remote repository to your local repository by running the following command:
现在访问 GitHub 并创建一个新的仓库。你可以随意命名。创建仓库后,你可以通过运行以下命令将远程仓库添加到你的本地仓库:
use the
main branch as the default branch:
使用 main 分支作为默认分支:You can then push the changes to the remote repository by running the following command:
然后,你可以通过运行以下命令将更改推送到远程仓库:
Vercel Vercel 部署
We will be deploying the project to Vercel. Make sure that you create a Vercel account and sign in. You can sign in with your GitHub account.
我们将把项目部署到 Vercel。请确保你创建了 Vercel 账户并登录。你可以使用 GitHub 账户登录。
Once you are signed in, click on "Add New" and then "Project".
登录后,点击 "Add New",然后选择 "Project"。
Select the repository that you just created and click "Import".
选择你刚刚创建的仓库,然后点击 "Import"。
Click on the "Environment Variables" tab. Copy all of the code in your
.env file and paste it into the "Environment Variables" section. This will allow you to use the environment variables in your Vercel project.
点击 "Environment Variables" 标签页。复制 .env 文件中的所有内容,并粘贴到 "Environment Variables" 部分。这将允许你在 Vercel 项目中使用环境变量。
Click on "Deploy".
点击 "Deploy"。
Once it's done, you should be able to go to the URL that Vercel gives you and see the project.
部署完成后,你应该能够访问 Vercel 提供的 URL 并查看项目。
If you get any errors that show something about dependencies, then go to the "Build & Output" settings where it says "Install Command" and add the following:
如果你遇到任何与依赖项相关的错误,请转到 "Build & Output" 设置,在 "Install Command" 处添加以下内容:
Then try again
然后重试
Update the Server URL 更新服务器 URL
Now that we have deployed the project, we need to update the
SERVER_URL in the .env file to the URL that Vercel gives you. This will allow the project to work correctly.
现在项目已部署,我们需要将 .env 文件中的 SERVER_URL 更新为 Vercel 提供的 URL。这将确保项目能够正常工作。Our initial deployment is complete! What is great is when you push to your repository, Vercel will automatically deploy the project. So we are pretty much all set as far as that goes.
我们的初始部署已完成!很棒的是,当你推送到仓库时,Vercel 会自动部署项目。因此,在这方面我们基本上已经准备就绪。
If you had any issues, be sure to check the logs in Vercel to see what went wrong.
如果你遇到任何问题,请务必查看 Vercel 中的日志,了解出了什么问题。
11. A Note on ESLint/TS Errors On Build 关于构建时 ESLint/TS 错误的说明
I wanted to mention this now because it is something you will run into during development. We're using ESLint and TypeScript and when you run 'npm run build' or push to Github and have your continuous deployment setup with Vercel, which we did in the last video, you're going to get errors such as:
我现在想提一下这个问题,因为这是你在开发过程中会遇到的事情。我们正在使用 ESLint 和 TypeScript,当你运行 'npm run build' 或推送到 Github 并使用 Vercel 的持续部署设置时(我们在上一个视频中已经设置好了),你会遇到如下错误:
This particular error is because there are variables that we have not yet used. It will prevent your deployment by default. There are a few options that you have.
这个特定的错误是因为有一些变量我们还没有使用。默认情况下,它会阻止你的部署。你有几个选项可以选择。
The one that I would definitely suggest is to address the things that you can and ignore what you can't. For instance these errors are just because we haven't written the code yet that will use those variables. So even though there's an error now, it will go away once we use them. Your deployment will fail when you push to Github, but that's ok because your in development. If you were in production, that's different.
我绝对建议的做法是处理你能处理的问题,暂时忽略那些无法处理的。例如,这些错误只是因为我们还没有编写使用这些变量的代码。所以即使现在有错误,一旦我们使用它们,错误就会消失。当你推送到 Github 时,部署会失败,但这没关系,因为你正在开发中。如果是在生产环境,那就不同了。
If this really bothers you and you always want your production site to work even though you're in development and nobody is going to it, you could add the following to your
next.config.ts file:
如果这真的困扰你,而且你总是希望你的生产站点能够正常工作(即使你正在开发中,而且没有人访问它),你可以在你的 next.config.ts 文件中添加以下内容:I would not suggest that though.
不过,我不建议这样做。
Another thing that you can do that I would not suggest is disabling certain TS rules such as the
@typescript-eslint/no-unused-vars in your eslintrc.json file.
另一个我不建议的做法是在你的 eslintrc.json 文件中禁用某些 TypeScript 规则,例如 @typescript-eslint/no-unused-vars。For certain pages:
针对特定页面:
Again, I would not suggest that. I say just fix anything you can and ignore the rest until you can fix it.
同样,我不建议这样做。我的建议是修复你能修复的问题,其余的暂时忽略,直到你能修复它们。
📎 参考文章
- 一些引用
- 引用文章
欢迎您在底部评论区留言,一起交流~
Loading...
