Next.js 结合 MySQL 数据库全站开发教程
前言
随着现代 Web 应用的发展,前后端分离架构已成为主流。Next.js 作为一款强大的 React 框架,不仅提供了出色的前端开发体验,还内置了 API 路由功能,使得在同一项目中开发前后端成为可能。结合 MySQL 这样成熟的关系型数据库,我们可以构建功能完善、性能优异的全栈应用。
本教程将带您一步步学习如何使用 Next.js 和 MySQL 构建全栈应用,内容涵盖项目初始化、数据库配置、实体设计、服务层实现、API 路由开发以及前端页面展示等方面。通过本教程,您将掌握使用 Next.js 和 MySQL 进行全栈开发的完整流程和最佳实践。
1. 环境准备
1.1 安装 Node.js 和 npm/yarn
确保您已安装 Node.js (推荐 v18 或更高版本):
bash
node -v
1.2 安装 MySQL
您可以通过以下方式安装 MySQL:
- 直接下载并安装 MySQL 社区版
- 使用 Docker 安装:
bash
docker run --name mysql -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql:8
1.3 创建 Next.js 项目
使用 create-next-app 创建新项目:
bash
npx create-next-app@latest my-nextjs-mysql-app
cd my-nextjs-mysql-app
按照提示选择项目配置,推荐启用 TypeScript、ESLint、Tailwind CSS 等功能。
1.4 安装必要依赖
bash
npm install mysql2 typeorm reflect-metadata class-validator
# 或使用 yarn
yarn add mysql2 typeorm reflect-metadata class-validator
这些依赖的作用:
mysql2
:MySQL 数据库驱动typeorm
:强大的 ORM 框架,提供了实体映射、查询构建等功能reflect-metadata
:为装饰器提供元数据反射功能class-validator
:用于验证类和对象属性
1.5 配置 TypeScript
在 tsconfig.json
中添加以下配置,以支持装饰器和元数据反射:
json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
// 其他配置保持不变
}
}
2. 项目结构设计
一个良好的项目结构可以提高代码的可维护性和可扩展性。以下是本教程推荐的项目结构:
csharp
my-nextjs-mysql-app/
├── src/
│ ├── app/ # Next.js App Router 目录
│ │ ├── api/ # API 路由
│ │ └── ... # 页面组件
│ ├── components/ # UI 组件
│ ├── config/ # 配置文件
│ │ └── database.ts # 数据库配置
│ ├── entities/ # TypeORM 实体定义
│ ├── services/ # 业务逻辑层
│ └── utils/ # 工具函数
├── public/ # 静态资源
└── ... # 其他配置文件
3. 数据库配置
3.1 创建数据库连接配置
创建 src/config/database.ts
文件,配置 TypeORM 数据源:
typescript
import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "../entities/user.entity";
import { Products } from "../entities/product.entity";
// 声明全局变量以在开发环境热重载中保持数据库连接
declare global {
var dbConnection: {
dataSource: DataSource | undefined;
promise: Promise<DataSource> | null;
};
}
// 初始化全局变量
if (!global.dbConnection) {
global.dbConnection = {
dataSource: undefined,
promise: null
};
}
// 创建数据库连接实例
export const AppDataSource = new DataSource({
type: "mysql",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "3306"),
username: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD || "password",
database: process.env.DB_NAME || "nextjs_db",
synchronize: process.env.NODE_ENV !== "production", // 开发环境自动同步数据库结构
logging: process.env.NODE_ENV !== "production",
entities: [User, Products], // 注册所有实体
migrations: [],
subscribers: [],
connectTimeout: 20000
});
// 初始化数据库连接
export const initDatabase = async (): Promise<DataSource> => {
// 如果已有数据源实例且已初始化,则直接返回
if (
global.dbConnection.dataSource &&
global.dbConnection.dataSource.isInitialized
) {
return global.dbConnection.dataSource;
}
// 如果已经有初始化中的Promise,则等待它完成
if (global.dbConnection.promise) {
return global.dbConnection.promise;
}
// 创建新的初始化Promise
global.dbConnection.promise = (async () => {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
console.log("数据库连接成功");
}
global.dbConnection.dataSource = AppDataSource;
return AppDataSource;
} catch (error) {
console.error("数据库连接失败:", error);
global.dbConnection.promise = null;
throw error;
}
})();
return global.dbConnection.promise;
};
3.2 创建数据库初始化工具
为了方便在服务中使用数据库连接,创建 src/utils/databaseUtils.ts
文件:
typescript
import { initDatabase } from "../config/database";
// 确保数据库已初始化
export const ensureInitialized = async () => {
try {
await initDatabase();
} catch (error) {
console.error("数据库初始化失败:", error);
throw error;
}
};
3.3 环境变量配置
创建 .env.local
文件,设置数据库连接参数:
ini
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=password
DB_NAME=nextjs_db
注意:在生产环境中,请确保这些敏感信息的安全性。
4. 实体(Entity)设计
TypeORM 使用实体来映射数据库表。实体是一个简单的类,使用装饰器来定义其属性和关系。
4.1 创建用户实体
创建 src/entities/user.entity.ts
文件:
typescript
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity("user")
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
name: string;
@Column({ nullable: true })
email: string;
@Column({ unique: true })
account: string;
@Column()
password: string;
}
4.2 创建产品实体
创建 src/entities/product.entity.ts
文件:
typescript
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity("products")
export class Products {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
imagePath: string;
@Column()
type: number;
@Column({ type: "float", default: 0 })
carbonBlack: number;
@Column({ type: "float", default: 0 })
ashContent: number;
@Column({ type: "float", default: 0 })
dispersibility: number;
@Column({ default: false })
hot: boolean;
}
5. 服务层(Service)开发
服务层负责处理业务逻辑,是连接数据访问层和控制器层的桥梁。
5.1 创建产品服务
创建 src/services/productService.ts
文件:
typescript
import { AppDataSource } from "../config/database";
import { Products } from "../entities/product.entity";
import { Like, Repository } from "typeorm";
import { validate } from "class-validator";
import { ensureInitialized } from "../utils/databaseUtils";
// 定义查询过滤参数接口
export interface ProductFilter {
page?: number;
pageSize?: number;
name?: string;
type?: string;
isHot?: boolean;
}
class ProductService {
// 获取仓库实例
private async getRepository(): Promise<Repository<Products>> {
// 确保数据库连接已初始化
await ensureInitialized();
return AppDataSource.getRepository(Products);
}
// 获取产品列表,支持分页和过滤
async getProducts(filter: ProductFilter = {}): Promise<{
data: Products[];
total: number;
page: number;
pageSize: number;
}> {
try {
const productRepository = await this.getRepository();
const { page = 1, pageSize = 10, name, type, isHot } = filter;
// 构建查询条件
const where: any = {};
if (name) {
where.name = Like(`%${name}%`); // 模糊搜索
}
if (type) {
where.type = type;
}
if (isHot !== undefined) {
where.hot = isHot;
}
// 执行查询,获取分页数据
const [data, total] = await productRepository.findAndCount({
where,
skip: (page - 1) * pageSize,
take: pageSize
});
return {
data,
total,
page,
pageSize
};
} catch (error) {
console.error("获取产品列表失败:", error);
throw error;
}
}
// 根据ID获取单个产品
async getProductById(id: number): Promise<Products | null> {
try {
const productRepository = await this.getRepository();
return await productRepository.findOneBy({ id });
} catch (error) {
console.error(`获取产品 ID ${id} 失败:`, error);
throw error;
}
}
// 创建产品
async createProduct(productData: Partial<Products>): Promise<Products> {
try {
const productRepository = await this.getRepository();
const product = productRepository.create(productData);
// 使用class-validator验证实体
const errors = await validate(product);
if (errors.length > 0) {
throw new Error(
`验证错误: ${errors.map(error => Object.values(error.constraints || {}).join(", ")).join("; ")}`
);
}
return await productRepository.save(product);
} catch (error) {
console.error("创建产品失败:", error);
throw error;
}
}
// 更新产品
async updateProduct(
id: number,
productData: Partial<Products>
): Promise<Products | null> {
try {
const productRepository = await this.getRepository();
const product = await productRepository.findOneBy({ id });
if (!product) {
return null;
}
// 合并更新数据
Object.assign(product, productData);
// 验证更新后的数据
const errors = await validate(product);
if (errors.length > 0) {
throw new Error(
`验证错误: ${errors.map(error => Object.values(error.constraints || {}).join(", ")).join("; ")}`
);
}
return await productRepository.save(product);
} catch (error) {
console.error(`更新产品 ID ${id} 失败:`, error);
throw error;
}
}
// 删除产品
async deleteProduct(id: number): Promise<boolean> {
try {
const productRepository = await this.getRepository();
const result = await productRepository.delete(id);
return result.affected ? result.affected > 0 : false;
} catch (error) {
console.error(`删除产品 ID ${id} 失败:`, error);
throw error;
}
}
}
// 导出单例实例
export const productService = new ProductService();
export default productService;
5.2 创建用户服务
创建 src/services/userService.ts
文件:
typescript
import { AppDataSource } from "../config/database";
import { User } from "../entities/user.entity";
import { Repository } from "typeorm";
import { validate } from "class-validator";
import { ensureInitialized } from "../utils/databaseUtils";
class UserService {
// 获取仓库实例
private async getRepository(): Promise<Repository<User>> {
await ensureInitialized();
return AppDataSource.getRepository(User);
}
// 根据账号查找用户
async findByAccount(account: string): Promise<User | null> {
try {
const userRepository = await this.getRepository();
return await userRepository.findOneBy({ account });
} catch (error) {
console.error(`查找用户 ${account} 失败:`, error);
throw error;
}
}
// 创建新用户
async createUser(userData: Partial<User>): Promise<User> {
try {
const userRepository = await this.getRepository();
const user = userRepository.create(userData);
// 验证用户数据
const errors = await validate(user);
if (errors.length > 0) {
throw new Error(
`验证错误: ${errors.map(error => Object.values(error.constraints || {}).join(", ")).join("; ")}`
);
}
return await userRepository.save(user);
} catch (error) {
console.error("创建用户失败:", error);
throw error;
}
}
}
// 导出单例实例
export const userService = new UserService();
export default userService;
6. API 路由开发
Next.js 的 App Router 为我们提供了强大的 API 路由功能,可以直接在同一项目中开发后端 API。
6.1 创建 API 响应工具
首先,创建一个工具函数来统一 API 响应格式。创建 src/utils/apiResponse.ts
文件:
typescript
// 定义错误码枚举
export enum ErrorCode {
VALIDATION_ERROR = "VALIDATION_ERROR",
NOT_FOUND = "NOT_FOUND",
UNAUTHORIZED = "UNAUTHORIZED",
FORBIDDEN = "FORBIDDEN",
CONFLICT = "CONFLICT",
QUERY_ERROR = "QUERY_ERROR",
SERVER_ERROR = "SERVER_ERROR"
}
// 成功响应
export const successResponse = (data: any, message = "操作成功", meta = {}) => {
return Response.json({
success: true,
message,
data,
meta
});
};
// 错误响应
export const errorResponse = (
message: string,
error: string,
status = 500,
code = ErrorCode.SERVER_ERROR
) => {
return Response.json(
{
success: false,
message,
error,
code
},
{ status }
);
};
6.2 创建产品列表 API
创建 src/app/api/products/list/route.ts
文件:
typescript
import { NextRequest } from "next/server";
import { successResponse, errorResponse, ErrorCode } from "@/utils/apiResponse";
import productService from "@/services/productService";
/**
* 获取产品列表
* GET /api/products/list
*/
export async function GET(request: NextRequest) {
try {
// 从URL中获取查询参数
const searchParams = request.nextUrl.searchParams;
const {
name,
type,
page = "1",
pageSize = "10",
isHot
} = Object.fromEntries(searchParams.entries());
// 调用服务获取产品列表
const result = await productService.getProducts({
name,
type,
page: Number(page),
pageSize: Number(pageSize),
isHot: isHot === "true"
});
// 返回成功响应
return successResponse(result.data, "获取产品列表成功", {
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: Math.ceil(result.total / result.pageSize)
});
} catch (error) {
// 记录错误
console.error("获取产品列表失败:", error);
// 返回错误响应
return errorResponse(
"获取产品列表失败",
error instanceof Error ? error.message : "未知错误",
500,
ErrorCode.QUERY_ERROR
);
}
}
6.3 创建产品详情 API
创建 src/app/api/products/[id]/route.ts
文件:
typescript
import { NextRequest } from "next/server";
import { successResponse, errorResponse, ErrorCode } from "@/utils/apiResponse";
import productService from "@/services/productService";
/**
* 获取产品详情
* GET /api/products/[id]
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
if (isNaN(id)) {
return errorResponse(
"无效的产品ID",
"产品ID必须是数字",
400,
ErrorCode.VALIDATION_ERROR
);
}
const product = await productService.getProductById(id);
if (!product) {
return errorResponse(
"产品不存在",
`找不到ID为 ${id} 的产品`,
404,
ErrorCode.NOT_FOUND
);
}
return successResponse(product, "获取产品详情成功");
} catch (error) {
console.error(`获取产品详情失败:`, error);
return errorResponse(
"获取产品详情失败",
error instanceof Error ? error.message : "未知错误",
500,
ErrorCode.QUERY_ERROR
);
}
}
/**
* 更新产品
* PUT /api/products/[id]
*/
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
if (isNaN(id)) {
return errorResponse(
"无效的产品ID",
"产品ID必须是数字",
400,
ErrorCode.VALIDATION_ERROR
);
}
// 获取请求体
const productData = await request.json();
// 更新产品
const updatedProduct = await productService.updateProduct(id, productData);
if (!updatedProduct) {
return errorResponse(
"产品不存在",
`找不到ID为 ${id} 的产品`,
404,
ErrorCode.NOT_FOUND
);
}
return successResponse(updatedProduct, "更新产品成功");
} catch (error) {
console.error(`更新产品失败:`, error);
return errorResponse(
"更新产品失败",
error instanceof Error ? error.message : "未知错误",
500,
ErrorCode.SERVER_ERROR
);
}
}
/**
* 删除产品
* DELETE /api/products/[id]
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
if (isNaN(id)) {
return errorResponse(
"无效的产品ID",
"产品ID必须是数字",
400,
ErrorCode.VALIDATION_ERROR
);
}
// 删除产品
const success = await productService.deleteProduct(id);
if (!success) {
return errorResponse(
"产品不存在",
`找不到ID为 ${id} 的产品`,
404,
ErrorCode.NOT_FOUND
);
}
return successResponse(null, "删除产品成功");
} catch (error) {
console.error(`删除产品失败:`, error);
return errorResponse(
"删除产品失败",
error instanceof Error ? error.message : "未知错误",
500,
ErrorCode.SERVER_ERROR
);
}
}
6.4 创建创建产品 API
创建 src/app/api/products/route.ts
文件:
typescript
import { NextRequest } from "next/server";
import { successResponse, errorResponse, ErrorCode } from "@/utils/apiResponse";
import productService from "@/services/productService";
/**
* 创建产品
* POST /api/products
*/
export async function POST(request: NextRequest) {
try {
// 获取请求体
const productData = await request.json();
// 创建新产品
const newProduct = await productService.createProduct(productData);
return successResponse(newProduct, "创建产品成功", {}, 201);
} catch (error) {
console.error("创建产品失败:", error);
// 判断是否是验证错误
const errorMessage = error instanceof Error ? error.message : "未知错误";
const isValidationError = errorMessage.includes("验证错误");
return errorResponse(
"创建产品失败",
errorMessage,
isValidationError ? 400 : 500,
isValidationError ? ErrorCode.VALIDATION_ERROR : ErrorCode.SERVER_ERROR
);
}
}
7. 前端页面开发
有了后端 API,现在我们可以开发前端页面来展示和操作数据。
7.1 创建数据获取工具
首先,创建一个用于前端页面数据获取的工具函数。创建 src/services/api_products.ts
文件:
typescript
// 产品相关API
import axios from "axios";
// 产品列表查询参数接口
export interface ProductQuery {
page?: number;
pageSize?: number;
name?: string;
type?: string;
isHot?: boolean;
}
// 产品数据接口
export interface Product {
id: number;
name: string;
imagePath: string;
type: number;
carbonBlack: number;
ashContent: number;
dispersibility: number;
hot: boolean;
}
// 获取产品列表
export async function getProducts(query: ProductQuery = {}) {
try {
// 构建查询参数
const params = new URLSearchParams();
if (query.page) params.append("page", query.page.toString());
if (query.pageSize) params.append("pageSize", query.pageSize.toString());
if (query.name) params.append("name", query.name);
if (query.type) params.append("type", query.type);
if (query.isHot !== undefined)
params.append("isHot", query.isHot.toString());
// 发送请求
const response = await axios.get(`/api/products/list?${params.toString()}`);
return response.data;
} catch (error) {
console.error("获取产品列表失败:", error);
throw error;
}
}
// 获取产品详情
export async function getProductById(id: number) {
try {
const response = await axios.get(`/api/products/${id}`);
return response.data;
} catch (error) {
console.error(`获取产品 ID ${id} 详情失败:`, error);
throw error;
}
}
// 创建产品
export async function createProduct(data: Omit<Product, "id">) {
try {
const response = await axios.post("/api/products", data);
return response.data;
} catch (error) {
console.error("创建产品失败:", error);
throw error;
}
}
// 更新产品
export async function updateProduct(id: number, data: Partial<Product>) {
try {
const response = await axios.put(`/api/products/${id}`, data);
return response.data;
} catch (error) {
console.error(`更新产品 ID ${id} 失败:`, error);
throw error;
}
}
// 删除产品
export async function deleteProduct(id: number) {
try {
const response = await axios.delete(`/api/products/${id}`);
return response.data;
} catch (error) {
console.error(`删除产品 ID ${id} 失败:`, error);
throw error;
}
}
7.2 创建产品列表页面
创建 src/app/products/page.tsx
文件:
tsx
"use client";
import { useState, useEffect } from "react";
import { getProducts, Product } from "@/services/api_products";
import Link from "next/link";
// 分页数据结构
interface PageData {
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pageData, setPageData] = useState<PageData>({
total: 0,
page: 1,
pageSize: 10,
totalPages: 0
});
// 查询参数
const [searchName, setSearchName] = useState("");
const [selectedType, setSelectedType] = useState("");
// 加载产品数据
const loadProducts = async (
page = 1,
name = searchName,
type = selectedType
) => {
try {
setLoading(true);
const response = await getProducts({
page,
pageSize: pageData.pageSize,
name: name || undefined,
type: type || undefined
});
if (response.success) {
setProducts(response.data);
setPageData({
total: response.meta.total,
page: response.meta.page,
pageSize: response.meta.limit,
totalPages: response.meta.totalPages
});
setError(null);
} else {
setError(response.message || "获取产品列表失败");
}
} catch (err) {
setError("获取产品列表时发生错误");
console.error(err);
} finally {
setLoading(false);
}
};
// 首次加载
useEffect(() => {
loadProducts();
}, []);
// 处理搜索
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
loadProducts(1, searchName, selectedType);
};
// 处理页码变化
const handlePageChange = (page: number) => {
loadProducts(page, searchName, selectedType);
};
// 产品类型列表
const productTypes = [
{ value: "1", label: "类型一" },
{ value: "2", label: "类型二" },
{ value: "3", label: "类型三" }
];
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">产品列表</h1>
{/* 搜索表单 */}
<form onSubmit={handleSearch} className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label htmlFor="name" className="block mb-1">
产品名称
</label>
<input
type="text"
id="name"
value={searchName}
onChange={e => setSearchName(e.target.value)}
className="w-full p-2 border rounded"
placeholder="搜索产品名称"
/>
</div>
<div>
<label htmlFor="type" className="block mb-1">
产品类型
</label>
<select
id="type"
value={selectedType}
onChange={e => setSelectedType(e.target.value)}
className="w-full p-2 border rounded"
>
<option value="">全部类型</option>
{productTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
<div className="flex items-end">
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
搜索
</button>
</div>
</div>
</form>
{/* 错误提示 */}
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">{error}</div>
)}
{/* 加载状态 */}
{loading ? (
<div className="text-center py-8">加载中...</div>
) : (
<>
{/* 产品列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map(product => (
<div
key={product.id}
className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow"
>
<div className="h-48 bg-gray-200 relative">
{product.imagePath ? (
<img
src={product.imagePath}
alt={product.name}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
暂无图片
</div>
)}
{product.hot && (
<span className="absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded">
热门
</span>
)}
</div>
<div className="p-4">
<h3 className="text-lg font-semibold mb-2">{product.name}</h3>
<div className="text-sm text-gray-600 mb-4">
<p>
类型:{" "}
{productTypes.find(
t => t.value === product.type.toString()
)?.label || "未知"}
</p>
<p>炭黑含量: {product.carbonBlack}</p>
<p>灰分: {product.ashContent}</p>
<p>分散性: {product.dispersibility}</p>
</div>
<Link
href={`/products/${product.id}`}
className="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
查看详情
</Link>
</div>
</div>
))}
</div>
{/* 分页控件 */}
{products.length > 0 ? (
<div className="flex justify-center mt-8">
<nav className="inline-flex rounded-md shadow-sm">
<button
onClick={() => handlePageChange(pageData.page - 1)}
disabled={pageData.page === 1}
className={`px-3 py-1 rounded-l-md border ${
pageData.page === 1
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white text-blue-500 hover:bg-blue-50"
}`}
>
上一页
</button>
{Array.from(
{ length: pageData.totalPages },
(_, i) => i + 1
).map(page => (
<button
key={page}
onClick={() => handlePageChange(page)}
className={`px-3 py-1 border-t border-b ${
pageData.page === page
? "bg-blue-500 text-white"
: "bg-white text-blue-500 hover:bg-blue-50"
}`}
>
{page}
</button>
))}
<button
onClick={() => handlePageChange(pageData.page + 1)}
disabled={pageData.page === pageData.totalPages}
className={`px-3 py-1 rounded-r-md border ${
pageData.page === pageData.totalPages
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-white text-blue-500 hover:bg-blue-50"
}`}
>
下一页
</button>
</nav>
</div>
) : (
<div className="text-center py-8 text-gray-500">
没有找到符合条件的产品
</div>
)}
</>
)}
</div>
);
}
7.3 创建产品详情页面
创建 src/app/products/[id]/page.tsx
文件:
tsx
"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { getProductById, Product } from "@/services/api_products";
export default function ProductDetailPage() {
const params = useParams();
const router = useRouter();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// 产品类型映射
const productTypes = {
1: "类型一",
2: "类型二",
3: "类型三"
};
useEffect(() => {
// 获取产品ID
const productId = params?.id ? parseInt(params.id as string) : 0;
if (!productId) {
setError("无效的产品ID");
setLoading(false);
return;
}
// 加载产品详情
const loadProductDetail = async () => {
try {
setLoading(true);
const response = await getProductById(productId);
if (response.success) {
setProduct(response.data);
setError(null);
} else {
setError(response.message || "获取产品详情失败");
}
} catch (err) {
setError("获取产品详情时发生错误");
console.error(err);
} finally {
setLoading(false);
}
};
loadProductDetail();
}, [params?.id]);
// 返回列表页
const handleBackToList = () => {
router.push("/products");
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8 text-center">
<div className="text-2xl">加载中...</div>
</div>
);
}
if (error || !product) {
return (
<div className="container mx-auto px-4 py-8">
<div className="bg-red-100 text-red-700 p-4 rounded mb-4">
{error || "找不到产品信息"}
</div>
<button
onClick={handleBackToList}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
返回产品列表
</button>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<button
onClick={handleBackToList}
className="mb-4 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 flex items-center"
>
<span>← 返回产品列表</span>
</button>
<div className="bg-white rounded-lg shadow-md overflow-hidden">
<div className="md:flex">
<div className="md:w-1/2">
{product.imagePath ? (
<img
src={product.imagePath}
alt={product.name}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-64 bg-gray-200 text-gray-500">
暂无图片
</div>
)}
</div>
<div className="p-6 md:w-1/2">
<div className="flex justify-between items-start">
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
{product.hot && (
<span className="bg-red-500 text-white text-sm px-3 py-1 rounded-full">
热门产品
</span>
)}
</div>
<div className="mb-6 pb-6 border-b">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-gray-600">产品类型</p>
<p className="font-semibold">
{productTypes[product.type as keyof typeof productTypes] ||
"未知"}
</p>
</div>
<div>
<p className="text-gray-600">炭黑含量</p>
<p className="font-semibold">{product.carbonBlack}</p>
</div>
<div>
<p className="text-gray-600">灰分</p>
<p className="font-semibold">{product.ashContent}</p>
</div>
<div>
<p className="text-gray-600">分散性</p>
<p className="font-semibold">{product.dispersibility}</p>
</div>
</div>
</div>
<div className="flex space-x-4">
<button className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
联系我们
</button>
<button className="px-6 py-3 border border-blue-500 text-blue-500 rounded-lg hover:bg-blue-50">
了解更多
</button>
</div>
</div>
</div>
</div>
</div>
);
}
8. 数据库管理与迁移
在实际开发过程中,数据库结构往往需要随着业务需求的变化而演变。TypeORM 提供了强大的迁移功能,帮助我们管理数据库架构变更。
8.1 创建迁移配置
首先,在项目根目录创建 typeorm.config.ts
文件:
typescript
import "reflect-metadata";
import { DataSource } from "typeorm";
import * as path from "path";
import * as dotenv from "dotenv";
// 加载环境变量
dotenv.config();
// 创建数据源
export const AppDataSource = new DataSource({
type: "mysql",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "3306"),
username: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD || "password",
database: process.env.DB_NAME || "nextjs_db",
entities: [path.join(__dirname, "src/entities/**/*.entity.{ts,js}")],
migrations: [path.join(__dirname, "src/migrations/**/*.{ts,js}")],
migrationsTableName: "migrations",
logging: true,
synchronize: false // 迁移时禁用同步
});
8.2 添加迁移脚本
在 package.json
的 scripts
部分添加以下命令:
json
{
"scripts": {
// 其他脚本...
"typeorm": "typeorm-ts-node-commonjs",
"migration:create": "npm run typeorm -- migration:create src/migrations/",
"migration:generate": "npm run typeorm -- -d ./typeorm.config.ts migration:generate src/migrations/",
"migration:run": "npm run typeorm -- -d ./typeorm.config.ts migration:run",
"migration:revert": "npm run typeorm -- -d ./typeorm.config.ts migration:revert"
}
}
8.3 生成迁移文件
当您更改实体定义后,可以自动生成迁移文件:
bash
npm run migration:generate -- InitialMigration
这会根据您当前的实体与数据库结构的差异,自动生成迁移文件。
8.4 运行迁移
执行所有待执行的迁移:
bash
npm run migration:run
如需回滚最后一次迁移:
bash
npm run migration:revert
9. Docker 部署
使用 Docker 可以简化应用的部署流程,确保开发和生产环境的一致性。
9.1 创建 Dockerfile
在项目根目录创建 Dockerfile
:
dockerfile
# 使用 Node.js 18 作为基础镜像
FROM node:18-alpine AS base
# 设置工作目录
WORKDIR /app
# 安装依赖
FROM base AS deps
COPY package.json yarn.lock* ./
RUN yarn install --frozen-lockfile
# 构建应用
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
# 生产环境
FROM base AS runner
ENV NODE_ENV production
# 创建非root用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
# 复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 设置环境变量
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# 暴露端口
EXPOSE 3000
# 启动命令
CMD ["node", "server.js"]
9.2 创建 docker-compose.yml
创建 docker-compose.yml
文件,用于同时启动应用和 MySQL 数据库:
yaml
version: "3.8"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=password
- DB_NAME=nextjs_db
depends_on:
- db
db:
image: mysql:8
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=nextjs_db
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
9.3 使用 docker-compose 部署
使用以下命令启动应用:
bash
docker-compose up -d
停止应用:
bash
docker-compose down
10. 项目优化和最佳实践
10.1 性能优化
- 合理使用缓存:对于频繁访问但不常变化的数据,可以使用 Redis 进行缓存
typescript
// 示例:Redis缓存的产品服务
async getProductById(id: number): Promise<Products | null> {
try {
// 尝试从缓存获取
const cachedProduct = await redisClient.get(`product:${id}`);
if (cachedProduct) {
return JSON.parse(cachedProduct);
}
// 从数据库获取
const productRepository = await this.getRepository();
const product = await productRepository.findOneBy({ id });
// 设置缓存
if (product) {
await redisClient.set(`product:${id}`, JSON.stringify(product), {
EX: 3600 // 1小时过期
});
}
return product;
} catch (error) {
console.error(`获取产品 ID ${id} 失败:`, error);
throw error;
}
}
- 数据库索引优化:为常用查询字段添加索引
typescript
@Entity("products")
export class Products {
@PrimaryGeneratedColumn()
id: number;
@Column({ index: true }) // 添加索引
name: string;
@Column({ index: true }) // 添加索引
type: number;
// 其他字段...
}
- 分页和延迟加载:使用 TypeORM 的关系延迟加载优化性能
typescript
@Entity("products")
export class Products {
// ...
@ManyToOne(() => Category)
@JoinColumn({ name: "category_id" })
category: Category;
@OneToMany(() => ProductImage, image => image.product, { lazy: true })
images: Promise<ProductImage[]>;
}
10.2 安全最佳实践
- 输入验证:使用 class-validator 验证所有输入
typescript
export class CreateProductDto {
@IsNotEmpty()
@IsString()
@Length(2, 100)
name: string;
@IsString()
imagePath: string;
@IsNumber()
@Min(1)
@Max(3)
type: number;
@IsNumber()
@IsOptional()
carbonBlack: number;
@IsNumber()
@IsOptional()
ashContent: number;
@IsNumber()
@IsOptional()
dispersibility: number;
@IsBoolean()
@IsOptional()
hot: boolean;
}
-
使用环境变量存储敏感信息:确保数据库凭证等敏感信息通过环境变量注入
-
实现 API 认证:使用 JWT 或其他认证机制保护 API 端点
typescript
// 中间件:验证 API 令牌
export async function validateApiToken(req: NextRequest) {
const token = req.headers.get("Authorization")?.split(" ")[1];
if (!token) {
return new Response(JSON.stringify({ message: "未提供授权令牌" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
try {
// 验证令牌
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
return null; // 验证通过
} catch (error) {
return new Response(JSON.stringify({ message: "无效的授权令牌" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
}
// 在 API 路由中使用
export async function GET(request: NextRequest) {
// 验证令牌
const authError = await validateApiToken(request);
if (authError) return authError;
// 处理请求...
}
11. 结语
通过本教程,我们学习了如何使用 Next.js 和 MySQL 构建全栈应用,包括数据库配置、实体设计、服务层实现、API 路由开发和前端页面构建。这些知识和技能将帮助您开发功能完善、性能优异的 Web 应用程序。
随着项目的发展,您可能需要考虑引入更多高级功能,如用户认证、文件上传、实时通信等。Next.js 生态系统提供了丰富的工具和库,可以满足各种复杂需求。
希望本教程对您有所帮助,祝您开发愉快!