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

阅读本文的知识前提:熟悉 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 目录结构 目录:


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

该初始化项目后就能可以跑通本地开发调试、构建、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 目录用来存放:



其次,在 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 共同作用,形成了一套架构体系;

此图来源于 《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/resolver' 目录下


由于 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 )、搭建体系、源码体系、运营中台、工程套件物料体系、前端智能化等前沿技术,还可以与层层选拔的各路优秀同学共同战斗,共同成长!

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

简历投递至:muqin.lmq@alibaba-inc.com(点击“阅读原文”查看详情)

✿  拓展阅读

作者|玄农

编辑|橙子君

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值