admin管理员组文章数量:1567252
前言
- 记录下ts实现后端接口的操作。
安装
- 先进目录初始化,然后安装依赖
cnpm i express mongoose body-parser bcryptjs jsonwebtoken morgan cors validator helmet dotenv multer http-status-codes -S
cnpm i typescript @types/node @types/express @types/mongoose @types/bcryptjs @types/jsonwebtoken @types/morgan @types/cors @types/validator ts-node-dev @types/helmet @types/multer -D
生成tsconfig
- 使用命令
npx tsconfig.json
生成tsconfig文件。 - 选项选择node模式。
- 相对于
tsc --init
来说,这种方法相当于直接用适配好的ts配置。
配置启动命令
- 新建目录src,里面新建文件index.ts。console.log一句话。
- package.json里修改:
"scripts": {
"build": "tsc",
"start": "ts-node-dev --respawn src/index.ts"
},
- 除了用ts-node-dev,还可以使用
nodemon --exec ts-node --files src/index.ts
来执行。
配置环境信息
- 新建文件.env。里面配置密钥数据库地址之类:
JWT_SECRET_KEY = 密钥
MONGODB_URL=数据库地址
- 使用dotenv/config,可以把这个文件的东西写入env里去。
- 例如通过
process.env.JWT_SECRET_KEY
拿到值。
编写入口
- 先让其运行起来。
import express from 'express'
import mongoose from 'mongoose'
import cors from 'cors'
import morgan from 'morgan'
import helmet from 'helmet'
import 'dotenv/config'
import path from 'path';
const app =express()
app.use(cors())
app.use(morgan('dev'))
app.use(helmet())
app.use(express.static(path.join(__dirname,'public')))
app.use(express.json())
app.use(express.urlencoded({extended:true}))
app.get('/',(_req,res,_next)=>{
res.json({success:true,data:'xx'})
});
(async function(){
await mongoose.set('useNewUrlParser',true)
await mongoose.set('useUnifiedTopology',true)
const MONGODB_URL = process.env.MONGODB_URL||'mongodb://localhost/tsbackend'
await mongoose.connect(MONGODB_URL)
const PORT =process.env.PORT||3000
app.listen(PORT,()=>{
console.log(`running on http://localhost:${PORT}`);
})
})()
- cors是跨域的中间件,morgan是打印日志的,helmet是可以让express增加点安全性的中间件。
自定义错误中间件
- 我们需要知道中间件那几个参数是什么类型。
- 通过app.use点进去看use的描述:
export interface Application extends EventEmitter, IRouter, Express.Application {
...
...
use: ApplicationRequestHandler<this>;
}
export type ApplicationRequestHandler<T> = IRouterHandler<T> & IRouterMatcher<T> & ((...handlers: RequestHandlerParams[]) => T);
export type RequestHandlerParams<P extends Params = ParamsDictionary, ResBody = any, ReqBody = any>
= RequestHandler<P, ResBody, ReqBody>
| ErrorRequestHandler<P, ResBody, ReqBody>
| Array<RequestHandler<P>
| ErrorRequestHandler<P>>;
export type ErrorRequestHandler<P extends Params = ParamsDictionary, ResBody = any, ReqBody = any> = (err: any, req: Request<P, ResBody, ReqBody>, res: Response<ResBody>, next: NextFunction) => any;
- 因为err是any类型,我们可以自定义个err类型,src下建立exceptions文件夹,里面建立HTTPException.ts:
class HTTPException {
constructor(public status:number,public message:string,public errors?:any){
this.status=status
this.message=message
this.errors = errors||{}
}
}
export default HTTPException
- 用类来做类型,后面还需要把状态码和信息传给它然后new它。
- 然后src下建个middlewares文件夹,里面建errorMiddleware.ts:
import {Request,Response,NextFunction}from 'express'
import {INTERNAL_SERVER_ERROR} from 'http-status-codes'
import HTTPException from '../exceptions/HTTPException'
const errorMiddleware = (err:HTTPException,_req:Request,res:Response,_next:NextFunction)=>{
res.status(err.status||INTERNAL_SERVER_ERROR).json({
success:false,
message:err.message,
errors:err.errors
})
}
export default errorMiddleware
- 这个http-status-code包就是通过字符串找状态码的。
- 错误中间件需要上面一个中间件给next传一个参数。不传参数则收不到err,所以上面一个中间件负责提供个状态码和信息。
app.get('/',(_req,res,_next)=>{
res.json({success:true,data:'xx'})
});
app.use((_req:Request,_res:Response,next:NextFunction)=>{
const error :HTTPException=new HTTPException(404,'未分配路由')
next(error)
})
app.use(errorMiddleware);
- 将中间件按顺序排,走完所有路由后未匹配到的就给404,传错误信息给错误中间件。
编写models
- 写一个都能用到的用户名密码头像邮件的模型:
import mongoose ,{Schema,Model,Document} from 'mongoose'
import validator from 'validator'
export interface UserDocument extends Document {
username:string,
password:string,
avatar:string,
email:string
}
const UserSchema :Schema<UserDocument>= new Schema({
username:{
type:String,
required:[true,'用户名不为空'],
minlength:[6,'最小长度不能小于6位'],
maxlength:[12,'最大长度不得大于12位']
},
password:String,
avatar:String,
email:{
type:String,
validate:{
validator:validator.isEmail
},
trim:true
}
},{timestamps:true})
export const User:Model<UserDocument> = mongoose.model('User',UserSchema)
- 这个schema本来里面传的泛型可以是任意值,但是使用mongoose.model这个方法必须限定要继承Document:
export function model<T extends Document>(name: string, schema?: Schema, collection?: string,
skipInit?: boolean): Model<T>;
- 否则返回值没有Document上面的属性。
- valitator是一个验证的库。
- schema第二个参数可以让其自动添加创建时间和更新时间。
编写注册路由
- 模型写完需要用起来,下面做个路由来注册:
import {register} from './controllers'
app.post('/user/register',register)
- 在src文件夹下建立controllers的文件夹,里面一个index.ts用于导入导出,一个user.ts用来处理逻辑:
src/controllers/user.ts
import {NextFunction,Response,Request} from 'express'
import { User } from '../models'
export const register = async(req:Request,res:Response,_next:NextFunction)=>{
let {username,password,confirmpassword,email}=req.body;
let user = new User({username,password,confirmpassword,email})
await user.save()
res.json({
success:true,
data:user
})
}
src/controllers/index.ts
export * from './user'
- 使用工具发请求试试,注意Content-Type是application/json。body使用json。
{
"username": "1111111",
"password": "11",
"confirmpassword": "11",
"email": "111@qq"
}
-
成功后会返回success和data的对象。
-
下面完善用户提交信息校验,在src下建个utils目录,下面建个validator.ts
import validator from 'validator'
import {UserDocument} from '../models'
export interface RegisterInput extends Partial<UserDocument>{
confirmpassword?:string
}
export interface RegisterResult {
valid:boolean,
errors:RegisterInput
}
export const validatorRegisterInput = (username:string,password:string,
confirmpassword:string,email:string)=>{
let errors:RegisterInput={}
if(username===undefined||username.length==0){
errors.username='用户名不能为空'
}
if(password===undefined||password.length==0){
errors.password='密码不能为空'
}
if(confirmpassword===undefined||confirmpassword.length==0){
errors.confirmpassword='确认密码不能为空'
}
if(email===undefined||email.length==0){
errors.email='邮箱不能为空'
}
if(!validator.isEmail(email)){
errors.email='邮箱不正确'
}
return {valid:Object.keys(errors).length==0,errors}
}
- 这里就是写个方法,把收到的表单数据拿来验证一遍,不对的就给errors对象附上键值对。如果最后是空对象,那么valid也是就为true。
- 再重新改写controllers下面的user.ts,加入这个校验器:
import {NextFunction,Response,Request} from 'express'
import { User } from '../models'
import {validatorRegisterInput} from '../utils/validator'
import HTTPException from '../exceptions/HTTPException';
import { UNPROCESSABLE_ENTITY } from 'http-status-codes';
export const register = async(req:Request,res:Response,_next:NextFunction)=>{
let {username,password,confirmpassword,email}=req.body;
let {valid,errors}= validatorRegisterInput(username,password,confirmpassword,email)
try {
if(!valid){
throw new HTTPException(UNPROCESSABLE_ENTITY,'用户提交数据不正确',errors)
}
let users = await User.findOne({username})
if(users){
throw new HTTPException(UNPROCESSABLE_ENTITY,'用户名重复',errors)
}
let user = new User({username,password,confirmpassword,email})
await user.save()
res.json({
success:true,
data:user
})
} catch (error) {
_next(error)
}
}
- 通过trycatch捕获错误,有错误后,使用next把错误传给错误中间件。不满足schema的也会抛出错误。
- 下面完成加密功能。
- 一般在用户传来密码通过验证后就对其进行加密也可以,更好的方式是插入文档前进行:
- schema有个pre的方法可以完成这个工作,加密使用bcryptjs进行加密。
- schema文档
UserSchema.pre<UserDocument>('save',async function(next){
if(!this.isModified('password')){
return next()
}
try {
this.password= await bcryptjs.hash(this.password,10)
next()
} catch (error) {
next(error)
}
})
编写登录路由
- 老样子,先把路由写上
import {register,login} from './controllers'
app.post('/user/login',login)
- 然后编写controller:
export const login = async(req:Request,res:Response,_next:NextFunction)=>{
let {username,password}=req.body;
try {
let user =await User.login(username,password)
if(!user){
throw new HTTPException(UNAUTHORIZED,'登录失败')
}else{
res.json({
success:true,
data:user
})
}
} catch (error) {
_next(error)
}
}
- 这里把登录验证逻辑交给model了,所以需要给model扩展个方法。
- 在model里加入login的方法:
UserSchema.static('login',async function(this:any,username:string,password:string){
let user=await this.findOne({username})
if(user){
const match =await bcryptjs.compare(password,user.password)
if(match){
return user
}else{
return null
}
}else{
return null
}
})
interface UserModel extends Model<UserDocument>{
login:(username:string,password:string)=>UserDocument|null
}
export const User:UserModel = mongoose.model<UserDocument,UserModel>('User',UserSchema)
- 使用static可以增加model的方法,这个static在上面发的链接里有使用例子。
- 这个this指的是下面导出的User模型,因为要用model来查用户名,所以要使用this,ts可以在第一个参数声明this来防止报错。
- 查到的user就是文档,然后通过bcryptjs比对。
- 由于我们添加了login的方法,所以导出的User类型对不上了,因为原本导出的类型是
Model<UserDocument>
,这个类型是没有login的方法的。所以我们需要对其扩展。 - 使用UserModel继承其原本类型,里面写上扩展的login方法。这样UserModel类型就是完整的我们要的model类型了。
- 但是下面导出不改任然会报错,还好mongoose的声明文件里用重载写了这种扩展情况:
export function model<T extends Document>(name: string, schema?: Schema, collection?: string,
skipInit?: boolean): Model<T>;
export function model<T extends Document, U extends Model<T>>(
name: string,
schema?: Schema,
collection?: string,
skipInit?: boolean
): U;
- 本来如果使用第一种方式,T继承document返回
Model<T>
,如果T后来又进行修改,那类型就不是T了。所以使用第二种方式,U来继承model<T>
,而T继承了Document,最后返回了U,也就是U是修改后的T。 - 下面服务端需要返回一个token。这次需要在document上添加方法。因为每个user都是一个实例,生成不同的token。
export const login = async(req:Request,res:Response,_next:NextFunction)=>{
let {username,password}=req.body;
try {
let user =await User.login(username,password)
if(!user){
throw new HTTPException(UNAUTHORIZED,'登录失败')
}else{
let access_token = await user.getToken()
res.json({
success:true,
data:access_token
})
}
} catch (error) {
_next(error)
}
}
- 因为是实例上方法,所以UserDocument需要加属性。
export interface UserDocument extends Document {
username:string,
password:string,
avatar:string,
email:string,
getToken:()=>string
}
UserSchema.methods.getToken = function(this:UserDocument){
let payload = {id:this._id}
return jwt.sign(payload,process.env.JWT_SECRET_KEY||'YEHUOZHILI',{expiresIn:'1h'})
}
- 通过methods添加实例方法。
- 返回的jwt生成的token,jwt默认采用HMAC SHA256算法。
- token就完成了,客户端还需要带token过来进行验证。
- 做一个路由验证token。
app.get('/user/validate',validate)
- 这里需要提取authorization字段。客户端发来的请求头会这么带:
Authorization:Bearer token
- 提取出token后验证:
export const validate=async(req:Request,res:Response,_next:NextFunction)=>{
const authorization =req.headers.authorization
if(authorization){
const access_token = authorization.split(' ')[1]
if(access_token){
try {
const UserPayload:UserPayload=jwt.verify(access_token,process.env.JWT_SECRET_KEY||'YEHUOZHILI')as UserPayload
const user = await User.findById(UserPayload.id)
if(user){
res.json({
success:true,
data:user.toJSON()
})
}else{
_next(new HTTPException(UNAUTHORIZED,'无此用户'))
}
} catch (error) {
_next(new HTTPException(UNAUTHORIZED,'access_token无效'))
}
}else{
_next(new HTTPException(UNAUTHORIZED,'access_token无效'))
}
}else{
_next(new HTTPException(UNAUTHORIZED,'authorization未提供'))
}
}
- 这里由于ts报错取不到id所以额外自己加了个类型。
export interface UserPayload{
id:string
}
- 由于不能把密码返回给前端,所以配置下toJson方法。
const UserSchema :Schema<UserDocument>= new Schema({
username:{
type:String,
required:[true,'用户名不为空'],
minlength:[6,'最小长度不能小于6位'],
maxlength:[12,'最大长度不得大于12位']
},
password:String,
avatar:String,
email:{
type:String,
validate:{
validator:validator.isEmail
},
trim:true
}
},{timestamps:true,toJSON:{
transform:function(_doc:any,result:any){
result.id = result._id
delete result._id
delete result.__v
delete result.password
delete result.createdAt
delete result.updatedAt
return result
}
}})
- 因为前面加密时是拿用户id加密的,解密解出来也是个用户id,根据id去查用户信息,然后再返回筛选过的用户信息。
本文标签: 接口typescriptExpressts
版权声明:本文标题:【typescript】express结合ts制作注册登录接口 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1726722380a1081933.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论