4. zhuzhuketang-hook-express-server

184 阅读3分钟

pakage.json

  • 要想起一个ts版本的后台服务
    • 有以下两种方式
"scripts": {
    "build": "tsc",
    "start": "ts-node-dev --respawn src/index.ts",
    "dev": "nodemon --exec ts-node --files src/index.ts"
  },

.env

JWT_SECRET_KEY=zhuzhu
MONGODB_URL=mongodb://localhost/zhuzhuapp

index.ts

  • 要想使用ts声明 要使用import 不能使用require
//  要想使用ts类型声明,不能使用require,要使用import
import express , { Express, Request, Response, NextFunction }from 'express'; // 启动服务
import mongoose from 'mongoose'; // 链接数据库
import cors from 'cors';  // 用来跨域的中间件
import morgan  from 'morgan'; // 用来输入访问日志
import helmet from 'helmet'; // 用来做安全过滤,比如一些攻击性脚本,xss
import multer from 'multer';  // 用于上传文件呢
import "dotenv/config";  // 这个包的作用是读取.env文件然后写入process.env环境变量,可以通过process.env.变量名拿到.env文件中定义了的值
import path from 'path';

import { Slider, Lesson } from './models/index';


// 指定上传文件的存储空间
const storage = multer.diskStorage({
    destination: path.join(__dirname, 'public', 'uploads'),
    filename(_req:Request, _file: Express.Multer.File, callback) {
        // callback第二个参数是文件名 时间戳.jpg
        callback(null, Date.now() + path.extname(_file.originalname))
    }
});
const upload = multer({ storage });

import errorMiddleware from './middlewares/errorMiddleware';
import HttpException from './exception/httpException';
import * as userController from './controllers/user';
import * as sliderController from './controllers/slider';
import * as lessonController from './controllers/lesson';

console.log(userController, 'user')

const app: Express = express();

// 中间件
app.use(cors());
app.use(morgan('dev'));
app.use(helmet());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.json()); // express.json = bodyParser.json 只是一个别名而已
app.use(express.urlencoded({extended: true }));

// 路由
app.get("/", (_req,res,_next) =>  {
    res.json({success: true, data: "hello zhuzhu"});
});
app.post("/user/register", userController.register); // 注册
app.post("/user/login", userController.login); // 登录
app.get("/user/validate", userController.validate); // 客户端把token传给服务器,服务器返回当前用户。如果token不合法或者过期了,则会返回null
// 当服务器接收到上传文件请求的时候,处理单文件上传,字段名为avatar
// multer中间件会找提交过来的表单数据里面的avatar字段,对应的值是一个文件,对文件进行处理
// 处理过程:将这个文件的二进制流拿出来保存到public/uploads目录下,完成之后会往req上增加 req.file属性指向Express.Multer.File这样一个对象
app.post("/user/uploadAvatar", upload.single('avatar'), userController.uploadAvatar);

app.get("/slider/list", sliderController.list);
app.get("/lesson/list", lessonController.list);
app.get("/lesson/:id", lessonController.getLesson);

// 如果没有匹配到任何路由,则会创建一个自定义404错误对象并传递给错误处理中间件
app.use((_req:Request, _res:Response, next: NextFunction) => {
    // 正常中间件没有err参数,只有next向下传递参数的时候才走到错误中间件,第一个参数为err
    const error: HttpException = new HttpException(404, "尚未为此路径分配路由");
    next(error);
})
//使用自定义错误中间件
app.use(errorMiddleware);



(async function() {
    // 连接数据库
    await mongoose.set('useNewUrlParser', true);
    await mongoose.set('useUnifiedTopology', true);
    const MONGODB_URL  = process.env.MONGODB_URL || 'mongodb://localhost/zfketangapp';
    await mongoose.connect(MONGODB_URL);
    await createInitialSliders();
    await createInitialLessons();
    const PORT  =  process.env.PORT || 8001;
    app.listen(PORT, () => {
        console.log(`running on port ${PORT}`);
    })
})()


