博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
ThinkJS 3.0 如何实现对 TypeScript 的支持
阅读量:5739 次
发布时间:2019-06-18

本文共 6648 字,大约阅读时间需要 22 分钟。

是一款面向未来开发的 Node.js 框架,内核基于 。 3.0 相比 2.0 版本进行了模块化改造,使得内核本身只包含了最少量必须的代码,甚至还不足以构成一个完整的 Web MVC 框架,除了内核里面实现的 Controller, View 和 Model 被实现为(Extend)模块 和 ,这样实现的好处也是显而易见的,如果我的 Web 服务只是简单的 RESTful API,就不需要引入 View 层,让代码保持轻快。

think-cli 2.0 新版发布

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

npm install -g thinkjs-cli@2thinkjs new project-name typescript复制代码

实现支持 TypeScript

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

继承 Koa 的定义

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

npm install koa @types/koa

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

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

// 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复制代码

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

基本雏形

接下来先实现一版基本的架子,这个架子基本上反应了 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,要求实现构造器并返回本身类型的一个实例。架子基本确定,开始定义接口。

定义接口

定义接口是整个实现最难的部分,在过程中走了不少弯路。主要原因是 ThinkJS 3.0 高度模块化,程序里面用到的 Extend 方法都由具体模块生成,我们的实现方案也经历了几个阶段,简单列举一下这个过程。

全量定义

这是第一阶段 ThinkJS 3.0 支持 TypeScript 的方案, 当时对全局 scope 和模块 scope 的问题还不是很清晰,以至于一些想法得不到验证,也渐渐偏离了最佳的方案。当时考虑到扩展模块不是很多,直接全量定义所有扩展接口,这样用户不管有没有引入某个 Extend 模块,都能获得模块的接口提示。这样做的弊端有很多,比如无法支持项目内 Extend 等,但这个方案的好处是需要用户关注的东西最少,代码开箱即用。

增量模块

我们清楚按需引入才是最理想的方案,后来我们发现 TypeScript 有一个特性叫 ,其实这个特性最大用处就是可以在不同模块扩充某一个模块的接口定义,让增量模块定义生效很重要的一点前提是,需要用户在文件中显式加载对应的模块,也就是让 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-viewdeclare module 'thinkjs' {  interface Controller {    dispay(): void  }}复制代码
// in think-modeldeclare module 'thinkjs' {  interface Controller {    model(): void  }}复制代码

这样写很麻烦,但如果不去 import TypeScript 是无法完成提示和追溯的,一个简化版本是我们可以在一个文件里面定义所有的用到的 Extend 模块,并输出 think 对象,比如

// think.jsimport { think } from "thinkjs";import "think-view";import "think-model";// import the rest extend module// import project exnted filesexport default think;复制代码
// some_controller.jsimport 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个,为了方便,在 版本中,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 needexport const think = ThinkJS.think;复制代码

完善接口

最后就是一些接口的定义和添加文档,相当于从源代码结合着文档,把所有 ThinkJS 3.0 的接口都定义出来, 最终目的是能提供一个清晰的开发接口提示,举个例子

** get config* @memberOf Controller*/config(name: string): Promise
;/** * set config * @memberOf Controller */config(name: string, value: string): Promise
;复制代码

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 的同学,赶紧来试试吧。

转载地址:http://nvfzx.baihongyu.com/

你可能感兴趣的文章
查看历史操作记录_git操作方法
查看>>
5怎么选国外节点_房子装修,床垫怎么选?这5家床垫值得买
查看>>
变成一列_Excel一列数据转多行多列,这4条函数公式可以学起来
查看>>
手机超广角拍摄软件_如何用超广角“看开一点”?OPPO官方教学,这些大片装下整个夏天...
查看>>
rip协议中周期性广播路由信息的报文_关于RIP的一点小笔记--华为
查看>>
linux oracle无法解析指定的连接标识符_pl/sql连接oracle时候,提示无法解析指定的连接标识符...
查看>>
webstorm如何编写jsp_WebStorm中怎样运行JSP页面在浏览器中显示
查看>>
python自动填日志_python接口自动化(四十)- logger 日志 - 下(超详解)
查看>>
python文本可视化数据分析软件_数据探索很麻烦?推荐一款史上最强大的特征分析可视化工具:yellowbrick...
查看>>
python range(30)_python的range()函数
查看>>
windows python3 paramiko安装_Python3.3 Paramiko Windows安装错误
查看>>
ref获取元素 vue 删除子元素_vue 添加删除子元素
查看>>
mysql有回收站吗_mysql 回收站
查看>>
mysql时间戳参数_MySQL 时间戳(Timestamp)函数
查看>>
mysql 分组返回_MySQL-分组和总计,但返回每个分组中的所有行
查看>>
centos6 mysql 同步_centos 6.5设置mysql主从同步过程记录
查看>>
在MySQL中两个实体怎么匹配_如何设计数据库约束,以便两个实体只有其中两个字段值匹配才可以有多对多关系?...
查看>>
mysql索引 回表_记录下mysql索引以及回表
查看>>
php mysql 数组存储过程_php调用MySQL存储过程的方法集合(推荐)
查看>>
cd usr local mysql_不想每次都到: /usr/local/mysql/bin
查看>>