ThinkJS3 升級小記

ThinkJS3 距離初次發佈已有半年的時間,最近花了點時間將 Firekylin 的依賴從 ThinkJS2 升級到 ThinkJS3。這裏記錄一下升級碰到的一些變化,但願能幫助到你們。html

CommonJS

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

Logic

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}}
};

Controller

控制器這塊改動的東西比較多,變化比較大的是路由和 RESTful 這塊。架構

路由

首先是自定義路由的寫法有變化,具體可參考 Router / 路由 - ThinkJS 文檔。另外多模塊狀況下彷佛不會讀取子模塊的路由配置。自定義路由正則匹配的時候須要包括路由的開頭 / 。當你路由配置不少的時候這點就有點讓人煩躁。好在 ThinkJS3 中間件化後可配置的東西多了,think-router 中間件支持配置 prefix ,只要設置 prefix: '/' 便可過濾掉通用的前綴。更多的配置能夠查看文檔。koa

另外就是多字符拼接的路由,例如 /index/sync_comment。在 ThinkJS2 中該路由解析出來的 action 爲 syncCommentAction。ThinkJS3 中將這個自動處理去除了,因此解析出來的 action 仍是 sync_commentAction異步

還有就是 ThinkJS2 對路由的大小寫不敏感,在 ThinkJS3 中也一併去除了這些操做,/index/Index 是不同的路由。async

RESTful

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 最大的變化就是把普通模型和關聯模型的基類進行了合併,全部的 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 屬性會報錯。

View

模板這塊的變化不是很是大。除了 升級指南 - ThinkJS 文檔 中說的那些以外,有一個須要稍微注意的是在 beforeRender() 方法中彷佛沒辦法獲取到 ctx 了,沒辦法拿到請求相關的一些數據。

其它

還有一些比較小的一些變化,主要是一些函數方法的變化,例如:

  • this.ip() 更新爲 this.ctx.ip
  • think.isDir()更新爲 think.isDirectory()
  • this.isGet(), this.isPost()修改成 this.isGetthis.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);
  }
}

headers have already been sent

你們想一想看爲何上文裏我說同時執行了 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 都能解決。

相關文章
相關標籤/搜索