async function createInitialSliders() {
    const sliders = await Slider.find(); //查看数据库中是否有轮播图数据
    if(sliders.length == 0) { // 如果没有就插入几条数据
        const sliders = [
            // { url: "https://c-ssl.duitang.com/uploads/blog/202012/18/20201218100629_47f9d.thumb.1000_0.jpg"},
            {url:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.52souluo.com%2Fwp-content%2Fuploads%2F2014%2F11%2Fgongqijun08.jpg&refer=http%3A%2F%2Fwww.52souluo.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620137772&t=c6f7f415e591d19c56e42a0157b0500f"},
            { url: "https://c-ssl.duitang.com/uploads/item/201502/11/20150211150925_mKQMa.jpeg"},
            { url: "https://c-ssl.duitang.com/uploads/blog/201508/17/20150817185916_YnyvC.jpeg"},
            { url: "https://c-ssl.duitang.com/uploads/item/201510/02/20151002133212_Cmvys.jpeg"},
            { url: "https://c-ssl.duitang.com/uploads/blog/202012/18/20201218100630_8ec8f.thumb.1000_0.jpg"}
        ];
        await Slider.create(sliders);
    }
}

async function createInitialLessons() {
    const lessons = await Lesson.find();
    if(lessons.length == 0) {
        await Lesson.create(lessonarr);
    }
}

const lessonarr = [
    {
        order: 1,
        title: "1.react全栈架构",
        video: "http://img.zhufengpeixun.cn/gee2.mp4",
        poster: "http://www.zhufengpeixun.cn/react/img/react.jpg",
        url: "http://www.zhufengpeixun.cn/themes/jianmo2/images/react.png",
        price: "¥100.00元",
        category: "vue"
    },
    {
        order: 2,
        title: "2.vue从入门到项目实战",
        video: "http://img.zhufengpeixun.cn/gee2.mp4",
        poster: "http://www.zhufengpeixun.cn/react/img/react.jpg",
        url: "http://www.zhufengpeixun.cn/themes/jianmo2/images/react.png",
        price: "¥200.00元",
        category: "vue"
    },
    {
        order: 3,
        title: "3.vue从入门到项目实战",
        video: "http://img.zhufengpeixun.cn/gee2.mp4",
        poster: "http://www.zhufengpeixun.cn/react/img/react.jpg",
        url: "http://www.zhufengpeixun.cn/themes/jianmo2/images/react.png",
        price: "¥300.00元",
        category: "vue"
    },
]

错误中间件

import { Request, Response, NextFunction} from 'express';
import HttpException from "../exception/HttpException";
import { StatusCodes } from 'http-status-codes'; // 包含各种状态码

// 错误处理中间件
const errorMiddleware = (err:HttpException,  _req:Request, res:Response, _next:  NextFunction) => {
    console.log("返回错误")
    res.status(err.status || StatusCodes.INTERNAL_SERVER_ERROR)
    .json({
        success: false,
        message: err.message,
        errors: err.errors
    })

};
export default errorMiddleware;

HttpException 错误类型

class HttpException extends Error {
    // status:http 错误状态码  message错误提示信息  errors 错误对象
    constructor(public status:number, public message:string, public errors?:any){
        super(message);
    }   
}
export default HttpException;

mongodb

arc/models文件夹

index.ts

// 将定义好的模型一并导出
export * from './user'; // 意思是把从'./user'里面导入的全部再导出
export * from './slider';
export * from './lession';

lession.ts

import mongoose, {Schema, Document, Model} from 'mongoose';

export interface LessonDocument extends Document {
    order: number;
    title: string; // 标题
    video: string; // 视频地址
    poster: string; // 海报地址
    url: string; // url地址
    price: string; // 价格
    category: string; // 分类
}

const LessonSchema:Schema<LessonDocument>= new Schema({
    order: Number,
    title: String, // 标题
    video: String, // 视频地址
    poster: String, // 海报地址
    url: String, // url地址
    price: String, // 价格
    category: String, // 分类
}, {
    timestamps: true,
    toJSON: {
        transform: function(_doc:any, result:any) {
            result.id = result._id;
            delete result._id;
            delete result.__v;
            delete result.createdAt;
            delete result.updatedAt;
            return result;
        }
    }
});

export const Lesson:Model<LessonDocument> = mongoose.model("Lesson", LessonSchema);

slider.ts

import mongoose, {Schema,Document, Model} from 'mongoose';

export interface SliderDocument extends Document {
    url: string;
}

const SliderSchema: Schema<SliderDocument> = new Schema({
    url: String,
}, { timestamps: true });

export const Slider:Model<SliderDocument> = mongoose.model("Slider", SliderSchema);

user.ts

import mongoose, { Schema, Model, Document, HookNextFunction} from 'mongoose';
import validator from 'validator'; // 用于校验
import bcryptjs from 'bcryptjs'; // 对密码进行加密处理
import jwt from 'jsonwebtoken';
import {UserPayload} from '../typings/payload';

export interface UserDocument extends Document {
    username: string;
    password: string;
    avatar: string;
    email: string;
    getAccessToken: () => 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,
        trim: true, // email='  xx@qq.com  ' 表示存的时候是否要去前后空格
        validate: { // 自定义校验器
            validator: validator.isEmail
        }
    }
// { timestamps: true } 使用时间戳,会自动添加两个字段 createdAt updatedAt
}, { 
    timestamps: true,
    toJSON: { // 调用toJSON方法对数据进行处理
        transform: function(_doc, result) {
            result.id = result._id;
            delete result._id;
            delete result.__v;
            delete result.password; // 不返回密码
            return result;
        }
    }
});

