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

前言

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

综上, 因而就有了今天这个话题:react

如何在工做时间之余完成本身的我的项目并实现按时上床睡觉webpack

答案是: 不存在的😅ios

项目简介

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

地址

client ts-react-webpackgithub

server showcaseweb

线上体验mongodb

依赖

typescript是两端的基调typescript

client

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

详看express

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的掌握程度有限, 只是拿来练练手. 可能后续会基于这篇文章继续深刻地去讲讲, 好比部署之类的, 两个项目也会不断去维护. 后续也有计划会合二为一. 看时间吧!

相关文章
相关标签/搜索