手把手帶你學習Midwayjs實戰(zhàn)
前言
哈嘍,大家好,我是migor,一個樂于分享工作中所用的一些知識的人,目前專注于前端和Node.js技術棧的分享,工作中目前負責提效平臺的搭建和開發(fā)。
Node.js么?我想這個問題,可能每個前端開發(fā)者,都會在工作到一定階段思考這個問題??梢院苊鞔_的告訴大家,學習Node.js 可能是將來每個前端開發(fā)者必備的一項技能。
在 Angular 發(fā)布的同一年(2009年),Node.js 也隨之登臺,Node.js 的出現(xiàn)帶來的第一個好處就是前端工程化的成熟,前端構建工具開始百花齊放。這時的前端已經(jīng)不再是一個簡單編寫幾行 JavaScript 即可完成的事情,前端開發(fā)開始出現(xiàn)了前端工程師這個職位,專職前端研發(fā)人員開始在各個公司中普及,前后端協(xié)作問題也開始加劇。
BFF
隨著 Node.js 的成熟,在2015年,基于BFF(Backgroud For Frontend, 服務于前端的后端)的架構理念被提出,BFF 架構通過在UI 和服務端之間加入中間層,解決了前后端職責難以劃分的問題。

如圖所示,由于前端的邏輯復雜性不斷增加,增加了專門用于處理用戶界面邏輯的服務層,同時后端邏輯也完成下沉,基于微服務架構的后端服務逐漸成型,通過基于Node.js 的BFF 層,前后端形成了比較清晰的分工,也就是進入了前端工程師時代。
Node.js的基本原理
先看一下早期的Node.js 結構圖,來自Node.js 之父 Ryan Dahl的演講稿,它簡要的介紹了Node.js 是基于Chrome V8引擎構建的,由于事件循環(huán)Event Loop 分發(fā)I/O 任務, 最終工作線程Work Thread 將任務丟到線程池Thread Pool 里去執(zhí)行, 而事件循環(huán)只要等待執(zhí)行結果就可以了

核心
Chrome V8 解釋并執(zhí)行 JavaScript 代碼(這就是為什么瀏覽器能執(zhí)行 JavaScript 原因) libuv由事件循環(huán)和線程池組成,負責所有 I/O 任務的分發(fā)與執(zhí)行
常用的框架
| 框架名稱 | 特性 |
|---|---|
| Express | 簡單、實用、路由中間件等俱全 |
| Nest.js | 支持ts,易于拓展,結合了函數(shù)式編程等 |
| Koa.js | 體積更小,代表現(xiàn)代和未來 |
| egg.js | 基于Koa,在開發(fā)上有更大便利 |
| Midway | 支持ts, 漸進式的Node框架,更接近與nest |
為什么選擇Midway
如果說這兩年那個語言在前端最火,我想
TypeScript肯定有一席之地,強約束性的語言使得在構建Node.js應用時,提供了類型檢查等約束能力,使得Node.js更安全等。Midway基于TypeScript開發(fā),對于TypeScript的支持更好一些。最近在深耕于公司的基礎建設,使用的
Node.js框架剛好是Midwayjs。Midwayjs提供了Web中間件的能力。
Midway簡介
Midway 是阿里巴巴 - 淘寶前端架構團隊,基于漸進式理念研發(fā)的 Node.js 框架。
Midway 基于 TypeScript 開發(fā),結合了面向對象(OOP + Class + IoC)與函數(shù)式(FP + Function + Hooks)兩種編程范式,并在此之上支持了 Web / 全棧 / 微服務 / RPC / Socket / Serverless 等多種場景,致力于為用戶提供簡單、易用、可靠的 Node.js 服務端研發(fā)體驗。
多編程范式
Midway 支持面向對象與函數(shù)式兩種編程范式,你可以根據(jù)實際研發(fā)的需要,選擇不同的編程范式來開發(fā)應用。
面向對象(OOP + Class + IoC)
Midway 支持面向對象的編程范式,為應用提供更優(yōu)雅的架構。
下面是基于面向對象,開發(fā)路由的示例。
//?src/controller/home.ts
import?{?Controller,?Get?}?from?'@midwayjs/decorator';
import?{?Context?}?from?'@midwayjs/koa';
@Controller('/')
export?class?HomeController?{
??@Inject()
??ctx:?Context
??@Get('/')
??async?home()?{
????return?{
??????message:?'Hello?Midwayjs!',
??????query:?this.ctx.ip
????}
??}
}
函數(shù)式(FP + Function + Hooks)
Midway 也支持函數(shù)式的編程范式,為應用提供更高的研發(fā)效率。
下面是基于函數(shù)式,開發(fā)路由接口的示例。
//?src/api/index.ts
import?{?useContext?}?from?'@midwayjs/hooks'
import?{?Context?}?from?'@midwayjs/koa';
export?default?async?function?home?()?{
??const?ctx?=?useContext()
??return?{
????message:?'Hello?Midwayjs!',
????query:?ctx.ip
??}
}
環(huán)境準備
首先確保你已經(jīng)安裝了Node.js,Node.js 安裝會附帶npx 和一個npm包運行程序,Midway 3.0.0 最低版本要求12.x。如果需要幫助,請參考如何安裝Node.js環(huán)境[1]。
項目創(chuàng)建
使用npm init midway來創(chuàng)建項目
npm?init?midway

