1. Section Intro
- 开发工具:使用 Next.js 15 版本,搭配 shad cn ui 组件库
- 路由与页面组织:学习文件路由规则,使用括号命名文件夹实现页面组织
- 基础组件开发:创建头部、底部组件,实现响应式菜单
- 主题功能:借助 next themes 包实现浅色、深色、系统模式切换
- 特殊页面:创建加载页面、404 未找到页面
- 产品展示:使用 TypeScript 数组模拟产品数据,开发产品卡片组件
2. Create Next App & Assets 创建 Next.js 应用和资源
Let's get started with our project by creating a new Next.js app. Open a terminal in the folder where you want to create the project and run the following command:
让我们通过创建一个新的 Next.js 应用来开始我们的项目。在你想要创建项目的文件夹中打开终端,然后运行以下命令:
Here are the answers I am going to give:
以下是我将要给出的答案:
If you do not want to use TypeScript, you can answer No to that question or any others.
如果你不想使用 TypeScript,你可以对那个问题或任何其他问题回答 No。
Now open VS Code or whichever test editor you prefer:
现在打开 VS Code 或你喜欢的任何文本编辑器:
Now open the terminal in VS Code and run the following command:
现在在 VS Code 中打开终端并运行以下命令:
This will run the development server on port 3000.
这将在端口 3000 上运行开发服务器。
Font Setup 字体设置
Next.js now uses the Geist font by default. I want to change that to the Inter font.
Next.js 现在默认使用 Geist 字体。我想将其更改为 Inter 字体。
Open the
app/layout.tsx file.
打开 app/layout.tsx 文件。Change this line at the top:
将顶部的这一行:
to the following:
更改为以下内容:
Replace the following code:
将以下代码:
With this:
替换为:
And your body tag should look like this:
你的 body 标签应该看起来像这样:
Also, change the title and description:
另外,更改标题和描述:
Move globals.css 移动 globals.css
I like to have the global stylesheet in separate folder.
我喜欢将全局样式表放在单独的文件夹中。
Create a folder called
assets in the root and a folder called styles in the assets folder. Then move the global.css file into the styles folder.
在根目录中创建一个名为 assets 的文件夹,并在 assets 文件夹中创建一个名为 styles 的文件夹。然后将 global.css 文件移动到 styles 文件夹中。You can also delete the
fonts folder.
你也可以删除 fonts 文件夹。Replace the import in the
app/layout.tsx file with this:
将 app/layout.tsx 文件中的导入替换为:Here is the final code for
app/layout.tsx so far:
到目前为止,app/layout.tsx 的最终代码如下:If your layout does not include the
suppressHydrationWarning prop, I would suggest adding it especially if you are running a lot of browser extensions. What this does is it stops warnings when the server and client are not in sync. This can happen for many reasons such as dates and times, certain cookies as well as extensions that change the DOM.
如果你的布局不包含 suppressHydrationWarning 属性,我建议添加它,特别是如果你运行了很多浏览器扩展。它的作用是当服务器和客户端不同步时停止警告。这可能是由于许多原因造成的,例如日期和时间、某些 cookie 以及更改 DOM 的扩展。Clear Homepage 清空主页
Let's clear the homepage. Open the
app/page.tsx file and replace the code with this:
让我们清空主页。打开 app/page.tsx 文件并将代码替换为:Logo & Favicon
Attached to this lesson is a zip with the
logo.svg and favicon.ico file. Download the files from here or from the final repo.
本课程附带了一个包含 logo.svg 和 favicon.ico 文件的 zip 包。你可以从这里或最终仓库下载这些文件。Create a new folder
public/images and add the logo.svg file to it. Add the favicon.ico file to the app folder and overwrite the existing one.
创建一个新文件夹 public/images 并将 logo.svg 文件添加到其中。将 favicon.ico 文件添加到 app 文件夹并覆盖现有的文件。Tailwind Utilities Tailwind 工具类
We are going to add some Tailwind utilities to the
globals.css file. Replace the current utilities content with the following:
我们要向 globals.css 文件添加一些 Tailwind 工具类。将当前的 utilities 内容替换为以下内容:These are some basic Tailwind utilities that we will use throughout the course for style.
这些是我们将在整个课程中使用的一些基本 Tailwind 工具类,用于样式设计。
Now we have our app created. In the next lesson, we will talk about and setup ShadCN.
现在我们已经创建了我们的应用。在下一课中,我们将讨论并设置 ShadCN。
笔记1
Tailwind CSS 的特殊语法:
1. @tailwind 指令(最基础的部分)
这 3 行是 Tailwind CSS 的核心指令,告诉 Tailwind 要加载哪些样式:
- @tailwind base:加载基础样式(重置默认样式,比如去掉浏览器的默认边距)
- @tailwind components:加载组件样式(你自己定义的可复用组件样式)
- @tailwind utilities:加载工具类(比如
text-center、p-5这些类名)
@ 的含义:在 CSS 中,@ 开头的叫做 "At-rules"(at 规则),是 CSS 的特殊指令,用来告诉浏览器要做一些特殊处理。2. @layer 指令(分层管理)
@layer:这是 Tailwind 的分层系统,用来组织 CSS 代码,避免样式冲突。@layer utilities:定义工具类层
@layer components:定义组件层
@layer base:定义基础层
这样写的好处是:Tailwind 会自动处理这些层的优先级,确保样式按正确的顺序应用。
3. @apply 指令(复用 Tailwind 类)
@apply:这个指令的意思是"应用"(apply),它把多个 Tailwind 类名合并到一个自定义类中。比如:
max-w-7xl:最大宽度 1280px
lg:mx-auto:大屏幕时水平居中
p-5:内边距 20px
md:px-10:中等屏幕时水平内边距 40px
w-full:宽度 100%
为什么要这样写?:你可以把
.wrapper 当作一个"快捷方式",以后只需要写 <div class="wrapper">,就不用写那一长串类名了。4. CSS 变量(- 开头)
-开头:这是 CSS 自定义属性(也叫 CSS 变量)。
:root:表示文档根元素(就是<html>标签)
-background:定义一个变量名
0 0% 100%:这是 HSL 颜色值(色相、饱和度、亮度)
为什么要用变量?:这样可以在一个地方定义颜色,然后在多个地方使用。比如修改
--primary 的值,整个网站的主题色就会改变。5. .dark 类(暗黑模式)
当元素有
.dark 类时,这些变量会覆盖 :root 中的值,实现暗黑模式。总结
语法 | 含义 | 作用 |
@tailwind | Tailwind 指令 | 加载 Tailwind 的样式层 |
@layer | 分层指令 | 组织 CSS 代码,避免冲突 |
@apply | 应用指令 | 合并多个 Tailwind 类到一个自定义类 |
--变量名 | CSS 变量 | 定义可复用的颜色、尺寸等值 |
:root | 根选择器 | 定义全局变量 |
这些语法都是 Tailwind CSS 的扩展,让写 CSS 更高效、更易维护。你不需要一开始就完全理解,随着使用会越来越熟悉!
笔记2
1. @import "tailwindcss";
这是什么?
这是 Tailwind CSS v4 的新写法,相当于说:"我要使用 Tailwind CSS"。
为什么改?
- v3 版本要用三行:
@tailwind base; @tailwind components; @tailwind utilities;
- v4 版本简化了,只需要一行
2. @theme { ... }
这是什么?
这是 v4 的新功能,用来告诉 Tailwind 你有哪些自定义颜色/样式。
每一行什么意思?
部分 | 含义 |
--color-background | 定义一个叫 background 的颜色 |
hsl(...) | 一种颜色格式(色相、饱和度、亮度) |
var(--background) | 引用下面 :root 里定义的变量值 |
简单说:这行代码把 CSS 变量
--background 注册成 Tailwind 能用的颜色 bg-background。为什么要这样写?
- 你在 HTML 里写
class="bg-background"时,Tailwind 就知道用什么颜色了
- 支持暗黑模式:切换到
.dark时,-background变量值会变,颜色自动跟着变
3. @layer utilities { ... }
这是什么?
这里定义你自己的工具类(自定义的 class 名称)。
举个例子:
什么意思?
- 你创建了一个叫
.wrapper的 class
@apply意思是"把这堆 Tailwind 类组合在一起"
- 以后你直接在 HTML 写
class="wrapper",就等于写了那一长串
拆解
@apply 里的内容:代码 | 作用 |
max-w-7xl | 最大宽度 1280px |
lg:mx-auto | 大屏幕时水平居中 |
p-5 | 内边距 1.25rem |
md:px-10 | 中等屏幕时水平内边距 2.5rem |
w-full | 宽度 100% |
其他几个工具类:
.flex-start→ flex 布局,左对齐,垂直居中
.flex-center→ flex 布局,水平垂直都居中
.h1-bold→ 字体加粗,小屏 3xl,大屏 4xl
4. @layer base { :root { ... } }
这是什么?
定义CSS 变量(也叫 CSS 自定义属性)。
格式:
部分 | 含义 |
--background | 变量名 |
0 0% 100% | HSL 颜色值(白色) |
这些变量给谁用?
给上面的
@theme 用!@theme 里写的是 var(--background),就是引用这里的值。.dark 是什么?
暗黑模式的样式。当父元素有 class="dark" 时,这些变量会覆盖上面的值,颜色变深。5. @layer base { body { ... } }
这是什么?
设置页面的基础样式。
意思:
- 整个页面的背景色用
bg-background
- 文字颜色用
text-foreground
总结一下流程
为什么要这样改?(v3 → v4)
v3 的问题 | v4 的解决 |
需要 tailwind.config.js 配置文件 | 直接在 CSS 里用 @theme 配置 |
@tailwind 指令需要三行 | 一行 @import 搞定 |
配置分散在多个文件 | 所有配置集中在 CSS 文件里 |
简单说:v4 更简洁,把所有配置都放在 CSS 文件里,不用跳来跳去改配置文件。
3. ShadCN UI Setup
Setup Shadcn UI
In this lesson, we will start to setup ShadCN.
在本课中,我们将开始设置 ShadCN。
ShadCN is a modern UI library that provides a flexible set of components and styles for building web applications. It is built with Radix UI Primitives, which are low-level, unstyled components, and Tailwind CSS for theming and utility-first styling.
ShadCN 是一个现代 UI 库,为构建 Web 应用程序提供了一组灵活的组件和样式。它基于 Radix UI Primitives 构建,这些是低级别的无样式组件,并使用 Tailwind CSS 进行主题化和实用优先的样式设计。
Unlike other UI libraries where you import all components from a single package (e.g., Material UI, Chakra UI), ShadCN works differently. It allows you to selectively install and use only the components you need. This gives you more control over your codebase and makes customization easier.
与其他 UI 库(如 Material UI、Chakra UI)不同,你从单个包中导入所有组件,ShadCN 的工作方式不同。它允许你选择性地安装和使用仅需要的组件。这让你对代码库有更多的控制,并使自定义更容易。
Let's install it.
让我们安装它。
The init command generates a ShadCN component setup in your project. It doesn't install the entire library as a package but instead gives you access to a system where you can pick and choose specific components that will be installed directly into your project's codebase. This contrasts with typical UI libraries like Material UI or Chakra UI, where you import components from a single package. ShadCN, however, generates the components locally.
init 命令会在你的项目中生成 ShadCN 组件设置。它不会将整个库作为包安装,而是为你提供一个系统,你可以选择将特定组件直接安装到项目的代码库中。这与典型的 UI 库(如 Material UI 或 Chakra UI)形成对比,后者从单个包中导入组件。然而,ShadCN 在本地生成组件。
Note: It used to be
npx shadcn-ui@latest init but now it is npx shadcn@latest init.
注意:以前是 npx shadcn-ui@latest init,但现在改为 npx shadcn@latest init。Select
default for the style.
样式选择 default。Select
Slate for the base color.
基础颜色选择 Slate。Slect
yes for CSS variables for theming.
主题化的 CSS 变量选择 yes。Legacy Peer Dependencies Note传统对等依赖说明
It depends on when you're watching this but you may get this warning where it says that some dependencies may not be compatible with React 19. This happebns because React 19 is so new and some packages don't explicitly say they're compatible with React 19. If you don't get this warning, just ignore it. If you do, you'll have 2 options:
这取决于你什么时候观看这个教程,你可能会收到一个警告,说某些依赖可能与 React 19 不兼容。发生这种情况是因为 React 19 太新了,一些包没有明确说明它们与 React 19 兼容。如果你没有收到这个警告,就忽略它。如果你收到了,你将有 2 个选项:
- -force: 使用 --force 告诉 npm 或你的包管理器完全忽略任何依赖冲突。此选项强制安装,即使存在不兼容的对等依赖。你可以选择此选项,但如果依赖项依赖于特定版本,可能会引入运行时问题。
- -legacy-peer-deps: 此选项告诉 npm 忽略包的"对等依赖"要求。默认情况下,npm 强制执行对等依赖的严格兼容性,此标志只是绕过此检查。这通常比 --force 更安全,因为它尊重主要依赖项,而只忽略不兼容的对等依赖。
I'm going to select the
--legacy-peer-deps option. Either way you shouldn't have issues. All the packages we're using are compatible with React 19.
我将选择 --legacy-peer-deps 选项。无论哪种方式,你都不应该有问题。我们使用的所有包都与 React 19 兼容。Now Shadcn has been initialized.
现在 Shadcn 已经初始化完成。
Test With a Button使用按钮测试
Let's test it with a button. We can use the
Button component from Shadcn. You can read more about the button component here.
让我们用按钮来测试它。我们可以使用 Shadcn 的 Button 组件。你可以在这里阅读更多关于按钮组件的信息。Run the following command:
运行以下命令:
This will create a
components/ui folder in the root of the project with the button.tsx file.
这将在项目根目录中创建一个 components/ui 文件夹,其中包含 button.tsx 文件。In your
app/page.tsx file, import the button component and use it:
在你的 app/page.tsx 文件中,导入按钮组件并使用它:You should see the button on the page.
你应该能在页面上看到按钮。
So this will be the process anytime we want to use a component from Shadcn. We will install the component and import it in the file we want to use it.
因此,每当我们想要使用 Shadcn 的组件时,这就是这个过程。我们将安装组件,并在我们想要使用它的文件中导入它。
You can now remove the
Button from the page.
现在你可以从页面中移除 Button。you can keep the
Button component in the components folder because we will use it later.
你可以将 Button 组件保留在 components 文件夹中,因为我们稍后会使用它。TypeScript Rule
There is a TypeScript error that gets thrown with the
input component that has to do with the no-empty-object-type rule. I want to fix that by adding a rule to the eslintrc.json file. Open it up and add the following:
input 组件会抛出一个与 no-empty-object-type rule 相关的 TypeScript 错误。我想通过向 eslintrc.json 文件添加规则来修复它。打开它并添加以下内容:This will disable the rule for the empty object type.
这将禁用空对象类型的规则。
Now let's move on to the layout in the next lesson.
现在让我们在下一课中继续布局部分。
4. Root Layout & Constants
In Next.js we can have groups and multiple layouts. Groups are folders that are not pages and are used for structure. You can create a group by creating a folder with parenrentheses around the name.
在 Next.js 中,我们可以拥有分组和多个布局。分组是用于结构组织的文件夹,它们不是页面。你可以通过在文件夹名称周围加上括号来创建一个分组。
Create a folder called
(root) in the app folder. Move the app/page.tsx into the (root) folder. We are also going to have a sub layout here. Instead of adding the container classes and embedding our header, footer and things like that in the main layout, we will do that here.在app文件夹中创建一个名为(root)的文件夹。将app/page.tsx移动到(root)文件夹中。我们还将在这里设置一个子布局。我们不会在主布局中添加容器类和嵌入页眉、页脚等内容,而是在这里进行设置。
Add the following code to the
(root)/layout.tsx file:将以下代码添加到(root)/layout.tsx文件中:
It is very similar to the main layout except we added some container classes. We will add the header and footer components here.
这与主布局非常相似,只是我们添加了一些容器类。我们将在这里添加页眉和页脚组件。
Your main
app/layout.tsx file should look like this:你的主app/layout.tsx文件应该如下所示:
So that only the
{children} prop is rendered in the <body> tag. No classes or anything.这样只有{children}属性会在<body>标签中渲染。没有任何类或其他内容。
This will give us a more flexible layout. When we create our header and stuff, the will go in the
(root)/layout.tsx file. Now if we want to have multiple layoutss, we can create a new group and add the layout there.这将给我们一个更灵活的布局。当我们创建页眉等内容时,它们将放在(root)/layout.tsx文件中。现在如果我们想要多个布局,我们可以创建一个新分组并在那里添加布局。
Constants 常量
Another thing I want to do in this lesson is create a file to store some constants and info that we will use throughout the course. Create a file at
lib/constants/index.ts and add the following code:在本节课中,我想做的另一件事是创建一个文件来存储一些我们将在整个课程中使用的常量和信息。在lib/constants/index.ts创建一个文件并添加以下代码:
We will add to this file as we go. The purpose of this file is so that we can use these values in multiple places without having to repeat ourselves. In this case, we are using the
process.env object to get the values from the .env file and creating a default value if the environment variable is not set.我们将在后续继续向这个文件添加内容。这个文件的目的是让我们可以在多个地方使用这些值,而不需要重复自己。在这种情况下,我们使用process.env对象从.env文件中获取值,并在环境变量未设置时创建一个默认值。
.env File
Even though we have the defaults in the constants file, I stil want to set the environment variables in the
.env file.尽管我们在常量文件中设置了默认值,我仍然希望在.env文件中设置环境变量。
Create a file called
.env in the root of the project and add the following values:在项目根目录创建一个名为.env的文件,并添加以下值:
Add the
.env file to your .gitignore file as well. You don't want this to be committed to the repository.同时将.env文件添加到你的.gitignore文件中。你不希望这个文件被提交到代码仓库中。
Add To Metadata
添加到元数据
In the
app/layout.tsx file, we can add the APP_NAME and APP_DESCRIPTION to the metadata object.在app/layout.tsx文件中,我们可以将APP_NAME和APP_DESCRIPTION添加到元数据对象中。
Import the contstants and add them to the metadata object:
导入常量并将它们添加到元数据对象中:
For the
title, we are using a template to set the title. So if we set a title for an individual page, it will be prepended with the APP_NAME and a pipe character.对于title,我们使用模板来设置标题。因此如果我们为单个页面设置标题,它将以APP_NAME和一个管道符开头。
metadataBase is used to set the base URL for the metadata.metadataBase用于设置元数据的基础 URL。
In the next lesson we will work on the header and footer components.
在下一节课中,我们将制作页眉和页脚组件。
5. Header & Footer Components
Let's start to work on the main components of our website. We will start with the header component.
让我们开始制作网站的主要组件。我们将从页眉组件开始。
The way we are going to structure our component files will be a bit different than I usually do. If the component will include multiple embedded components, we will have them in a folder inside of a folder called shared. For instance, the header will consist of not only the header component, but other ahared components such as the mode toggle.
我们组织组件文件的方式会与我通常的做法略有不同。如果组件包含多个嵌入式组件,我们会将它们放在一个叫 shared 的文件夹内的文件夹中。例如,页眉不仅包含页眉组件本身,还包含其他共享组件,如模式切换器。
Create a folder at
components/shared/header. For the main header component, the file will be called index.tsx. So we are doing this in a Next.js-like way. Like the pages and routes.在components/shared/header创建一个文件夹。对于主页眉组件,文件将命名为index.tsx。所以我们以类似 Next.js 的方式来做,就像页面和路由一样。
It's important to mention that you can structure your components in any way you like. This is just the way I want to do it.
需要提到的是,你可以用任何你喜欢的方式来组织组件。这只是我想采用的方式。
Create a file at
components/shared/header/index.tsx and add the following code:在components/shared/header/index.tsx创建一个文件,并添加以下代码:
Embedding the Header Component
嵌入页眉组件
We are going to embed the header in our root group layout file (
app/(root)/layout.tsx). Not the main layout file (app/layout.tsx).我们将在根分组布局文件(app/(root)/layout.tsx)中嵌入页眉,而不是在主布局文件(app/layout.tsx)中。
Open the
app/(root)/layout.tsx file and add the following import:打开app/(root)/layout.tsx文件,添加以下导入:
Since the file is called
index.tsx, we don't need to specify the file name when importing it.由于文件名为index.tsx,我们在导入时不需要指定文件名。
Now embed it:
现在嵌入它:
Now we can see the text
Header on the page.现在我们可以在页面上看到Header文本。
Hint: If you want to import something that is not imported, you can press
Ctrl + . and select to import it.提示:如果你想导入某些尚未导入的内容,可以按Ctrl + .并选择导入。
Add the following to the
Header component:将以下内容添加到Header组件中:
We are bringing in the
ShoppingCart and UserIcon from the lucide-react library. We are also using the Button component from the @/components/ui/button file. We are using the Link component from next/link to create a link to the /cart page. We are also using the APP_NAME constant from the @/lib/constants file.我们从lucide-react库引入了ShoppingCart和UserIcon。我们还使用了来自@/components/ui/button文件的Button组件。我们使用next/link的Link组件来创建到/cart页面的链接。我们还使用了来自@/lib/constants文件的APP_NAME常量。
The logo will have the app name on larger screens and on smaller screens it will be hidden.
Logo 在较大屏幕上会显示应用名称,在较小屏幕上会隐藏。
We are using the
ghost variant of the Button component. This is because we want the button to have a transparent background. We are also using the asChild prop to make the button a child of the Link component. This is because we want the button to be a child of the Link component.我们使用了Button组件的ghost变体。这是因为我们希望按钮具有透明背景。我们还使用了asChild属性使按钮成为Link组件的子元素。这是因为我们希望按钮成为Link组件的子元素。
Footer Component
页脚组件
Create a file at
components/footer.tsx and add the following code:在components/footer.tsx创建一个文件,并添加以下代码:
Bring the footer into the
app/(root)/layout.tsx file.将页脚引入到app/(root)/layout.tsx文件中。
Now embed it:
现在嵌入它:
Now we have a header and footer on the page.
现在我们在页面上有了页眉和页脚。
lucide-react图标
npm install lucide-react
https://lucide.dev/guide/installation
理解
Button asChild variant='ghost' React 生态中(尤其 NextUI、Shadcn/ui 等组件库)按钮组件的常用配置,核心是让 Button 组件仅提供交互逻辑,不渲染自身 DOM 结构,同时使用“幽灵/透明”样式。
核心概念拆解
先帮你理清两个关键属性的作用:
属性 | 核心作用 |
asChild | 让 Button 组件“透传”,将自身的 props(点击事件、样式、交互状态)传递给子元素,自身不再生成额外 DOM 节点(如 <button>),而是渲染子元素(如 <a>、<Link>) |
variant="ghost" | 按钮使用“幽灵样式”:无背景色、无边框,仅保留文字/图标和 hover/active 状态的样式,视觉上是“透明”的,常用于导航、操作类按钮 |
实际使用示例(以 Shadcn/ui 为例)
这是最常见的用法场景——把 Button 包装路由链接(Next.js 的 Link),既保留按钮的交互逻辑,又用幽灵样式,且 DOM 结构更简洁:
渲染后的 DOM 结构(关键!)
- 不加
asChild:会生成<button><a href="/dashboard">...</a></button>(多余的 button 嵌套,可能导致交互/样式问题)
- 加
asChild:直接渲染<a href="/dashboard" class="btn btn-ghost">...</a>(仅保留子元素,DOM 更干净)
更多场景示例(NextUI 组件库)
NextUI 的 Button 组件同样支持这两个属性,用法一致:
关键注意事项
asChild必须搭配子元素使用,否则会报错;
variant="ghost"的样式可自定义:通过 CSS 变量或组件库的主题配置修改 hover 背景色、文字颜色等;
- 幽灵按钮适合用于:导航栏选项、表格操作按钮、弹窗关闭按钮等“非主要操作”场景,视觉上不抢焦点;
- 透传后,子元素会继承 Button 的所有交互 props(如
onClick、disabled、loading等)。
总结
asChild核心是DOM 透传,让 Button 仅提供交互逻辑,渲染子元素的 DOM 结构,避免嵌套冗余;
variant="ghost"是幽灵样式,按钮无背景/边框,仅保留基础交互样式,视觉轻量化;
- 两者组合是 React 组件库中“按钮样式的链接/操作元素”的最佳实践,兼顾交互和样式简洁性。
6. Theme Mode Toggle
We are going to add a theme toggle to our app. This will allow the user to switch between a light and dark theme as well as a system theme.
我们将为应用添加一个主题切换器。这将允许用户在浅色主题、深色主题以及系统主题之间切换。
Dependencies 依赖
We are going to use a dropdown menu to allow the user to select a theme. Run the following command in your terminal:
我们将使用下拉菜单让用户选择主题。在终端中运行以下命令:
We are also using a package called
next-themes, which allows us to easily switch between light and dark themes. Run the following command in your terminal:我们还将使用一个叫next-themes的包,它可以让我们轻松地在浅色和深色主题之间切换。在终端中运行以下命令:
Mode Toggle Component 模式切换组件
Create a new file at
components/shared/header/mode-toggle.tsx.在components/shared/header/mode-toggle.tsx创建一个新文件。
Create a basic component:
创建一个基础组件:
Bring it into the
components/shared/header/index.tsx file:将它引入到components/shared/header/index.tsx文件中:
Embed it right above the button that surrounds the shopping cart link and inside the div with the class
space-x-2:将它嵌入到购物车链接按钮的上方,放在 class 为space-x-2的 div 内部:
You should see the text in the header.
你应该能在页眉中看到这段文字。
Go back to the
components/shared/header/mode-toggle.tsx file and add the following imports. We also need to make this a client component since we are using hooks:回到components/shared/header/mode-toggle.tsx文件,添加以下导入。由于我们使用了 hooks,还需要将其设为客户端组件:
We are bringing in the
useTheme hook from next-themes to allow us to switch between light and dark themes. We are also bringing in the useState and useEffect hooks and some dropdown components and icons.我们从next-themes引入了useThemehook,用于在浅色和深色主题之间切换。我们还引入了useState和useEffecthooks 以及一些下拉菜单组件和图标。
Add the following inside the function above the return statement:
在函数内部、return 语句之前添加以下内容:
The theme switching is easy. We simply get the theme state from the hook. We can then use the
setTheme function to switch between light and dark themes.主题切换很简单。我们只需从 hook 获取主题状态,然后就可以使用setTheme函数在浅色和深色主题之间切换。
Output 输出
Now we just need to add the output in the return statement. Replace the
<>ModeToggle</> with the following:现在我们只需要在 return 语句中添加输出。将<>ModeToggle</>替换为以下内容:
We are checking the theme state and then rendering the appropriate icon. We are also adding a dropdown menu with three options: system, light, and dark. We are also adding a checkbox item for each option. We are also setting the theme state when the checkbox item is clicked. The logic here is pretty simple.
我们检查主题状态,然后渲染相应的图标。我们还添加了一个包含三个选项的下拉菜单:system、light 和 dark。我们还为每个选项添加了一个复选框项。当点击复选框项时,我们设置主题状态。这里的逻辑非常简单。
Theme Provider主题提供者
In order to use the theme state in our app we need to wrap our app in a theme provider. This is something that will be used throughout the entire project, so we want this in our main layout. Open the
app/layout.tsx file and add the following import:为了在我们的应用中使用主题状态,我们需要用主题提供者包裹我们的应用。这将在整个项目中使用,所以我们希望它出现在主布局中。打开app/layout.tsx文件,添加以下导入:
Now just wrap the output with the theme provider:
现在只需用主题提供者包裹输出:
We are adding a couple of options to the theme provider. The
attribute prop is used to set the data-theme attribute on the html element. The defaultTheme prop is used to set the default theme. The enableSystem prop is used to enable the system theme. The disableTransitionOnChange prop is used to disable the transition when the theme changes. I chose to set the default to light but you can set it to dark or system if you want.我们为主题提供者添加了一些选项。attribute属性用于在 html 元素上设置 data-theme 属性。defaultTheme属性用于设置默认主题。enableSystem属性用于启用系统主题。disableTransitionOnChange属性用于在主题变化时禁用过渡效果。我选择将默认值设为 light,但如果你想,也可以设为 dark 或 system。
Fix Hydration Issue修复水合问题
If you run the app now you will see an error in the console about hydration. You can fix this by adding the
surpressHydrationWarning attribute to the <html> tag in the main layout.如果你现在运行应用,你会在控制台看到关于水合的错误。你可以通过在主布局的<html>标签上添加surpressHydrationWarning属性来修复这个问题。
One of the reasons is listed as "Server/client branch like if (typeof window !== 'undefined')"
其中一个原因被列为"服务器/客户端分支,如 if (typeof window !== 'undefined')"
This is a common cause with next-themes because it uses window to detect and set the theme. Since window isn't available on the server, the theme sometimes behaves differently between SSR and client-side rendering.
这是 next-themes 的一个常见原因,因为它使用 window 来检测和设置主题。由于 window 在服务器上不可用,主题在 SSR 和客户端渲染之间的表现有时会有所不同。
We can fix this by making sure the component is mounted before the theme is set. We can do this by setting a
mounted state and then checking if the component is mounted before setting the theme. Add the following inside the function above the return statement:我们可以通过确保组件在设置主题之前已挂载来修复这个问题。我们可以通过设置一个mounted状态,然后在设置主题之前检查组件是否已挂载来实现。在函数内部、return 语句之前添加以下内容:
So when you see this hydration error, this is what you can do to fix it.
所以当你看到这个水合错误时,这就是你可以用来修复它的方法。
If you see any other errors that say something like the client doesn't match the server, it is most likely from a browser extension that changes the HTML of the page. You can leave it or try and find the extension that is causing the issue. Lastpass and some of the color picker extensions are known to cause this issue.
如果你看到任何其他错误,说客户端与服务器不匹配,这很可能是来自改变页面 HTML 的浏览器扩展。你可以忽略它,或者尝试找到导致问题的扩展。Lastpass 和一些取色器扩展已知会导致这个问题。
Now we have a cool theme toggler. Let's move on.
现在我们有了一个酷炫的主题切换器。让我们继续。
React 水合(Hydration)不匹配
问题:服务器渲染时用的是
dark 主题(月亮图标),但客户端渲染时用的是 light 主题(太阳图标)。这是因为
next-themes 在服务器端和客户端检测到的主题不一致。需要在
mode-toggle.tsx 组件中添加 挂载检查,确保组件只在客户端渲染:问题解释
什么是水合(Hydration)?
- Next.js 先在服务器上渲染 HTML
- 然后发送到浏览器
- React 再把事件绑定到这个 HTML 上(叫"水合")
- 如果服务器和客户端渲染的内容不一样,就会报错
为什么会不一样?
- 服务器不知道用户的系统主题设置
- 客户端(浏览器)知道
- 所以服务器可能渲染
dark,客户端渲染light
解决方案原理
阶段 | 显示内容 |
服务器渲染 | <Button><SunIcon /></Button> |
客户端首次渲染 | <Button><SunIcon /></Button>(和服务器一致) |
客户端挂载后 | 显示实际的主题图标 |
这样服务器和客户端第一次渲染的内容完全一致,就不会报错了!
还需要在 layout.tsx 添加属性
同时确保你的
app/layout.tsx 的 <html> 标签有:这个属性会抑制水合警告(可选,但建议加上)。
关键点:mounted 检查只影响图标,不影响主题颜色
让我澄清一下:
两个层面的东西
层面 | 控制者 | 决定什么 |
主题颜色 | ThemeProvider | class="light" 或 class="dark" |
图标显示 | ModeToggle 组件 | 显示太阳 ☀️ 或月亮 🌙 |
代码实际效果
注意:这个按钮没有设置主题的功能,只是占位显示!
真正的主题由
ThemeProvider 控制:- 如果用户之前选了 dark,
ThemeProvider会把<html class="dark">
- 页面背景还是黑色的
- 只是图标暂时显示太阳,100ms 后变成月亮
视觉上的"闪烁"
确实会有极短暂的闪烁:
但通常很快(< 100ms),用户几乎察觉不到。
如果你不想有任何闪烁
可以把默认图标也改成根据系统偏好:
或者更简单:不显示任何图标
总结
问题 | 答案 |
主题颜色会变吗? | 不会,由 ThemeProvider 控制,立即正确 |
图标会错吗? | 会短暂显示太阳,100ms 后纠正 |
用户能察觉吗? | 基本不能,很快纠正 |
能完全避免吗? | 可以,用中性占位符或不显示图标 |
7. Loading & Not Found Pages
In Next.js, you can create a loading page and a not found page simply by naming the files
loading and not-found respectively.在 Next.js 中,你可以通过将文件命名为loading和not-found来简单地创建加载页面和404页面。
Loading Page 加载页面
Let's create a loading page with a spinner. There are a lot of ways to do this. There are packages like react-spinners that you can use to create a loading page. You could also just use text or an image. I am going to use an image that is visible in both light and dark mode. I have attached a download for the image to this lesson. You can also get it from the final repository.
让我们创建一个带旋转动画的加载页面。有很多方法可以做到这一点。有一些像 react-spinners 这样的包可以用来创建加载页面。你也可以只使用文字或图片。我将使用一个在浅色和深色模式下都可见的图片。我已将图片下载附件添加到本节课中。你也可以从最终代码仓库中获取它。
The image is called
loader.gif and we're going to put it in the assets folder.图片名为loader.gif,我们将把它放在assets文件夹中。
Now create a file called
loading.tsx in the app folder and add the following code:现在在app文件夹中创建一个名为loading.tsx的文件,并添加以下代码:
We are just bringing in the image and displaying it using the
Image component from Next.js. We are also setting the height and width of the image to 150 pixels and aligning it to the center of the screen.我们只是引入图片并使用 Next.js 的Image组件显示它。我们还将图片的高度和宽度设置为150像素,并将其对齐到屏幕中心。
Now if you refresh the page, you will see the loading page. If you don't see it it's because it loaded too fast. You can test by adding the following to the
app/(root)/page.tsx:现在如果你刷新页面,你会看到加载页面。如果你看不到它,那是因为加载太快了。你可以通过将以下内容添加到app/(root)/page.tsx来测试:
Not Found Page 404页面
Now we want a not found page. Go to any page that does not exist and you will see the default not found page. You can keep this if you want but I like to create a custom one with a button or link to go to the homepage.
现在我们想要一个404页面。访问任何不存在的页面,你会看到默认的404页面。如果你愿意可以保留它,但我喜欢创建一个自定义的,带有一个按钮或链接可以返回首页。
Create a file called
not-found.tsx in the app folder and add the following code:在app文件夹中创建一个名为not-found.tsx的文件,并添加以下代码:
We have the logo, a heading, a paragraph, and a button. The button will take you back to the homepage. You could use a link instead of a button if you want.
我们有 Logo、标题、段落和按钮。按钮会带你回到首页。如果你愿意,也可以用链接代替按钮。
Feel free to use a different layout for these pages. I just wanted to create something simple.
你可以随意为这些页面使用不同的布局。我只是想创建一些简单的东西。
8. Responsive Sheet Menu
In this lesson, I want to make the navigation responsive. We will use a
Sheet component from the shadcn/ui library. The Sheet component is a modal that slides in from the right side of the screen. We will use it to display the navigation links on smaller screens.在本节课中,我想让导航具有响应式。我们将使用 shadcn/ui 库中的Sheet组件。Sheet组件是一个从屏幕右侧滑入的模态框。我们将用它在小屏幕上显示导航链接。
Install the sheet component:
安装 sheet 组件:
Create The Menu Component 创建 Menu 组件
We are going to create a separate component for the menu.
我们将为菜单创建一个单独的组件。
Creare a new file at
components/shared/header/menu.tsx and add the following imports:在components/shared/header/menu.tsx创建一个新文件,并添加以下导入:
Add the following code:
添加以下代码:
Let's replace the right side of the header with this Menu component. Open the
components/shared/header/index.tsx file and remove the Button, icons and ModeToggle imports and import the Menu. Then replace the <div className='space-x-2'> and everything in it with the <Menu /> component.让我们用 Menu 组件替换页眉的右侧部分。打开components/shared/header/index.tsx文件,移除 Button、icons 和 ModeToggle 的导入,并导入 Menu。然后将<div className='space-x-2'>及其内部所有内容替换为<Menu />组件。
It should look like this:
它应该看起来像这样:
You should basically see the same thing. The only difference is that the menu is now a separate component.
你应该基本上看到相同的东西。唯一的区别是菜单现在是一个单独的组件。
Add The Sheet Component 添加 Sheet 组件
Now let's create the sheet. Add the following code under the ending
</nav> tag in the Menu component:现在让我们创建 sheet。在Menu组件的结束</nav>标签下方添加以下代码:
The sheet component is a modal that slides in from the right side of the screen. We are using it to display the navigation links on smaller screens. The
SheetTrigger component is a button that triggers the sheet to slide in. The SheetContent component is the content of the sheet. We are using it to display the navigation links. The SheetTitle and SheetDescription components are required or you will get a warning in the console. I am just adding the text "Menu" in the title and leaving the description blank.sheet 组件是一个从屏幕右侧滑入的模态框。我们用它在小屏幕上显示导航链接。SheetTrigger组件是一个触发 sheet 滑入的按钮。SheetContent组件是 sheet 的内容。我们用它显示导航链接。SheetTitle和SheetDescription组件是必需的,否则你会在控制台收到警告。我只在标题中添加 "Menu" 文字,描述留空。
Now when you make the screen smaller, you should see the EllipsisVertical icon. Click on it and you should see the navigation links. You can't see the sign in, but don't worry about that because we will be changing that around in a future lesson.
现在当你缩小屏幕时,你应该能看到 EllipsisVertical 图标。点击它,你应该能看到导航链接。你看不到登录按钮,但不用担心,因为我们在未来的课程中会修改它。
Here is the final code for the Menu component:
这是 Menu 组件的最终代码:
9. Sample Products & Product List
In this lesson, we are going to start to display some products on our page. We are going to use a sample data set and sample images. Right now, the data will come from a file. Later on, we will use a database and the Prisma ORM to fetch the data. You could start with the database, but this is how I typically start a project. I like to get some of the basic layout and functionality working first and then add the database.
在本节课中,我们将开始在页面上显示一些产品。我们将使用示例数据集和示例图片。目前,数据将来自一个文件。稍后,我们将使用数据库和 Prisma ORM 来获取数据。你可以从数据库开始,但这通常是我启动项目的方式。我喜欢先让一些基本布局和功能运行起来,然后再添加数据库。
Sample Data 示例数据
In this lesson, you will have a download for the sample data, which will include the data file and the images. You can also get them from the main Github repository.
在本节课中,你将获得示例数据的下载,其中包括数据文件和图片。你也可以从主 Github 仓库中获取它们。
Download the data and move the
images/sample-products folder with the images to the public/images folder. There are also two banners. Move those to the public/images folder as well.下载数据并将包含图片的images/sample-products文件夹移动到public/images文件夹。还有两个横幅图片,也把它们移动到public/images文件夹。
Move the
db folder with the sample-data.ts file to the root of the project. This db folder will have other stuff in it later such as a seeder.将包含sample-data.ts文件的db文件夹移动到项目根目录。这个db文件夹稍后还会有其他内容,比如种子文件。
Look at the sample data and get a feel for what the data looks like. There are only products for now but later, we will have users as well.
查看示例数据,了解数据的结构。目前只有产品数据,但稍后我们还会有用户数据。
Product Fields 产品字段
name: The product name.
name: 产品名称。
slug: The product slug.
slug: 产品别名(URL 友好名称)。
category: The product category.
category: 产品类别。
description: The product description.
description: 产品描述。
images: Array of product image paths.
images: 产品图片路径数组。
price: The product price.
price: 产品价格。
brand: The product brand.
brand: 产品品牌。
rating: The product rating.
rating: 产品评分。
numReviews: The number of reviews for the product.
numReviews: 产品评论数量。
stock: The product stock.
stock: 产品库存。
isFeatured: Whether the product is featured.
isFeatured: 是否为推荐产品。
banner(optional): The product banner.
banner(可选):产品横幅。
Product List Component 产品列表组件
We are going to have a component to list products. Create a new file at
components/shared/product/product-list.tsx and add the following code:我们将创建一个用于列出产品的组件。在components/shared/product/product-list.tsx创建一个新文件,并添加以下代码:
This component will take in the data and display the product name for now. Ultimately, we will display the product card with the data, but for now, this is ok.
这个组件目前将接收数据并显示产品名称。最终,我们将使用数据展示产品卡片,但目前这样就可以了。
Another thing that I want to mention is that we're using the
any type for the data. This isn't really a good practice, but for now, it's ok because we're going to be implementing something called "Zod" later to validate the data.另一件我想提到的事是,我们对数据使用了any类型。这不是一个好的实践,但目前没问题,因为我们稍后将实现一个叫 "Zod" 的东西来验证数据。
Use the Product List Component 使用产品列表组件
Open the homepage at
app/(root)page.tsx and import the data and the component:打开app/(root)page.tsx首页,导入数据和组件:
Then add the following to the return:
然后将以下内容添加到 return 中:
You should now see the product names in the browser.
你现在应该在浏览器中看到产品名称了。
Limit The Products 限制产品数量
Let's also add a
limit prop so we don't have to show all the products:让我们也添加一个limit属性,这样我们就不必显示所有产品:
Now change the embed to the following:
现在将嵌入改为以下内容:
You should only see 4 products now.
你现在应该只能看到4个产品了。
In the next lesson, we will create product cards.
在下一节课中,我们将创建产品卡片。
10. Product Card Component
Now, let's display the products in a card. We will use the ShadCN card component.
现在,让我们用卡片形式展示产品。我们将使用 ShadCN 的卡片组件。
Open the terminal and run the following command to install the ShadCN card component:
打开终端并运行以下命令来安装 ShadCN 卡片组件:
Create a new file at
components/shared/product/product-card.tsx and add the following code:在components/shared/product/product-card.tsx创建一个新文件,并添加以下代码:
Now, let's use the component in the product list component. Open the
product-list.tsx file and replace the code with the following:现在,让我们在产品列表组件中使用这个组件。打开product-list.tsx文件,将代码替换为以下内容:
You should now see the product cards in the browser.
你现在应该在浏览器中看到产品卡片了。
11. Product Price Component
Next we will make the price look a bit better and create a component for it.
接下来我们将让价格看起来更好看,并为此创建一个组件。
Create a file at
components/product/product-price.tsx and add the following:在components/product/product-price.tsx创建一个文件,并添加以下内容:
This component will display the price of the product. It takes in the price as a number and converts it to a string. It then splits the string into the integer and decimal parts. It then displays the integer part with a
$ sign and the decimal part with a . in between.这个组件将显示产品的价格。它接收数字形式的价格并将其转换为字符串。然后将字符串分割为整数部分和小数部分。接着显示带有$符号的整数部分,以及中间带有.的小数部分。
It also takes an optional
className prop that can be used to add additional classes to the component. The cn function is a utility function that will combine the class names and add the text-2xl class.它还接收一个可选的className属性,可用于向组件添加额外的类。cn函数是一个工具函数,它会合并类名并添加text-2xl类。
Add ProductPrice to ProductCard component 将 ProductPrice 添加到 ProductCard 组件
Open the
components/product/product-card.tsx file and replace the <p> with the price with the ProductPrice component.打开components/product/product-card.tsx文件,将带有价格的<p>标签替换为ProductPrice组件。
📎 参考文章
- 一些引用
- 引用文章
欢迎您在底部评论区留言,一起交流~
上一篇
03 Shopping Platform From Scratch - Database, Prisma & Product Display
下一篇
01 Shopping Platform From Scratch - Introduction
Loading...
