二月份的时候我写了一篇React + Express + Socket.io之间的实时通信【2】:注册登录,那时候我还在用Express作为后端框架。
因为中途想到使用TypeScript,所以我决定迁移到NestJS。
前端
先说一下我对前端的改动。
因为是想要整个项目都用TypeScript,所以我把src
目录下的所有.js
文件都改成了.tsx
。
很多文件都不需要改动,例如App.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import "./App.css"; import "bootstrap/dist/css/bootstrap.min.css"; import Home from "./components/Home"; import Login from "./components/Login"; import Register from "./components/Register"; import PrivateMessageHomepage from "./components/private_message_homepage/Private_Message_Homepage"; import PrivateMessageChatpage from "./components/private_message_chatpage/Private_Message_Chatpage"; import socket from "./components/utils/actions/authActions";
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={ <Navigate to="/channels/@me" replace /> } /> <Route path="/channels/@me" element={ <Home /> }> <Route path="" element={ <PrivateMessageHomepage style={{ flex: '1 1 auto' }} /> } /> <Route path="dummy" element={ <PrivateMessageChatpage style={{ flex: '1 1 auto' }} /> }/> </Route>
<Route path="/login" element={ <Login socket={ socket } /> } /> <Route path="/register" element={ <Register /> } /> </Routes> </BrowserRouter> ); }
export default App;
|
App.tsx
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import "bootstrap/dist/css/bootstrap.min.css"; import "./App.css"; import Home from "./components/Home"; import Login from "./components/Login"; import Register from "./components/Register"; import Guard from "./components/utils/guard";
import PrivateMessageChatPage from "./components/private_message_chat_page/Private_Message_Chat_Page";
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={ <Navigate to="/channels/@me" replace /> } /> <Route path="/channels/@me" element={ <Guard /> }> <Route path="" element={ <Home />} /> <Route path="dummy" element={ <PrivateMessageChatPage style={{ flex: '1 1 auto' }} /> }/> </Route>
<Route path="/login" element={ <Login /> } /> <Route path="/register" element={ <Register /> } /> </Routes> </BrowserRouter> ); }
export default App;
|
对比一下会发现其实没有变化,PrivateMessageHomepage
改成了Guard
只是因为业务逻辑的改动,跟TypeScript无关。
Redux
涉及到Redux的文件多多少少都有一些改动。
store.js
:
1 2 3 4 5 6 7 8
| import { configureStore } from "@reduxjs/toolkit"; import authSlice from "./reducers/authSlice";
export default configureStore({ reducer: { auth: authSlice } });
|
store.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { configureStore } from "@reduxjs/toolkit"; import { useDispatch } from "react-redux"; import authSlice from "./reducers/authSlice";
const store = configureStore({ reducer: { auth: authSlice, } })
export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>();
export default store;
|
原先的store.js
是直接导出了configureStore
的返回值;store.ts
先是导出了RootState
和AppDispatch
这两个类型,然后道出了useAppDispatch
这个自定义Hook、以替代useDispatch
。
authSlice.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { createSlice } from "@reduxjs/toolkit";
export const authSlice = createSlice({ name: "auth", initialState: { isAuthenticated: false, user: {}, error: null }, reducers: { setCurrentUser: (state, action) => { state.isAuthenticated = true; state.user = action.payload; }, setError: (state, action) => { state.error = action.payload; } } });
export const { setCurrentUser, setError } = authSlice.actions; export default authSlice.reducer;
|
authSlice.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { User } from '../interfaces';
interface AuthState { isAuthenticated: boolean; user: User | null; error: string | null; }
const initialState: AuthState = { isAuthenticated: false, user: null, error: null, };
export const authSlice = createSlice({ name: 'auth', initialState, reducers: { setCurrentUser: (state, action: PayloadAction<User>) => { state.isAuthenticated = true; state.user = action.payload; }, setError: (state, action: PayloadAction<string>) => { state.error = action.payload; }, }, });
export const { setCurrentUser, setError } = authSlice.actions; export default authSlice.reducer;
|
authSlice.js
和authSlice.ts
的区别在于action
的类型声明。TypeScript是JavaScript的超集,目的是为了更好地进行静态类型检查,以避免各种各样的错误。要知道JavaScript是弱类型语言,这意味着你可以在不同的地方使用不同的类型而不报错。
interface
关键字用于定义一个接口,接口是一种抽象的结构、定义了一个对象应该具有的属性和方法。AuthState
接口被定义后,有三个属性:isAuthenticated
、user
和error
。然后设置initialState
为AuthState
类型。
也就是说initialState
无论怎么改动,都必须符合AuthState
的结构。假设我在initialState
的isAuthenticated
属性后面加了一个isRegistered
属性,那么在reducers
中的state
就会报错,因为isRegistered
属性并不在AuthState
中。
setCurrentUser
和setError
的action
参数都是PayloadAction
类型,PayloadAction
是一个泛型接口,接受一个类型参数,这个类型参数就是action.payload
的类型。这样一来,action.payload
的类型就被限制了,不会出现不符合预期的情况。
比方说setError
的action
参数就被限制为string
类型。
我还新建了一个interfaces.ts
文件来存放会被多个文件引用的接口:
1 2 3 4 5 6 7
| export interface User { id?: number; emailAddress?: string; username: string; password?: string; access_token?: string; }
|
属性后面的?
表示这个属性是可选的。想象一下,User
接口会被注册、登录以及登出的组件引用:
- 注册时,用户传来的数据中不会有
id
、access_token
这两个属性
- 登录时,用户传来的数据中不会有
emailAddress
这个属性
- 登出时,用户传来的数据中不会有
password
这个属性
所以这些属性都是可选的。
Axios
我本来是全程使用Socket.io来进行通信,这也包括了登录、注册等操作。但是Socket.io并不适合用来做这些操作,所以我还是用了Axios。
比方说注册用户,使用Socket.io的话就是这样:
1 2 3 4 5 6 7 8 9
| const registerUser = (userData, navigate) => { return dispatch => { socket.emit("register", userData); socket.on("newRegisteredUser", data => { data.status === "00000" ? navigate("/login") : console.log(data.message); }); } };
|
先发送register
事件,然后接收从服务端发来的newRegisteredUser
事件,根据data.status
的值来决定跳转到登录页面还是打印错误信息。
换成Axios的话,要先创建一个服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import axios from 'axios'; import { User } from "../interfaces";
const api = axios.create({ baseURL: 'http://localhost:4000/api', });
export const UsersService = { register: (data: User) => { return api.post('/users/register', data); },
login: (data: User) => { return api.post('/users/login', data); }, }
|
使用baseURL
的好处是如果后端地址改变、只需要改动一次就行了。
UsersService
对象中有两个方法:register
和login
,分别用于注册和登录。当我们需要向后端发送请求时,只需要调用这些方法即可:
1 2 3 4 5 6 7 8 9 10 11
| const registerUser = (userData: User, navigate: (path: string) => void) => { return async (dispatch: Dispatch) => { try { const response = await UsersService.register(userData); const data = response.data; if (data.status === "00000") navigate("/login"); } catch (error) { console.error(error); } } }
|
我还添加了一个try...catch
语句,用于捕获请求失败的情况。
登录和注册
在写函数组件时,需要定义函数组件的类型:
1 2 3 4 5
| import React from "react";
const Register: React.FC = () => { }
|
如果该函数组件还有props,那么就需要定义props的类型:
1 2 3 4 5 6 7 8 9
| import React from 'react';
interface PrivateMessageHomepageProps { style: React.CSSProperties; }
const PrivateMessageHomepage: React.FC<PrivateMessageHomepageProps> = ({ style }) => { }
|
例如这里的PrivateMessageHomepage
组件有一个style
属性,所以需要定义其类型为React.CSSProperties
,毕竟是一个CSS样式对象嘛。
之前讲到共用的User
接口,但对于注册来说还需要4个必需的属性:emailAddress
、birthYear
、birthMonth
和birthDay
。其中emailAddress
虽在User
接口中,但是是可选的,不符合注册的要求。
所以我们可以使用&
运算符来合并两个接口:
1 2 3 4 5 6 7 8
| import { User } from "./utils/interfaces";
type RegisterUser = User & { emailAddress: string; birthYear: string; birthMonth: string; birthDay: string; }
|
RegisterUser
接口继承了User
接口,并添加了4个必需的属性。后加的属性会覆盖前面的属性,所以User
接口中的emailAddress
属性被覆盖了。
之后再使用useState
来定义userData
:
1 2 3 4 5 6 7 8
| const [userData, setUserData] = useState<RegisterUser>({ username: "", emailAddress: "", password: "", birthYear: "", birthMonth: "", birthDay: "" });
|
登录时我们不需要User
接口中其他的属性,只需要username
和password
属性。所以我们可以挑选出需要的属性:
1
| type LoginUser = Pick<User, "username" | "password">;
|
Pick
的用处是从一个对象中挑选出一些属性,返回一个新的对象。这意味着LoginUser
接口只包含User
接口中的username
和password
属性。
像是需要展现出用户名的地方我们也可以这样写:
1
| type UserProfile = Pick<User, "username">;
|
页面跳转
最前面提及到的Guard
组件是用来判断用户是否登录的,如果用户没有登录,那么就跳转到登录页面:
1 2 3 4 5 6 7 8 9 10 11
| import React from "react"; import { Navigate, Outlet } from "react-router-dom";
const Guard: React.FC = () => { const auth = localStorage.getItem("auth");
if (auth) return <Outlet/>; else return <Navigate to="/login" />; }
export default Guard;
|
如果localStorage
中有auth
这个键,那么就渲染Outlet
组件,否则就跳转到登录页面。
Outlet
组件是用来渲染子路由的。回到App.tsx
,能看到/channels/@me
路由被Guard
组件保护,子路由分别是默认的Home
组件和dummy
子路由。
也就是说用户在登陆后跳转到/channels/@me
路由,会被Guard
组件验证,然后渲染Home
组件。
/channels/@me/dummy
路由是用来测试私聊的,但是它也在Guard
组件的保护之下。
其他
我的项目中有一些按钮在被鼠标悬停时会有一些样式变化,原先的逻辑是这样的:
1 2 3 4
| const [hoverStates, setHoverStates] = useState({}); const updateHoverState = (item, isHovered) => { setHoverStates(prev => ({ ...prev, [item]: isHovered })); }
|
转换到TypeScript后:
1 2 3 4 5 6 7
| const [hoverStates, setHoverStates] = useState<Record<string, boolean>>({}); const updateHoverState = (item: string, isHovered: boolean) => { setHoverStates({ ...hoverStates, [item]: isHovered }); }
|
Record
的第一个参数定义了键的类型,第二个参数定义了值的类型。hoverStates
被定义为一个字典,键和值再被定义为string
和boolean
类型。
输入框组件里分别有着:
- 判断用户是否按下了回车键的
handleKeyDown
函数
- 处理用户输入信息的
handleChange
函数
- 处理用户提交表单的
handleSendMessage
函数
这些函数都需要定义类型:
1 2 3 4 5
| const handleKeyDown = (e: KeyboardEvent) => {}
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {}
const handleSendMessage = (e: FormEvent) => {}
|
后端
NestJS是一个基于Node.js的后端框架,它使用TypeScript编写,提供了一些装饰器来简化开发。
我目前有的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { "@nestjs/common": "^10.3.3", "@nestjs/core": "^10.3.3", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "*", "@nestjs/mongoose": "^10.0.4", "@nestjs/platform-express": "^10.3.3", "@nestjs/platform-socket.io": "^10.3.3", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.3", "bcrypt": "^5.1.1", "class-validator": "^0.14.1", "cookie-parser": "^1.4.6", "dotenv": "^16.4.5", "mongoose": "^8.2.1", "morgan": "^1.10.0", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1", "typeorm": "^0.3.20" }
|
NestJS的入口文件是main.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import * as dotenv from 'dotenv' import { Server } from 'socket.io'
dotenv.config()
async function bootstrap() { const app = await NestFactory.create(AppModule) app.enableCors({ origin: process.env.CLIENT_ORIGIN || 'http://localhost:3000', credentials: true, }) app.setGlobalPrefix('api')
const server = app.getHttpServer() new Server(server)
await app.listen(process.env.PORT || 4000) }
bootstrap().catch((err) => console.error(err))
|
dotenv
是用来读取.env
文件的,.env
文件用来存放环境变量。CLIENT_ORIGIN
是前端的地址,PORT
是后端的端口。
因为前后端分离的项目中,前端和后端是不同的域名,所以会有跨域问题。app.enableCors
方法用来解决跨域问题,origin
参数是前端的地址,credentials
参数是true
表示允许携带cookie。
app.setGlobalPrefix
方法用来设置全局前缀,所有的路由都会加上这个前缀。比方说后面设置的/users/register
路由会变成/api/users/register
。
app.getHttpServer
方法返回一个http.Server
实例,new Server(server)
用来创建一个Socket.io服务器。
最后调用app.listen
方法来启动服务器。
NestJS概念
NestJS目前支持两个HTTP平台:Express和Fastify。这是因为NestJS的开发团队认为NestJS立志于成为一个模块化的框架,不单单是一个HTTP框架。只要创建了适配器,NestJS就可以在任何平台上运行。
NestJS的核心概念有:
- 控制器(Controller)
- 服务(Service)
- 模块(Module)
控制器
控制器是处理传入请求的地方,它们会调用服务来完成请求。控制器的方法可以使用装饰器来定义路由。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Controller, Get } from '@nestjs/common' import { AppService } from './app.service'
@Controller() export class AppController { constructor(private readonly appService: AppService) {}
@Get() getHello(): string { return this.appService.getHello() } }
|
@Controller
装饰器用来定义一个控制器。假设我们的后端地址是http://localhost:4000
,那么@Controller()
装饰器的参数就是http://localhost:4000
。
@Get()
装饰器用来定义一个GET请求,这个请求的路径就是控制器的路径,也就是请求http://localhost:4000
。
假设我想要请求http://localhost:4000/api/users/register
,那么就要写成:
1 2 3
| @Controller('users')
@Get('register')
|
为什么不写成@Controller('api/users')
呢?因为全局前缀已经被我们设置为api
了。
服务
刚才的控制器中有一个AppService
服务,服务是处理业务逻辑的地方。服务可以被控制器调用,也可以被其他服务调用。
1 2 3 4 5 6 7 8 9
| import { Injectable } from '@nestjs/common'
@Injectable() export class AppService { getHello(): string { return 'Welcome to HotaruTS!!' } }
|
@Injectable
装饰器用来定义一个服务。服务中的方法可以被其他服务调用,也可以被控制器调用。
刚才调用的是getHello
方法,返回的是一个字符串。如果使用Postman请求http://localhost:4000
,响应的内容就是Welcome to HotaruTS!!
。
模块
模块是一个用来组织应用程序的地方,每个应用程序至少有一个根模块。模块中可以包含控制器、服务、提供器等。
根模块可以看成是Express里的app
对象,它是所有模块的入口。
先来看一下我Express项目中的app.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const createError = require('http-errors'); const express = require('express'); const path = require('path'); const cookieParser = require('cookie-parser'); const logger = require('morgan');
const indexRouter = require('./routes/index'); const usersRouter = require('./routes/users');
const app = express();
app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug');
app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); app.use('/users.js', usersRouter);
app.use(function(req, res, next) { next(createError(404)); });
app.use(function(err, req, res, next) { res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500); res.render('error'); });
module.exports = app;
|
- 导入依赖
- 创建路由器
indexRouter
和usersRouter
- 创建
app
对象,也就是Express的实例
- 设置视图引擎和视图路径
- 使用中间件,分别是:
logger
:记录请求日志,dev
参数表示开发环境
express.json
:解析JSON格式的请求体
express.urlencoded
:解析URL编码的请求体,extended
参数表示是否使用qs
库
cookieParser
:解析cookie
express.static
:设置静态文件目录,也就是public
目录
- 配置路由,让
indexRouter
和usersRouter
分别处理/
和/users
路径
- 处理404错误
- 处理其他错误
- 导出
app
对象
得知了这些,我们就可以仿照Express的写法来写NestJS的模块。
首先导入依赖:
1 2 3 4 5 6 7 8 9 10 11 12
| import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common' import { APP_FILTER } from '@nestjs/core' import { MongooseModule } from '@nestjs/mongoose' import * as express from 'express' import * as cookieParser from 'cookie-parser' import * as morgan from 'morgan' import { AppController } from './app.controller' import { AppService } from './app.service' import { AnyExceptionFilter } from './any-exception.filter' import { LoggerMiddleware } from './common/middleware/logger.middleware' import { SocketModule } from './socket/socket.module' import { UsersModule } from './users/users.module'
|
@Module
装饰器可以定义一个模块,参数分别是:
imports
:导入其他模块
controllers
:控制器
providers
:提供器
因为这个项目的数据库是MongoDB,所以我导入了MongooseModule
模块。SocketModule
和UsersModule
是自定义的模块。
UsersModule
后面会详细讲解,SocketModule
等未来写到消息传递时再讲。
控制器就不用多说了,模块本来就是用来组织控制器的。AppModule
中只有一个控制器AppController
。
提供器是一个用来提供服务的地方,服务可以被控制器调用。AppService
就是一个提供器。除此之外,还有一个全局异常过滤器AnyExceptionFilter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Module({ imports: [ MongooseModule.forRoot(process.env.DATABASE_URL || 'mongodb://localhost:27017/hotaru'), SocketModule, UsersModule, ], controllers: [AppController], providers: [ AppService, { provide: APP_FILTER, useClass: AnyExceptionFilter, }, ], })
|
为AppModule
添加了一个configure
方法,这个方法是NestModule
接口的一个方法,作用是添加中间件。
consumer.apply
方法用来添加中间件,参数是一个或多个中间件。这里添加了morgan
、express.json
、express.urlencoded
、cookieParser
、express.static
和LoggerMiddleware
中间件。
forRoutes('*')
表示所有路由都会使用这些中间件。
最终导出AppModule
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply( morgan('dev'), express.json(), express.urlencoded({ extended: false }), cookieParser(), express.static('public'), LoggerMiddleware, ) .forRoutes('*') } }
|
这样,我们就完成了一个NestJS的模块。
日志中间件
LoggerMiddleware
中间件是一个自定义的中间件,用来记录请求日志:
1 2 3 4 5 6 7 8 9 10
| import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express';
@Injectable() export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log('Request...', req.method, req.originalUrl); next(); } }
|
目前这个中间件只是简单地打印请求方法和请求路径。
用户模块
用户模块是一个用来处理用户注册、登录、登出的模块。
在NestJS里创建一个模块可以使用CLI:
这个命令会在src
目录下创建一个users
目录,里面有一个users.module.ts
文件。
首先我们得知道原先的Express项目里,用户注册的逻辑是怎么写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| socket.on("register", async userData => { const existingUserEmail = await User.findOne({ emailAddress: userData.emailAddress }); const existingUsername = await User.findOne({ username: userData.username }); if (existingUserEmail || existingUsername) { console.log(`[U0102] User already exists: ${userData.username}`); User.find({}).then((docs) => { console.log(docs); }).catch((err) => { console.error(err); });
socket.emit("newRegisteredUser", { status: "U0102", message: "User already exists." }); return; } await User.create({ emailAddress: userData.emailAddress, username: userData.username, password: userData.password, DOBYear: userData.birthYear, DOBMonth: MonthToNumber[userData.birthMonth], DOBDay: userData.birthDay }) console.log(`[00000] User registered: ${userData.username}`); socketIO.emit("newRegisteredUser", { status: "00000", token: generateJWT(userData.username) }); });
|
因为使用的是Socket.io,所以要监听register
事件,然后验证用户填写的信息。如果用户已经存在,就返回错误信息;如果用户不存在,就创建一个新用户。
其中的状态码是自定义的,采用的是类似于阿里巴巴代码规约的状态码。
在NestJS中,鉴于客户端已经改为使用Axios这样的HTTP库,我们就不再使用Socket.io了。先创建一个路径为/users
的控制器:
1 2 3 4
| @Controller('users') export class UsersControllers { constructor(private readonly usersService: UsersService) { } }
|
constructor
方法中注入了一个私有且只读的usersService
服务,这意味着这个服务只能在这个控制器中使用。
1 2
| @Post('register') async register(@Body() registerUserDto: RegisterUserDto, @Res() res: Response) {}
|
@Post('register')
装饰器用来定义一个POST请求,请求路径是/users/register
。
@Body()
装饰器用来获取请求体,registerUserDto
是一个数据传输对象,包含了用户注册时需要的信息,也就是客户端传来的数据:
emailAddress
username
password
birthYear
birthMonth
birthDay
@Res()
装饰器用来获取响应对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const existingUser = await this.usersService.findByEmail(registerUserDto.emailAddress) if (existingUser) { throw new HttpException('Email address already in use', HttpStatus.BAD_REQUEST) }
if (!registerUserDto.emailAddress) { throw new HttpException('Email address is required', HttpStatus.BAD_REQUEST) }
try { const user = this.usersService.register(registerUserDto)
res.status(HttpStatus.OK).json({ status: '00000', message: 'User registered successfully', user: user, }) } catch (error) { res.status(HttpStatus.BAD_REQUEST).json({ status: 'U0100', message: 'Failed to register user', }) }
|
首先调用this.usersService.findByEmail
方法来查找用户是否已经存在,如果存在就返回错误信息。接着判断用户填写的信息是否完整,如果不完整就返回错误信息。
最后调用this.usersService.register
方法来注册用户,如果注册成功就返回成功信息,否则返回错误信息。
每次返回响应时都要设置状态码,比方说请求成功时写的HttpStatus.OK
,请求失败时写的HttpStatus.BAD_REQUEST
。尽管已经自定义了一套状态码,但是还是要遵循HTTP协议的状态码,谁叫我们是用HTTP协议的呢。
那么UsersService
里到底是什么样的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import { Injectable, UnauthorizedException } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' import { InjectModel } from '@nestjs/mongoose' import { v4 as uuidv4 } from 'uuid' import * as bcrypt from 'bcrypt' import { Model } from 'mongoose' import { RegisterUserDto, LoginUserDto } from './dto' import { User, UserDocument } from './user.schema'
@Injectable() export class UsersService { constructor( @InjectModel(User.name) private usersModel: Model<UserDocument>, private jwtService: JwtService, ) { }
async register(registerUserDto: RegisterUserDto): Promise<void | User> { const id = uuidv4() const hashedPassword = await bcrypt.hash(registerUserDto.password, 10) const newUser = new this.usersModel({ id, ...registerUserDto, password: hashedPassword, }) let savedUser: void | User try { savedUser = await newUser.save().then(() => console.log('User registered successfully')) } catch (err) { console.error(err) } return savedUser }
async findByEmail(emailAddress: string): Promise<User | null> { return this.usersModel.findOne({ emailAddress }).exec() } }
|
@InjectModel(User.name)
注入了一个Mongoose模型,用来操作数据库。下面的jwtService
是用来生成JWT的服务,以后再聊聊JWT。
register
方法中我们先用uuidv4
方法生成一个唯一的ID,然后用bcrypt
库对密码进行加密。接着使用new
关键字创建一个用户实例、传入所有创建用户时需要的信息。 最后调用save
方法保存用户信息,如果保存成功就返回用户信息,否则返回void
。
findByEmail
方法用来查找用户是否已经存在,如果存在就返回用户信息,否则返回null
。
现在已经看到了很多次...Dto
,这是什么呢?
DTO的全程为Data Transfer Object,数据传输对象。它是一个用来传输数据的对象,通常用来传输数据给服务端或者从服务端传输数据给客户端。在NestJS中,DTO是一个用来定义数据结构的类,用来规范数据的传输。
例如RegisterUserDto
类用来定义用户注册时需要的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { IsEmail, IsNotEmpty, IsString } from 'class-validator'
export class RegisterUserDto { @IsEmail() emailAddress: string
@IsString() @MinLength(6) username: string
@IsString() @MinLength(8) password: string
@IsString() birthYear: string
@IsString() birthMonth: string
@IsString() birthDay: string }
|
这里使用了多个装饰器来定义每个属性的类型,有助于进行数据验证。
UserSchema
则是用来定义用户模型的,是MongoDB要求的数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { Document } from 'mongoose'
export type UserDocument = User & Document
@Schema() export class User { @Prop({ required: true, unique: true }) username: string
@Prop({ required: false, unique: true }) emailAddress: string
@Prop({ required: true }) password: string
@Prop({ required: false }) birthYear: string
@Prop({ required: false }) birthMonth: string
@Prop({ required: false }) birthDay: string }
export const UserSchema = SchemaFactory.createForClass(User)
|
@Prop
装饰器用来定义一个属性,@Schema
装饰器用来定义一个模式。UserDocument
是一个用户文档,继承了User
和Document
。
这里我们定义的属性和RegisterUserDto
中的属性是一样的,但UserSchema
注重于定义会被存储在数据库中的数据结构,RegisterUserDto
注重于定义会在客户端和服务端之间传输的数据结构。
最终,我们在UsersModule
中导入这些模块:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Module } from '@nestjs/common' import { MongooseModule } from '@nestjs/mongoose' import { JwtService } from '@nestjs/jwt' import { User, UserSchema } from './user.schema' import { UsersControllers } from './users.controllers' import { UsersService } from './users.service'
@Module({ imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])], controllers: [UsersControllers], providers: [UsersService, JwtService], }) export class UsersModule {}
|
不要忘了,新建的模块要在AppModule
中导入:
1
| @Module({ imports: [UsersModule] })
|