前言
在大概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:
nestjs
將typeScript
引入並基於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 | 此模組提供的提供者的子集, 並應在其他模組中使用 |
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.ts
和service.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
的掌握程度有限, 只是拿來練練手. 可能後續會基於這篇文章繼續深入地去講講, 比如部署之類的, 兩個專案也會不斷去維護. 後續也有計劃會合二為一. 看時間吧!