// 在每次保存文档之前执行的操作  通过model模型new出来的文档实例在调用save方法进行保存数据之前还会走下面的方法
UserSchema.pre<UserDocument>('save', async function(next: HookNextFunction){
    if(!this.isModified('password')) { // 如果密码没有更改,就不用重新加密,直接下一步
        return next();
    }
    try{
        this.password = await bcryptjs.hash(this.password,  10);
        next();
    }catch(err) {
        next(err);
    }
})

// 给model模型扩展方法,叫login,用于在登录的时候校验输入的密码是否匹配数据库存入的
UserSchema.static('login', async function(this:any,username:string, password:string):Promise<UserDocument | null> {
    // let user: UserDocument | null  = await this.findOne({username})
    let user: UserDocument | null  = await this.model('User').findOne({username})
    if(user) {
        // 判断密码是否匹配
        const matched = await bcryptjs.compare(password, user.password);
        if(matched) {
            return user;
        }else {
            return null;
        }
    }
    return null;
})

// 给模型实例扩展方法 生成token
UserSchema.methods.getAccessToken = function(this: UserDocument) :string {
    let payload: UserPayload = {id: this._id}; // payload是防在jwt token里存放的数据
    return jwt.sign(payload, process.env.JWT_SECRET_KEY || 'zhuzhu', { expiresIn: '1h'});
}

interface UserModel<T extends Document> extends Model<T> {
    login : (username:string, password:string) => UserDocument |  null
}

// 定义模型
export const User:UserModel<UserDocument> = mongoose.model<UserDocument, UserModel<UserDocument>>('User', UserSchema);

src/ controllers 分类路由

lession.ts

import {Lesson, LessonDocument} from '../models/index';
import { Response, Request} from 'express';

export const list = async (req:any, res:Response) => {
    let {category='all', offset, limit} = req.query;
    offset = isNaN(offset)? 0 : Number(offset);
    limit  = isNaN(limit)?  5 : Number(limit);

    // let query:Partial<LessonDocument> = {}; //查询条件对象
    let query:any = {};
    if(category && category !== 'all') {
        query.category = category;
    }
    let total: number = await Lesson.count(query); //符合条件的总数量
    let lessons: LessonDocument[] = await Lesson.find(query)
    .sort({order:1}).skip(offset).limit(limit)
    setTimeout(() => {
        res.json({
            success: true,
            data: {
                list: lessons,
                hasMore: total > offset + limit
            }
        })
    }, 2000);
};

export const getLesson = async (req:Request, res:Response) => {
    let id = req.params.id;
    let lesson = await Lesson.findById(id);
    setTimeout(() => {
        res.json({
            success: true,
            data: lesson
        })
    }, 2000);
};

slider.ts

import { Request, Response, NextFunction } from 'express';
// import HttpException from '../exception/httpException';
import { Slider, SliderDocument } from '../models/index';

export const list = async (_req:Request, res:Response, _next:NextFunction) => {
    let sliders: SliderDocument[] = await Slider.find();
    res.json({
        success: true,
        data: sliders
    })
}

user.ts

