29

混搭 TypeScript + GraphQL + DI + Decorator 风格写 Node.js 应用

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ%3D%3D&%3Bmid=2650411021&%3Bidx=1&%3Bsn=6cd532f28b94ef94b7ebafbf83b07698
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MVb2ai6.gif!mobile

阅读本文的知识前提:熟悉 TypeScript + GraphQL + Node.js + Decorator + Dependency Inject 等概念。

前言

恰逢最近需要编写一个简单的后端 Node.js 应用, 由于是全新的小应用,没有历史包袱  ,所以趁着这次机会换了一种全新的开发模式:

  • 语言使用 TypeScript, 不仅仅是强类型那么简单,它还提供很多高级语法糖,提高编程效率。

  • 兼顾 Restful + GraphQL 方式提供数据接口, 前两年 GraphQL 特别流行,最近这段时间有些平淡下来(现在比较火热的是 Serverless);GraphQL 这种查询语言对前端来讲还是很友好的,自己写的话能减少不少的接口开发量;

  • 使用 Decorator(装饰器语法) + DI(依赖注入)风格写业务逻辑。 因后端 Java 开发服务的模式已经非常成熟,前端在 Node.js 的开发模式基本上是依照 Java 那套开发模子来的,尤其是 DI(依赖注入)设计模式的编程思想。这几年随着 ECMAScript 的标准迭代,以及 TypeScript 的成熟发展,在语言层面提供了很多现代化语法糖的支持, 现在也可以利用 Decorator(装饰器)+ DI(依赖注入)风格来写了,个人认为这种风格也将成为书写 Node.js 应用的常用范式之一。

  • 选用支持 TS + Decorator + DI 的 Node.js框架。 在集团内使用Midway,因为 Midway 在集团内部已经是事实标准了,而且发展得很成熟了;如果选非集团内部的话,可以考虑选流行的 Next.js 框架;——  这类框架功能都很强大,而且提供完善的工具链和生态,就算你不熟,通读他们的官方文档都能收获很多;

前端内部写的后端应用基本上功能并不会太多(太专业的后端服务交给后端开发来做),绝大部分是基础的操作,在这样的情况下会涉及到很多重复工作量要做,基本都是一样的套路:

  1. 初始化项目脚手架

  2. 数据库的连接操作 + CRUD 操作

  3. 创建数据 model 层 + service 层

  4. 提供诸如 Restful 接口供多端消费

  5. ...

复杂的业务逻辑功能一般是直接调用后端提供的服务,前端很少介入太深的后端功能开发

这意味着每次开发新应用都得重新来一遍 —— 这就跟前端平时切页面一样,重复劳动多了之后就内心还是比较烦的,甚至有抗拒心理。繁琐的事大概涉及在工程链路 & 业务代码这么两方面,如果有良好的解决方案,将大大提升开发的幸福感:

  1. 第一个方面是结构目录的生成。这个问题比较好解决,市面上成熟的框架(Nest.js, Midway.js,Prisma.io 等)都提供了相应的脚手架工具,直接生成相应的服务端代码结构,写代码既可靠又高效。同时这类成熟框架都能一键搞定部署发布等流程,这样我们就可以将大部分时间用在业务代码上、而不是折腾环境搭建细节上。

  2. 第二个方面是业务代码的书写风格。同样是写业务代码,语言风格不一样,代码效率也是不同的,你用 JS 写业务代码,跟 TypeScript + Decorator 来写的效率大相径庭 —— 这也就是技术发展带来的福利。

这里的第一个方面中的目录生成, 一键生成支持 Midway 6 ts 和 Ant Design Pro 4 ts 的版本前后端目录,在这种强强联合下,几行代码下来就能生成非常专业高效的前后端分离的架构体系 —— 包含 client & server 两个文件夹,分别对应标准的  Ant Design Pro 4 目录结构  和   Midway6 TS 目录结构  目录:

YVZZbej.png!mobile

然后在最外层根目录执行即可,启动后使用  http://localhost:6001/  打开提示即可:

QJJN3eI.gif!mobile

该初始化项目后就能可以跑通本地开发调试、构建、aone 部署发布流程,整个很顺畅。

不过这里需要说明的是,这套方案里使用的是 antd 3.x 的版本,建议进行升级到 antd 4.x 版本,升级成本并不算很高,antd 官方提供升级工具,基本一行代码就能搞定:

# 通过 npx 直接运行
npx -p @ant-design/codemod-v4 antd4-codemod client/src

建议通读一下  官方文档 - 从 v3 到 v4  内容,了解升级的地方在哪些。

