<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          混搭 TypeScript + GraphQL + DI + Decorator 風(fēng)格寫(xiě) Node.js 應(yīng)用

          共 15596字,需瀏覽 32分鐘

           ·

          2020-08-26 01:36

          點(diǎn)擊上方?“JSCON簡(jiǎn)時(shí)空關(guān)注,獲取走心的前端原創(chuàng)文章


          閱讀本文的知識(shí)前提:熟悉 TypeScript + GraphQL + Node.js + Decorator + Dependency Inject 等概念。本文選用技術(shù)框架是 Midway.js,設(shè)計(jì)思路可以遷移到 Nest.js 等框架上,改動(dòng)量應(yīng)該不會(huì)太大。

          本文長(zhǎng)約 1.3w 字,閱讀時(shí)間約?20min?

          1. 前言

          恰逢最近需要編寫(xiě)一個(gè)簡(jiǎn)單的后端 Node.js 應(yīng)用,由于是全新的小應(yīng)用,沒(méi)有歷史包袱 ,所以趁著這次機(jī)會(huì)換了一種全新的開(kāi)發(fā)模式:

          • 語(yǔ)言使用 TypeScript,不僅僅是強(qiáng)類型那么簡(jiǎn)單,它還提供很多高級(jí)語(yǔ)法糖,提高編程效率。
          • 兼顧 Restful + GraphQL 方式提供數(shù)據(jù)接口,前兩年 GraphQL 特別流行,最近這段時(shí)間有些平淡下來(lái)(現(xiàn)在比較火熱的是 Serverless);GraphQL 這種查詢語(yǔ)言對(duì)前端來(lái)講還是很友好的,自己寫(xiě)的話能減少不少的接口開(kāi)發(fā)量;
          • 使用 Decorator(裝飾器語(yǔ)法) + DI(依賴注入)風(fēng)格寫(xiě)業(yè)務(wù)邏輯。因后端 Java 開(kāi)發(fā)服務(wù)的模式已經(jīng)非常成熟,前端在 Node.js 的開(kāi)發(fā)模式基本上是依照 Java 那套開(kāi)發(fā)模子來(lái)的,尤其是 DI(依賴注入)設(shè)計(jì)模式的編程思想。這幾年隨著 ECMAScript 的標(biāo)準(zhǔn)迭代,以及 TypeScript 的成熟發(fā)展,在語(yǔ)言層面提供了很多現(xiàn)代化語(yǔ)法糖的支持,現(xiàn)在也可以利用 Decorator(裝飾器)+ DI(依賴注入)風(fēng)格來(lái)寫(xiě)了,個(gè)人認(rèn)為這種風(fēng)格也將成為書(shū)寫(xiě) Node.js 應(yīng)用的常用范式之一。
          • 選用支持 TS + Decorator + DI 的 Node.js框架。市面上成熟的框架,如 Nest.js, Midway.js 等可以 —— 這類框架功能都很強(qiáng)大,而且提供完善的工具鏈和生態(tài),就算你不熟,通讀他們的官方文檔都能收獲很多;本文**因工作內(nèi)容緣故選用 Midway 框架。


          前端內(nèi)部寫(xiě)的后端應(yīng)用基本上功能并不會(huì)太多(太專業(yè)的后端服務(wù)交給后端開(kāi)發(fā)來(lái)做),絕大部分是基礎(chǔ)的操作,在這樣的情況下會(huì)涉及到很多重復(fù)工作量要做,基本都是一樣的套路:

          1. 初始化項(xiàng)目腳手架
          2. 數(shù)據(jù)庫(kù)的連接操作 + CRUD 操作
          3. 創(chuàng)建數(shù)據(jù) Model 層 + Service 層
          4. 提供諸如 Restful 接口供多端消費(fèi)
          5. ...


          這意味著每次開(kāi)發(fā)新應(yīng)用都得重新來(lái)一遍 —— 這就跟前端平時(shí)切頁(yè)面一樣,重復(fù)勞動(dòng)多了之后就內(nèi)心還是比較煩的,甚至有抗拒心理。繁瑣的事大概涉及在工程鏈路 & 業(yè)務(wù)代碼這么兩方面,如果有良好的解決方案,將大大提升開(kāi)發(fā)的幸福感:

          1. 第一個(gè)方面是結(jié)構(gòu)目錄的生成。這個(gè)問(wèn)題比較好解決,市面上成熟的框架(Nest.js, Midway.js,Prisma.io 等)都提供了相應(yīng)的腳手架工具,直接生成相應(yīng)的服務(wù)端代碼結(jié)構(gòu),寫(xiě)代碼既可靠又高效。同時(shí)這類成熟框架都能一鍵搞定部署發(fā)布等流程,這樣我們就可以將大部分時(shí)間用在業(yè)務(wù)代碼上、而不是折騰環(huán)境搭建細(xì)節(jié)上。
          2. 第二個(gè)方面是業(yè)務(wù)代碼的書(shū)寫(xiě)風(fēng)格。同樣是寫(xiě)業(yè)務(wù)代碼,語(yǔ)言風(fēng)格不一樣,代碼效率也是不同的,你用 JS 寫(xiě)業(yè)務(wù)代碼,跟 **TypeScript + Decorator **來(lái)寫(xiě)的效率大相徑庭 —— 這也就是技術(shù)發(fā)展帶來(lái)的福利。


          本文著重講解第二部分,即如何使用 TypeScript + Decorator ?+ DI 風(fēng)格編寫(xiě) Node.js 應(yīng)用,讓你感受到使用這些技術(shù)框架帶來(lái)的暢快感。本文涉及的知識(shí)點(diǎn)比較多,主要是敘述邏輯思路,最后會(huì)以實(shí)現(xiàn)常見(jiàn)的 分頁(yè)功能 作為案例講解。


          2. 數(shù)據(jù)庫(kù) ORM

          首先我們需要解決數(shù)據(jù)庫(kù)相關(guān)的技術(shù)選項(xiàng),這里說(shuō)的技術(shù)選型是指 ORM 相關(guān)的技術(shù)選型(數(shù)據(jù)庫(kù)固定使用 MySQL),選型的基本原則是能力強(qiáng)大、用法簡(jiǎn)單。

          2.1 ORM 選型

          除了直接拼 SQL 語(yǔ)句這種略微硬核的方式外,Node.js 應(yīng)用開(kāi)發(fā)者更多地會(huì)選擇使用開(kāi)源的 ORM 庫(kù),如 Sequelize。而在 Typescript 面前,工具庫(kù)層面目前兩種可選項(xiàng),可以使用?sequelize-typescript?或者?TypeORM?來(lái)進(jìn)行數(shù)據(jù)庫(kù)的管理。做了一下技術(shù)調(diào)研后,決定選用 TypeORM ,總結(jié)原因如下:

          • 原生類型聲明,與 Typescript 有更好的相容性
          • 支持裝飾器寫(xiě)法,用法上簡(jiǎn)單直觀;且足夠強(qiáng)的擴(kuò)展能力,能支持復(fù)雜的數(shù)據(jù)操作;
          • 該庫(kù)足夠受歡迎,Github Star 數(shù)量高達(dá) 20.3k(截止此文撰寫(xiě) 2020.08 時(shí)),且官方文檔友好

          并非說(shuō) Sequelize-typescript 不行,這兩個(gè)工具庫(kù)都很強(qiáng)大,都能滿足業(yè)務(wù)技術(shù)需求;Sequelize 一方面是 Model 定義方式比較 JS 化在 Typescript 天然的類型環(huán)境中顯得有些怪異,所以我個(gè)人更加傾向于用?TypeORM?。


          2.2. 兩種操作模式

          這里簡(jiǎn)單說(shuō)明一下,ORM 架構(gòu)模式中,最流行的實(shí)現(xiàn)模式有兩種:Active Record 和 ?Data Mapper。比如 Ruby 的 ORM 采取了 Active Record 的模式是這樣的:

          $user?=?new?User;
          $user->username?=?'philipbrown';
          $user->save();


          再來(lái)看使用 Data Mapper 的 ORM 是這樣的:

          $user?=?new?User;
          $user->username?=?'philipbrown';
          EntityManager::persist($user);


          現(xiàn)在我們察看到了它們最基本的區(qū)別:Active Record 中,領(lǐng)域?qū)ο笥幸粋€(gè) save() 方法,領(lǐng)域?qū)ο笸ǔ?huì)繼承一個(gè) ActiveRecord 的基類來(lái)實(shí)現(xiàn)。而在 Data Mapper 模式中,領(lǐng)域?qū)ο蟛淮嬖?save() 方法,持久化操作由一個(gè)中間類來(lái)實(shí)現(xiàn)。

          這兩種模式?jīng)]有誰(shuí)比誰(shuí)好之分,只有適不適合之別:

          1. 簡(jiǎn)單的 CRUD、試水型的 Demo 項(xiàng)目,用 Active Records 模式的 ORM 框架更好
          2. 業(yè)務(wù)流程和規(guī)則較多的、成熟的項(xiàng)目改造用 Data Mapper 型,其允許將業(yè)務(wù)規(guī)則綁定到實(shí)體。


          Active Records 模式最大優(yōu)點(diǎn)是簡(jiǎn)單 , 直觀, 一個(gè)類就包括了數(shù)據(jù)訪問(wèn)和業(yè)務(wù)邏輯,恰好我現(xiàn)在這個(gè)小應(yīng)用基本都是單表操作,所以就用 Active Records 模式了。


          3. TypeORM 的使用

          3.1 數(shù)據(jù)庫(kù)連接

          這里主要涉及到修改 3 處地方。

          首先,提供數(shù)據(jù)庫(kù)初始化 service 類:

          //?src/lib/database/service.ts

          import?{?config,?EggLogger,?init,?logger,?provide,?scope,?ScopeEnum,?Application,?ApplicationContext?}?from?'@ali/midway';
          import?{?ConnectionOptions,?createConnection,?createConnections,?getConnection?}?from?'typeorm';

          const?defaultOptions:?any?=?{
          ??type:?'mysql',
          ??synchronize:?false,
          ??logging:?false,
          ??entities:?[
          ????'src/app/entity/**/*.ts'
          ??],
          };

          @scope(ScopeEnum.Singleton)
          @provide()
          export?default?class?DatabaseService?{
          ??static?identifier?=?'databaseService';
          ??//?private?connection:?Connection;

          ??/**?初始化數(shù)據(jù)庫(kù)服務(wù)實(shí)例?*/
          ??static?async?initInstance(app:?Application)?{
          ????const?applicationContext:?ApplicationContext?=?app.applicationContext;
          ????const?logger:?EggLogger?=?app.getLogger();
          ????//?手動(dòng)實(shí)例化一次,啟動(dòng)數(shù)據(jù)庫(kù)連接
          ????const?databaseService?=?await?applicationContext.getAsync(DatabaseService.identifier);
          ????const?testResult?=?await?databaseService.getConnection().query('SELECT?1+1');
          ????logger.info('數(shù)據(jù)庫(kù)連接測(cè)試:SELECT 1+1 =>',?testResult);
          ??}

          ??@config('typeorm')
          ??private?ormconfig:?ConnectionOptions?|?ConnectionOptions[];

          ??@logger()
          ??logger:?EggLogger;

          ??@init()
          ??async?init()?{

          ????const?options?=?{
          ??????...defaultOptions,
          ??????...this.ormconfig
          ????};
          ????try?{
          ??????if?(Array.isArray(options))?{
          ????????await?createConnections(options);
          ??????}?else?{
          ????????await?createConnection(options);
          ??????}
          ??????this.logger.info('[%s]?數(shù)據(jù)庫(kù)連接成功~',?DatabaseService.name);
          ????}?catch?(err)?{
          ??????this.logger.error('[%s]?數(shù)據(jù)庫(kù)連接失??!',?DatabaseService.name);
          ??????this.logger.info('數(shù)據(jù)庫(kù)鏈接信息:',?options);
          ??????this.logger.error(err);
          ????}
          ??}

          ??/**
          ???*?獲取數(shù)據(jù)庫(kù)鏈接
          ???*?@param?connectionName?數(shù)據(jù)庫(kù)鏈接名稱
          ???*/

          ??getConnection(connectionName?:?string)?{
          ????return?getConnection(connectionName);
          ??}
          }

          說(shuō)明:

          1. 這里一定是單例 @scope(ScopeEnum.Singleton),因?yàn)閿?shù)據(jù)庫(kù)連接服務(wù)只能有一個(gè)。但是可以初始化多個(gè)連接,比如用于多個(gè)數(shù)據(jù)庫(kù)連接或讀寫(xiě)分離
          2. 默認(rèn)配置項(xiàng) defaultOptions 中的 entities 表示數(shù)據(jù)庫(kù)實(shí)體對(duì)象存放的路徑,推薦專門(mén)創(chuàng)建一個(gè) entity 目錄用來(lái)存放:



          其次,在 Midway 的配置文件中指定數(shù)據(jù)庫(kù)連接配置

          ?//?src/config/config.default.ts
          export?const?typeorm?=?{
          ????type:?'mysql',
          ????host:?'xxxx',
          ????port:?3306,
          ????username:?'xxx',
          ????password:?'xxxx',
          ????database:?'xxxx',
          ????charset:?'utf8mb4',
          ????logging:?['error'],?//?["query",?"error"]
          ????entities:?[`${appInfo.baseDir}/entity/**/!(*.d|base){.js,.ts}`],
          ??};

          //?server/src/config/config.local.ts
          export?const?typeorm?=?{
          ??type:?'mysql',
          ??host:?'127.0.0.1',
          ??port:?3306,
          ??username:?'xxxx',
          ??password:?'xxxx',
          ??database:?'xxxx',
          ??charset:?'utf8mb4',
          ??synchronize:?false,
          ??logging:?false,?
          ??entities:?[`src/entity/**/!(*.d|base){.js,.ts}`],
          }

          說(shuō)明:

          1. 因?yàn)橐獏^(qū)分線上環(huán)境運(yùn)行和本地開(kāi)發(fā),所以需要配置兩份
          2. entities的配置項(xiàng)本地和線上配置是不同的,本地直接用 src/entity 就行,而 aone 環(huán)境需要使用 ${appInfo.baseDir} 變量


          最后,在應(yīng)用啟動(dòng)時(shí)觸發(fā)實(shí)例化:

          //?src/app.ts

          import?{?Application?}?from?'@ali/midway';
          import?"reflect-metadata";

          import?DatabaseService?from?'./lib/database/service';

          export?default?class?AppBootHook?{
          ??readonly?app:?Application;

          ??constructor(app:?Application)?{
          ????this.app?=?app;
          ??}

          ??//?所有的配置已經(jīng)加載完畢
          ??//?可以用來(lái)加載應(yīng)用自定義的文件,啟動(dòng)自定義的服務(wù)
          ??async?didLoad()?{
          ????await?DatabaseService.initInstance(this.app);
          ??}
          }

          說(shuō)明:

          1. 選擇在 app 的配置加載完畢之后來(lái)啟動(dòng)自定義的數(shù)據(jù)庫(kù)服務(wù),具體參考 《Egg.js - 啟動(dòng)動(dòng)自定義的聲明周期參考文檔》 說(shuō)明
          2. 為了不侵入 AppBootHook 代碼太多,我把初始化數(shù)據(jù)庫(kù)服務(wù)實(shí)例的代碼放在了 DatabaseService 類的靜態(tài)方法中。


          3.2 數(shù)據(jù)庫(kù)操作

          數(shù)據(jù)庫(kù)連接上之后,就可以直接使用 ORM 框架進(jìn)行數(shù)據(jù)庫(kù)操作。不同于現(xiàn)有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active RecordData Mapper 模式(在我這次寫(xiě)的項(xiàng)目中,使用的是 ?Active Record 模式),這意味著你可以根據(jù)實(shí)際情況選用合適有效的方法編寫(xiě)高質(zhì)量的、松耦合的、可擴(kuò)展的應(yīng)用程序。

          首先看一下用 Active Records 模式的寫(xiě)法:

          import?{Entity,?PrimaryGeneratedColumn,?Column,?BaseEntity}?from?"typeorm";

          @Entity()
          export?class?User?extends?BaseEntity?{

          ????@PrimaryGeneratedColumn()
          ????id:?number;

          ????@Column()
          ????firstName:?string;

          ????@Column()
          ????lastName:?string;

          ????@Column()
          ????age:?number;

          }

          說(shuō)明:

          1. 類需要用 @Entity() 裝飾
          2. 需要繼承 BaseEntity 這個(gè)基類


          對(duì)應(yīng)的業(yè)務(wù)域?qū)懛ǎ?/p>

          const?user?=?new?User();
          user.firstName?=?"Timber";
          user.lastName?=?"Saw";
          user.age?=?25;
          await?user.save();


          ------

          其次看一下 Data Mapper 型的寫(xiě)法

          //?模型定義
          import?{Entity,?PrimaryGeneratedColumn,?Column}?from?"typeorm";

          @Entity()
          export?class?User?{

          ????@PrimaryGeneratedColumn()
          ????id:?number;

          ????@Column()
          ????firstName:?string;

          ????@Column()
          ????lastName:?string;

          ????@Column()
          ????age:?number;

          }

          說(shuō)明:

          1. 類同樣需要用 @Entity() 裝飾
          2. 不需要繼承 BaseEntity 這個(gè)基類

          對(duì)應(yīng)的業(yè)務(wù)域邏輯是這樣的:

          const?user?=?new?User();
          user.firstName?=?"Timber";
          user.lastName?=?"Saw";
          user.age?=?25;
          await?repository.save(user);

          無(wú)論是 Active Record 模式還是 Data Mapper 模式,**TypeORM 在 API 上的命名使用上幾乎是保持一致,這大大降低了使用者記憶上的壓力:**比如上方保存操作,都稱為 save 方法,只不過(guò)前者是放在 Entity 實(shí)例上,后者是放在 Repository 示例上而已。

          3.3 MVC架構(gòu)

          整個(gè)服務(wù)器的設(shè)計(jì)模式,就是經(jīng)典的 MVC 架構(gòu),主要就是通過(guò) ControllerService、Model 、View 共同作用,形成了一套架構(gòu)體系;

          image.png

          此圖來(lái)源于 《Express 教程 4:路由和控制器》https://developer.mozilla.org/zh-CN/docs/learn/Server-side/Express_Nodejs/routes

          上圖是最為基礎(chǔ)的 MVC 架構(gòu),實(shí)際開(kāi)發(fā)過(guò)程中還會(huì)有更細(xì)分的優(yōu)化,主要體現(xiàn)兩方面:

          1. 為了方便后期擴(kuò)展,還會(huì)引入 中間件(middleware) 機(jī)制,這些概念相信但凡寫(xiě)過(guò) Koa/Express 的都知道 —— 不過(guò)這里還是重述一下,因?yàn)楹竺?GraphQL 就是通過(guò)中間件方式引入的。
          2. 一般不推薦直接讓 Controller 調(diào)用到 Model 對(duì)象,**而是要中間添加一層 Service 層來(lái)進(jìn)行解耦(具體的優(yōu)勢(shì)詳見(jiàn) Egg.js 官方文檔《服務(wù)(Service)》,里面有詳細(xì)的解釋); **簡(jiǎn)單來(lái)講,這樣的好處在于解耦 Model 和 Controller,同時(shí)保持業(yè)務(wù)邏輯的獨(dú)立性(從而帶來(lái)更好的擴(kuò)展性、更方便的單元測(cè)試等),抽象出來(lái)的 Service 可以被多個(gè) Controller 重復(fù)調(diào)用 —— 比如,GraphQL Resolver 和 Controller 就可以共用同一份 Service;


          現(xiàn)代 Node.js 框架初始化的時(shí)候都默認(rèn)幫你做了這事情 —— Midway 也不例外,初始化后去看一下它的目錄結(jié)構(gòu)就基本上懂了。

          更多關(guān)于該架構(gòu)的實(shí)戰(zhàn)可參考以下文章:

          1. Node Service-oriented Architecture: 介紹面向 Service 的 Node.js 架構(gòu)
          2. Designing a better architecture for a Node.js API:初學(xué)者教程,從實(shí)踐中感受面向 Service 架構(gòu)
          3. Bulletproof node.js project architecture: 如何打造一個(gè)堅(jiān)固的 Node.js 服務(wù)端架構(gòu)

          3.4 RESTful API

          在 Midway 初始化項(xiàng)目的時(shí)候,其實(shí)已經(jīng)具備完整的 RESTful API 的能力,你只要照樣去擴(kuò)展就可以了,而且基于裝飾器語(yǔ)法和 DI 風(fēng)格,編寫(xiě)路由非常的方便直觀,正如官方《Midway - 路由裝飾器》里所演示的代碼那樣,幾行代碼下來(lái)就輸出標(biāo)準(zhǔn)的 RESTful 風(fēng)格的 API:

          import?{?provide,?controller,?inject,?get?}?from?'midway';

          @provide()
          @controller('/user')
          export?class?UserController?{

          ??@inject('userService')
          ??service:?IUserService;

          ??@inject()
          ??ctx;

          ??@get('/:id')
          ??async?getUser():?Promise<void>?{
          ????const?id:?number?=?this.ctx.params.id;
          ????const?user:?IUserResult?=?await?this.service.getUser({id});
          ????this.ctx.body?=?{success:?true,?message:?'OK',?data:?user};
          ??}
          }


          4. GraphQL

          RESTful API 方式用得比較多,不過(guò)我還是想在自己的小項(xiàng)目里使用 GraphQL,具體的優(yōu)點(diǎn)我就不多說(shuō)了,可以參考《GraphQL 和 Apollo 為什么能幫助你更快地完成開(kāi)發(fā)需求?》等相關(guān)文章。

          GraphQL 的理解成本和接入成本還是有一些的,建議直接通讀官方文檔 《GraphQL 入門(mén)》 去了解 GraphQL 中的概念和使用。

          4.1 接入 GraphQL 服務(wù)中間件

          整體的技術(shù)選型陣容就是 apollo-server-koa 和 type-graphql :

          • apollo-server 是一個(gè)在 Node.js 上構(gòu)建 GraphQL 服務(wù)端的 Web 中間件,支持 Koa 也就天然的支持了 Midway
          • TypeGraphQL:它通過(guò)一些 TypeScript + Decorator 規(guī)范了 Schema 的定義,避免在 GraphQL 中分別寫(xiě) Schema Type DSL 和數(shù)據(jù) Modal 的重復(fù)勞動(dòng)。


          只需要將 Koa 中間件 轉(zhuǎn) Midway 中間件就行。根據(jù) Midway項(xiàng)目目錄約定,在 /src/app/middleware/ 下新建文件?graphql.ts,將 apollo-server-koa 中間件簡(jiǎn)單包裝一下

          import?*?as?path?from?'path';
          import?{?Context,?async,?Middleware?}?from?'@ali/midway';
          import?{?ApolloServer,?ServerRegistration?}?from?'apollo-server-koa';
          import?{?buildSchemaSync?}?from?'type-graphql';

          export?default?(options:?ServerRegistration,?ctx:?Context)?=>?{
          ??const?server?=?new?ApolloServer({
          ????schema:?buildSchemaSync({
          ??????resolvers:?[path.resolve(ctx.baseDir,?'resolver/*.ts')],
          ??????container:?ctx.applicationContext
          ????})
          ??});
          ??return?server.getMiddleware(options);
          };

          說(shuō)明:

          • 利用?apollo-server-koa?暴露的?getMiddleware?方法取得中間件函數(shù),注入 TypeGraphQL 所管理的?schema?并導(dǎo)出該函數(shù)。
          • 我們所有的 GraphQL Resolver 都放在 'app/resolver' 目錄下


          由于 Midway 默認(rèn)集成了 CSRF 的安全校驗(yàn),我們針對(duì) /graphql 路徑的這層安全需要忽略掉:

          export?const?security?=?{
          ????csrf:?{
          ??????//?忽略?graphql?路由下的?csrf?報(bào)錯(cuò)
          ??????ignore:?'/graphql'
          ????}
          ??}


          接入的準(zhǔn)備工作到這里就算差不多了,接下來(lái)就是編寫(xiě) GraphQL 的 Resolver 相關(guān)邏輯

          4.2 Resolvers

          對(duì)于 Resolver 的處理,TypeGraphQL 提供了一些列的 Decorator 來(lái)聲明和處理數(shù)據(jù)。通過(guò) Resolver 類的方法來(lái)聲明 QueryMutation,以及動(dòng)態(tài)字段的處理 FieldResolver。幾個(gè)主要的 Decorator 說(shuō)明如下:

          • @Resolver:來(lái)聲明當(dāng)前類是數(shù)據(jù)處理的
          • @Query:聲明改方法是一個(gè) Query 查詢操作
          • @Mutation:聲明改方法是一個(gè) Mutation 修改操作
          • @FieldResovler:對(duì) @Resolver(of => Recipe) 返回的對(duì)象添加一個(gè)字段處理


          方法參數(shù)相關(guān)的 Decorator:

          • @Root:獲取當(dāng)前查詢對(duì)象
          • @Ctx:獲取當(dāng)前上下文,這里可以拿到 egg 的 Context (見(jiàn)上面中間件集成中的處理)
          • @Arg:定義 input 參數(shù)


          這里涉及到比較多的知識(shí)點(diǎn),不可能一一羅列完,還是建議先去官網(wǎng) https://typegraphql.com/docs/introduction.html 閱讀一遍

          接下來(lái)我們從接入開(kāi)始,然后以如何創(chuàng)建一個(gè) 分頁(yè)(Pagination) 功能為案例來(lái)演示在如何在 Midway 框架里使用 GraphQL,以及如何應(yīng)用上述這些裝飾器 。


          5. 案例:利用 GraphQL 實(shí)現(xiàn)分頁(yè)功能

          5.1 分頁(yè)的數(shù)據(jù)結(jié)構(gòu)

          從使用者角度來(lái),我們希望傳遞的參數(shù)只有兩個(gè) pageNopageSize ,比如我想訪問(wèn)第 2 頁(yè)、每頁(yè)返回 10 條內(nèi)容,入?yún)⒏袷骄褪牵?/p>

          {
          ??pageNo:?2,
          ??pageSize:?10
          }


          而分頁(yè)返回的數(shù)據(jù)結(jié)構(gòu)如下:

          {
          ??articles?{
          ????totalCount?#?總數(shù)
          ????pageNo?????#?當(dāng)前頁(yè)號(hào)
          ????pageSize???#?每頁(yè)結(jié)果數(shù)
          ????pages???????#?總頁(yè)數(shù)
          ????list:?{?????#?分頁(yè)結(jié)果
          ??????title,
          ??????author
          ????}
          ??}
          }


          5.2 Schema 定義

          首先利用 TypeGraphQL 提供的 Decorator 來(lái)聲明入?yún)㈩愋鸵约胺祷亟Y(jié)果類型:

          //?src/entity/pagination.ts

          import?{?ObjectType,?Field,?ID,?InputType?}?from?'type-graphql';
          import?{?Article?}?from?'./article';

          //?查詢分頁(yè)的入?yún)?/span>
          @InputType()
          export?class?PaginationInput?{
          ??@Field({?nullable:?true?})
          ??pageNo?:?number;

          ??@Field({?nullable:?true?})
          ??pageSize?:?number;
          }

          //?查詢結(jié)果的類型
          @ObjectType()
          export?class?Pagination?{
          ??//?總共有多少條
          ??@Field()
          ??totalCount:?number;

          ??//?總共有多少頁(yè)
          ??@Field()
          ??pages:?number;

          ??//?當(dāng)前頁(yè)數(shù)
          ??@Field()
          ??pageNo:?number;

          ??//?每頁(yè)包含多少條數(shù)據(jù)
          ??@Field()
          ??pageSize:?number;

          ??//?列表
          ??@Field(type?=>?[Article]!,?{?nullable:?"items"?})
          ??list:?Article[];
          }

          export?interface?IPaginationInput?extends?PaginationInput?{?}

          說(shuō)明:

          1. 通過(guò)這里的 @ObjectType() 、@Field() 裝飾注解后,會(huì)自動(dòng)幫你生成 GraphQL 所需的 Schema 文件,可以說(shuō)非常方便,這樣就不用擔(dān)心自己寫(xiě)的代碼跟 Schema 不一致;
          2. 對(duì) list 字段,它的類型是 Article[] ,在使用 @Field 注解時(shí)需要注意,因?yàn)槲覀兿氡硎緮?shù)組一定存在但有可能為空數(shù)組情況,需要使用 {nullable: "items"}(即 [Item]!),具體查閱 官方文檔 - Types and Fields 另外還有兩種配置:
            • 基礎(chǔ)的 { nullable: true | false } 只能表示整個(gè)數(shù)組是否存在(即[Item!]或者 [Item!]!
            • 如果想表示數(shù)組或元素都有可能為空時(shí),需要使用 {nullable: "itemsAndList"}(即 [Item]


          5.3 Resolver 方法

          基于上述的 Schema 定義,接下來(lái)我們要寫(xiě) Resolver,用來(lái)解析用戶實(shí)際的請(qǐng)求:

          //?src/app/resolver/pagination.ts
          import?{?Context,?inject,?provide?}?from?'@ali/midway';
          import?{?Resolver,?Query,?Arg,?Root,?FieldResolver,?Mutation?}?from?'type-graphql';
          import?{?Pagination,?PaginationInput?}?from?'../../entity/pagination';
          import?{?ArticleService?}?from?'../../service/article';


          @Resolver(of?=>?Articles)
          @provide()
          export?class?PaginationResolver?{

          ??@inject('articleService')
          ??articleService:?ArticleService;

          ??@Query(returns?=>?Articles)
          ??async?articles(@Arg("query")?pageInput:?PaginationInput)?{
          ????return?this.articleService.getArticleList(pageInput);
          ??}
          }
          • 實(shí)際解析用戶請(qǐng)求,調(diào)用的是 Service 層中 articleService.getArticleList 方法,只要讓返回的結(jié)果跟我們想要的 Pagination 類型一致就行。
          • 這里的 articleService 對(duì)象就是通過(guò)容器注入(inject)到當(dāng)前 Resolver ,該對(duì)象的提供來(lái)自 Service 層


          5.4 Service 層

          從上可以看到,請(qǐng)求參數(shù)是傳到 GraphQL 服務(wù)器,而真正進(jìn)行分頁(yè)操作的還是 Service 層,內(nèi)部利用 ORM 提供的方法;在TypeORM 中的分頁(yè)功能實(shí)現(xiàn),可以參考一下官方的 find 選項(xiàng)的完整示例:

          userRepository.find({
          ????select:?["firstName",?"lastName"],
          ????relations:?["profile",?"photos",?"videos"],
          ????where:?{
          ????????firstName:?"Timber",
          ????????lastName:?"Saw"
          ????},
          ????order:?{
          ????????name:?"ASC",
          ????????id:?"DESC"
          ????},
          ????skip:?5,
          ????take:?10,
          ????cache:?true
          });


          其中和 分頁(yè) 相關(guān)的就是 skiptake 兩個(gè)參數(shù)( where 參數(shù)是跟 過(guò)濾 有關(guān),order 參數(shù)跟排序有關(guān))。

          所以最終我們的 Service 核心層代碼如下:

          //?server/src/service/article.ts
          import?{?provide,?logger,?EggLogger,?inject,?Context?}?from?'@ali/midway';
          import?{?plainToClass?}?from?'class-transformer';
          import?{?IPaginationInput,?Pagination?}?from?'../../entity/pagination';
          ...

          @provide('articleService')
          export?class?ArticleService?{
          ?...
          ??
          ??/**
          ???*?獲取?list?列表,支持分頁(yè)
          ???*/

          ??async?getArticleList(query:?IPaginationInput):?Promise?{
          ????const?{pageNo?=?1,?pageSize?=?10}?=?query;

          ????const?[list,?total]?=?await?Article.findAndCount({
          ??????order:?{?create_time:?"DESC"?},
          ??????take:?pageSize,
          ??????skip:?(pageNo?-?1)?*?pageSize
          ????});

          ????return?plainToClass(Pagination,?{
          ??????totalCount:?total,
          ??????pages:?Math.floor(total?/?pageSize)?+?1,
          ??????pageNo:?pageNo,
          ??????pageSize:?pageSize,
          ??????list:?list,
          ????})
          ??}
          ??...
          }
          • 這里通過(guò) @provide('articleService') 向容器提供 articleService 對(duì)象實(shí)例,這就上面 Resolver 中的 @inject('articleService') 相對(duì)應(yīng)
          • 由于我們想要返回的是 Pagination 類實(shí)例,所以需要調(diào)用 plainToClass 方法進(jìn)行一層轉(zhuǎn)化


          5.5 Model 層

          Service 層其實(shí)也是調(diào)用 ORM 中的實(shí)體方法 Article.findAndCount(由于我們是用** Active Records **模式的),這個(gè) Article 類就是 ORM 中的實(shí)體,其定義也非常簡(jiǎn)單:

          //?src/entity/article.ts

          import?{?Entity,?PrimaryGeneratedColumn,?Column,?BaseEntity?}?from?"typeorm";
          import?{?InterfaceType,?ObjectType,?Field,?ID?}?from?'type-graphql';

          @Entity()
          @InterfaceType()
          export?class?Article?extends?BaseEntity?{

          ??@PrimaryGeneratedColumn()
          ??@Field(type?=>?ID)
          ??id:?number;

          ??@Column()
          ??@Field()
          ??title:?string;

          ??@Column()
          ??@Field()
          ??author:?string;
          }

          仔細(xì)觀察,這里的 Article 類,同時(shí)接受了 TypeORMTypeGraphQL 兩個(gè)庫(kù)的裝飾器,寥寥幾行代碼就支持了 GraphQL 類型聲明和 ORM 實(shí)體映射,非常清晰明了。

          到這里一個(gè)簡(jiǎn)單的 GraphQL 分頁(yè)功能就開(kāi)發(fā)完畢,從流程步驟來(lái)看,一路下來(lái)幾乎都是裝飾器語(yǔ)法,整個(gè)編寫(xiě)過(guò)程干凈利落,很利于后期的擴(kuò)展和維護(hù)。


          6. 小結(jié)

          距離上次寫(xiě) Node.js 后臺(tái)應(yīng)用有段時(shí)間了,當(dāng)時(shí)的技術(shù)棧和現(xiàn)在的沒(méi)法比,現(xiàn)在尤其得益于使用 Decorator(裝飾器語(yǔ)法) + DI(依賴注入)風(fēng)格寫(xiě)業(yè)務(wù)邏輯,再搭配使用 typeorm (數(shù)據(jù)庫(kù)的連接)、 type-graphql (GraphQL的處理)工具庫(kù)來(lái)使用,整體代碼風(fēng)格更加簡(jiǎn)潔,同樣的業(yè)務(wù)功能,代碼量減少非??捎^且維護(hù)性也提升明顯。

          emm,這種感覺(jué)怎么描述合適呢?之前寫(xiě) Node.js 應(yīng)用時(shí),能用,但是總覺(jué)得哪里很憋屈 —— 就像是白天在交通擁擠的道路上堵車,那種感覺(jué)有點(diǎn)糟;而這次混搭了這幾種技術(shù),會(huì)感受神清氣爽 —— 就像是在高速公路上行車,暢通無(wú)阻。

          前端的技術(shù)發(fā)展迭代相對(duì)來(lái)說(shuō)迭代比較快,這是好事,能讓你用新技術(shù)做得更少、收獲地更多;當(dāng)然不可否認(rèn)這對(duì)前端同學(xué)也是挑戰(zhàn),需要你都保持不斷學(xué)習(xí)的心態(tài),去及時(shí)補(bǔ)充這些新的知識(shí)。學(xué)無(wú)止境,與君共勉。

          本文完。

          文章預(yù)告:因?yàn)橐蕾囎⑷牒涂刂品崔D(zhuǎn)的思想在 Node.js 應(yīng)用特別重要,所以計(jì)劃接下來(lái)要寫(xiě)一些文章來(lái)解釋這種設(shè)計(jì)模式,然后再搭配一個(gè)依賴注入工具庫(kù)的源碼解讀來(lái)加深理解,敬請(qǐng)期待。


          參考文章

          • ORM 實(shí)例教程:阮一峰教程,解釋 ORM,通俗易懂
          • 架構(gòu)模式中的 Active Record 和 Data Mapper
          • 什么是 ActiveRecord 模式
          • typeorm數(shù)據(jù)庫(kù)ORM框架中文文檔
          • Active Record vs Data Mapper :官方文檔對(duì)兩者的解釋
          • TypeGraphQL - Resolvers 章節(jié),具體的代碼參考可以前往 recipe-resolver
          • TypeScript + GraphQL = TypeGraphQL:阿里 CCO 體驗(yàn)技術(shù)部的文章,介紹地比較詳細(xì)到位,推薦閱讀(結(jié)合 egg.js 的開(kāi)發(fā)實(shí)踐)
          • Apollo Server: GraphQL 數(shù)據(jù)分頁(yè)概述
          • How to implement pagination in nestjs with typeorm :這里給出了使用 Repository API 實(shí)現(xiàn)的方式
          • TypeORM Find 選項(xiàng):官方 Find API 文檔



          分享前端好文,點(diǎn)亮?在看?

          瀏覽 75
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  秋霞网在线伦日伦 | 亚洲AV无码一区二区三区动漫 | 大鸡吧在线观看视频 | 天天干,天天干 | 青娱乐A V |