手把手帶你學習Midwayjs實戰(zhàn),學不會算我輸
以下文章來源于前端修煉師 ,作者migor
前言
哈嘍,大家好,我是migor,一個樂于分享工作中所用的一些知識的人,目前專注于前端和Node.js技術(shù)棧的分享,工作中目前負責提效平臺的搭建和開發(fā)。
經(jīng)常有人問,現(xiàn)在都2022年了,還要學習Node.js么?我想這個問題,可能每個前端開發(fā)者,都會在工作到一定階段思考這個問題??梢院苊鞔_的告訴大家,學習Node.js 可能是將來每個前端開發(fā)者必備的一項技能。
在 Angular 發(fā)布的同一年(2009年),Node.js 也隨之登臺,Node.js 的出現(xiàn)帶來的第一個好處就是前端工程化的成熟,前端構(gòu)建工具開始百花齊放。這時的前端已經(jīng)不再是一個簡單編寫幾行 JavaScript 即可完成的事情,前端開發(fā)開始出現(xiàn)了前端工程師這個職位,專職前端研發(fā)人員開始在各個公司中普及,前后端協(xié)作問題也開始加劇。
BFF
隨著 Node.js 的成熟,在2015年,基于BFF(Backgroud For Frontend, 服務(wù)于前端的后端)的架構(gòu)理念被提出,BFF 架構(gòu)通過在UI 和服務(wù)端之間加入中間層,解決了前后端職責難以劃分的問題。
如圖所示,由于前端的邏輯復雜性不斷增加,增加了專門用于處理用戶界面邏輯的服務(wù)層,同時后端邏輯也完成下沉,基于微服務(wù)架構(gòu)的后端服務(wù)逐漸成型,通過基于Node.js 的BFF 層,前后端形成了比較清晰的分工,也就是進入了前端工程師時代。
Node.js的基本原理
先看一下早期的Node.js 結(jié)構(gòu)圖,來自Node.js 之父 Ryan Dahl的演講稿,它簡要的介紹了Node.js 是基于Chrome V8引擎構(gòu)建的,由于事件循環(huán)Event Loop 分發(fā)I/O 任務(wù), 最終工作線程Work Thread 將任務(wù)丟到線程池Thread Pool 里去執(zhí)行, 而事件循環(huán)只要等待執(zhí)行結(jié)果就可以了
核心
Chrome V8 解釋并執(zhí)行 JavaScript 代碼(這就是為什么瀏覽器能執(zhí)行 JavaScript 原因)
libuv 由事件循環(huán)和線程池組成,負責所有 I/O 任務(wù)的分發(fā)與執(zhí)行
常用的框架
框架名稱 特性
Express 簡單、實用、路由中間件等俱全
Nest.js 支持ts,易于拓展,結(jié)合了函數(shù)式編程等
Koa.js 體積更小,代表現(xiàn)代和未來
egg.js 基于Koa,在開發(fā)上有更大便利
Midway 支持ts, 漸進式的Node框架,更接近與nest
為什么選擇Midway
如果說這兩年那個語言在前端最火,我想TypeScript 肯定有一席之地,強約束性的語言使得在構(gòu)建Node.js應(yīng)用時,提供了類型檢查等約束能力,使得Node.js 更安全等。Midway 基于TypeScript開發(fā),對于TypeScript的支持更好一些。
最近在深耕于公司的基礎(chǔ)建設(shè),使用的Node.js 框架剛好是Midwayjs。
Midwayjs 提供了Web中間件的能力。
Midway簡介
Midway 是阿里巴巴 - 淘寶前端架構(gòu)團隊,基于漸進式理念研發(fā)的 Node.js 框架。
Midway 基于 TypeScript 開發(fā),結(jié)合了面向?qū)ο螅∣OP + Class + IoC)與函數(shù)式(FP + Function + Hooks)兩種編程范式,并在此之上支持了 Web / 全棧 / 微服務(wù) / RPC / Socket / Serverless 等多種場景,致力于為用戶提供簡單、易用、可靠的 Node.js 服務(wù)端研發(fā)體驗。
多編程范式
Midway 支持面向?qū)ο笈c函數(shù)式兩種編程范式,你可以根據(jù)實際研發(fā)的需要,選擇不同的編程范式來開發(fā)應(yīng)用。
面向?qū)ο螅∣OP + Class + IoC)
Midway 支持面向?qū)ο蟮木幊谭妒?,為?yīng)用提供更優(yōu)雅的架構(gòu)。
下面是基于面向?qū)ο?,開發(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ù)式的編程范式,為應(yīng)用提供更高的研發(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<Context>()
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.json Node.js 項目基礎(chǔ)的包管理配置文件,這個想必大家都很熟悉
tsconfig.json TypeScript 編譯配置文件.
在src目錄下面,常用的有:
config 業(yè)務(wù)的配置目錄
controller web controller 目錄
filter 過濾器目錄
interface.ts 業(yè)務(wù)的ts定義文件
middleware 中間件目錄
service 服務(wù)邏輯目錄
啟動項目
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ù),用于進行路由前綴,這樣控制器下面的所有路由都會帶上這個前綴。
我們修改一下裝飾器中的內(nèi)容
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,我們得到了我們想要的結(jié)果,這里我們可以知道裝飾器中的參數(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 調(diào)用接口,將請求方式改為post,可以看到我們拿到我們請求的接口了。
全局路由前綴
在工程項目中,我們常常使用一些路由前綴去區(qū)分不同服務(wù)之間的作用,那么相同的路由前綴,在每個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;
保存文件之后,服務(wù)不需要我們手動重啟,我們請求一下http://127.0.0.1/demo/test,服務(wù)返回了我們的內(nèi)容。
依賴注入
依賴注入(DI)、控制反轉(zhuǎn)(IoC)等是Spring的核心思想,那么在midwayjs中通過裝飾器的輕量特性,讓依賴注入變得非常優(yōu)雅.
舉個例子??:
.
├── package.json
├── src
│ ├── controller # 控制器目錄
│ │ └── api.controller.ts
│ └── service # 服務(wù)目錄
│ └── 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: 'xxx.xxx@xxx.com',
};
}
}
@Provide 的作用是告訴 依賴注入容器 ,我需要被容器所加載。@Inject 裝飾器告訴容器,我需要將某個實例注入到屬性上。
上面例子??上,我們實現(xiàn)了一個UserService并通過@Provide注入到容器中,在app.controller中,我們通過@Inject 拿到了userService的實例。
那么我們請求一下接口:
調(diào)試
我們在擴展里面搜索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ù)庫服務(wù)啟動
安裝完成之后啟動Mysql服務(wù)
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ū)最成熟的對象關(guān)系映射器(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對應(yīng)
@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<UserModel>;
async getUser(options: IUserOptions) {
return {
uid: options.uid,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com',
};
}
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,啟動服務(wù)之后,我們在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中調(diào)用add_user接口
我們可以看到已經(jīng)能正常返回我們保存的值了,那么我們?nèi)?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中增加相應(yīng)的接口
@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
啟用之后可以查看到對應(yīng)的接口
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界面,標簽增加成功。
總結(jié)
至此我們已經(jīng)完成了Midwayjs基本功能的學習,包括搭建,數(shù)據(jù)庫的映射,簡單的CRUD,以及ORM和Swagger的接入了。
不知不覺搞到了12點,時間有點太晚了,關(guān)于接口傳參,數(shù)據(jù)校驗等問題,在后續(xù)的文章中會繼續(xù)寫,我們后面會進行一個博客前后端搭建的系列文章,后續(xù)帶你繼續(xù)學習midway。
參考資料
[1]
如何安裝Node.js環(huán)境: http://midwayjs.org/docs/how_to_install_nodejs
[2]
Homebrew: https://brew.sh/
[3]
TypeORM: https://github.com/typeorm/typeorm
作者:migor
歡迎關(guān)注微信公眾號 :前端晚間課
更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