混搭 TypeScript + GraphQL + DI + Decorator 風格寫 Node.js 應用

本文同步自我的公衆號 「JSCON簡時空」,歡迎關注: https://mp.weixin.qq.com/s/y74j1gxcJndxD21W5v-NUw

1. 前言

恰逢最近須要編寫一個簡單的後端 Node.js 應用,因爲是全新的小應用,沒有歷史包袱 ,因此趁着此次機會換了一種全新的開發模式:javascript

  • 語言使用 TypeScript,不只僅是強類型那麼簡單,它還提供不少高級語法糖,提升編程效率。
  • 兼顧 Restful + GraphQL 方式提供數據接口,前兩年 GraphQL 特別流行,最近這段時間有些平淡下來(如今比較火熱的是 Serverless); GraphQL 這種查詢語言對前端來說仍是很友好的,本身寫的話能減小很多的接口開發量;
  • 使用 Decorator(裝飾器語法) + DI(依賴注入)風格寫業務邏輯。因後端 Java 開發服務的模式已經很是成熟,前端在 Node.js 的開發模式基本上是依照 Java 那套開發模子來的,尤爲是 DI(依賴注入)設計模式的編程思想。這幾年隨着 ECMAScript 的標準迭代,以及 TypeScript 的成熟發展,在語言層面提供了不少現代化語法糖的支持,如今也能夠利用 Decorator(裝飾器)+ DI(依賴注入)風格來寫了,我的認爲這種風格也將成爲書寫 Node.js 應用的經常使用範式之一
  • 選用支持 TS + Decorator + DI 的 Node.js框架。市面上成熟的框架,如 Nest.js, Midway.js 等能夠 —— 這類框架功能都很強大,並且提供完善的工具鏈和生態,就算你不熟,通讀他們的官方文檔都能收穫不少;本文因工做內容緣故選用 Midway 框架。


前端內部寫的後端應用基本上功能並不會太多(太專業的後端服務交給後端開發來作),絕大部分是基礎的操做,在這樣的狀況下會涉及到不少重複工做量要作,基本都是同樣的套路:html

  1. 初始化項目腳手架
  2. 數據庫的鏈接操做 + CRUD 操做
  3. 建立數據 Model 層 + Service 層
  4. 提供諸如 Restful 接口供多端消費
  5. ...


這意味着每次開發新應用都得從新來一遍 —— 這就跟前端平時切頁面同樣,重複勞動多了以後就心裏仍是比較煩的,甚至有抗拒心理。繁瑣的事大概涉及在工程鏈路 & 業務代碼這麼兩方面,若是有良好的解決方案,將大大提高開發的幸福感:前端

  1. 第一個方面是結構目錄的生成。這個問題比較好解決,市面上成熟的框架(Nest.js, Midway.js,Prisma.io 等)都提供了相應的腳手架工具,直接生成相應的服務端代碼結構,寫代碼既可靠又高效。同時這類成熟框架都能一鍵搞定部署發佈等流程,這樣咱們就能夠將大部分時間用在業務代碼上、而不是折騰環境搭建細節上。
  2. 第二個方面是業務代碼的書寫風格。一樣是寫業務代碼,語言風格不同,代碼效率也是不一樣的,你用 JS 寫業務代碼,跟 TypeScript + Decorator 來寫的效率截然不同 —— 這也就是技術發展帶來的福利。


本文着重講解第二部分,即如何使用 TypeScript + Decorator + DI 風格編寫 Node.js 應用,讓你感覺到使用這些技術框架帶來的暢快感。本文涉及的知識點比較多,主要是敘述邏輯思路,最後會以實現常見的 分頁功能 做爲案例講解。
java

本文選用技術框架是 Midway.js,設計思路能夠遷移到 Nest.js 等框架上,改動量應該不會太大。

2. 數據庫 ORM

首先咱們須要解決數據庫相關的技術選項,這裏說的技術選型是指 ORM 相關的技術選型(數據庫固定使用 MySQL),選型的基本原則是能力強大、用法簡單。
node

2.1 ORM 選型

除了直接拼 SQL 語句這種略微硬核的方式外,Node.js 應用開發者更多地會選擇使用開源的 ORM 庫,如 Sequelize。而在 Typescript 面前,工具庫層面目前兩種可選項,可使用 sequelize-typescript 或者 TypeORM 來進行數據庫的管理。作了一下技術調研後,決定選用 TypeORM ,總結緣由以下:mysql

  • 原生類型聲明,與 Typescript 有更好的相容性
  • 支持裝飾器寫法,用法上簡單直觀;且足夠強的擴展能力,能支持複雜的數據操做;
  • 該庫足夠受歡迎,Github Star 數量高達 20.3k(截止此文撰寫 2020.08 時),且官方文檔友好


並不是說 Sequelize-typescript 不行,這兩個工具庫都很強大,都能知足業務技術需求; Sequelize 一方面是 Model 定義方式比較 JS 化在 Typescript 自然的類型環境中顯得有些怪異,因此我我的更加傾向於用  TypeORM 。

2.2. 兩種操做模式

這裏簡單說明一下,ORM 架構模式中,最流行的實現模式有兩種:Active RecordData Mapper。好比 Ruby 的 ORM 採起了 Active Record 的模式是這樣的:git

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


再來看使用 Data Mapper 的 ORM 是這樣的:github

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


如今咱們察看到了它們最基本的區別:Active Record 中,領域對象有一個 save() 方法,領域對象一般會繼承一個 ActiveRecord 的基類來實現。而在 Data Mapper 模式中,領域對象不存在 save() 方法,持久化操做由一箇中間類來實現


