ThinkJS 3.0 如何實現對 TypeScript 的支持

ThinkJS 3.0 是一款面向將來開發的 Node.js 框架,內核基於 Koa 2.0。 3.0 相比 2.0 版本進行了模塊化改造,使得內核自己只包含了最少許必須的代碼,甚至還不足以構成一個完整的 Web MVC 框架,除了內核裏面實現的 Controller, View 和 Model 被實現爲擴展(Extend)模塊 think-viewthink-model,這樣實現的好處也是顯而易見的,若是個人 Web 服務只是簡單的 RESTful API,就不須要引入 View 層,讓代碼保持輕快。javascript

think-cli 2.0 新版發佈

在本文發佈的同時 ThinkJS 團隊發佈了新版的腳手架 think-cli 2.0,新版腳手架最大的特色是腳手架和模板分離,能夠在不修改腳手架的基礎上添加各類項目啓動模板,若是老司機想跳過下面實現細節,快速開始嘗試 TypeScript 下的 ThinkJS 3.0, 能夠用 think-cli 2.0 和 TypeScript 的官方模板:html

npm install -g thinkjs-cli@2
thinkjs new project-name typescript
複製代碼

實現支持 TypeScript

TypeScript 是 JavaScript 的超集,其最大的的特性是引入了靜態類型檢查,按照通常的經驗,在中大型的項目上引入 TypeScript 收穫顯著,並有至關的使用羣體,這也就堅決了 ThinkJS 3.0 支持 TypeScript 的決心。咱們但願 TS 版本的代碼對用戶的侵入儘量的小,配置足夠簡單,而且接口定義準確,清晰。基於這樣的目的,本文在接下來的章節會探討在實現過程當中的一些思考和方案。java

繼承 Koa 的定義

由於 ThinkJS 3.0 基於 Koa,咱們須要把類型定義構建在其定義之上,大概的思路就是用繼承的方式定義 ThinkJS 本身的接口並添加本身的擴展實現,最後再組織起來。話是這麼說,仍是趕忙寫點代碼驗證一下。發現 Koa 的 TS 定義沒有本身實現而是在 DefinitelyTyped 裏面,這種狀況多數是庫的做者沒有實現 TypeScript 接口定義,由社區的夥伴實現出來了並上傳,方便你們使用,而 ThinkJS 自己計劃支持 TypeScript,全部後面的實現都是定義在項目的 index.d.ts 文件裏面。好回到代碼,首先安裝 Koa 和類型定義。node

npm install koa @types/koagit

而後在 ThinkJS 項目裏面添加 index.d.ts, 並在 package.json 裏面添加 "type": "index.d.ts",,這樣 IDE (好比 VSCode)就能知道這個項目的類型定義文件的位置,咱們須要一個原型來驗證想法的可行性:github

// in thinkjs/index.d.ts

  import * as Koa from 'koa';

  interface Think {
    app: Koa;
  }
  // expect think to be global variable
  declare var think: Think;
複製代碼
// in Controller

  import 」thinkjs「;
  // bellow will cause type error
  think.app
複製代碼

出師不利,這樣的定義是不能正常工做的,IDE 的輸入感知也不會生效,緣由是 TypeScript 爲了不全局污染,嚴格區分模塊 scope 和全局定義的 scope, 一旦使用了 import 或者 export 就會認爲是模塊,think 變量就只存在於模塊 scope 裏面了。仔細一想這種設定也合理,因而修改代碼,改爲模塊。改爲模塊後與JS版本的區別是 TypeScript 裏面須要顯式獲取 think 對象:typescript

// in thinkjs/index.d.ts

  import * as Koa from 'koa';

  declare namespace ThinkJS {
    interface Think {
      app: Koa;
    }
    export var think: Think;
  }
  export = ThinkJS
複製代碼
// in Controller
  import { think } from 」thinkjs「;

  // working!
  think.app
複製代碼

通過驗證果真行得通,準備添加更多實現。npm

基本雛形

接下來先實現一版基本的架子,這個架子基本上反應了 ThinkJS 裏面最重要的類和他們之間的關係。json

import * as Koa from 'koa';
import * as Helper from 'think-helper';
import * as ThinkCluster from 'think-cluster';

declare namespace 'ThinkJS' {

  export interface Application extends Koa {
    think: Think;
    request: Request;
    response: Response;
  }

  export interface Request extends Koa.Request {
  }

  export interface Response extends Koa.Response {
  }

  export interface Context extends Koa.Context {
    request: Request;
    response: Response;
  }

  export interface Controller {
    new(ctx: Context): Controller;
    ctx: Context;
    body: any;
  }

  export interface Service {
    new(): Service;
  }

  export interface Logic {
    new(): Logic;
  }

  export interface Think extends Helper.Think {
    app: Application;
    Controller: Controller;
    Logic: Logic;
    Service: Service; 
  }

  export var think: Think;
}


export = ThinkJS;
複製代碼

這裏面定義到的類都是 ThinkJS 裏面支持擴展的類型,爲了簡潔起見省略了許多方法和字段的定義,須要指出的是 ControllerServiceLogic這三個接口須要被繼承 extends,要求實現構造器並返回自己類型的一個實例。架子基本肯定,開始定義接口。後端

定義接口

定義接口是整個實現最難的部分,在過程當中走了很多彎路。主要緣由是 ThinkJS 3.0 高度模塊化,程序裏面用到的 Extend 方法都由具體模塊生成,咱們的實現方案也經歷了幾個階段,簡單列舉一下這個過程。

全量定義

