js node 打包mac应用_混搭 TypeScript + GraphQL + DI + Decorator 风格写 Node.js 应用
恰逢最近需要編寫一個(gè)簡(jiǎn)單的后端 Node.js 應(yīng)用,由于是全新的小應(yīng)用,沒有歷史包袱,所以趁著這次機(jī)會(huì)換了一種全新的開發(fā)模式:
語(yǔ)言使用 TypeScript,不僅僅是強(qiáng)類型那么簡(jiǎn)單,它還提供很多高級(jí)語(yǔ)法糖,提高編程效率。
兼顧 Restful + GraphQL 方式提供數(shù)據(jù)接口,前兩年 GraphQL 特別流行,最近這段時(shí)間有些平淡下來(現(xiàn)在比較火熱的是 Serverless);GraphQL 這種查詢語(yǔ)言對(duì)前端來講還是很友好的,自己寫的話能減少不少的接口開發(fā)量;
使用 Decorator(裝飾器語(yǔ)法) + DI(依賴注入)風(fēng)格寫業(yè)務(wù)邏輯。因后端 Java 開發(fā)服務(wù)的模式已經(jīng)非常成熟,前端在 Node.js 的開發(fā)模式基本上是依照 Java 那套開發(fā)模子來的,尤其是 DI(依賴注入)設(shè)計(jì)模式的編程思想。這幾年隨著 ECMAScript 的標(biāo)準(zhǔn)迭代,以及 TypeScript 的成熟發(fā)展,在語(yǔ)言層面提供了很多現(xiàn)代化語(yǔ)法糖的支持,現(xiàn)在也可以利用 Decorator(裝飾器)+ DI(依賴注入)風(fēng)格來寫了,個(gè)人認(rèn)為這種風(fēng)格也將成為書寫 Node.js 應(yīng)用的常用范式之一。
選用支持 TS + Decorator + DI 的 Node.js框架。在集團(tuán)內(nèi)使用Midway,因?yàn)?Midway 在集團(tuán)內(nèi)部已經(jīng)是事實(shí)標(biāo)準(zhǔn)了,而且發(fā)展得很成熟了;如果選非集團(tuán)內(nèi)部的話,可以考慮選流行的 Next.js 框架;——?這類框架功能都很強(qiáng)大,而且提供完善的工具鏈和生態(tài),就算你不熟,通讀他們的官方文檔都能收獲很多;
前端內(nèi)部寫的后端應(yīng)用基本上功能并不會(huì)太多(太專業(yè)的后端服務(wù)交給后端開發(fā)來做),絕大部分是基礎(chǔ)的操作,在這樣的情況下會(huì)涉及到很多重復(fù)工作量要做,基本都是一樣的套路:
初始化項(xiàng)目腳手架
數(shù)據(jù)庫(kù)的連接操作 + CRUD 操作
創(chuàng)建數(shù)據(jù) model 層 + service 層
提供諸如 Restful 接口供多端消費(fèi)
...
復(fù)雜的業(yè)務(wù)邏輯功能一般是直接調(diào)用后端提供的服務(wù),前端很少介入太深的后端功能開發(fā)
這意味著每次開發(fā)新應(yīng)用都得重新來一遍 —— 這就跟前端平時(shí)切頁(yè)面一樣,重復(fù)勞動(dòng)多了之后就內(nèi)心還是比較煩的,甚至有抗拒心理。繁瑣的事大概涉及在工程鏈路 & 業(yè)務(wù)代碼這么兩方面,如果有良好的解決方案,將大大提升開發(fā)的幸福感:
第一個(gè)方面是結(jié)構(gòu)目錄的生成。這個(gè)問題比較好解決,市面上成熟的框架(Nest.js, Midway.js,Prisma.io 等)都提供了相應(yīng)的腳手架工具,直接生成相應(yīng)的服務(wù)端代碼結(jié)構(gòu),寫代碼既可靠又高效。同時(shí)這類成熟框架都能一鍵搞定部署發(fā)布等流程,這樣我們就可以將大部分時(shí)間用在業(yè)務(wù)代碼上、而不是折騰環(huán)境搭建細(xì)節(jié)上。
第二個(gè)方面是業(yè)務(wù)代碼的書寫風(fēng)格。同樣是寫業(yè)務(wù)代碼,語(yǔ)言風(fēng)格不一樣,代碼效率也是不同的,你用 JS 寫業(yè)務(wù)代碼,跟 TypeScript + Decorator 來寫的效率大相徑庭 —— 這也就是技術(shù)發(fā)展帶來的福利。
這里的第一個(gè)方面中的目錄生成,一鍵生成支持 Midway 6 ts 和 Ant Design Pro 4 ts 的版本前后端目錄,在這種強(qiáng)強(qiáng)聯(lián)合下,幾行代碼下來就能生成非常專業(yè)高效的前后端分離的架構(gòu)體系 —— 包含?client?&?server?兩個(gè)文件夾,分別對(duì)應(yīng)標(biāo)準(zhǔn)的 ?Ant Design Pro 4 目錄結(jié)構(gòu)?和 ?Midway6 TS 目錄結(jié)構(gòu)?目錄:
然后在最外層根目錄執(zhí)行即可,啟動(dòng)后使用?http://localhost:6001/?打開提示即可:
該初始化項(xiàng)目后就能可以跑通本地開發(fā)調(diào)試、構(gòu)建、aone 部署發(fā)布流程,整個(gè)很順暢。
不過這里需要說明的是,這套方案里使用的是 antd 3.x 的版本,建議進(jìn)行升級(jí)到 antd 4.x 版本,升級(jí)成本并不算很高,antd 官方提供升級(jí)工具,基本一行代碼就能搞定:
# 通過 npx 直接運(yùn)行npx -p @ant-design/codemod-v4 antd4-codemod client/src建議通讀一下?官方文檔 - 從 v3 到 v4?內(nèi)容,了解升級(jí)的地方在哪些。
本文著重講解第二部分,即如何使用 TypeScript + Decorator + DI 風(fēng)格編寫 Node.js 應(yīng)用,讓你感受到使用這些技術(shù)框架帶來的暢快感。本文涉及的知識(shí)點(diǎn)比較多,代碼盡可能少放,主要是敘述邏輯思路,最后會(huì)以實(shí)現(xiàn)常見的?分頁(yè)功能?作為案例來詳細(xì)展示。
數(shù)據(jù)庫(kù) ORM
首先我們需要解決數(shù)據(jù)庫(kù)相關(guān)的技術(shù)選項(xiàng),這里說的技術(shù)選型是指 ORM 相關(guān)的技術(shù)選型(數(shù)據(jù)庫(kù)固定使用 MySQL),選型的基本原則是能力強(qiáng)大、用法簡(jiǎn)單。
? ?ORM 選型
ORM 實(shí)例教程:阮一峰教程,解釋 ORM,通俗易懂
使用 Typeorm 在 Midway 6 中獲得靈活一致的數(shù)據(jù)管理體驗(yàn)?:一篇很受啟發(fā)的文章
數(shù)據(jù)訪問庫(kù)的選擇之TypeORM:討論了如何在 Midway 中接入 TypeORM,還討論了一些高級(jí)用法
除了直接拼 SQL 語(yǔ)句這種略微硬核的方式外,Node.js 應(yīng)用開發(fā)者更多地會(huì)選擇使用開源的 ORM 庫(kù),如 Sequelize。而在 Typescript 面前,工具庫(kù)層面目前兩種可選項(xiàng),可以使用?sequelize-typescript?或者?TypeORM?來進(jìn)行數(shù)據(jù)庫(kù)的管理。總結(jié)原因如下:
原生類型聲明,與?Typescript?有更好的相容性
支持裝飾器寫法,用法上簡(jiǎn)單直觀;且足夠強(qiáng)的擴(kuò)展能力,能支持復(fù)雜的數(shù)據(jù)操作;
該庫(kù)足夠受歡迎,Github Star 數(shù)量高達(dá)?20.3k(截止此文撰寫 2020.08 時(shí)),且官方文檔友好
并非說?Sequelize-typescript?不行,這兩個(gè)工具庫(kù)都很強(qiáng)大,都能滿足業(yè)務(wù)技術(shù)需求;Sequelize 一方面是 Model 定義方式比較 JS 化在 Typescript 天然的類型環(huán)境中顯得有些怪異;另一方面也與 Midway 6 整體的編碼風(fēng)格不太統(tǒng)一,所以我個(gè)人更加傾向于用?TypeORM?。
? ?兩種操作模式
架構(gòu)模式中的 Active Record 和 Data Mapper
什么是 ActiveRecord 模式
這里簡(jiǎn)單說明一下,ORM 架構(gòu)模式中,最流行的實(shí)現(xiàn)模式有兩種:Active Record 和 Data Mapper。比如 Ruby 的 ORM 采取了 Active Record 的模式是這樣的:
$user = new User;$user->username = 'philipbrown';$user->save();再來看使用 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 的基類來實(shí)現(xiàn)。而在 Data Mapper 模式中,領(lǐng)域?qū)ο蟛淮嬖?save()?方法,持久化操作由一個(gè)中間類來實(shí)現(xiàn)。
這兩種模式?jīng)]有誰(shuí)比誰(shuí)好之分,只有適不適合之別:
簡(jiǎn)單的 CRUD、試水型的 Demo 項(xiàng)目,用?Active Records?模式的 ORM 框架更好
業(yè)務(wù)流程和規(guī)則較多的、成熟的項(xiàng)目改造用 Data Mapper 型,其允許將業(yè)務(wù)規(guī)則綁定到實(shí)體。
Active Records 模式最大優(yōu)點(diǎn)是簡(jiǎn)單 , 直觀,?一個(gè)類就包括了數(shù)據(jù)訪問和業(yè)務(wù)邏輯,恰好我現(xiàn)在這個(gè)小應(yīng)用基本都是單表操作,所以就用 Active Records 模式了。
TypeORM 的使用
? ?數(shù)據(jù)庫(kù)連接
首先,提供數(shù)據(jù)庫(kù)初始化 service 類:
說明:
這里一定是單例?@scope(ScopeEnum.Singleton),因?yàn)閿?shù)據(jù)庫(kù)連接服務(wù)只能有一個(gè)。但是可以初始化多個(gè)連接,比如用于多個(gè)數(shù)據(jù)庫(kù)連接或讀寫分離
默認(rèn)配置項(xiàng)?defaultOptions?中的?entities?表示數(shù)據(jù)庫(kù)實(shí)體對(duì)象存放的路徑,推薦專門創(chuàng)建一個(gè)?entity?目錄用來存放:
其次,在 Midway 的配置文件中指定數(shù)據(jù)庫(kù)連接配置:
// src/config/config.default.tsexport 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.tsexport 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}`],}說明:
因?yàn)橐獏^(qū)分 aone 環(huán)境運(yùn)行和本地開發(fā),所以需要配置兩份
entities的配置項(xiàng)本地和線上配置是不同的,本地直接用?src/entity?就行,而 aone 環(huán)境需要使用?${appInfo.baseDir}?變量
最后,在應(yīng)用啟動(dòng)時(shí)觸發(fā)實(shí)例化:
// src/app.tsimport { 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)加載完畢 // 可以用來加載應(yīng)用自定義的文件,啟動(dòng)自定義的服務(wù) async didLoad() { await DatabaseService.initInstance(this.app); }}說明:
選擇在 app 的配置加載完畢之后來啟動(dòng)自定義的數(shù)據(jù)庫(kù)服務(wù),具體參考?啟動(dòng)自定義的聲明周期參考文檔?說明
為了不侵入?AppBootHook?代碼太多,我把初始化數(shù)據(jù)庫(kù)服務(wù)實(shí)例的代碼放在了?DatabaseService?類的靜態(tài)方法中。
? ?數(shù)據(jù)庫(kù)操作
typeorm數(shù)據(jù)庫(kù)ORM框架中文文檔
Active Record vs Data Mapper?:官方文檔對(duì)兩者的解釋
數(shù)據(jù)庫(kù)連接上之后,就可以直接使用 ORM 框架進(jìn)行數(shù)據(jù)庫(kù)操作。不同于現(xiàn)有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active Record 和 Data Mapper 模式(在我這次寫的項(xiàng)目中,使用的是 Active Record 模式),這意味著你可以根據(jù)實(shí)際情況選用合適有效的方法編寫高質(zhì)量的、松耦合的、可擴(kuò)展的應(yīng)用程序。
首先看一下用 Active Records 模式的寫法:
說明:
類需要用?@Entity()?裝飾
需要繼承?BaseEntity?這個(gè)基類
對(duì)應(yīng)的業(yè)務(wù)域?qū)懛?#xff1a;
const user = new User();user.firstName = "Timber";user.lastName = "Saw";user.age = 25;await user.save();其次看一下 Data Mapper 型的寫法:
// 模型定義import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";@Entity()export class User { @PrimaryGeneratedColumn() id: number; @Column() firstName: string; @Column() lastName: string; @Column() age: number;}說明:
類同樣需要用?@Entity()?裝飾
不需要繼承?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 方法,只不過前者是放在 Entity 實(shí)例上,后者是放在 Repository 示例上而已。
? ?MVC架構(gòu)
整個(gè)服務(wù)器的設(shè)計(jì)模式,就是經(jīng)典的 MVC 架構(gòu),主要就是通過 Controller、Service、Model 、View 共同作用,形成了一套架構(gòu)體系;
此圖來源于 《Express 教程 4:路由和控制器》https://developer.mozilla.org/zh-CN/docs/learn/Server-side/Express_Nodejs/routes
上圖是最為基礎(chǔ)的 MVC 架構(gòu),實(shí)際開發(fā)過程中還會(huì)有更細(xì)分的優(yōu)化,主要體現(xiàn)兩方面:
為了方便后期擴(kuò)展,還會(huì)引入?中間件(middleware)?機(jī)制,這些概念相信但凡寫過 Koa/Express 的都知道 —— 不過這里還是重述一下,因?yàn)楹竺?GraphQL 就是通過中間件方式引入的。
一般不推薦直接讓?Controller?調(diào)用到?Model?對(duì)象,而是要中間添加一層 Service 層來進(jìn)行解耦(具體的優(yōu)勢(shì)詳見 Egg.js 官方文檔《服務(wù)(Service)》,里面有詳細(xì)的解釋);簡(jiǎn)單來講,這樣的好處在于解耦 Model 和 Controller,同時(shí)保持業(yè)務(wù)邏輯的獨(dú)立性(從而帶來更好的擴(kuò)展性、更方便的單元測(cè)試等),抽象出來的 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)可參考以下文章:
Node Service-oriented Architecture: 介紹面向 Service 的 Node.js 架構(gòu)
Designing a better architecture for a Node.js API:初學(xué)者教程,從實(shí)踐中感受面向 Service 架構(gòu)
Bulletproof node.js project architecture: 如何打造一個(gè)堅(jiān)固的 Node.js 服務(wù)端架構(gòu)
? ?RESTful API
在 Midway 初始化項(xiàng)目的時(shí)候,其實(shí)已經(jīng)具備完整的 RESTful API 的能力,你只要照樣去擴(kuò)展就可以了,而且基于裝飾器語(yǔ)法和 DI 風(fēng)格,編寫路由非常的方便直觀,正如官方《路由裝飾器》里所演示的代碼那樣,幾行代碼下來就輸出標(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 { const id: number = this.ctx.params.id; const user: IUserResult = await this.service.getUser({id}); this.ctx.body = {success: true, message: 'OK', data: user}; }}GraphQL
其次需要閱讀 type-graph 的教程,比如?Resolvers?章節(jié),具體的代碼參考可以前往?recipe-resolver
TypeScript + GraphQL = TypeGraphQL:阿里 CCO 體驗(yàn)技術(shù)部的文章,介紹地比較詳細(xì)到位,推薦閱讀(結(jié)合 egg.js 的開發(fā)實(shí)踐)
RESTful API 方式用得比較多,不過我還是想在自己的小項(xiàng)目里使用 GraphQL,具體的優(yōu)點(diǎn)我就不多說了,可以參考《GraphQL 和 Apollo 為什么能幫助你更快地完成開發(fā)需求?》等相關(guān)文章。
GraphQL 的理解成本和接入成本還是有一些的,建議直接通讀官方文檔 《GraphQL 入門》 去了解 GraphQL 中的概念和使用。
? ?接入 GraphQL 服務(wù)中間件
整體的技術(shù)選型陣容就是?apollo-server-koa?和?type-graphql?:
apollo-server?是一個(gè)在 Node.js 上構(gòu)建 GraphQL 服務(wù)端的 Web 中間件,支持 Koa 也就天然的支持了 Midway
TypeGraphQL:它通過一些 TypeScript + Decorator 規(guī)范了 Schema 的定義,避免在 GraphQL 中分別寫?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);};說明:
利用?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)備工作到這里就算差不多了,接下來就是編寫 GraphQL 的?Resolver?相關(guān)邏輯
? ?Resolvers
對(duì)于 Resolver 的處理,TypeGraphQL 提供了一些列的 Decorator 來聲明和處理數(shù)據(jù)。通過 Resolver 類的方法來聲明 Query 和 Mutation,以及動(dòng)態(tài)字段的處理 FieldResolver。幾個(gè)主要的 Decorator 說明如下:
@Resolver:來聲明當(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 (見上面中間件集成中的處理)
@Arg:定義 input 參數(shù)
這里涉及到比較多的知識(shí)點(diǎn),不可能一一羅列完,還是建議先去官網(wǎng)?https://typegraphql.com/docs/introduction.html?閱讀一遍
接下來我們從接入開始,然后以如何創(chuàng)建一個(gè)分頁(yè)(Pagination)?功能為案例來演示在如何在 Midway 框架里使用 GraphQL,以及如何應(yīng)用上述這些裝飾器 。
案例:利用 GraphQL 實(shí)現(xiàn)分頁(yè)功能
? ?分頁(yè)的數(shù)據(jù)結(jié)構(gòu)
Apollo Server: GraphQL 數(shù)據(jù)分頁(yè)概述
從使用者角度來,我們希望傳遞的參數(shù)只有兩個(gè) pageNo 和 pageSize ,比如我想訪問第 2 頁(yè)、每頁(yè)返回 10 條內(nèi)容,入?yún)⒏袷骄褪?#xff1a;
{ 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 } }}? ?Schema 定義
首先利用 TypeGraphQL 提供的 Decorator 來聲明入?yún)㈩愋鸵约胺祷亟Y(jié)果類型:
// src/entity/pagination.tsimport { ObjectType, Field, ID, InputType } from 'type-graphql';import { Article } from './article';// 查詢分頁(yè)的入?yún)?#64;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 { }說明:
通過這里的?@ObjectType()?、@Field()?裝飾注解后,會(huì)自動(dòng)幫你生成 GraphQL 所需的 Schema 文件,可以說非常方便,這樣就不用擔(dān)心自己寫的代碼跟 Schema 不一致;
對(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])
? ?Resolver 方法
基于上述的 Schema 定義,接下來我們要寫 Resolver,用來解析用戶實(shí)際的請(qǐng)求:
// src/app/resolver/pagination.tsimport { 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ì)象就是通過容器注入(inject)到當(dāng)前 Resolver ,該對(duì)象的提供來自 Service 層
? ?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)的就是 skip 和 take 兩個(gè)參數(shù)( where 參數(shù)是跟?過濾?有關(guān),order 參數(shù)跟排序有關(guān))。
How to implement pagination in nestjs with typeorm :這里給出了使用 Repository API 實(shí)現(xiàn)的方式
Find 選項(xiàng): 官方 Find API 文檔
所以最終我們的 Service 核心層代碼如下:
// server/src/service/article.tsimport { 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, }) } ...}這里通過?@provide('articleService')?向容器提供?articleService?對(duì)象實(shí)例,這就上面 Resolver 中的?@inject('articleService')?相對(duì)應(yīng)
由于我們想要返回的是 Pagination 類實(shí)例,所以需要調(diào)用?plainToClass?方法進(jìn)行一層轉(zhuǎn)化
? ?Model 層
Service 層其實(shí)也是調(diào)用 ORM 中的實(shí)體方法 Article.findAndCount(由于我們是用Active Records模式的),這個(gè) Article 類就是 ORM 中的實(shí)體,其定義也非常簡(jiǎn)單:
// src/entity/article.tsimport { 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í)接受了 TypeORM 和 TypeGraphQL 兩個(gè)庫(kù)的裝飾器,寥寥幾行代碼就支持了 GraphQL 類型聲明和 ORM 實(shí)體映射,非常清晰明了。
到這里一個(gè)簡(jiǎn)單的 GraphQL 分頁(yè)功能就開發(fā)完畢,從流程步驟來看,一路下來幾乎都是裝飾器語(yǔ)法,整個(gè)編寫過程干凈利落,很利于后期的擴(kuò)展和維護(hù)。
小結(jié)
距離上次寫 Node.js 后臺(tái)應(yīng)用有段時(shí)間了,當(dāng)時(shí)的技術(shù)棧和現(xiàn)在的沒法比,現(xiàn)在尤其得益于使用 Decorator(裝飾器語(yǔ)法) + DI(依賴注入)風(fēng)格寫業(yè)務(wù)邏輯,再搭配使用 typeorm (數(shù)據(jù)庫(kù)的連接)、 type-graphql (GraphQL的處理)工具庫(kù)來使用,整體代碼風(fēng)格更加簡(jiǎn)潔,同樣的業(yè)務(wù)功能,代碼量減少非常可觀且維護(hù)性也提升明顯。
emm,這種感覺怎么描述合適呢?之前寫 Node.js 應(yīng)用時(shí),能用,但是總覺得哪里很憋屈 —— 就像是白天在交通擁擠的道路上堵車,那種感覺有點(diǎn)糟;而這次混搭了這幾種技術(shù),會(huì)感受神清氣爽 —— 就像是在高速公路上行車,暢通無(wú)阻。
前端的技術(shù)發(fā)展迭代相對(duì)來說迭代比較快,這是好事,能讓你用新技術(shù)做得更少、收獲地更多;當(dāng)然不可否認(rèn)這對(duì)前端同學(xué)也是挑戰(zhàn),需要你都保持不斷學(xué)習(xí)的心態(tài),去及時(shí)補(bǔ)充這些新的知識(shí)。學(xué)無(wú)止境,與君共勉。
鏈接:
https://pro.ant.design/docs/getting-started-cnhttps://midway.alibaba-inc.com/midwayhttps://ant.design/docs/react/migration-v4-cnhttp://www.ruanyifeng.com/blog/2019/02/orm-tutorial.htmlhttps://github.com/RobinBuschmann/sequelize-typescripthttps://github.com/typeorm/typeormhttps://blog.csdn.net/Frankltf/article/details/86626338https://blog.csdn.net/YamateDD/article/details/6826255https://eggjs.org/zh-cn/basics/app-start.htmlhttps://juejin.im/post/6844903920578330631https://typeorm.io/https://eggjs.org/zh-cn/basics/service.htmlhttps://www.codementor.io/@evanbechtol/node-service-oriented-architecture-12vjt9zs9ihttps://dev.to/pacheco/designing-a-better-architecture-for-a-node-js-api-24dhttps://softwareontheroad.com/ideal-nodejs-project-structure/https://midwayjs.org/midway/guide.htmlhttps://typegraphql.com/docs/resolvers.htmlhttps://zhuanlan.zhihu.com/p/56516614https://segmentfault.com/a/1190000018706816https://graphql.cn/learn/https://npm.alibaba-inc.com/package/apollo-server-koahttps://npm.alibaba-inc.com/package/type-graphql
https://segmentfault.com/a/1190000009565131
https://typegraphql.com/docs/types-and-fields.html
https://stackoverflow.com/questions/53922503/how-to-implement-pagination-in-nestjs-with-typeorm
https://github.com/typeorm/typeorm/blob/master/docs/zh_CN/find-options.md
淘系技術(shù)部-拍賣前端團(tuán)隊(duì)淘系拍賣前端團(tuán)隊(duì)負(fù)責(zé)全球最大的在線拍賣平臺(tái),豐富的場(chǎng)景、廣闊的平臺(tái)等你一起來挑戰(zhàn)!在這里你可以接觸到淘系全鏈路技術(shù),主流框架( weex, rax, react )、搭建體系、源碼體系、運(yùn)營(yíng)中臺(tái)、工程套件物料體系、前端智能化等前沿技術(shù),還可以與層層選拔的各路優(yōu)秀同學(xué)共同戰(zhàn)斗,共同成長(zhǎng)!歡迎資深前端工程師/專家加入我們,一起打造全新一代的電商運(yùn)營(yíng)操作系統(tǒng),支撐拍賣創(chuàng)新業(yè)務(wù)。簡(jiǎn)歷投遞至:muqin.lmq@alibaba-inc.com(點(diǎn)擊查看詳情)???拓展閱讀作者|玄農(nóng)編輯|橙子君出品|阿里巴巴新零售淘系技術(shù)總結(jié)
以上是生活随笔為你收集整理的js node 打包mac应用_混搭 TypeScript + GraphQL + DI + Decorator 风格写 Node.js 应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue 怎么样不重复往数组里插入数据_V
- 下一篇: yshon对讲机如何调频率_99%的人都