我們這里使用3.0版本,因此我們這里選擇koa-v3,輸入項目名稱, 腳手架會幫我們創(chuàng)建一個簡單的項目工程,等安裝完成。

我們使用Vscode 打開項目??梢缘玫浆F(xiàn)在的工程目錄
midway-demo
├──?README.md
├──?README.zh-CN.md
├──?bootstrap.js
├──?jest.config.js
├──?package.json
├──?src
│???├──?config
│???│???├──?config.default.ts
│???│???└──?config.unittest.ts
│???├──?configuration.ts
│???├──?controller
│???│???├──?api.controller.ts
│???│???└──?home.controller.ts
│???├──?filter
│???│???├──?default.filter.ts
│???│???└──?notfound.filter.ts
│???├──?interface.ts
│???├──?middleware
│???│???└──?report.middleware.ts
│???└──?service
│???????└──?user.service.ts
├──?test
│???└──?controller
│???????├──?api.test.ts
│???????└──?home.test.ts
└──?tsconfig.json
整個項目包括了一些最基本的文件和目錄
src整個工程的源碼目錄,之后所有的開發(fā)代碼都將放在這個文件夾下面test測試目錄,之后所有的代碼測試文件都在這里package.jsonNode.js項目基礎的包管理配置文件,這個想必大家都很熟悉tsconfig.jsonTypeScript編譯配置文件.
在src目錄下面,常用的有:
config業(yè)務的配置目錄controllerweb controller目錄filter過濾器目錄interface.ts業(yè)務的ts定義文件middleware中間件目錄service服務邏輯目錄
啟動項目
yarn?dev
warning?../../../../../package.json:?No?license?field
$?cross-env?NODE_ENV=local?midway-bin?dev?--ts
[?Midway?]?Start?Server?at??http://127.0.0.1:7001
在瀏覽器中輸入127.0.0.1:7001

路由
我們來看一下代碼中的controller 文件夾下面的home.controller.ts 文件
import?{?Controller,?Get?}?from?'@midwayjs/decorator';
@Controller('/')
export?class?HomeController?{
??@Get('/')
??async?home():?Promise<string>?{
????return?'Hello?Midwayjs!';
??}
}
我們找到了瀏覽器中的輸出Hello Midwayjs!
路由裝飾器
@controller 裝飾器標注了控制器,裝飾器有一個可選參數(shù),用于進行路由前綴,這樣控制器下面的所有路由都會帶上這個前綴。
我們修改一下裝飾器中的內容
import?{?Controller,?Get?}?from?'@midwayjs/decorator';
@Controller('/test')
export?class?HomeController?{
??@Get('/')
??async?home():?Promise<string>?{
????return?'Hello?Midwayjs!';
??}
}
在瀏覽器中輸入127.0.0.1:7001 報錯

