一次TypeScript, React, Node, MongoDB的模板式前后端分离开发实践

·  阅读 4708
一次TypeScript, React, Node, MongoDB的模板式前后端分离开发实践

前言

在大概1年前接触了typescript之后, 日渐被它所吸引. 甚至一个简单的本地测试文件node ./test.js有时也会切到ts-node ./test.ts. 在同样的时间节点之前, 还是会不时地去学学node, mongodb相关的. 可是, 由于懒(需)惰(求), 在很久没碰之后, 很多知识点都忘了!😴

综上, 于是就有了今天这个话题:

如何在工作时间之余完成自己的个人项目并实现按时上床睡觉

答案是: 不存在的😅

项目简介

项目会不断维护. 无论是client端还是server端, 都只提供简单的模板式的功能.

地址

client ts-react-webpack

server showcase

线上体验

依赖

typescript是两端的基调

client

  • webpack-4.x
  • typescript-3.0.x
  • react-16.4.x
  • mobx-5.x
  • ant design
  • ...

详看

server

centos上mongodb的官网安装教程, 其他系统请自行查阅.

  • nestjs
  • dotenv
  • jsonwebtoken
  • mongodb(mongoose)
  • ...

需要讲一下我为什么选了nestjs:

nestjstypeScript引入并基于express封装. 意味着, 它与绝大部分express插件的兼容性都很好.

nestjs的核心概念是提供一种体系结构, 它帮助开发人员实现层的最大分离, 并在应用程序中增加抽象.

此外, 它对测试是非常友好的...

也需要声明的是, nestjs的依赖注入特性是受到了angular框架的启发, 相信做angular开发的对整个程序体系会更容易看懂.

查看中文文档

具体实现

server

简单介绍下几个主流程模块

main.ts

我是用nest-cli工具初始化项目的, 一切从src/main.ts开始

import { NestFactory } from '@nestjs/core'
import * as dotenv from 'dotenv'
import { DOTENV_PATH } from 'config'

// 优先执行, 避免引用项目模块时获取环境变量失败
dotenv.config({ path: DOTENV_PATH })

import { AppModule } from './app.module'

async function bootstrap() {
    const app = await NestFactory.create(AppModule)
    // 支持跨域
    app.enableCors()
    await app.listen(9999)
}
bootstrap()

同样地, 我们可以提供一个express实例到NestFactory.create:

const server = express();
const app = await NestFactory.create(ApplicationModule, server);

这样我们就可以完全控制express实例生命周期, 比如官方FAQ中说到的创建几个同时运行的服务器

在我本地开发的时候, 根目录上还有一个.dev.env, 这是未提交到github的, 因为里面包含了我个人的mongodb远程ip地址 其他内容与github上的.env一致, 因为我本地并不想再安装一遍mongodb, 如果是想把项目拉下来就跑起来的, 无论如何你都需要一个mongodb服务, 当然你是可以本地安装就好了.

还需要提及到一点就是调试:

以前在vscode上调试node程序都需要在调试栏新增配置, 然后利用该配置去跑起应用才能实现断点调试, 新版的vscode支持autoAttach功能, 使用Command + Shift + P 唤起设置功能面板

启动它!

这样, 在项目的.vscode/setting.json里面会多了一个选项: "debug.node.autoAttach": "on", 在我们的启动script里面加上--inspect-brk就可以实现vscode的断点调试了. 对应地, npm run start:debug是我的启动项, 可参考nodemon.debug.json

app.module.ts

import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'

import { DB_CONN } from 'config/db'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import modules from 'routers'

