一次TypeScript, React, Node, MongoDB的模板式前後端分離開發實踐

YDJFE發表於2018-09-01

前言

在大概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 喚起設定功能皮膚

一次TypeScript, React, Node, MongoDB的模板式前後端分離開發實踐

啟動它!

這樣, 在專案的.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/#/

一次TypeScript, React, Node, MongoDB的模板式前後端分離開發實踐

沒錯, 的確失敗了!!! 因為我們使用了jsonwebtoken, 在modules/auth可以看到它的實現.

現在我們在postman中登入了再試試吧!

一次TypeScript, React, Node, MongoDB的模板式前後端分離開發實踐

一次TypeScript, React, Node, MongoDB的模板式前後端分離開發實踐

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的掌握程度有限, 只是拿來練練手. 可能後續會基於這篇文章繼續深入地去講講, 比如部署之類的, 兩個專案也會不斷去維護. 後續也有計劃會合二為一. 看時間吧!

相關文章