本文着重讲解第二部分,即如何使用 TypeScript + Decorator + DI 风格编写 Node.js 应用,让你感受到使用这些技术框架带来的畅快感。本文涉及的知识点比较多,代码尽可能少放,主要是叙述逻辑思路,最后会以实现常见的 分页功能 作为案例来详细展示。

数据库 ORM

首先我们需要解决数据库相关的技术选项,这里说的技术选型是指 ORM 相关的技术选型(数据库固定使用 MySQL),选型的基本原则是能力强大、用法简单。

  ORM 选型

  1. ORM 实例教程 :阮一峰教程,解释 ORM,通俗易懂

  2. 使用 Typeorm 在 Midway 6 中获得灵活一致的数据管理体验  : 一篇很受启发的文章

  3. 数据访问库的选择之TypeORM:讨论 了如何在 Midway 中接入 TypeORM,还讨论了一些高级用法

除了直接拼 SQL 语句这种略微硬核的方式外,Node.js 应用开发者更多地会选择使用开源的 ORM 库,如 Sequelize。而在 Typescript 面前,工具库层面目前两种可选项,可以使用  sequelize-typescript  或者  TypeORM  来进行数据库的管理。 总结原因如下:

  • 原生类型声明,与 Typescript 有更好的相容性

  • 支持装饰器写法,用法上简单直观; 且足够强的扩展能力,能支持复杂的数据操作;

  • 该库足够受欢迎,Github Star 数量高达  20.3k (截止此文撰写 2020.08 时),且官方文档友好

并非说  Sequelize-typescript  不行,这两个工具库都很强大,都能满足业务技术需求;Sequelize 一方面是 Model 定义方式比较 JS 化在 Typescript 天然的类型环境中显得有些怪异;另一方面也与 Midway 6 整体的编码风格不太统一,所以我个人更加倾向于用  TypeORM  。

  两种操作模式

  1. 架构模式中的 Active Record 和 Data Mapper

  2. 什么是 ActiveRecord 模式

这里简单说明一下,ORM 架构模式中,最流行的实现模式有两种: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);

现在我们察看到了它们最基本的区别:在 Active Record 中,领域对象有一个 save() 方法,领域对象通常会继承一个 ActiveRecord 的基类来实现。而在 Data Mapper 模式中,领域对象不存在 save() 方法,持久化操作由一个中间类来实现。
这两种模式没有谁比谁好之分,只有适不适合之别:

  1. 简单的 CRUD、试水型的 Demo 项目,用 Active Records 模式的 ORM 框架更好

  2. 业务流程和规则较多的、成熟的项目改造用 Data Mapper 型,其允许将业务规则绑定到实体。

Active Records 模式最大优点是简单 , 直观, 一个类就包括了数据访问和业务逻辑,恰好我现在这个小应用基本都是单表操作,所以就用 Active Records 模式了。

TypeORM 的使用

  数据库连接


首先,提供数据库初始化 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;


/** 初始化数据库服务实例 */
static async initInstance(app: Application) {
const applicationContext: ApplicationContext = app.applicationContext;
const logger: EggLogger = app.getLogger();
// 手动实例化一次,启动数据库连接
const databaseService = await applicationContext.getAsync<DatabaseService>(DatabaseService.identifier);
const testResult = await databaseService.getConnection().query('SELECT 1+1');
logger.info('数据库连接测试: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] 数据库连接成功~', DatabaseService.name);
} catch (err) {
this.logger.error('[%s] 数据库连接失败!', DatabaseService.name);
this.logger.info('数据库链接信息:', options);
this.logger.error(err);
}
}


/**
* 获取数据库链接
* @param connectionName 数据库链接名称
*/
getConnection(connectionName?: string) {
return getConnection(connectionName);
}
}

说明:

  1. 这里一定是单例 @scope(ScopeEnum.Singleton),因为数据库连接服务只能有一个。但是可以初始化多个连接,比如用于多个数据库连接或读写分离

  2. 默认配置项  defaultOptions  中的  entities  表示 数据库实体对象 存放的路径,推荐专门创建一个  entity  目录用来存放:

yq2iaiR.png!mobile其次,在 Midway 的配置文件中指定数据库连接配置:

 // 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}`],
}


说明:

  1. 因为要区分 aone 环境运行和本地开发,所以需要配置两份

  2. entities 的配置项本地和线上配置是不同的,本地直接用  src/entity  就行,而 aone 环境需要使用  ${appInfo.baseDir}  变量

最后,在应用启动时触发实例化:

// 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;
}


