前言
在本篇博客中,我将分享如何构建一个类似于小米商城的移动端全栈项目。项目采用前后端分离的架构,后端使用了 Nest.js 框架,数据库选择 MySQL 来存储用户信息和商品数据。前端通过 ** Fetch** 与后端进行数据交互,确保数据的实时性与准确性。作为一个刚刚学习 Nest.js 的开发者,在项目开发过程中遇到了不少问题,并通过实践找到了相应的解决方案。希望这篇博客能对正在进行全栈开发的你有所帮助。
小米商城地址: 点击这里<<<
项目目录结构
在开始项目之前,了解项目的目录结构是非常重要的。以下是本项目的目录结构及其说明:
mishop/
├── dist/ # 编译后的输出文件
├── node_modules/ # 项目依赖的模块
├── public/ # 前端静态资源
│ ├── CSS/ # 样式文件
│ │ ├── all.css
│ │ ├── home.css
│ │ ├── login.css
│ │ ├── my.css
│ │ ├── shopping.css
│ │ ├── sort.css
│ ├── html/ # HTML页面
│ │ ├── Home.html
│ │ ├── login.html
│ │ ├── my.html
│ │ ├── shopping.html
│ │ ├── sort.html
│ │ └── Rice-Circle.html
│ └── images/ # 图片资源
├── src/ # 后端源码
│ ├── goods/ # 商品相关模块
│ ├── swiper/ # 轮播图模块
│ ├── user/ # 用户模块
│ │ ├── user.controller.ts
│ │ ├── user.entity.ts
│ │ ├── user.module.ts
│ │ ├── user.service.ts
│ ├── app.controller.ts # 应用控制器
│ ├── app.controller.spec.ts # 控制器测试文件
│ ├── app.module.ts # 应用模块
│ ├── app.service.ts # 应用服务
│ └── main.ts # 应用入口
├── .eslintrc.js # ESLint配置
├── .gitignore # Git忽略文件
├── .prettierrc # Prettier配置
├── nest-cli.json # Nest CLI配置
├── package-lock.json # 依赖锁定文件
├── package.json # 项目依赖和脚本
├── README.md # 项目说明文件
├── tsconfig.build.json # TypeScript构建配置
└── tsconfig.json # TypeScript配置
目录说明:
- dist/:存放编译后的后端代码。
- node_modules/:项目的依赖模块,由
npm
或pnpm
管理。 - public/:存放前端的静态资源,包括CSS、HTML和图片。
- CSS/:存放各个页面的样式文件。
- html/:存放各个页面的HTML文件。
- images/:存放项目所需的图片资源。
- src/:后端源码目录。
- goods/:商品相关的模块,负责商品数据的管理。
- swiper/:轮播图模块,管理首页的轮播图数据。
- user/:用户模块,处理用户的注册、登录等功能。
- 其他文件如
app.controller.ts
、app.module.ts
等是Nest.js应用的核心文件。
- 其他配置文件如
.eslintrc.js
、.prettierrc
等用于代码规范和格式化。
注意事项
小米商城移动端项目主要针对移动设备开发,如果您在PC端进行访问,需要使用浏览器的移动模拟器来查看效果:
- 按下 F12 进入开发者工具。
- 按下 Ctrl + Shift + M 切换到移动设备模式。
否则,您可能会被重定向到小米官网。
1. 引入 Nest.js
首先,确保您的电脑已安装 Node.js,安装 Node.js 时会附带 npx 和 npm。接下来,使用以下命令全局安装 Nest CLI 并创建新项目:
pnpm i -g @nestjs/cli # 全局安装 Nest CLI
nest new mishop # 创建项目
在创建项目过程中,选择使用 pnpm 作为包管理工具。如果遇到问题,可以切换到 npm。
创建完成后,项目结构会自动生成。接下来,我们需要设置数据库,并在数据库中创建相应的表,最后通过 Nest.js 连接数据库。
2. 连接数据库
创建数据库与表
我选择使用 MySQL 作为数据库,并使用 phpMyAdmin 或 MySQL Workbench 创建数据库和表。以下是我创建的主要表结构:
goods:商品信息
users:用户信息
swiper:轮播图数据
配置数据库连接
在项目根目录下安装 TypeORM 及相关依赖:
npm install @nestjs/typeorm typeorm mysql
然后,在 src/app.module.ts 中配置数据库连接:
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { SwiperModule } from './swiper/swiper.module';
import { GoodsModule } from './goods/goods.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'your_password',
database: 'mishop',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // 开发环境下可以使用,生产环境建议关闭
}),
UserModule,
SwiperModule,
GoodsModule,
// 其他模块
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
安装 VSCode 插件
推荐安装 Database Client 插件,方便在 VSCode 中管理和操作 MySQL 数据库。
3. 首页开发
创建轮播图模块
首先,使用 Nest CLI 生成 Swiper 模块:
nest g res swiper
选择 REST API 模式。然后,在 src/swiper/entities/swiper.entity.ts 中定义实体:
@Entity('swiper')
export class Swiper {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'varchar',
length: 255,
comment: '图片URL',
})
imageUrl: string;
@Column({
type: 'varchar',
length: 100,
comment: '链接地址',
})
link: string;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
createdAt: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
comment: '更新时间',
})
updatedAt: Date;
}
编写控制器与服务
在 src/swiper/swiper.controller.ts 中添加获取所有轮播图的接口:
import { SwiperService } from './swiper.service';
import { Swiper } from './entities/swiper.entity';
@Controller('api/swiper')
export class SwiperController {
constructor(private readonly swiperService: SwiperService) {}
@Get('all')
findAll(): Promise<Swiper[]> {
return this.swiperService.findAll();
}
}
在 src/swiper/swiper.service.ts 中实现数据查询:
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Swiper } from './entities/swiper.entity';
@Injectable()
export class SwiperService {
constructor(
@InjectRepository(Swiper)
private readonly swiperRepository: Repository<Swiper>,
) {}
findAll(): Promise<Swiper[]> {
return this.swiperRepository.find();
}
}
前端获取并渲染轮播图
在前端页面中,通过 Fetch 获取轮播图数据并渲染:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>小米商城首页</title>
<link rel="stylesheet" href="../CSS/home.css">
<!-- 引入 Swiper 样式 -->
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css">
</head>
<body>
<div class="swiper-container">
<div class="swiper-wrapper" id="carouselBox">
<!-- 轮播图内容 -->
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<!-- 引入 Swiper JS -->
<script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const carouselBox = document.getElementById('carouselBox');
fetch('http://localhost:3000/api/swiper/all')
.then(response => response.json())
.then(data => {
let html = '';
data.forEach(item => {
html += `
<div class="swiper-slide">
<a href="${item.link}">
<img src="${item.imageUrl}" alt="轮播图" />
</a>
</div>
`;
});
carouselBox.innerHTML = html;
// 初始化 Swiper
new Swiper('.swiper-container', {
loop: true,
pagination: {
el: '.swiper-pagination',
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
});
})
.catch(error => {
console.error('获取轮播图数据失败:', error);
});
});
</script>
</body>
</html>
4. 登录功能开发
后端实现登录接口
首先,生成 User 模块:
nest g res user
在 src/user/entities/user.entity.ts 中定义用户实体:
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'varchar',
length: 50,
unique: true,
comment: '用户名',
})
username: string;
@Column({
type: 'varchar',
length: 100,
comment: '密码',
})
password: string;
@Column({
type: 'varchar',
length: 255,
nullable: true,
comment: '头像URL',
})
avatar: string;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
createdAt: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
comment: '更新时间',
})
updatedAt: Date;
}
在 src/user/user.controller.ts 中添加登录接口:
import { UserService } from './user.service';
@Controller('api/user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
login(@Body() loginDto: { username: string; password: string }) {
return this.userService.login(loginDto);
}
}
在 src/user/user.service.ts 中实现登录逻辑:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async login(data: { username: string; password: string }) {
const user = await this.userRepository.findOne({
where: { username: data.username, password: data.password },
});
if (user) {
return {
code: 0,
data: {
id: user.id,
username: user.username,
avatar: user.avatar,
// 可以添加 Token 生成逻辑
},
message: '登录成功',
};
} else {
return {
code: 1,
message: '用户名或密码错误',
data: null,
};
}
}
}
前端实现登录功能
在登录页面中,用户输入用户名和密码后,通过 Fetch 将数据发送到后端进行验证:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>小米商城登录</title>
<link rel="stylesheet" href="../CSS/login.css">
</head>
<body>
<form id="loginForm">
<input type="text" id="username" placeholder="用户名" required />
<input type="password" id="password" placeholder="密码" required />
<button type="submit">登录</button>
<p id="errorMsg" style="color: red;"></p>
</form>
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('errorMsg');
fetch('http://localhost:3000/api/user/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
})
.then(response => response.json())
.then(data => {
if (data.code === 0) {
console.log('登录成功');
localStorage.setItem('avatar', data.data.avatar);
localStorage.setItem('username', data.data.username);
// 可以存储 Token
window.location.href = './Home.html';
} else {
errorMsg.textContent = '用户名或密码错误';
}
})
.catch(error => {
console.error('登录请求失败:', error);
errorMsg.textContent = '服务器错误,请稍后再试';
});
});
</script>
</body>
</html>
5. 其他页面开发
根据项目目录结构,其他页面如 Home.html、my.html、shopping.html、sort.html 和 Rice-Circle.html 的开发过程类似。以下以分类页面 (sort.html) 为例进行说明。
创建分类模块
首先,使用 Nest CLI 生成 Goods 模块:
nest g res goods
在 src/goods/entities/goods.entity.ts 中定义商品实体:
@Entity('goods')
export class Goods {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'varchar',
length: 100,
comment: '商品名称',
})
name: string;
@Column({
type: 'decimal',
precision: 10,
scale: 2,
comment: '价格',
})
price: number;
@Column({
type: 'int',
comment: '库存数量',
})
stock: number;
@Column({
type: 'varchar',
length: 255,
comment: '商品图片URL',
})
imageUrl: string;
@Column({
type: 'uuid',
comment: '分类ID',
})
categoryId: string;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
createdAt: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
comment: '更新时间',
})
updatedAt: Date;
}
编写控制器与服务
在 src/goods/goods.controller.ts 中添加获取分类商品的接口:
import { GoodsService } from './goods.service';
import { Goods } from './entities/goods.entity';
@Controller('api/goods')
export class GoodsController {
constructor(private readonly goodsService: GoodsService) {}
@Get('category/:id')
findByCategory(@Param('id') categoryId: string): Promise<Goods[]> {
return this.goodsService.findByCategory(categoryId);
}
}
在 src/goods/goods.service.ts 中实现数据查询:
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Goods } from './entities/goods.entity';
@Injectable()
export class GoodsService {
constructor(
@InjectRepository(Goods)
private readonly goodsRepository: Repository<Goods>,
) {}
findByCategory(categoryId: string): Promise<Goods[]> {
return this.goodsRepository.find({ where: { categoryId } });
}
}
前端获取并渲染分类商品
在分类页面中,通过 Fetch 获取商品数据并渲染:
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>小米商城分类</title>
<link rel="stylesheet" href="../CSS/sort.css">
</head>
<body>
<div id="goodsList">
<!-- 商品列表内容 -->
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const goodsList = document.getElementById('goodsList');
const categoryId = 'your_category_id'; // 替换为实际分类ID
fetch(`http://localhost:3000/api/goods/category/${categoryId}`)
.then(response => response.json())
.then(data => {
let html = '';
data.forEach(item => {
html += `
<div class="goods-item">
<img src="${item.imageUrl}" alt="${item.name}" />
<h3>${item.name}</h3>
<p>价格:¥${item.price}</p>
<p>库存:${item.stock}</p>
</div>
`;
});
goodsList.innerHTML = html;
})
.catch(error => {
console.error('获取商品数据失败:', error);
});
});
</script>
</body>
</html>
6、完成小米商城时出现的错误记录
在开发过程中,我遇到了一些问题,以下是我遇到的几个主要错误及其解决方法:
数据库连接失败
问题描述:启动服务器时报错,提示无法连接到数据库。
解决方法:检查数据库配置文件中的主机、端口、用户名和密码是否正确。同时,确保 MySQL 服务已启动。
1,跨域请求被拒绝
问题描述:前端通过 Fetch 请求后端接口时,出现跨域错误。
解决方法:在 Nest.js 中配置 CORS,允许前端域名的请求。
// main.ts
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:8080', // 前端地址
credentials: true,
});
await app.listen(3000);
}
bootstrap();
2,前端数据渲染不正确
问题描述:前端页面无法正确显示从后端获取的数据。
解决方法:检查 Fetch 请求的URL是否正确,确保后端接口正常返回数据。同时,检查前端的渲染逻辑是否有误。
3,Token鉴权失败
问题描述:用户登录后无法正确获取Token,导致鉴权失败。
解决方法:在后端登录接口中正确生成并返回Token,前端在存储和传递Token时确保其正确性。
总结
这是我第一次尝试进行全栈开发,整个小米商城项目让我受益匪浅。从前端页面的设计到后端接口的实现,再到数据库的管理,每一个环节都让我学到了新的知识和技能。
个人感悟:
之前我一直专注于前端开发,对于后端和数据库的了解非常有限。这次全栈项目的开发让我认识到 Node.js 在全栈开发中的重要性。通过 Nest.js,我能够轻松地编写后端接口,进行数据库操作,并且通过 Fetch 实现前后端的数据交互。
项目收获:
全栈思维:学会了如何将前端和后端结合起来,构建一个完整的应用。
技术提升:掌握了 Nest.js、TypeORM、MySQL 和 Fetch 的基本使用方法。
问题解决能力:在遇到各种错误时,通过查阅资料和实践,逐步解决了问题,提高了自己的问题解决能力。