ThinkJS3 距离初次发布已有半年的时间,最近花了点时间将 Firekylin 的依赖从 ThinkJS2 升级到 ThinkJS3。这里记录一下升级碰到的一些变化,但愿能帮助到你们。html
ThinkJS3 升级后原生支持 async/await
了就想着干脆把 babel 抛弃吧。 以前 Firekylin 都是使用 import
的 ES Module 模块规则,因此都须要修改为 CommonJS 原生的 require
方式。git
还有就是继承的基类都发生了变化,以前都是 think.xxx.base
如今都变成了 think.Xxx
,例如:github
think.controller.base => think.Controller think.model.base => think.Model think.logic.base => think.Logic think.service.base => think.Service
ThinkJS2 中 logic 的写法丰富,支持字符串和对象两种方式。因为字符串的解析维护成本过高,在 ThinkJS3 中将字符串规则的支持去除了。另外对象支持里一些具体规则的写法也有略微的变化,例如:babel
//ThinkJS2 this.rules = { username: {minLength: 4}, password: {length: [32, 32]} }; //ThinkJS3 this.rules = { username: {length: {min: 4}}, password: {length: {min: 32, max: 32}} };
控制器这块改动的东西比较多,变化比较大的是路由和 RESTful 这块。架构
首先是自定义路由的写法有变化,具体可参考 Router / 路由 - ThinkJS 文档。另外多模块状况下彷佛不会读取子模块的路由配置。自定义路由正则匹配的时候须要包括路由的开头 /
。当你路由配置不少的时候这点就有点让人烦躁。好在 ThinkJS3 中间件化后可配置的东西多了,think-router
中间件支持配置 prefix
,只要设置 prefix: '/'
便可过滤掉通用的前缀。更多的配置能够查看文档。koa
另外就是多字符拼接的路由,例如 /index/sync_comment
。在 ThinkJS2 中该路由解析出来的 action 为 syncCommentAction
。ThinkJS3 中将这个自动处理去除了,因此解析出来的 action 仍是 sync_commentAction
。异步
还有就是 ThinkJS2 对路由的大小写不敏感,在 ThinkJS3 中也一并去除了这些操做,/index
和 /Index
是不同的路由。async
ThinkJS2 里面提供的 RESTful 路由在写接口的时候很是方便,继承 think.controller.rest
以后自动会将请求 method 映射到对应的 action 中,而且支持在参数中切换请求方法。然而 ThinkJS3 由于架构发生变动,使用方法上没法完美的同步过来。全部的 RESTful 路由使用以前都须要使用自定义路由配置一下,并且也不支持参数切换请求方法。函数
我我的以为这种方法使用起来极其烦人,因此就写了一个 think-router-rest 的中间件对官方的 RESTful 操做进行补完。安装后的 RESTful 路由基本上就和 ThinkJS2 中的使用体验一致了。也能完美的支持参数切换请求方法,这个功能在 CLI 运行路由的时候很是有用。post
module.exports = class extends think.Controller { // 标记后该 Controller 会被识别为 RESTful 控制器 static get _REST() { return true; } // 请求方法切换参数名称 static get _method() { return 'method'; } getAction() { } postAction() { } }
固然 RESTful 多级路由的话,若是是中间没有参数由于 ThinkJS3 中支持多级子文件夹路由了,因此没有问题。若是是中间有参数例如 /user/:id/post
这种的话仍是须要使用自定义路由的。
file 对象修改
若是以前有文件上传的话也须要注意一下。由于使用了外部模块来处理上传,因此 this.file()
获取的 file
对象有变化,file.originalFilename
字段修改成 file.name
而且没有 file.fieldName
字段了。
service 默认实例化
在 ThinkJS2 Controller 中,使用 this.service
获取到的是 Service 的基类,须要本身手动实例化。ThinkJS3 中默认拿到的就是实例化好的实例了,不须要手动实例化。
model 最大的变化就是把普通模型和关联模型的基类进行了合并,全部的 model 都只要继承 think.Model 基类便可。若是是关联模型的话则须要单独配置 relation 属性设置关联关系便可。
module.exports = class extends think.Model { get relation() { return { cate: think.Model.MANY_TO_MANY, user: { type: think.Model.BELONG_TO, field: 'id,name,display_name' } }; } }
这里有一点是必须使用 getter 的形式设置,直接设置 this.relation 属性会报错。
模板这块的变化不是很是大。除了 升级指南 - ThinkJS 文档 中说的那些以外,有一个须要稍微注意的是在 beforeRender()
方法中彷佛没办法获取到 ctx
了,没办法拿到请求相关的一些数据。
还有一些比较小的一些变化,主要是一些函数方法的变化,例如:
this.ip()
更新为 this.ctx.ip
think.isDir()
更新为 think.isDirectory()
this.isGet()
, this.isPost()
修改成 this.isGet
和this.isPos
在升级过程当中有两个问题(需求),日常 issue 和开发群中也有不少人会碰到,这里也记录一下。
咱们常常会碰到以下的需求,判断完后就返回结果不作后续操做了。在 ThinkJS2 中阻止后续执行不须要特别的操做,直接使用 this.success()
或者 this.fail()
返回数据便可。
model.exports = class extends think.controller.base { userCheck() { if(this.post('user') !== 'admin') { return this.fail(); } } indexAction() { this.userCheck(); return this.success(); } }
实现的原理是在 this.fail()
等返回结果的方法中使用 think.prevent()
方法抛出一个错误来阻止后续的执行。但在 ThinkJS3 中由于 think.prevent
方法被移除,因此你在 3.x 中写上面的这部分代码的话会致使 this.fail()
和 this.success()
都被执行而致使程序报错。官方目前给的方法是返回 false 来组织后续代码执行。也就是:
model.exports = class extends think.Controller { userCheck() { return this.post('user') === 'admin'; } indexAction() { const result = this.userCheck(); if(!result) { return this.fail(); } return this.success(); } }
使用布尔值将结果传递回 Action 中,保证只有在 Action 的最后才会执行 this.success()
和 this.fail()
方法。这种方法的确能解决问题,但无疑是很蛋疼的,当方法嵌套层级多了的时候,一级一级的返回值会多写不少无用代码。
因此在 Firekylin 里我补全了 think.prevent()
方法,这里也感谢 ThinkJS3 提供了强大的扩展能力。补全了 prevent()
方法以后就能使用 ThinkJS2 的逻辑来处理阻止后续执行功能了。
// src/common/extend/think.js const preventMessage = 'PREVENT_NEXT_PROCESS'; module.exports = { prevent() { throw new Error(preventMessage); }, isPrevent(err) { return think.isError(err) && err.message === preventMessage; } }; // src/common/extend/controller.js module.exports = { success(...args) { this.ctx.success(...args); return think.prevent(); }, fail(...args) { this.ctx.fail(...args); return think.prevent(); } };
能够看到其实就是在 this.success()
以后抛了一个 Error 来阻止后续的执行。不过这样也会带来一个问题若是用户捕捉错误的话有可能会捕捉到这个 Error。
try { const data = JSON.parse(this.get('data')); this.success(data); } catch(e) { this.fail(e.message); }
如上代码由于 this.success
本质上是抛了一个错,因此 catch 也是会被执行了,致使会再次执行 this.fail
而导致程序报错。 解决的办法是须要手动的判断一下错误类型:
try { const data = JSON.parse(this.get('data')); this.success(data); } catch(e) { if(!think.isPrevent()) { this.fail(e.message); } }
你们想一想看为何上文里我说同时执行了 this.success()
和 this.fail()
程序就会报错呢。报的错又是什么呢?没错就是标题里的 headers have already been sent
。正如字面意思上说的就是响应头已经像客户端发送过了。
由于 this.success()
和 this.fail()
最终都是调用 koa 的方法将响应内容写到 this.ctx.body
中,koa 判断 this.ctx.body
中写入内容后就会发送数据给客户端。当你再次向 this.ctx.body
中写入时,由于数据已经发送了,因此这次写入就会无效而致使抛出错误。
除了同事执行了 this.success()
和 this.fail()
以外,还有一种比较常见的操做是这样的:
indexAction() { fs.readFile('a.txt', 'utf-8', data => this.success(data)); }
因为 Action 的异步操做中才有 this.success()
写入数据,因此会优先触发 koa 默认的请求返回,而后等异步执行完了以后才会触发 this.success()
返回。这样一样是屡次返回响应数据的问题。固然这个解决的方法就比较简单了,async/await
或者 Promise 都能解决。