import { Request, Response, NextFunction } from 'express';
import HttpException from '../exception/httpException';
import { User, UserDocument } from '../models/index';
import { validateRegisterInput } from '../utils/validator';
import { StatusCodes } from 'http-status-codes'; // 包含各种状态码
import jwt from 'jsonwebtoken';
import { UserPayload} from '../typings/payload';
// 注册
export const register = async (req:Request, res: Response, next: NextFunction) => {
    let { username, password, confirmPassword, email } = req.body;
    try {
        // 验证传入的数据是否合法
        let { valid, errors } = validateRegisterInput(username, password, confirmPassword, email);
        if(!valid) {
            throw new HttpException(StatusCodes.UNPROCESSABLE_ENTITY, '参数校验失败', errors);// 为了能抛出异常,被错误中间件捕获,在外层套上try catch
        }

        // 校验用户名是否已经存在
        let exituser: UserDocument | null = await User.findOne({username});
        if(exituser) {
            throw new HttpException(StatusCodes.UNPROCESSABLE_ENTITY, '用户名已存在', errors);
        }
        // 实例化UserModel,创建user实体
        let user: UserDocument = new User({ username, password, confirmPassword, email }); 
        // 保存数据到数据库
        await user.save();
        // 接口返回 
        res.json({ 
            success: true,
            data: user.toJSON()
        })
    }catch(err) {
        next(err);  // 捕获 throw new HttpException
    }
}


// 登录
export const login = async (req:Request, res:Response, next: NextFunction) => {
    try{
        let { password, username } = req.body;
        let user: UserDocument | null = await User.login(username, password); // 这是给模型扩展方法
        if(user) {
            let access_token = user.getAccessToken();  //  这是要给模型实例上扩展方法
            res.json({
                success: true,
                // data: user
                data: access_token, // 登录成功应该返回token
            })
        }else{
            throw new HttpException(StatusCodes.UNAUTHORIZED,  '登录失败')
        }
    }catch(err) {
        next(err);
    }
}


// 验证从客户端传回来的token
/*
token的传递方式
1、存放在cookie中 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域。

2、存放在localstorage中,添加到header中发送 请求时放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
Authorization: Bearer 复制代码

3、通过接口参数 可以把 JWT 放在 POST 请求的数据体里,或者通过 URL 的 queryString 传输。
*/
export const validate = async (req:Request, res:Response, next:NextFunction) => {
    // 客户端会把token放在请求头发给服务端
    // 获取到token
    const authorization = req.headers.authorization;
    if(authorization)  {
        const access_token  = authorization.split(' ')[1];
        console.log(access_token, "111111111111111")
        if(access_token) {
            try{
                const payload: UserPayload = jwt.verify(access_token, process.env.JWT_SECRET_KEY || "zhuzhu") as UserPayload; // 内部会验证是否过期
                const user =  await User.findById(payload.id);
                if(user) {
                    res.json({
                        success: true,
                        data: user.toJSON()
                    })
                }else{
                    next(new HttpException(StatusCodes.UNAUTHORIZED, '用户未找到'))
                }
            }catch(err) {
                next(new HttpException(StatusCodes.UNAUTHORIZED, 'accesstoken不合法'));
            }
        }else{
            next(new HttpException(StatusCodes.UNAUTHORIZED, 'authorization未提供'));
        }

    }else {
        next(new HttpException(StatusCodes.UNAUTHORIZED, 'authorization未提供'));
    }
}


// 上传头像
export const uploadAvatar = async(req:Request, res:Response, next: NextFunction) => {
    try{
        let { userId } = req.body;
        // 因为设置了public为静态文件夹
        let avatar = `${req.protocol}://${req.headers.host}/uploads/${req.file.filename}`;
        await User.updateOne({_id: userId}, {avatar});
        res.json({
            success: true,
            data: avatar
        })
    }catch(err) {
        next(err);
    }
}


utils/validator.ts

import validator from 'validator';
import {  UserDocument }  from '../models/index';

export interface RegisterInput extends Partial<UserDocument> {
    confirmPassword? : string
}
/*
interface RegisterInput extends UserDocument {
    confirmPassword? : string
}
let errors: RegisterInput = {} 定义了interface后会报错, 想消除报错有两种方式
1.强行断言
let errors: RegisterInput = {} as RegisterInput;
2.将继承自UserDocument里的属性全部转成带有?的,则所有属性都可有可无 Partial<UserDocument>
interface RegisterInput extends Partial<UserDocument> {
    confirmPassword? : string
}
*/

export interface RegisterInputValidateResult {
    errors: RegisterInput,
    valid: boolean
}
// 校验注册用户提交信息合法性
export const validateRegisterInput = (
    username: string,
    password:string,
    confirmPassword: string,
    email: string
):RegisterInputValidateResult => {
    // let errors: RegisterInput = {} as RegisterInput;
    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(password !== confirmPassword) {
        errors.email = '密码和确认密码不相等';
    }
    if(!validator.isEmail(email)) {
        errors.email = '邮箱格式不正确';
    }
    return {
        valid: Object.keys(errors).length == 0,
        errors
    }
}