使用Nestjs+mongodb+vue2构建一套完整的应用。

2,107 阅读3分钟

由于业务拓展,目前手上一个纯前端的小项目需要添加后台登陆以及数据保存功能。然后后端开发人员人手不足,所以决定用nodejs写一个后端应用,框架就选用nestjs,数据库就用mongodb。

首先、需要规划好目录结构。

image.png

前后端应用共享一个package.json。server目录存放nestjs构建的应用,client目录存放前端应用。

image.png 前端client目录:

  • build - 前端构建打包模块
  • config - 一些基础配置模块
  • deploy - 自动打包,添加tag,上传git模块
  • server - 前端web服务器部署模块,由于前端应用没有使用nginx,因此使用koa2搭建的web服务器
  • src - 源码目录,不多说
  • 其他是eslint配置以及自动添加厂商前缀的配置文件。

image.png 后端nestjs目录:

一、 middleware

中间件是在路由处理程序之前调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next()中间件函数。next() 中间件函数通常由名为 next 的变量表示。

image.png 中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起 我这里目前用到的功能就是单点登录的cookie拦截。
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import utils from '../utils/index'
// import config from '../../config/config'

@Injectable()
export class PermissionMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    let user = utils.getCookie('user', req?.headers?.cookie)
    let targetUrl = req.url.split('?')[0]
    if (targetUrl !== '/oauthCallback') {
      if (!user) {
        let return_uri = ''
        const curConfig = utils.getServerConfig(req?.headers?.host)
        console.log('当前环境配置是', targetUrl)
        res.redirect(`${curConfig.oauthAuthorizeUrl}?response_type=code&scope=read&client_id=${curConfig.client_id}&redirect_uri=${encodeURIComponent(curConfig.baseUrl + curConfig.redirect_uri)}&state=${utils.guid()}&return_uri=${encodeURIComponent(curConfig.return_uri)}`)
        return
      }
    }
    next();
  }
}

二、 module模块

image.png 每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能

@module() 装饰器接受一个描述模块属性的对象:

providers由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers必须创建的一组控制器
imports导入模块的列表,这些模块导出了此模块中所需提供者
exports由本模块提供并应在其他模块中可用的提供者的子集。

默认情况下,该模块封装提供程序。这意味着无法注入既不是当前模块的直接组成部分,也不是从导入的模块导出的提供程序。因此,您可以将从模块导出的提供程序视为模块的公共接口或API。

考虑到后面的拓展,新建了shared分享与templates模版模块。根模块启动时连接mongodb。

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { templateModule } from './modules/template/template.module';
import { PermissionMiddleware } from './middleware/permission.middleware';
import databaseConfig  from '../config/databaseConfig'

