【typescript】express结合ts制作注册登录接口

前言

  • 记录下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": "[email protected]"
}
  • 成功后会返回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去查用户信息,然后再返回筛选过的用户信息。
发布了163 篇原创文章 · 获赞 9 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/104306072
今日推荐