由于业务拓展,目前手上一个纯前端的小项目需要添加后台登陆以及数据保存功能。然后后端开发人员人手不足,所以决定用nodejs写一个后端应用,框架就选用nestjs,数据库就用mongodb。
首先、需要规划好目录结构。
前后端应用共享一个package.json。server目录存放nestjs构建的应用,client目录存放前端应用。
前端client目录:
- build - 前端构建打包模块
- config - 一些基础配置模块
- deploy - 自动打包,添加tag,上传git模块
- server - 前端web服务器部署模块,由于前端应用没有使用nginx,因此使用koa2搭建的web服务器
- src - 源码目录,不多说
- 其他是eslint配置以及自动添加厂商前缀的配置文件。
后端nestjs目录:
一、 middleware
中间件是在路由处理程序之前调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next()中间件函数。next() 中间件函数通常由名为 next 的变量表示。
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用
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模块
每个 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等环境。
自此,一个基本的后端应用就ok了。