這兩種模式沒有誰比誰好之分,只有適不適合之別:sql

  1. 簡單的 CRUD、試水型的 Demo 項目,用 Active Records 模式的 ORM 框架更好
  2. 業務流程和規則較多的、成熟的項目改造用 Data Mapper 型,其容許將業務規則綁定到實體。


Active Records 模式最大優勢是簡單 , 直觀, 一個類就包括了數據訪問和業務邏輯,剛好我如今這個小應用基本都是單表操做,因此就用 Active Records 模式了。
typescript

3. TypeORM 的使用

3.1 數據庫鏈接

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


首先,提供數據庫初始化 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 目錄用來存放:

image.png


其次,在 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. 由於要區分線上環境運行和本地開發,因此須要配置兩份
  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 的配置加載完畢以後來啓動自定義的數據庫服務,具體參考 《Egg.js - 啓動動自定義的聲明週期參考文檔》 說明
  2. 爲了避免侵入 AppBootHook 代碼太多,我把初始化數據庫服務實例的代碼放在了 DatabaseService 類的靜態方法中。

3.2 數據庫操做

數據庫鏈接上以後,就能夠直接使用 ORM 框架進行數據庫操做。不一樣於現有的全部其餘 JavaScript ORM 框架,TypeORM 支持 Active RecordData 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 示例上而已。

3.3 MVC架構

整個服務器的設計模式,就是經典的 MVC 架構,主要就是經過 ControllerServiceModelView 共同做用,造成了一套架構體系;

image.png

此圖來源於 《 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 服務端架構

3.4 RESTful API

在 Midway 初始化項目的時候,其實已經具有完整的 RESTful API 的能力,你只要照樣去擴展就能夠了,並且基於裝飾器語法和 DI 風格,編寫路由很是的方便直觀,正如官方《Midway - 路由裝飾器》裏所演示的代碼那樣,幾行代碼下來就輸出標準的 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};
  }
}

4. GraphQL

RESTful API 方式用得比較多,不過我仍是想在本身的小項目裏使用 GraphQL,具體的優勢我就很少說了,能夠參考《GraphQL 和 Apollo 爲何能幫助你更快地完成開發需求?》等相關文章。


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

4.1 接入 GraphQL 服務中間件

總體的技術選型陣容就是 apollo-server-koatype-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' 目錄下

image.png
因爲 Midway 默認集成了 CSRF 的安全校驗,咱們針對 /graphql 路徑的這層安全須要忽略掉:

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


接入的準備工做到這裏就算差很少了,接下來就是編寫 GraphQL 的 Resolver 相關邏輯

4.2 Resolvers

對於 Resolver 的處理,TypeGraphQL 提供了一些列的 Decorator 來聲明和處理數據。經過 Resolver 類的方法來聲明 QueryMutation,以及動態字段的處理 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,以及如何應用上述這些裝飾器 。

5. 案例:利用 GraphQL 實現分頁功能

5.1 分頁的數據結構

從使用者角度來,咱們但願傳遞的參數只有兩個 pageNopageSize ,好比我想訪問第 2 頁、每頁返回 10 條內容,入參格式就是:

{
     pageNo: 2,
  pageSize: 10
}


而分頁返回的數據結構以下:

{
  articles {
    totalCount # 總數
    pageNo     # 當前頁號
    pageSize      # 每頁結果數
    pages       # 總頁數
    list: {     # 分頁結果
      title,
      author
    }
  }
}

5.2 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 另外還有兩種配置:

    • 基礎的 { nullable: true | false } 只能表示整個數組是否存在(即 [Item!] 或者 [Item!]!
    • 若是想表示數組或元素都有可能爲空時,須要使用 {nullable: "itemsAndList"}(即 [Item]

5.3 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 層

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


其中和 分頁 相關的就是 skiptake 兩個參數( where 參數是跟 過濾 有關,order 參數跟排序有關)。


因此最終咱們的 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 方法進行一層轉化

5.5 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 類,同時接受了 TypeORMTypeGraphQL 兩個庫的裝飾器,寥寥幾行代碼就支持了 GraphQL 類型聲明和 ORM 實體映射,很是清晰明瞭。


到這裏一個簡單的 GraphQL 分頁功能就開發完畢,從流程步驟來看,一路下來幾乎都是裝飾器語法,整個編寫過程乾淨利落,很利於後期的擴展和維護。

6. 小結

距離上次寫 Node.js 後臺應用有段時間了,當時的技術棧和如今的無法比,如今尤爲得益於使用 Decorator(裝飾器語法) + DI(依賴注入)風格寫業務邏輯,再搭配使用 typeorm (數據庫的鏈接)、 type-graphql (GraphQL的處理)工具庫來使用,總體代碼風格更加簡潔,一樣的業務功能,代碼量減小很是可觀且維護性也提高明顯。


emm,這種感受怎麼描述合適呢?以前寫 Node.js 應用時,能用,可是總以爲哪裏很憋屈 —— 就像是白天在交通擁擠的道路上堵車,那種感受有點糟;而此次混搭了這幾種技術,會感覺神清氣爽 —— 就像是在高速公路上行車,暢通無阻。


前端的技術發展迭代相對來講迭代比較快,這是好事,能讓你用新技術作得更少、收穫地更多;固然不能否認這對前端同窗也是挑戰,須要你都保持不斷學習的心態,去及時補充這些新的知識。學無止境,與君共勉。

本文完。

文章預告:由於依賴注入和控制反轉的思想在 Node.js 應用特別重要,因此計劃接下來要 寫一些文章來解釋這種設計模式,而後再搭配一個依賴注入工具庫的源碼解讀來加深理解,敬請期待。

參考文章

相關文章
相關標籤/搜索