// 所有的配置已经加载完毕
// 可以用来加载应用自定义的文件,启动自定义的服务
async didLoad() {
await DatabaseService.initInstance(this.app);
}
}

说明:

  1. 选择在 app 的配置加载完毕之后来启动自定义的数据库服务,具体参考  启动自定义的声明周期参考文档  说明

  2. 为了不侵入  AppBootHook  代码太多, 我把初始化数据库服务实例的代码放在了  DatabaseService  类的静态方法中。

  数据库操作

  • typeorm数据库ORM框架中文文档

  • Active Record vs Data Mapper  :官方文档对两者的解释

数据库连接上之后,就可以直接使用 ORM 框架进行数据库操作。不同于现有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active Record 和 Data Mapper 模式(在我这次写的项目中,使用的是 Active Record 模式),这意味着你可以根据实际情况选用合适有效的方法编写高质量的、松耦合的、可扩展的应用程序。
首先看一下用 Active Records 模式的写法:

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;


}

说明:

  1. 类需要用 @Entity() 装饰

  2. 需要继承  BaseEntity  这个基类

对应的业务域写法:

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;


}

说明:

  1. 类同样需要用 @Entity() 装饰

  2. 不需要 继承  BaseEntity  这个基类

对应的业务域逻辑是这样的:

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

无论是 Active Record 模式还是 Data Mapper 模式, TypeORM 在 API 上的命名使用上几乎是保持一致, 这大大降低了使用者记忆上的压力 :比如上方保存操作,都称为 save 方法,只不过前者是放在 Entity 实例上,后者是放在 Repository 示例上而已。

  MVC架构

整个服务器的设计模式,就是经典的 MVC 架构,主要就是通过 Controller、Service、Model 、View 共同作用,形成了一套架构体系;

fiUN3u7.png!mobile

此图来源于 《Express 教程 4:路由和控制器》 https://developer.mozilla.org/zh-CN/docs/learn/Server-side/Express_Nodejs/routes

上图是最为基础的 MVC 架构,实际开发过程中还会有更细分的优化,主要体现两方面:

  1. 为了方便后期扩展,还会引入 中间件(middleware) 机制,这些概念相信但凡写过 Koa/Express 的都知道 —— 不过这里还是重述一下,因为后面 GraphQL 就是通过中间件方式引入的。

  2. 一般不推荐直接让  Controller 调用到  Model 对象,而是要中间添加一层 Service 层来进行解耦 (具体的优势详见 Egg.js 官方文档 服务(Service) ,里面有详细的解释);简单来讲,这样的好处在于解耦 Model 和 Controller,同时保持业务逻辑的独立性(从而带来更好的扩展性、更方便的单元测试等), 抽象出来的 Service 可以被多个 Controller 重复调用 —— 比如,GraphQL Resolver 和 Controller 就可以共用同一份 Service;


现代 Node.js 框架初始化的时候都默认帮你做了这事情 —— Midway 也不例外,初始化后去看一下它的目录结构就基本上懂了。

更多关于该架构的实战可参考以下文章:

  1. Node Service-oriented Architecture: 介绍面向 Service 的 Node.js 架构

  2. Designing a better architecture for a Node.js API 初学者教程,从实践中感受面向 Service 架构

  3. Bulletproof node.js project architecture : 如何打造一个坚固的 Node.js 服务端架构

  RESTful API

在 Midway 初始化项目的时候,其实已经具备完整的 RESTful API 的能力,你只要照样去扩展就可以了,而且基于装饰器语法和 DI 风格,编写路由非常的方便直观,正如官方 《路由装饰器》 里所演示的代码那样,几行代码下来就输出标准的 RESTful 风格的 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};
}
}

GraphQL

  • 其次需要阅读 type-graph 的教程,比如  Resolvers  章节,具体的代码参考可以前往  recipe-resolver

  • TypeScript + GraphQL = TypeGraphQL 阿里 CCO 体验技术部的文章,介绍地比较详细到位,推荐阅读(结合 egg.js 的开发实践)

RESTful API 方式用得比较多,不过我还是想在自己的小项目里使用 GraphQL,具体的优点我就不多说了,可以参考 《GraphQL 和 Apollo 为什么能帮助你更快地完成开发需求?》

等相关文章。

GraphQL 的理解成本和接入成本还是有一些的,建议直接通读官方文档

《GraphQL 入门》 去了解 GraphQL 中的概念和使用。

  接入 GraphQL 服务中间件

