前端中間件Midway的使用教程
一、 關于midway
Midway 是阿里巴巴 - 淘寶前端架構團隊,基于漸進式理念研發(fā)的 Node.js 框架,通過自研的依賴注入容器,搭配各種上層模塊,組合出適用于不同場景的解決方案。
Midway 基于 TypeScript 開發(fā),結合了面向對象(OOP + Class + IoC)與函數式(FP + Function + Hooks)兩種編程范式,并在此之上支持了 Web / 全棧 / 微服務 / RPC / Socket / Serverless 等多種場景,致力于為用戶提供簡單、易用、可靠的 Node.js 服務端研發(fā)體驗。
1. 解決什么痛點
以往的開發(fā)中,前端直接項目中直接調取后臺服務接口,就會從在過度依賴后臺數據,或者只能請求服務后再到渲染層進行數據加工大大影響開發(fā)效率,或者存在多個部門協同開發(fā),接口數據格式達不到統(tǒng)一,前端數據處理任務量加重等溝通問題。
2. 期望達到什么效果
在項目/產品開發(fā)中存在某種中間件進行服務接口的二次加工或者轉發(fā)以達到前端所需的統(tǒng)一數據結構。比如如下分工:
數據:負責數據開發(fā),對外提供服務數據接口
后端:負責業(yè)務邏輯開發(fā),對外提供服務業(yè)務邏輯接口
中間層:根據前端需要,調用多端不同的服務接口并進行拼接、加工數據,對前端提供加工后接口
前端:負責數據渲染
二、創(chuàng)建應用并使用
1. 創(chuàng)建midway應用
$npm init midway
選擇 koa-v3 項目進行初始化創(chuàng)建,項目名可以自定,比如 weather-sample。
現在可以啟動應用來體驗下。
$ npm run dev
則創(chuàng)建了一個類似下面結構的文件
.
├── src ## midway 項目源碼
│ └── controller ## Web Controller 目錄
│ └── home.controller.ts
├── test
├── package.json
└── tsconfig.json
整個項目包括了一些最基本的文件和目錄。
• src 整個 Midway 項目的源碼目錄,你之后所有的開發(fā)源碼都將存放于此
• test 項目的測試目錄,之后所有的代碼測試文件都在這里
• package.json Node.js 項目基礎的包管理配置文件
• tsconfig.json TypeScript 編譯配置文件
2. 認識Midway
2.1 目錄結構
• controller Web Controller 目錄
• middleware 中間件目錄
• filter 過濾器目錄
• aspect 攔截器
• service 服務邏輯目錄
• entity 或 model 數據庫實體目錄
• config 業(yè)務的配置目錄
• util 工具類存放的目錄
• decorator 自定義裝飾器目錄
• interface.ts 業(yè)務的 ts 定義文件
2.2 Controller
控制器常用于對用戶的請求參數做一些校驗,轉換,調用復雜的業(yè)務邏輯,拿到相應的業(yè)務結果后進行數據組裝,然后返回。
在 Midway 中,控制器 也承載了路由的能力,每個控制器可以提供多個路由,不同的路由可以執(zhí)行不同的操作。
import { Controller, Get } from '@midwayjs/decorator'; @Controller('/') export class WeatherController { // 這里是裝飾器,定義一個路由 @Get('/weather') async getWeatherInfo(): Promise<string> { // 這里是 http 的返回,可以直接返回字符串,數字,JSON,Buffer 等 return 'Hello Weather!'; } }
@Controller 裝飾器告訴框架,這是一個 Web 控制器類型的類,而 @Get 裝飾器告訴框架,被修飾的 home 方法,將被暴露為 / 這個路由,可以由 GET 請求來訪問
通過訪問 /weather 接口返回數據了;整個方法返回了一個字符串,在瀏覽器中你會收到 text/plain 的響應類型,以及一個 200 的狀態(tài)碼。
2.3 路由
上面創(chuàng)建了一個 GET 路由。一般情況下,我們會有其他的 HTTP Method,Midway 提供了更多的路由方法裝飾器。
例如:
import { Controller, Get, Post } from '@midwayjs/decorator'; @Controller('/') export class HomeController { @Get('/') async home() { return 'Hello Midwayjs!'; } @Post('/update') async updateData() { return 'This is a post method' } }
Midway 還提供了其他的裝飾器, @Get 、 @Post 、 @Put() 、@Del() 、 @Patch() 、 @Options() 、 @Head() 和 @All() ,表示各自的 HTTP 請求方法。
@All 裝飾器比較特殊,表示能接受以上所有類型的 HTTP Method。
你可以將多個路由綁定到同一個方法上。
@Get('/') @Get('/main') async home() { return 'Hello Midwayjs!'; }
返回內容類型將定義的內容放在 src/interface.ts 文件中
例如:
export interface User { id: number; name: string; age: number; }
使用:下方粗下劃線處
import { Controller, Get, Query } from "@midwayjs/decorator"; @Controller('/api/user') export class UserController { @Get('/') async getUser(@Query('id') id: string): Promise<User> { // xxxx } }
2.4 獲取請求參數
請求的數據一般都是動態(tài)的,會在 HTTP 的不同位置來傳遞,比如常見的 Query,Body 等。
第一種 query
@Query 裝飾器的有參數,可以傳入一個指定的字符串 key,獲取對應的值,賦值給入參,如果不傳入,則默認返回整個 Query 對象。
// URL = /?id=1 async getUser(@Query('id') id: string) // id = 1 async getUser(@Query() queryData) // {"id": "1"} 如果通過api獲取query中的參數 import { Controller, Get, Inject } from "@midwayjs/decorator"; import { Context } from '@midwayjs/koa'; @Controller('/user') export class UserController { @Inject() ctx: Context; @Get('/') async getUser(): Promise<User> { const query = this.ctx.query; // { // uid: '1', // sex: 'male', // } } }
注意:
當 Query String 中的 key 重復時,ctx.query 只取 key 第一次出現時的值,后面再出現的都會被忽略。
比如 GET /user?uid=1&uid=2 通過 ctx.query 拿到的值是 { uid: ‘1’ }。
第二種 body
為什么要用body’傳遞參數?
• 瀏覽器中會對 URL 的長度有所限制,如果需要傳遞的參數過多就會無法傳遞。
• 服務端經常會將訪問的完整 URL 記錄到日志文件中,有一些敏感數據通過 URL 傳遞會不安全。
注意:
框架內置了 bodyParser 中間件來對這兩類格式的請求 body 解析成 object 掛載到 ctx.request.body 上。HTTP 協議中并不建議在通過 GET、HEAD 方法訪問時傳遞 body,所以我們無法在 GET、HEAD 方法中按照此方法獲取到內容。
框架對 bodyParser 設置了一些默認參數,配置好之后擁有以下特性:
• 當請求的 Content-Type 為 application/json,application/json-patch+json,application/vnd.api+json 和 application/csp-report 時,會按照 json 格式對請求 body 進行解析,并限制 body 最大長度為 1mb。
• 當請求的 Content-Type 為 application/x-www-form-urlencoded 時,會按照 form 格式對請求 body 進行解析,并限制 body 最大長度為 1mb。
• 如果解析成功,body 一定會是一個 Object(可能是一個數組)。
獲取單個 body
? // src/controller/user.ts // POST /user/ HTTP/1.1 // Host: localhost:3000 // Content-Type: application/json; charset=UTF-8 // // {"uid": "1", "name": "harry"} import { Controller, Post, Body } from "@midwayjs/decorator"; @Controller('/user') export class UserController { @Post('/') async updateUser(@Body('uid') uid: string): Promise<User> { // id 等價于 ctx.request.body.uid } }
獲取整個 body
? // src/controller/user.ts // POST /user/ HTTP/1.1 // Host: localhost:3000 // Content-Type: application/json; charset=UTF-8 // // {"uid": "1", "name": "harry"} import { Controller, Post, Body } from "@midwayjs/decorator"; @Controller('/user') export class UserController { @Post('/') async updateUser(@Body() user: User): Promise<User> { // user 等價于 ctx.request.body 整個 body 對象 // => output user // { // uid: '1', // name: 'harry', // } } }
從 API 獲取
? // src/controller/user.ts // POST /user/ HTTP/1.1 // Host: localhost:3000 // Content-Type: application/json; charset=UTF-8 // // {"uid": "1", "name": "harry"} import { Controller, Post, Inject } from "@midwayjs/decorator"; import { Context } from '@midwayjs/koa'; @Controller('/user') export class UserController { @Inject() ctx: Context; @Post('/') async getUser(): Promise<User> { const body = this.ctx.request.body; // { // uid: '1', // name: 'harry', // } } }
此外裝飾器還可以組合使用,獲取 query 和 body 參數
@Post('/') async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise<User> { // user 從 body 獲取 // pageIdx 從 query 獲取 }
第三種 Params
如果路由上使用 :xxx 的格式來聲明路由,那么參數可以通過 ctx.params 獲取到。
示例:從裝飾器獲取
// src/controller/user.ts // GET /user/1 import { Controller, Get, Param } from "@midwayjs/decorator"; @Controller('/user') export class UserController { @Get('/:uid') async getUser(@Param('uid') uid: string): Promise<User> { // xxxx } }
示例:從 API 獲取
// src/controller/user.ts // GET /user/1 import { Controller, Get, Inject } from "@midwayjs/decorator"; import { Context } from '@midwayjs/koa'; @Controller('/user') export class UserController { @Inject() ctx: Context; @Get('/:uid') async getUser(): Promise<User> { const params = this.ctx.params; // { // uid: '1', // } } }
2.5 Web中間件
Web 中間件是在控制器調用 之前 和 之后(部分)調用的函數。 中間件函數可以訪問請求和響應對象。
import { IMiddleware } from '@midwayjs/core'; import { Middleware } from '@midwayjs/decorator'; import { NextFunction, Context } from '@midwayjs/koa'; @Middleware() export class ReportMiddleware implements IMiddleware<Context, NextFunction> { resolve() { return async (ctx: Context, next: NextFunction) => { // 控制器前執(zhí)行的邏輯 const startTime = Date.now(); // 執(zhí)行下一個 Web 中間件,最后執(zhí)行到控制器 // 這里可以拿到下一個中間件或者控制器的返回值 const result = await next(); // 控制器之后執(zhí)行的邏輯 console.log(Date.now() - startTime); // 返回給上一個中間件的結果 return result; }; } static getName(): string { return 'report'; } }
例如:
export class ErrorMiddleware implements IWebMiddleware { resolve() { return async (ctx: Context, next: IMidwayWebNext) => { try { await next() } catch (err: any) { const errorInter = RestCode.INTERNAL_SERVER_ERROR; console.info('錯誤信息' + err.name, err.message, err.status); const status = err.status || errorInter; // 生產環(huán)境時 500 錯誤的詳細錯誤內容不返回給客戶端,因為可能包含敏感信息 const errorMsg = status === errorInter && ctx.app.config.env === 'prod' ? 'Internal Server Error' : err.message; ctx.body = { code: err.name === 'ValidationError' ? RestCode.VALIDATE_ERROR : status, error: errorMsg.replaceAll('\"',''), } if (status === 422) { ctx.body.detail = err.errors; } ctx.status = 200 } }; } }
全局使用中間件
所有的路由都會執(zhí)行的中間件,比如 cookie、session 等等
// src/configuration.ts import { App, Configuration } from '@midwayjs/decorator'; import * as koa from '@midwayjs/koa'; import { ReportMiddleware } from './middleware/user.middleware'; @Configuration({ imports: [koa] // ... }) export class AutoConfiguration { @App() app: koa.Application; async onReady() { this.app.useMiddleware([ReportMiddleware1, ReportMiddleware2]); } }
路由使用中間件
單個/部分路由會執(zhí)行的中間件,比如某個路由的前置校驗,數據處理等等
import { Controller } from '@midwayjs/decorator'; import { ReportMiddleware } from '../middleware/report.middlweare'; @Controller('/', { middleware: [ ReportMiddleware ] }) export class HomeController { }
忽略和匹配路由
在中間件執(zhí)行時,我們可以添加路由忽略的邏輯。
ignore(ctx: Context): boolean { // 下面的路由將忽略此中間件 return ctx.path === '/' || ctx.path === '/api/auth' || ctx.path === '/api/login'; }
同理,也可以添加匹配的路由,只有匹配到的路由才會執(zhí)行該中間件。ignore 和 match 同時只有一個會生效。
match(ctx: Context): boolean { // 下面的匹配到的路由會執(zhí)行此中間件 if (ctx.path === '/api/index') { return true; } }
2.6 組件使用
參數校驗
Midway 提供了 Validate 組件。 配合 @Validate 和 @Rule 裝飾器,用來快速定義校驗的規(guī)則,幫助用戶減少這些重復的代碼。
注意:從 v3 開始,@Rule 和 @Validate 裝飾器從 @midwayjs/validate 中導出。
1、 安裝依賴:$ npm i @midwayjs/validate@3 –save
2、 開啟組件:
在 configuration.ts 中增加組件。
import * as validate from ‘@midwayjs/validate'; import { join } from ‘path'; @Configuration({ imports: [ validate], importConfigs: [join(__dirname, ‘./config')], }) }
3、 定義檢查規(guī)則:
為了方便后續(xù)處理,我們將 user 放到一個 src/dto 目錄中。
例如:
// src/dto/user.ts import { Rule, RuleType } from ‘@midwayjs/validate'; export class UserDTO { @Rule(RuleType.number().required()) id: number; @Rule(RuleType.string().required()) firstName: string; @Rule(RuleType.string().max(10)) lastName: string; @Rule(RuleType.number().max(60)) age: number; }
4、 應用:
定義完類型之后,就可以直接在業(yè)務代碼中使用了,開啟校驗能力還需要 @Validate 裝飾器。
// src/controller/home.ts import { Controller, Get, Provide } from ‘@midwayjs/decorator'; import { UserDTO } from ‘./dto/user'; @Controller(‘/api/user') export class HomeController { @Post(‘/') async updateUser(@Body() user: UserDTO ) { // user.id } }
Swagger-ui
基于最新的 OpenAPI 3.0.3 實現了新版的 Swagger 組件。
1、 安裝依賴:
npm install @midwayjs/swagger@3 --save
npm install swagger-ui-dist --save-dev
2、 開啟組件:
import { Configuration } from ‘@midwayjs/decorator'; import * as swagger from ‘@midwayjs/swagger'; @Configuration({ imports: [ { component: swagger, enabledEnvironment: [‘local'] //只在 local 環(huán)境下啟用 }]}) export class MainConfiguration { }
然后啟動項目,訪問地址:
• UI: http://127.0.0.1:7001/swagger-ui/index.html
• JSON: http://127.0.0.1:7001/swagger-ui/index.json
2.7 服務(service)
在業(yè)務中,只有控制器(Controller)的代碼是不夠的,一般來說會有一些業(yè)務邏輯被抽象到一個特定的邏輯單元中,我們一般稱為服務(Service)。
提供這個抽象有以下幾個好處:
• 保持 Controller 中的邏輯更加簡潔。
• 保持業(yè)務邏輯的獨立性,抽象出來的 Service 可以被多個 Controller 重復調用。
• 將邏輯和展現分離,更容易編寫測試用例。
創(chuàng)建服務
一般會存放到 src/service 目錄中。我們來添加一個 user 服務。
import { Provide, App, Inject } from '@midwayjs/decorator'; import { Application } from 'egg'; import { HttpService } from '@midwayjs/axios'; var fs = require('fs'); var path = require('path'); @Provide() //拋出服務 export class UserService { @App() app: Application; @Inject() //依賴注入 httpService: HttpService; async getUser(options: any) { const url = 'https://172.30.154.46:9998/samp/v1/auth/login'; const { data } = await this.httpService.post(url, options); return data; } }
使用服務
在 Controller 處,我們需要來調用這個服務。傳統(tǒng)的代碼寫法,我們需要初始化這個 Class(new),然后將實例放在需要調用的地方。在 Midway 中,你不需要這么做,只需要編寫我們提供的 “依賴注入” 的代碼寫法。
import { Inject, Controller, Get, Provide, Query } from '@midwayjs/decorator'; import { UserService } from '../service/user'; @Controller('/api/user') export class APIController { @Inject()//引入服務 userService: UserService; @Get('/') async getUser(@Query('id') uid) { const user = await this.userService.getUser(uid); return {success: true, message: 'OK', data: user}; } }
三、寫到最后
Midway 框架是在內部已經使用使用 5 年以上的 Node.js 框架,有著長期投入和持續(xù)維護的團隊做后盾,已經在每年的大促場景經過考驗,穩(wěn)定性無須擔心,并且有著豐富的組件和擴展能力,例如數據庫,緩存,定時任務,進程模型,部署以及 Web,Socket 甚至 Serverless 等新場景的支持。一體化調用方案可以方便快捷和前端頁面協同開發(fā)和良好的 TypeScript 定義支持。
所以在項目中應用Midway, 能夠為應用提供更優(yōu)雅的架構。
到此這篇關于前端中間件Midway的使用的文章就介紹到這了,更多相關Midway的使用內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
bootstrap模態(tài)框跳轉到當前模板頁面 框消失了而背景存在問題的解決方法
這篇文章主要介紹了bootstrap模態(tài)框跳轉到當前模板頁面,框消失了,而背景依然存在問題的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12