報錯信息告訴我們路由找不到,那么我們改一下瀏覽器中的路由127.0.0.1:7001/test,我們得到了我們想要的結果,這里我們可以知道裝飾器中的參數(shù)匹配我們的路由

Http裝飾器
常見的 Http裝飾器, ?@Get 、 @Post 、 @Put() 、 @Del() 、 @Patch() 、 @Options() 、 @Head() 和 @All() ,表示各自的 HTTP 請求方法。
我們改寫一下代碼
import?{?Controller,?Get,?Post?}?from?'@midwayjs/decorator';
@Controller('/test')
export?class?HomeController?{
??@Post('/')
??async?home():?Promise<string>?{
????return?'Hello?Midwayjs!';
??}
}
通過使用Postman 調用接口,將請求方式改為post,可以看到我們拿到我們請求的接口了。

全局路由前綴
在工程項目中,我們常常使用一些路由前綴去區(qū)分不同服務之間的作用,那么相同的路由前綴,在每個controller里面加入,顯然很麻煩,如果要改變前綴名稱,在后期工程相對較大,接口較多的時候,豈不是要一個個去改,在這里我們配置全局的路由前綴。
我們修改config/config.default.ts 文件,代碼修改如下
import?{?MidwayConfig?}?from?'@midwayjs/core';
export?default?{
??//?use?for?cookie?sign?key,?should?change?to?your?own?and?keep?security
??keys:?'1653223786698_4903',
??koa:?{
????port:?7001,
????globalPrefix:?'/demo',
??},
}?as?MidwayConfig;
保存文件之后,服務不需要我們手動重啟,我們請求一下http://127.0.0.1/demo/test,服務返回了我們的內容。