@Module({
    imports: [
        MongooseModule.forRoot(DB_CONN, {
            useNewUrlParser: true,
        }),
        ...modules,
    ],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

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

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

参考module的文档

AppController在这个程序当中只是为了测试能返回Hello World!!!, 其实它不是必须的, 我们可以把它直接干掉, 把全部接口, 全部逻辑放到各个module中实现, 以modules/user为例, 接着往下看.

modules/user

目录结构

user
├── dto -------------- 数据传输对象
├── index.ts --------- UserModule, 概念同AppModule
├── controller.ts ---- 传统意义的控制器, `Nest`会将控制器映射到相应的路由
├── interface.ts ----- 类型声明
├── schema.ts -------- mongoose schema
├── service.ts ------- 处理逻辑

有必要讲讲controller.tsservice.ts, 这是nestjs的概念中很重要的部分

controller.ts

import { Get, Post, Body, Controller } from '@nestjs/common'

import UserService from './service'
import CreateDto from './dto/create.dto'

@Controller('user')
export default class UserController {
    constructor(private readonly userService: UserService) {}

    @Get()
    findAll() {
        return this.userService.findAll()
    }

    @Post('create')
    create(@Body() req: CreateDto) {
        return this.userService.create(req)
    }
}

装饰器路由为每个路由声明了前缀,所以Nest会在这里映射每个/user的请求

@Get()装饰器告诉Nest创建此路由路径的端点

同样地, @Post()也是如此, 并且这类Method装饰器接收一个path参数, 如@Post('create'), 那么我们就可以实现post到路径/user/create

到此, 往后的逻辑交给service实现

service.ts

import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'

import logger from 'utils/logger'
import { cryptData } from 'utils/common'
import ServiceExt from 'utils/serviceExt'
import { IUser } from './interface'
import CreateDto from './dto/create.dto'

@Injectable()
export default class UserService extends ServiceExt {
    constructor(@InjectModel('User') private readonly userModel: Model<IUser>) {
        super()
    }

    async create(createDto: CreateDto) {
        if (!createDto || !createDto.account || !createDto.password) {
            logger.error(createDto)
            return this.createResData(null, '参数错误!', 1)
        }
        const isUserExist = await this.isDocumentExist(this.userModel, {
            account: createDto.account,
        })
        if (isUserExist) {
            return this.createResData(null, '用户已存在!', 1)
        }
        const createdUser = new this.userModel({
            ...createDto,
            password: cryptData(createDto.password),
        })
        const user = await createdUser.save()
        return this.createResData(user)
    }

    async findUserByAccount(account: string) {
        const user = await this.userModel.findOne({ account })
        return user
    }

    async findAll() {
        const users = await this.userModel.find({})
        return this.createResData(users)
    }
}

至此, 我们运行npm run start:dev启动一下服务:

直接在浏览器端访问http://localhost:9999/#/

没错, 的确失败了!!! 因为我们使用了jsonwebtoken, 在modules/auth可以看到它的实现.

现在我们在postman中登录了再试试吧!

bingo!!!

(如果是想拉下来跑的话, 也可以照着schema的格式用postman先伪造条用户数据, 把系统打通!!!)

client

关于client端的实现我不会细讲, 可以看项目github, 和我之前的文章(typescript-react-webpack4 起手与踩坑), 项目结构会有改动.

讲一下接入了真实服务器之后http请求对于token的一些处理, 查看http.ts

首先是创建axios实例时需要在header处把token带上

const axiosConfig: AxiosRequestConfig = {
    method: v,
    url,
    baseURL: baseUrl || DEFAULTCONFIG.baseURL,
    headers: { Authorization: `Bearer ${getCookie(COOKIE_KEYS.TOKEN)}` }
}
const instance = axios.create(DEFAULTCONFIG)

token也可以存放在localStorage

另外一点是, 对应服务端返回的token错误处理


const TOKENERROR = [401, 402, 403]
let authTimer: number = null
...

if (TOKENERROR.includes(error.response.status)) {
    message.destroy()
    message.error('用户认证失败! 请登录重试...')
    window.clearTimeout(authTimer)
    authTimer = window.setTimeout(() => {
        location.replace('/#/login')
    }, 300)
    return
}

总结

两端项目都是简单的模板项目, 不存在什么繁杂的业务, 属于比较初级的学习实践. 对nestjs的掌握程度有限, 只是拿来练练手. 可能后续会基于这篇文章继续深入地去讲讲, 比如部署之类的, 两个项目也会不断去维护. 后续也有计划会合二为一. 看时间吧!

收藏成功!
已添加到「」, 点击更改