整体的技术选型阵容就是  apollo-server-koa  和  type-graphql  :

  • apollo-server  是一个在 Node.js 上构建 GraphQL 服务端的 Web 中间件, 支持 Koa 也就天然的支持了 Midway

  • TypeGraphQL: 它通过一些 TypeScript + Decorator 规范了 Schema 的定义,避免在 GraphQL 中分别写  Schema Type DSL 和数据 Modal 的重复劳动。

只需要将 Koa 中间件 转 Midway 中间件就行。根据 Midway项目目录约定,在 /src/app/middleware/ 下新建文件 graphql.ts,将 apollo-server-koa 中间件简单包装一下:

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  方法取得中间件函数,注入 TypeGraphQL 所管理的  schema  并导出该函数。

  • 我们所有的 GraphQL Resolver 都放在  'app/resolve r' 目录下

vYBbeej.png!mobile

由于 Midway 默认集成了 CSRF 的安全校验,我们针对 /graphql 路径的这层安全需要忽略掉:

export const security = {
csrf: {
// 忽略 graphql 路由下的 csrf 报错
ignore: '/graphql'
}
}

接入的准备工作到这里就算差不多了,接下来就是编写 GraphQL 的 Resolver 相关逻辑

 Resolvers

对于 Resolver 的处理,TypeGraphQL 提供了一些列的 Decorator 来声明和处理数据。通过 Resolver 类的方法来声明 Query 和 Mutation,以及动态字段的处理 FieldResolver。几个主要的 Decorator 说明如下:

  • @Resolver:来声明当前类是数据处理的

  • @Query :声明改方法是一个 Query 查询操作

  • @Mutation :声明改方法是一个 Mutation 修改操作

  • @FieldResovler :对  @Resolver(of => Recipe)  返回的对象添加一个字段处理

方法参数相关的 Decorator:

  • @Root:获取当前查询对象

  • @Ctx :获取当前上下文,这里可以拿到 egg 的 Context (见上面中间件集成中的处理)

  • @Arg :定义 input 参数

这里涉及到比较多的知识点,不可能一一罗列完,还是建议先去官网  https://typegraphql.com/docs/introduction.html 

阅读一遍

接下来我们从接入开始,然后以如何创建一个分页(Pagination) 功能为案例来演示在如何在 Midway 框架里使用 GraphQL,以及如何应用上述这些装饰器 。

案例:利用 GraphQL 实现分页功能

  分页的数据结构

  • Apollo Server: GraphQL 数据分页概述

从使用者角度来,我们希望传递的参数只有两个 pageNo 和 pageSize ,比如我想访问第 2 页、每页返回 10 条内容,入参格式就是:

{
pageNo: 2,
pageSize: 10
}

而分页返回的数据结构如下:

{
articles {
totalCount # 总数
pageNo # 当前页号
pageSize # 每页结果数
pages # 总页数
list: { # 分页结果
title,
author
}
}
}

 Schema 定义

首先利用 TypeGraphQL 提供的 Decorator 来声明入参类型以及返回结果类型:

// src/entity/pagination.ts


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


// 查询分页的入参
@InputType()
export class PaginationInput {
@Field({ nullable: true })
pageNo?: number;


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


// 查询结果的类型
@ObjectType()
export class Pagination {
// 总共有多少条
@Field()
totalCount: number;


// 总共有多少页
@Field()
pages: number;


// 当前页数
@Field()
pageNo: number;


// 每页包含多少条数据
@Field()
pageSize: number;


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


export interface IPaginationInput extends PaginationInput { }

说明:

  1. 通过这里的 @ObjectType() 、@Field() 装饰注解后,会自动帮你生成 GraphQL 所需的 Schema 文件,可以说非常方便,这样就不用担心自己写的代码跟 Schema 不一致;

  2. 对 list 字段,它的类型是 Article[] ,在使用 @Field 注解时需要注意,因为我们想表示数组一定存在但有可能为空数组情况,需要使用 {nullable: "items"}(即 [Item]!),具体查阅  官方文档 - Types and Fields  另外还有两种配置:    

    1. 基础的  { nullable: true | false }  只能表示整个数组是否存在(即 [Item!]  或者  [Item!]!

    2. 如果想表示数组或元素都有可能为空时,需要使用  {nullable: "itemsAndList"} (即  [Item]

  Resolver 方法

基于上述的 Schema 定义,接下来我们要写 Resolver,用来解析用户实际的请求:

// 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);
}
}
  • 实际解析用户请求,调用的是 Service 层中 articleService.getArticleList 方法,只要让返回的结果跟我们想要的 Pagination 类型一致就行。

  • 这里的  articleService  对象就是通过容器注入( inject )到当前 Resolver ,该对象的提供来自 Service 层