@Module({
  imports: [
    MongooseModule.forRoot(databaseConfig.dataBaseUrl, {
      useNewUrlParser: true,
      readPreference: 'primaryPreferred',
      auth: {
        username: databaseConfig.username,
        password: databaseConfig.password
      }
    }),
    templateModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(PermissionMiddleware)
      .forRoutes('posterapi');
  }
}

然后就是具体模块的具体实现了。

// template.controller.ts
import { Controller, Get, Res, Req, HttpStatus, Param, NotFoundException, Post, Body, Query, Put, Delete } from '@nestjs/common';
import { TempService } from './template.service';
import { CreatePostDTO } from './dto/create-post.dto';
import { ValidateObjectId } from '../shared/pipes/validate-object-id.pipes';
import { map } from 'rxjs/operators';


@Controller('posterapi')
export class TempController {

    constructor(private tempService: TempService) { }

    @Get('oauthCallback')
    getOauthCallback(@Req() req, @Res() res, @Query() query) {
        const result = this.tempService.oauthCallback(req, query, res)
        const return_uri = query.returnUri
        result.subscribe(tokenObj => {
            const userInfoRequest = this.tempService.getUserInfo(req, tokenObj)
            userInfoRequest.subscribe(userInfo => {
                res.cookie('user', `${encodeURIComponent(JSON.stringify({
                    username: userInfo.username,
                    workId: userInfo.workId,
                    memberId: userInfo.memberId,
                    userId: process.env.NODE_ENV === 'development' ? '80168' : userInfo.userId, // 本地开发环境替换预发的userId
                    access_token: tokenObj.access_token
                }))}`)
                // Expires=${new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000).toUTCString()};path=/
                res.redirect(301, return_uri)
            })
        })
        // return res.end()
    }

    @Get('posts')
    async getPosts(@Res() res) {
        const posts = await this.tempService.getPosts();
        return res.status(HttpStatus.OK).json(posts);
    }

    @Get('getTempList')
    async getTemp(@Res() res, @Query() query) {
        const posts:any = await this.tempService.getTemp(query.user);
        return res.status(HttpStatus.OK).json({code:'200',data: posts});
    }

    @Get('getOneTemp')
    async getOneTemp(@Res() res, @Query() query, @Req() req) {
        const posts:any = await this.tempService.getOneTemp(query.id, req);
        if(posts.length){
            return res.status(HttpStatus.OK).json({code:'200',data: posts[0]});
        }else{
            return res.status(HttpStatus.OK).json({code:'0000',data: {msg: '未查询到该模板'}});
        }
    }

    @Post('/logout')
    async logout(@Req() req, @Res() res) {
        const logoutResult = await this.tempService.logout(req);
        if(logoutResult) {
            res.cookie('user', '')
            return res.status(HttpStatus.OK).json({code:'200'});
        }else{
            return res.status(HttpStatus.OK).json({code:'400'});
        }
    }

    @Post('/deleteTemp')
    async deleteTemp(@Req() req, @Res() res,  @Body() body) {
        const logoutResult = await this.tempService.deleteTemp(body.id, req);
        if(logoutResult) {
            return res.status(HttpStatus.OK).json({code:'200'});
        }else{
            return res.status(HttpStatus.OK).json({code:'0000'});
        }
    }

    @Post('/save')
    async addPost(@Res() res, @Body() createPostDTO: CreatePostDTO) {
        // console.log(createPostDTO)
        const newPost = await this.tempService.addPost(createPostDTO);
        return res.status(HttpStatus.OK).json({
            code: '200',
            message: "保存成功",
            data: newPost
        })
    }

    @Delete('/delete')
    async deletePost(@Res() res, @Query('postID', new ValidateObjectId()) postID) {
        const deletedPost = await this.tempService.deletePost(postID);
        if (!deletedPost) throw new NotFoundException('Post does not exist!');
        return res.status(HttpStatus.OK).json({
            message: 'Post has been deleted!',
            post: deletedPost
        })
    }
}
// template.service.ts
import { ConsoleLogger, Injectable, Response, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Post } from './interfaces/post.interface';
import { CreatePostDTO } from './dto/create-post.dto';
import utils from '../../utils/index';
import * as qs from 'qs';
import { map } from 'rxjs/operators';

@Injectable()
export class TempService {

    constructor(@InjectModel('Post') private readonly postModel: Model<Post>, private readonly httpService: HttpService) { }


    oauthCallback(req: any, query: any, responseInfo: any) {
        const curConfig = utils.getServerConfig(req?.headers?.host)
        const code = query.code
        const user = utils.getCookie('user', req.headers.cookie)
        if (!user) {
            // 获取token
            let tokenReq = this.httpService.post(curConfig.oauthTokenUrl, qs.stringify({
                client_id: curConfig.client_id,
                client_secret: curConfig.client_secret,
                redirect_uri: curConfig.baseUrl + curConfig.redirect_uri,
                code,
                grant_type: 'authorization_code'
            }), {
                headers: {
                    'content-type': 'application/x-www-form-urlencoded'
                },
            }).pipe(map(res => res.data))

            return tokenReq
        }
    }

    async logout(req: any) {
        const curConfig = utils.getServerConfig(req?.headers?.host)
        const user = decodeURIComponent(utils.getCookie('user', req.headers.cookie))
        const userInfo = JSON.parse(user);
        let request = this.httpService.post(curConfig.logoutUrl + '?access_token=' + userInfo.access_token, qs.stringify({}), {
            headers: {
                'content-type': 'application/json'
            },
        }).pipe(map(res => {
            return res.data
        }))

        return new Promise(resolve => {
            request.subscribe(res => {
                resolve(true)
            })
        })
    }

    getUserInfo(req: any, tokenObj: any) {
        const curConfig = utils.getServerConfig(req?.headers?.host)


        return this.httpService.post(curConfig.getuserinfoUrl, qs.stringify(tokenObj || {}), {
            headers: {
                'content-type': 'application/x-www-form-urlencoded'
            },
        })
            .pipe(map(res => {
                return res.data
            }))
    }

    async getPosts(): Promise<Post[]> {
        const posts = await this.postModel.find().exec();
        return posts;
    }

    async getTemp(id: any): Promise<Post[]> {
        let posts: any = await this.postModel.find({ "owner": id }).exec();
        return posts;
    }

    async getOneTemp(id: any, req:any): Promise<Post[]> {
        const user = decodeURIComponent(utils.getCookie('user', req.headers.cookie))
        const userInfo = JSON.parse(user);
        let posts: any = await this.postModel.find({ "owner": userInfo.workId, _id: id }).exec();
        return posts;
    }

    async deleteTemp(id: any, req:any): Promise<Boolean> {
        const user = decodeURIComponent(utils.getCookie('user', req.headers.cookie))
        const userInfo = JSON.parse(user);

        let list: any = await this.postModel.find({ "owner": userInfo.workId, _id: id }).exec();
        if(list.length){
            await this.postModel.remove({ "owner": userInfo.workId, _id: id }).exec();
            return true;
        }else{
            return false
        }
    }

    async getPost(postID): Promise<Post> {
        const post = await this.postModel
            .findById(postID)
            .exec();
        return post;
    }

    async addPost(createPostDTO: any): Promise<Post> {
        if(createPostDTO.id.length>5){
            createPostDTO._id = createPostDTO.id
            const newPost = await new this.postModel(createPostDTO);

            return newPost.update(createPostDTO);
        }else{
            const newPost = await new this.postModel(createPostDTO);

            return newPost.save()
        }
    }

    async deletePost(postID): Promise<any> {
        const deletedPost = await this.postModel
            .findByIdAndRemove(postID);
        return deletedPost;
    }

}

数据库相关配置都放在config目录下面,根据环境变量动态切换生产与qa等环境。

image.png 自此,一个基本的后端应用就ok了。