依賴注入
依賴注入(DI)、控制反轉(IoC)等是Spring的核心思想,那么在midwayjs中通過裝飾器的輕量特性,讓依賴注入變得非常優(yōu)雅.
舉個例子??:
.
├──?package.json
├──?src
│???├──?controller??????????????????????????????????????????#?控制器目錄
│???│???└──?api.controller.ts
│???└──?service?????????????????????????????????????????????#?服務目錄
│???????└──?user.service.ts
└──?tsconfig.json
我們實現(xiàn)一下文件的代碼
//?api.controller.ts
import?{?Inject,?Controller,?Get,?Query?}?from?'@midwayjs/decorator';
import?{?Context?}?from?'@midwayjs/koa';
import?{?UserService?}?from?'../service/user.service';
@Controller('/api')
export?class?APIController?{
??@Inject()
??ctx:?Context;
??@Inject()
??userService:?UserService;
??@Get('/get_user')
??async?getUser(@Query('uid')?uid)?{
????const?user?=?await?this.userService.getUser({?uid?});
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
}
//?user.service.ts
import?{?Provide?}?from?'@midwayjs/decorator';
import?{?IUserOptions?}?from?'../interface';
@Provide()
export?class?UserService?{
??async?getUser(options:?IUserOptions)?{
????return?{
??????uid:?options.uid,
??????username:?'mockedName',
??????phone:?'12345678901',
??????email:?'[email protected]',
????};
??}
}
@Provide 的作用是告訴 依賴注入容器 ,我需要被容器所加載。@Inject 裝飾器告訴容器,我需要將某個實例注入到屬性上。
上面例子??上,我們實現(xiàn)了一個UserService并通過@Provide注入到容器中,在app.controller中,我們通過@Inject 拿到了userService的實例。
那么我們請求一下接口:

調試
我們在擴展里面搜索JavaScript Debugger

點擊下拉箭頭,選擇JavaScript Debug Terminal, .

輸入命令yarn dev,在需要debugger的位置打上斷點

在Postman 中請求接口,可以看到代碼執(zhí)行到斷點位置

連接Mysql
前面我們已經(jīng)實現(xiàn)了接口的請求,那么作為后端項目,必然會涉及到數(shù)據(jù)的CURD,這里必須得使用數(shù)據(jù)庫實現(xiàn)數(shù)據(jù)的持久化了,數(shù)據(jù)庫我們這篇文章使用的是Mysql, 如果是使用的Mongoose可以參考筆者的另一篇文章MidwayJs多數(shù)據(jù)庫配置,并實現(xiàn)Mongoose自增Id。
數(shù)據(jù)庫安裝
筆者使用的是Homebrew來安裝的Mysql,如果沒有安裝Homebrew,可以直接下載安裝包安裝,或者先安裝Homebrew,詳細步驟參見Homebrew[2] 官網(wǎng)。

//?確認brew在正常工作
brew?doctor
//?更新包
brew?update
//?或者更新全局所有包
brew?upgrade
//?安裝mysql
brew?install?mysql
數(shù)據(jù)庫服務啟動
安裝完成之后啟動Mysql服務
mysql.server?start

啟動完成。
Mysql可視化
我們使用可視化工具來管理數(shù)據(jù)庫,這里筆者使用的是 Navicat Premium,可視化工具相對比較多,你可以使用自己喜歡的可視化工具管理數(shù)據(jù)庫。
我們創(chuàng)建一個Mysql數(shù)據(jù)庫連接,連接名稱可以隨意取自己喜歡的,輸入默認的端口,輸入自己數(shù)據(jù)庫的密碼。

連接成功之后,我們創(chuàng)建一個Midway的數(shù)據(jù)表

創(chuàng)建成功之后

引入TypeORM
TypeORM[3] 是 node.js 現(xiàn)有社區(qū)最成熟的對象關系映射器(ORM )。Midway 和 TypeORM 搭配,使開發(fā)更簡單。
安裝組件
安裝ORM組件,提供數(shù)據(jù)庫ORM 能力
yarn?add?@midwayjs/orm?typeorm?--save
引入組件
在src/configuration.ts引入ORM組件,代碼如下:
//?configuration.ts
import?{?Configuration?}?from?'@midwayjs/decorator';
import?*?as?orm?from?'@midwayjs/orm';
import?{?join?}?from?'path';
@Configuration({
??imports:?[
????//?...
????orm?????????????????????????????????????????????????????????//?加載?orm?組件
??],
??importConfigs:?[
????join(__dirname,?'./config')
??]
})
export?class?ContainerConfiguratin?{
}
安裝數(shù)據(jù)庫Driver
yarn?add?mysql?mysql2?--save
配置數(shù)據(jù)庫連接
在src/config/config.default.ts 中配置mysql 連接。
import?{?MidwayConfig?}?from?'@midwayjs/core';
export?default?{
??//?use?for?cookie?sign?key,?should?change?to?your?own?and?keep?security
??keys:?'1653223786698_4903',
??koa:?{
????port:?7001,
????globalPrefix:?'/demo',
??},
??orm:?{
????type:?'mysql',
????host:?'127.0.0.1',
????port:?3306,
????username:?'root',
????password:?'',?//?數(shù)據(jù)庫密碼
????database:?'midway',?//?數(shù)據(jù)表
????synchronize:?true,
????logging:?false,
??},
}?as?MidwayConfig;
保存之后重啟,數(shù)據(jù)庫連接成功

實現(xiàn)model
在src文件夾下面創(chuàng)建model文件夾,創(chuàng)建一個數(shù)據(jù)庫表
聲明一個實體table
//?user.ts
import?{?EntityModel?}?from?'@midwayjs/orm';
import?{?Column,?PrimaryGeneratedColumn?}?from?'typeorm';
//?映射user?table
@EntityModel({?name:?'user'?})
export?class?UserModel?{
??//?聲明主鍵
??@PrimaryGeneratedColumn('increment')?id:?number;
?
??//?映射userName和user表中的user_name對應
??@Column({?name:?'user_name'?})?userName:?string;
??@Column({?name:?'age'?})?age:?number;
??@Column({?name:?'description'?})?description:?string;
}
修改src/user.service.ts文件
import?{?Provide?}?from?'@midwayjs/decorator';
import?{?InjectEntityModel?}?from?'@midwayjs/orm';
import?{?Repository?}?from?'typeorm';
import?{?IUserOptions?}?from?'../interface';
import?{?UserModel?}?from?'../model/user';
@Provide()
export?class?UserService?{
??@InjectEntityModel(UserModel)?userModel:?Repository;
??async?getUser(options:?IUserOptions)?{
????return?{
??????uid:?options.uid,
??????username:?'mockedName',
??????phone:?'12345678901',
??????email:?'[email protected]',
????};
??}
??
??async?addUser()?{
????let?record?=?new?UserModel();
????record?=?this.userModel.merge(record,?{
??????userName:?'migor',
??????age:?18,
??????description:?'test',
????});
????try?{
??????const?created?=?await?this.userModel.save(record);
??????return?created;
????}?catch?(e)?{
??????console.log(e);
????}
??}
}
通過InjectEntityModel 裝飾器,注入實例化userModel,啟動服務之后,我們在midway數(shù)據(jù)表中增加user table

修改src/controller/api.controller.ts
import?{?Inject,?Controller,?Get,?Query?}?from?'@midwayjs/decorator';
import?{?Context?}?from?'@midwayjs/koa';
import?{?UserService?}?from?'../service/user.service';
@Controller('/api')
export?class?APIController?{
??@Inject()
??ctx:?Context;
??@Inject()
??userService:?UserService;
??@Get('/get_user')
??async?getUser(@Query('uid')?uid)?{
????const?user?=?await?this.userService.getUser({?uid?});
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
??@Get('/add_user')
??async?addUser()?{
????const?user?=?await?this.userService.addUser();
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
}
在Postman中調用add_user接口

我們可以看到已經(jīng)能正常返回我們保存的值了,那么我們去數(shù)據(jù)庫看一下,數(shù)據(jù)是否保存了,刷新一下數(shù)據(jù)庫,我們可以看到數(shù)據(jù)已經(jīng)保存成功。

大功告成,至此我們完成數(shù)據(jù)的保存,那么后面我們可以進行數(shù)據(jù)的查詢,刪除,更新等。代碼如下
在user.service.ts中添加如下代碼
// 刪除用戶
async deleteUser() {
const record = await this.userModel
.createQueryBuilder()
.delete()
.where({ userName: 'migor' })
.execute();
const { affected } = record || {};
return affected > 0;
}
// 更新用戶信息
async updateUser() {
try {
const result = await this.userModel
.createQueryBuilder()
.update()
.set({
description: '測試更新',
})
.where({ userName: 'migor' })
.execute();
const { affected } = result || {};
return affected > 0;
} catch (e) {
console.log('接口更新失敗');
}
}
// 查詢
async getUserList() {
const users = await this.userModel
.createQueryBuilder()
.where({ userName: 'migor' })
.getMany();
return users;
}
在api.controller.ts中增加相應的接口
@Get('/get_user_list')
async?getUsers()?{
??const?user?=?await?this.userService.getUserList();
??return?{?success:?true,?message:?'OK',?data:?user?};
}
@Get('/update_user')
async?updateUser()?{
??const?user?=?await?this.userService.updateUser();
??return?{?success:?true,?message:?'OK',?data:?user?};
}
@Get('/delete_user')
async?deleteUser()?{
??const?user?=?await?this.userService.deleteUser()
??return?{?success:?true,?message:?'OK',?data:?user?};
}
接入Swagger
安裝組件
接入swagger組件和swagger ui組件
yarn?add?@midwayjs/swagger?swagger-ui-dist
開啟組件
在configuration.ts 中增加組件
import?{?Configuration,?App?}?from?'@midwayjs/decorator';
import?*?as?koa?from?'@midwayjs/koa';
import?*?as?validate?from?'@midwayjs/validate';
import?*?as?info?from?'@midwayjs/info';
import?{?join?}?from?'path';
import?*?as?orm?from?'@midwayjs/orm';
import?*?as?swagger?from?'@midwayjs/swagger';
//?import?{?DefaultErrorFilter?}?from?'./filter/default.filter';
//?import?{?NotFoundFilter?}?from?'./filter/notfound.filter';
import?{?ReportMiddleware?}?from?'./middleware/report.middleware';
@Configuration({
??imports:?[
????koa,
????validate,
????{
??????component:?info,
??????enabledEnvironment:?['local'],
????},
????orm,
????swagger,
??],
??importConfigs:?[join(__dirname,?'./config')],
})
export?class?ContainerLifeCycle?{
??@App()
??app:?koa.Application;
??async?onReady()?{
????//?add?middleware
????this.app.useMiddleware([ReportMiddleware]);
????//?add?filter
????//?this.app.useFilter([NotFoundFilter,?DefaultErrorFilter]);
??}
}
項目自動重啟成功之后,訪問地址
UI: http://127.0.0.1:7001/swagger-ui/index.html JSON: http://127.0.0.1:7001/swagger-ui/index.json
啟用之后可以查看到對應的接口

swagger組件會自動識別各個@Controller中每個路由方法的@Body()、@Query()、@Param() 裝飾器,提取路由方法參數(shù)和類型。
增加接口標簽
我們希望給接口增加標簽注釋,這樣才能更好的列舉接口的定義
import?{?Inject,?Controller,?Get,?Query?}?from?'@midwayjs/decorator';
import?{?Context?}?from?'@midwayjs/koa';
import?{?ApiOperation?}?from?'@midwayjs/swagger';
import?{?UserService?}?from?'../service/user.service';
@Controller('/api')
export?class?APIController?{
??@Inject()
??ctx:?Context;
??@Inject()
??userService:?UserService;
??@ApiOperation({?summary:?'獲取單個用戶'?})
??@Get('/get_user')
??async?getUser(@Query('uid')?uid)?{
????const?user?=?await?this.userService.getUser({?uid?});
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
??@ApiOperation({?summary:?'增加單個用戶'?})
??@Get('/add_user')
??async?addUser()?{
????const?user?=?await?this.userService.addUser();
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
??@ApiOperation({?summary:?'獲取用戶列表'?})
??@Get('/get_user_list')
??async?getUsers()?{
????const?user?=?await?this.userService.getUserList();
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
??@ApiOperation({?summary:?'更新單個用戶'?})
??@Get('/update_user')
??async?updateUser()?{
????const?user?=?await?this.userService.updateUser();
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
??@ApiOperation({?summary:?'刪除單個用戶'?})
??@Get('/delete_user')
??async?deleteUser()?{
????const?user?=?await?this.userService.deleteUser();
????return?{?success:?true,?message:?'OK',?data:?user?};
??}
}
重啟之后,可以查看swagger ui界面,標簽增加成功。

總結
至此我們已經(jīng)完成了Midwayjs基本功能的學習,包括搭建,數(shù)據(jù)庫的映射,簡單的CRUD,以及ORM和Swagger的接入了。
不知不覺搞到了12點,時間有點太晚了,關于接口傳參,數(shù)據(jù)校驗等問題,在后續(xù)的文章中會繼續(xù)寫,我們后面會進行一個博客前后端搭建的系列文章,后續(xù)帶你繼續(xù)學習midway。
肝文不易,你的每個點贊和關注都是對我最大的鼓勵,比心??。
如何安裝Node.js環(huán)境: http://midwayjs.org/docs/how_to_install_nodejs
[2]Homebrew: https://brew.sh/
[3]TypeORM: https://github.com/typeorm/typeorm
