ThinkJS3.0是面向未来的Node.js框架,内核基于Koa2.0。3.0相对于2.0版本进行了模块化,使得内核本身只包含了最少的必要代码,甚至不足以构成一个完整的WebMVC框架。除了内核中实现的Controller之外,View和Model都是作为扩展(Extend)模块think-view和think-model来实现的,这样实现的好处也是显而易见的。如果我的web服务只是一个简单的RESTfulAPI,就没有必要引入View层来保持代码的轻量化。think-cli2.0新版本与本文同时发布。ThinkJS团队发布了新版脚手架think-cli2.0。新版脚手架最大的特点就是脚手架和模板分离。无需修改脚手架,即可添加各种项目启动模板。如果老司机想跳过下面的实现细节,赶紧开始尝试TypeScript下的ThinkJS3.0。可以使用think-cli2.0和官方TypeScript模板:npminstall-gthinkjs-cli@2thinkjsnewproject-nametypescript实现支持TypeScriptTypeScript是JavaScript的超集,最大的特点是引入了静态类型检查。从一般经验来看,大中型项目引入TypeScript取得了显著成效,拥有可观的用户群体,这也更加坚定了ThinkJS3.0支持TypeScript的决心。我们希望TS版本的代码对用户的侵入性尽可能小,配置足够简单,接口定义准确清晰。基于此目的,本文后续章节将探讨实现过程中的一些思考和解决方案。继承Koa的定义因为ThinkJS3.0是基于Koa的,所以我们需要在它的定义之上构建类型定义。大致的思路是通过继承来定义ThinkJS自己的接口并添加自己的扩展实现,最后组织起来。话虽如此,让我们快速编写一些代码来验证它。发现Koa的TS定义并不是自己实现的,而是在DefinitelyTyped中实现的。大多数情况下,库的作者并没有实现TypeScript接口定义,社区小伙伴已经实现并上传,供大家使用。ThinkJS本身计划支持TypeScript。以下所有实现都在项目的index.d.ts文件中定义。好了回到代码,首先安装Koa和类型定义。npminstallkoa@types/koa然后在ThinkJS项目中添加index.d.ts,在package.json中添加"type":"index.d.ts",这样IDE(比如VSCode)就可以知道这个project类型定义文件的位置,我们需要一个原型来验证想法的可行性://inthinkjs/index.d.tsimport*asKoafrom'koa';界面Think{app:Koa;}//期望认为是全局变量declarevarthink:Think;//在控制器中导入“thinkjs”;//bellow会导致类型错误think.apphasabadstart,这样的定义将无法正常工作,IDE的输入感知也不会生效,原因是为了避免全局污染,TypeScript严格区分模块作用域和全局定义的范围。一旦使用import或export,它就会被认为是一个模块,并且认为变量只存在于模块范围内。仔细一想,这样设置也是合理的,于是修改了代码,改成了一个模块。模块和JS版本的区别在于TypeScript需要显式获取think对象://inthinkjs/index.d.tsimport*asKoafrom'koa';声明命名空间ThinkJS{interfaceThink{app:Koa;}exportvarthink:思考;}export=ThinkJS//在控制器中import{think}from"thinkjs";//在职的!think.app已经过验证并且可以运行,我们将添加更多实现。基本原型接下来实现一个基本的架子,它基本上反映了ThinkJS中最重要的类以及它们之间的关系。import*asKoafrom'koa';import*asHelperfrom'think-helper';import*asThinkClusterfrom'think-cluster';declarenamespace'ThinkJS'{exportinterfaceApplicationextendsKoa{think:Think;请求:请求;响应:响应;}exportinterfaceRequestextendsKoa.Request{}exportinterfaceResponseextendsKoa.Response{}exportinterfaceContextextendsKoa.Context{request:Request;响应:响应;}exportinterfaceController{new(ctx:Context):Controller;ctx:上下文;正文:任何;}导出接口服务{new():服务;}导出接口逻辑{new():逻辑;}exportinterfaceThinkextendsHelper.Think{app:Application;控制器:控制器;逻辑:逻辑;服务:服务;}exportvarthink:Think;}export=ThinkJS;这里定义的类是ThinkJS中支持扩展的类型。为了简洁起见,省略了许多方法和字段的定义。需要指出的是,Controller、Service、Logic这三个接口需要继承自extends,并且需要实现constructor并返回一个自身类型的实例。架子基本确定了,接口也定义好了。定义接口定义接口是整个实现中最难的部分,过程中走了很多弯路。主要原因是ThinkJS3.0高度模块化。程序中使用的Extend方法是由特定模块生成的。我们的实施也经历了几个阶段。让我简要列出这个过程。完整定义这是ThinkJS3.0对TypeScript支持的第一阶段。当时对全局作用域和模块作用域的问题还不是很清楚,所以有些想法无法验证,逐渐偏离了最佳方案。当时考虑到扩展模块不多,所有的扩展接口都是直接全定义的,这样用户不管有没有引入Extend模块,都可以得到模块的接口提示。这样做有很多缺点,比如无法在项目中支持Extend等,但是这种方案的优点是需要用户的关注最少,代码开箱即用。增量模块我们知道按需引入是最理想的解决方案。后来,我们发现TypeScript有个特性叫ModuleAugmentation。其实这个特性最大的用处就是在不同的模块中扩展某个模块的接口定义,使增量模块定义生效。一个很重要的前提是用户需要显式加载文件中对应的模块,也就是让TypeScript知道是谁实现了模块的增量定义。例如获取think-view定义的增量接口,需要在Controller实现中引入:import{think}from"thinkjs";import"think-view";//import"think-model";export默认类扩展think.Controller{indexAction(){this.model();//报错this.display();//OK}}//在think-viewdeclaremodule'thinkjs'{interfaceController{dispay():void}}//在think-modeldeclaremodule'thinkjs'{interfaceController{model():void}}很麻烦这么写,但是如果不导入TypeScript,是无法完成hint和trace的。一个简化的版本是我们可以在一个文件中写它定义所有使用的Extend模块并输出think对象,例如//think.jsimport{think}from"thinkjs";import"think-view";import"think-model";//importtherestextendmodule//importprojectexntedfilesexportdefaultthink;//some_controller.jsimportthinkfrom'./think.js';exportdefaultclassextendsthink.Controller{indexAction(){this.model()复制代码;这个。显示();}}这个问题基本解决了,但是用的是相对路径。如果在多级目录下路径比较乱,有没有更好的解决办法?黑科技:path我们知道Webpack中一个非常好用的功能就是alias,用来解决相对路径引用的问题。我们发现类型Script也有一个类似的概念叫做compilerOptions.path,相当于定义了某个路径的缩写,所以只要在compilerOptions.path中加入刚才的定义文件,缩写名称为thinkjs(定义为thinkjs,编译后可以正常运行,下面会提到),那么Controller的实现就没有违和感了:import{think}from'thinkjs';导出默认类扩展think.Controller{indexAction(){this.model();这个.显示();}}import*asThinkJSfrom'../node_modules/thinkjs';import'think-view';import'think-model';//其他扩展模块//...exportconstthink=ThinkJS.think;请注意,这里ThinkJS是通过相对路径引用的,因为'thinkjs'模块已被重定向,需要一点技巧来欺骗TypeScript知道模块'../node_modules/thinkjs'是'thinkjs',//在thinkjs/index.d.ts中从'thinkjs'导入{Think};//这是一个外部模块declaremodule'thinkjs'{//将所有声明放在这里}//当前TypeScript认为这是在'../node_modules/thinkjs'moduledeclarenamespaceThinkJS{exportvarthink:Think;}export=ThinkJS;对于实现,其实我们更关心的是界面的优雅,后面可能会有更合理的实现,但是前提是保持写的简单引入项目扩展项目中的扩展也是通过增量模块来定义的,代码如下};导出默认控制器;ThinkJS总共支持8个扩展对象。为了方便起见,在think-cli2.0版本中,TypeScript官方模板默认生成了所有对象的定义,并导入到src/index.ts中。import*asThinkJSfrom'../node_modules/thinkjs';import'./extend/controller';import'./extend/logic';import'./extend/context';import'./extend/think';导入“./extend/service”;导入“./extend/application”;导入“./extend/request”;导入“./extend/response”;//在needexport上导入其余的扩展模块constthink=ThinkJS.思考;最后完善接口就是定义一些接口和添加文档,相当于把源码和文档结合起来,定义了ThinkJS3.0的所有接口。最终目的是提供一个清晰的开发界面提示,例如**getconfig*@memberOfController*/config(name:string):Promise