  Service 层

从上可以看到,请求参数是传到 GraphQL 服务器,而真正进行分页操作的还是 Service 层,内部利用 ORM 提供的方法;在TypeORM 中的分页功能实现,可以参考一下官方的 find 选项的完整示例:

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
});

其中和 分页 相关的就是 skip 和 take 两个参数( where 参数是跟 过滤 有关,order 参数跟排序有关)。

  • How to implement pagination in nestjs with typeorm : 这里给出了使用 Repository API 实现的方式

  • Find 选项 官方 Find API 文档

所以最终我们的 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 列表,支持分页
*/
async getArticleList(query: IPaginationInput): Promise<Pagination> {
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 对象实例,这就上面 Resolver 中的 @inject('articleService') 相对应

  • 由于我们想要返回的是 Pagination 类实例,所以需要调用  plainToClass  方法进行一层转化

  Model 层

Service 层其实也是调用 ORM 中的实体方法 Article.findAndCount(由于我们是用Active Records模式的),这个 Article 类就是 ORM 中的实体,其定义也非常简单:

// 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;
}

仔细观察,这里的 Article 类,同时接受了 TypeORM 和 TypeGraphQL 两个库的装饰器,寥寥几行代码就支持了 GraphQL 类型声明和 ORM 实体映射,非常清晰明了。
到这里一个简单的 GraphQL 分页功能就开发完毕,从流程步骤来看,一路下来几乎都是装饰器语法,整个编写过程干净利落,很利于后期的扩展和维护。

小结

距离上次写 Node.js 后台应用有段时间了,当时的技术栈和现在的没法比,现在尤其得益于使用 Decorator(装饰器语法) + DI(依赖注入)风格写业务逻辑,再搭配使用 typeorm (数据库的连接)、 type-graphql (GraphQL的处理)工具库来使用,整体代码风格更加简洁,同样的业务功能,代码量减少非常可观且维护性也提升明显。
emm,这种感觉怎么描述合适呢?之前写 Node.js 应用时,能用,但是总觉得哪里很憋屈 —— 就像是白天在交通拥挤的道路上堵车,那种感觉有点糟;而这次混搭了这几种技术,会感受神清气爽 —— 就像是在高速公路上行车,畅通无阻。
前端的技术发展迭代相对来说迭代比较快,这是好事,能让你用新技术做得更少、收获地更多;当然不可否认这对前端同学也是挑战,需要你都保持不断学习的心态,去及时补充这些新的知识。学无止境,与君共勉。

链接:

https://pro.ant.design/docs/getting-started-cn

https://midway.alibaba-inc.com/midway

https://ant.design/docs/react/migration-v4-cn

http://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html

https://github.com/RobinBuschmann/sequelize-typescript

https://github.com/typeorm/typeorm

https://blog.csdn.net/Frankltf/article/details/86626338

https://blog.csdn.net/YamateDD/article/details/6826255

https://eggjs.org/zh-cn/basics/app-start.html

https://juejin.im/post/6844903920578330631

https://typeorm.io/

https://eggjs.org/zh-cn/basics/service.html

https://www.codementor.io/@evanbechtol/node-service-oriented-architecture-12vjt9zs9i
https://dev.to/pacheco/designing-a-better-architecture-for-a-node-js-api-24d

https://softwareontheroad.com/ideal-nodejs-project-structure/

https://midwayjs.org/midway/guide.html

https://typegraphql.com/docs/resolvers.html

https://zhuanlan.zhihu.com/p/56516614

https://segmentfault.com/a/1190000018706816

https://graphql.cn/learn/

https://npm.alibaba-inc.com/package/apollo-server-koa

https://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

淘系技术部-拍卖前端团队

淘系拍卖前端团队负责全球最大的在线拍卖平台,丰富的场景、广阔的平台等你一起来挑战!

在这里你可以接触到淘系全链路技术,主流框架( weex, rax, react )、搭建体系、源码体系、运营中台、工程套件物料体系、前端智能化等前沿技术,还可以与层层选拔的各路优秀同学共同战斗,共同成长!

欢迎资深前端工程师/专家加入我们,一起打造全新一代的电商运营操作系统,支撑拍卖创新业务。

简历投递至: [email protected] (点击 “阅读原文” 查看详情)

✿  拓展阅读

Y3IR3av.png!mobile

RBb67vm.png!mobile

uI3Er2F.png!mobile

作者| 玄农

编辑| 橙子君

出品| 阿里巴巴新零售淘系技术

veE3IrM.jpg!mobile

uyQNNnZ.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK