ThinkJS 3.0 如何实现对 TypeScript 的支持

clipboard.png

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 层,让代码保持轻快。html

think-cli 2.0 新版发布

在本文发布的同时 ThinkJS 团队发布了新版的脚手架 think-cli 2.0,新版脚手架最大的特色是脚手架和模板分离,能够在不修改脚手架的基础上添加各类项目启动模板,若是老司机想跳过下面实现细节,快速开始尝试 TypeScript 下的 ThinkJS 3.0, 能够用 think-cli 2.0 和 TypeScript 的官方模板:node

npm install -g thinkjs-cli@2
thinkjs new project-name typescript

实现支持 TypeScript

TypeScript 是 JavaScript 的超集,其最大的的特性是引入了静态类型检查,按照通常的经验,在中大型的项目上引入 TypeScript 收获显著,并有至关的使用群体,这也就坚决了 ThinkJS 3.0 支持 TypeScript 的决心。咱们但愿 TS 版本的代码对用户的侵入尽量的小,配置足够简单,而且接口定义准确,清晰。基于这样的目的,本文在接下来的章节会探讨在实现过程当中的一些思考和方案。git

继承 Koa 的定义

由于 ThinkJS 3.0 基于 Koa,咱们须要把类型定义构建在其定义之上,大概的思路就是用继承的方式定义 ThinkJS 本身的接口并添加本身的扩展实现,最后再组织起来。话是这么说,仍是赶忙写点代码验证一下。发现 Koa 的 TS 定义没有本身实现而是在 DefinitelyTyped 里面,这种状况多数是库的做者没有实现 TypeScript 接口定义,由社区的伙伴实现出来了并上传,方便你们使用,而 ThinkJS 自己计划支持 TypeScript,全部后面的实现都是定义在项目的 index.d.ts 文件里面。好回到代码,首先安装 Koa 和类型定义。github

npm install koa @types/koa

而后在 ThinkJS 项目里面添加 index.d.ts, 并在 package.json 里面添加 "type": "index.d.ts",,这样 IDE (好比 VSCode)就能知道这个项目的类型定义文件的位置,咱们须要一个原型来验证想法的可行性:typescript

// 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 对象:npm

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

通过验证果真行得通,准备添加更多实现。json

基本雏形

接下来先实现一版基本的架子,这个架子基本上反应了 ThinkJS 里面最重要的类和他们之间的关系。后端

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,要求实现构造器并返回自己类型的一个实例。架子基本肯定,开始定义接口。app

定义接口

定义接口是整个实现最难的部分,在过程当中走了很多弯路。主要缘由是 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 的同窗,赶忙来试试吧。

相关文章
相关标签/搜索