這是第一階段 ThinkJS 3.0 支持 TypeScript 的方案, 當時對全局 scope 和模塊 scope 的問題還不是很清晰,以致於一些想法得不到驗證,也漸漸偏離了最佳的方案。當時考慮到擴展模塊不是不少,直接全量定義全部擴展接口,這樣用戶無論有沒有引入某個 Extend 模塊,都能得到模塊的接口提示。這樣作的弊端有不少,好比沒法支持項目內 Extend 等,但這個方案的好處是須要用戶關注的東西最少,代碼開箱即用。

增量模塊

咱們清楚按需引入纔是最理想的方案,後來咱們發現 TypeScript 有一個特性叫 Module Augmentation ,其實這個特性最大用處就是能夠在不一樣模塊擴充某一個模塊的接口定義,讓增量模塊定義生效很重要的一點前提是,須要用戶在文件中顯式加載對應的模塊,也就是讓 TypeScript 知道誰對模塊實現了增量定義。好比,要想得到 think-view 定義的增量接口,須要在 Controller 實現中引入:

import { think } from "thinkjs";
import "think-view";
// import "think-model";
export default class extends think.Controller {
  indexAction() {
    this.model();  // reports an error
    this.display(); // OK
  }
}
複製代碼
// in think-view
declare module 'thinkjs' {
  interface Controller {
    dispay(): void
  }
}
複製代碼
// in think-model
declare module 'thinkjs' {
  interface Controller {
    model(): void
  }
}
複製代碼

這樣寫很麻煩,但若是不去 import TypeScript 是沒法完成提示和追溯的,一個簡化版本是咱們能夠在一個文件裏面定義全部的用到的 Extend 模塊,並輸出 think 對象,好比

// think.js
import { think } from "thinkjs";
import "think-view";
import "think-model";
// import the rest extend module
// import project exnted files
export default think;
複製代碼
// some_controller.js
import think from './think.js';
export default class extends think.Controller {
  indexAction() {
    this.model();
    this.display();
  }
}
複製代碼

這樣問題已經基本解決了,只是用了相對路徑,若是在多級目錄下路徑就比較凌亂,有沒有更好的方案呢?

黑科技:path

咱們知道 Webpack 裏面有一個很是好用的功能是 alias,就是用來解決相對路徑引用問題的,發現 TypeScript 也有相似概念叫 compilerOptions.path,至關於對某個路徑定義了一個縮寫,這樣只要對剛纔的定義文件添加到 compilerOptions.path 裏面,而且縮寫名稱叫 thinkjs(定義成 thinkjs 這樣編譯後就能正常運行, 下面會提到),那 Controller 的實現就毫無違和感了:

import {think} from 'thinkjs';
export default class extends think.Controller {
  indexAction() {
    this.model();
    this.display();
  }
}
複製代碼
import * as ThinkJS from '../node_modules/thinkjs';
import 'think-view';
import 'think-model';

// other extend modules
// ...
export const think = ThinkJS.think;
複製代碼

注意到這裏 ThinkJS 是經過相對路徑引用的,由於 'thinkjs' 模塊已經被重定向,這裏還須要一個小小的黑科技來騙過 TypeScript 讓其知道模塊 '../node_modules/thinkjs'‘thinkjs'

// in thinkjs/index.d.ts

  import { Think } from 'thinkjs';

  // this is a external module
  declare module ‘thinkjs’ {
    // put all declaration in here
  }

  // curently TypeScript think this is in '../node_modules/thinkjs' module
  declare namespace ThinkJS {
    export var think: Think;
  }

  export = ThinkJS;
複製代碼

對於實現,其實咱們更關心接口的優雅,也許後面有更合理的實現,可是前提是寫法要保持簡潔。

引入項目擴展

項目裏面的擴展一樣使用增量模塊定義,代碼以下

declare module 'thinkjs' {
  export interface Controller {
    yourControllerExtend(): void
  }
}

const controller = {
  yourControllerExtend() {
    // do something
  }
};

export default controller;
複製代碼

ThinkJS 支持擴展的對象總共有8個,爲了方便,在 think-cli 2.0 版本中,TypeScript 的官方模板默認生成全部對象的定義,並在 src/index.ts 裏面引入。

import * as ThinkJS from '../node_modules/thinkjs';

import './extend/controller';
import './extend/logic';
import './extend/context';
import './extend/think';
import './extend/service';
import './extend/application';
import './extend/request';
import './extend/response'; 

// import the rest extends modules on need

export const think = ThinkJS.think;
複製代碼

完善接口

最後就是一些接口的定義和添加文檔,至關於從源代碼結合着文檔,把全部 ThinkJS 3.0 的接口都定義出來, 最終目的是能提供一個清晰的開發接口提示,舉個例子

*
* get config
* @memberOf Controller
*/
config(name: string): Promise<string>;
/**
 * set config
 * @memberOf Controller
 */
config(name: string, value: string): Promise<string>;
複製代碼

TSLint

咱們基於 ThinkJS 項目的特色配置了一套 tslint 的規則並保證開箱代碼符合規範。

編譯部署

在開發環境可使用 think-typescript 編譯,還支持 tsc 直接編譯,以前 import { think } from 'thinkjs' 會被編譯爲

const thinkjs_1 = require("thinkjs");
class default_1 extends thinkjs_1.think.Controller {
複製代碼

這個路徑並無按照 compileOptions.path 的配置進行相對路徑的計算,可是無論哪一種方式都能正常工做,並且當前方式的結果更爲理想,只是要求縮寫名必定是 thinkjs 。

最後

在用 VSCode 開發 TypeSccript 的 ThinkJS 3.0 過程當中,能得到智能感知和更多的錯誤提示,感受代碼獲得了更多的保護和約束,有點以前在後端寫 Java 的體驗,若是尚未嘗試過 TypeScript 的同窗,趕忙來試試吧。

相關文章
相關標籤/搜索