手把手帶你學習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)小兵