小米商城移动端全栈项目开发实战经验

前言

在本篇博客中,我将分享如何构建一个类似于小米商城的移动端全栈项目。项目采用前后端分离的架构,后端使用了 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/:项目的依赖模块,由 npmpnpm 管理。
  • public/:存放前端的静态资源,包括CSS、HTML和图片。
    • CSS/:存放各个页面的样式文件。
    • html/:存放各个页面的HTML文件。
    • images/:存放项目所需的图片资源。
  • src/:后端源码目录。
    • goods/:商品相关的模块,负责商品数据的管理。
    • swiper/:轮播图模块,管理首页的轮播图数据。
    • user/:用户模块,处理用户的注册、登录等功能。
    • 其他文件如 app.controller.tsapp.module.ts 等是Nest.js应用的核心文件。
  • 其他配置文件如 .eslintrc.js.prettierrc 等用于代码规范和格式化。

注意事项

小米商城移动端项目主要针对移动设备开发,如果您在PC端进行访问,需要使用浏览器的移动模拟器来查看效果:

  1. 按下 F12 进入开发者工具。
  2. 按下 Ctrl + Shift + M 切换到移动设备模式。

否则,您可能会被重定向到小米官网。


1. 引入 Nest.js

首先,确保您的电脑已安装 Node.js,安装 Node.js 时会附带 npxnpm。接下来,使用以下命令全局安装 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 的基本使用方法。

问题解决能力:在遇到各种错误时,通过查阅资料和实践,逐步解决了问题,提高了自己的问题解决能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值