YodaOS 中是如何生成 API 的?

Rokid技術團隊發表於2019-03-24

在 Node.js 社群中,其實不乏通過 Markdown 生成 RESTful API 的框架,按照一定的格式約定好 API 所需要的資料,然後再通過解析 Markdown 文件,將這些關鍵資料提取出來,最後生成資料庫模型和 HTTPS 服務。

YodaOS 作為一個前端作業系統,同樣使用了類似的技術。YodaOS 中的應用分為:lightapp 和 extapp,前者是整合在語音互動執行時(Vui-daemon)程式內部的輕應用,它主要是用於一個互動簡單,需要快速響應的場景,比如音量控制、系統控制等。後者作為一個獨立的程式,通過 Child Process 與主程式通訊,使用場景主要是音樂、遊戲、電話等需要長時期使用的應用。

為什麼要有輕應用? 輕應用更像是一個指令碼,每當使用者一次進行一次互動,只需要從預先載入的指令碼中呼叫定義在對應指令碼的函式即可完成一次響應,往往這類應用互動比較簡單,如果為此要建立在每次互動的過程中進行一次 ipc 甚至 fork 時,無論對效能還是記憶體來說,都是比較浪費的。

在設計之初,我們期望對於開發者來說,並不需要針對不同型別的應用,只需要在 package.json 中修改型別即可,YodaOS API 應當保持完全一致。這樣的話,我們則面對一個問題,即使是能做到高度抽象,也需要在每次新增一個介面時,修改兩處程式碼,這其實是有違我們的設計初衷的。

API Descriptor

為此,我們引入了 API Descriptor 的概念:github.com/yodaos-proj… JavaScript 寫的 DSL,它用於描述每個 YodaOS API,包括名稱空間、事件、方法等定義。系統在初始化時,會載入所有 API Descriptor,然後分別在 lightapp 和 extapp 生成對應的 API。

Object.assign(ActivityDescriptor.prototype,
  {
    /**
     * When the app is active.
     * @event yodaRT.activity.Activity#active
     */
    active: {
      type: 'event'
    },
    /**
     * When the Activity API is ready.
     * @event yodaRT.activity.Activity#ready
     */
    ready: {
      type: 'event'
    },
    /**
     * When an activity is created.
     * @event yodaRT.activity.Activity#create
     */
    created: {
      type: 'event'
    }
  }
)
複製程式碼

上面的程式碼分別定義了 Activity 中的幾個事件:activereadycreate。因此,在任何應用中都可以這樣寫:

module.exports = activity => {
  activity.on('active', () => console.log('app activated'))
  activity.on('ready', () => console.log('app is ready'))
  activity.on('created', () => console.log('app is created'))
}
複製程式碼

接下來我們再看看“方法”是如何定義:

Object.assign(ActivityDescriptor.prototype,
  {
    /**
     * Get all properties, it contains the following fields:
     * - `deviceId` the device id.
     * - `deviceTypeId` the device type id.
     * - `key` the cloud key.
     * - `secret` the cloud secret.
     * - `masterId` the userId or masterId.
     *
     * @memberof yodaRT.activity.Activity
     * @instance
     * @function get
     * @returns {Promise<object>}
     * @example
     * module.exports = function (activity) {
     *   activity.on('ready', () => {
     *     activity.get().then((props) => console.log(props))
     *   })
     * }
     */
    get: {
      type: 'method',
      returns: 'promise',
      fn: function get () {
        return Promise.resolve(this._runtime.getCopyOfCredential())
      }
    },
  }
)
複製程式碼

可以看到,與定義事件的方式一樣,只需要在 Descriptor 的原型鏈中,增加對應的物件,然後設定型別(type)為 method 即可,然後在 fn 中實現函式。

module.exports = activity => {
  activity.get().then(
    (data) => console.log('credentialse is', data),
    (err) => console.error('something went wrong', err))
}
複製程式碼

這樣除了 API 定義可以統一起來了,也能比較方便地基於 JSDoc 生成統一的 API Reference 給開發者,使得整個 API 的修改能做到簡單易讀、門檻低和修改成本低等。

API Translator

那麼在 YodaOS 中,又是如何將上述的 Descriptor 生成為開發者直接使用的介面的呢?下面就為大家介紹我們引入的 Translator。

Translator 是按照我們支援的應用型別對應的,因此對於 lightapp 和 extapp 來說,我們也分為兩個 translator:

本文並不具體展開每個 translator 的工作原理,但會做一些簡單的流程介紹。以 translator-ipc 為例:

module.exports.translate = translate
function translate (descriptor) {
  if (typeof process.send !== 'function') {
    throw new Error('IpcTranslator must work in child process.')
  }
  var activity = PropertyDescriptions.namespace(null, descriptor, null, null)
  listenIpc()
  return activity
}
複製程式碼

每個 translator 提供一個函式,即 translate(descriptor)。它接受一個 descriptor 物件,然後會遍歷原型鏈中的物件,並且分別按照 namespace、event 和 method 去生成一個叫 activity 的物件,最後將這個物件返回給開發者。

當開發者在使用某個 API 時,activity 物件會按照 translator 預先生成(約定)好的邏輯呼叫到服務端(Vui-daemon),最後再通過 Promise 返回撥用後的結果,從而完成一次介面呼叫。

後記

本文簡單介紹了 YodaOS 在 API 設計過程中,如何利用 DSL,解決 YodaOS API 在多種應用形態保持一致性。以此,我們希望拋磚引玉:

  • 幫助讀者更好地瞭解 YodaOS API 的生成過程
  • 幫助讀者瞭解到 DSL,也能將這種思路應用在自己的專案中

如有更多問題,歡迎評論,或者直接在 GitHub 上給我們提問題:github.com/yodaos-proj…

參考

相關文章