在 Node.js 社區(qū)中,其實不乏通過 Markdown 生成 RESTful API 的框架冲九,按照一定的格式約定好 API 所需要的數(shù)據(jù),然后再通過解析 Markdown 文檔跟束,將這些關鍵數(shù)據(jù)提取出來莺奸,最后生成數(shù)據(jù)庫模型和 HTTPS 服務。
YodaOS 作為一個前端操作系統(tǒng)冀宴,同樣使用了類似的技術灭贷。YodaOS 中的應用分為:lightapp 和 extapp,前者是集成在語音交互運行時(Vui-daemon)進程內(nèi)部的輕應用略贮,它主要是用于一個交互簡單甚疟,需要快速響應的場景,比如音量控制逃延、系統(tǒng)控制等览妖。后者作為一個獨立的進程,通過 Child Process 與主進程通訊真友,使用場景主要是音樂黄痪、游戲、電話等需要長時期使用的應用盔然。
為什么要有輕應用桅打?輕應用更像是一個腳本,每當用戶一次進行一次交互愈案,只需要從預先加載的腳本中調(diào)用定義在對應腳本的函數(shù)即可完成一次響應挺尾,往往這類應用交互比較簡單,如果為此要創(chuàng)建在每次交互的過程中進行一次 ipc 甚至 fork 時站绪,無論對性能還是內(nèi)存來說遭铺,都是比較浪費的。
在設計之初恢准,我們期望對于開發(fā)者來說魂挂,并不需要針對不同類型的應用,只需要在 package.json 中修改類型即可馁筐,YodaOS API 應當保持完全一致涂召。這樣的話,我們則面對一個問題敏沉,即使是能做到高度抽象果正,也需要在每次新增一個接口時炎码,修改兩處代碼,這其實是有違我們的設計初衷的秋泳。
API Descriptor
為此潦闲,我們引入了 API Descriptor 的概念:https://github.com/yodaos-project/yodart/blob/master/runtime/lib/descriptor/activity-descriptor.js∑戎澹可以把它看作是用 JavaScript 寫的 DSL歉闰,它用于描述每個 YodaOS API,包括命名空間舍杜、事件新娜、方法等定義。系統(tǒng)在初始化時既绩,會加載所有 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 中的幾個事件:active饲握、ready 和 create私杜。因此,在任何應用中都可以這樣寫:
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:functionget(){returnPromise.resolve(this._runtime.getCopyOfCredential())}},})
可以看到救欧,與定義事件的方式一樣衰粹,只需要在 Descriptor 的原型鏈中,增加對應的對象笆怠,然后設置類型(type)為 method 即可铝耻,然后在 fn 中實現(xiàn)函數(shù)。
module.exports=activity=>{activity.get().then((data)=>console.log('credentialse is',data),(err)=>console.error('something went wrong',err))}
這樣除了 API 定義可以統(tǒng)一起來了蹬刷,也能比較方便地基于 JSDoc 生成統(tǒng)一的 API Reference 給開發(fā)者瓢捉,使得整個 API 的修改能做到簡單易讀、門檻低和修改成本低等办成。
API Translator
那么在 YodaOS 中泡态,又是如何將上述的 Descriptor 生成為開發(fā)者直接使用的接口的呢?下面就為大家介紹我們引入的 Translator迂卢。
Translator 是按照我們支持的應用類型對應的某弦,因此對于 lightapp 和 extapp 來說,我們也分為兩個 translator:
進程內(nèi)的?https://github.com/yodaos-project/yodart/blob/master/runtime/client/translator-in-process.js
進程間的?https://github.com/yodaos-project/yodart/blob/master/runtime/client/translator-ipc.js
本文并不具體展開每個 translator 的工作原理而克,但會做一些簡單的流程介紹靶壮。以 translator-ipc 為例:
module.exports.translate=translatefunctiontranslate(descriptor){if(typeofprocess.send!=='function'){thrownewError('IpcTranslator must work in child process.')}varactivity=PropertyDescriptions.namespace(null,descriptor,null,null)listenIpc()returnactivity}
每個 translator 提供一個函數(shù),即 translate(descriptor)员萍。它接受一個 descriptor 對象亮钦,然后會遍歷原型鏈中的對象,并且分別按照 namespace充活、event 和 method 去生成一個叫 activity 的對象蜂莉,最后將這個對象返回給開發(fā)者。
當開發(fā)者在使用某個 API 時混卵,activity 對象會按照 translator 預先生成(約定)好的邏輯調(diào)用到服務端(Vui-daemon)映穗,最后再通過 Promise 返回調(diào)用后的結果,從而完成一次接口調(diào)用幕随。
后記
本文簡單介紹了 YodaOS 在 API 設計過程中蚁滋,如何利用 DSL,解決 YodaOS API 在多種應用形態(tài)保持一致性赘淮。以此辕录,我們希望拋磚引玉:
幫助讀者更好地了解 YodaOS API 的生成過程
幫助讀者了解到 DSL,也能將這種思路應用在自己的項目中
如有更多問題梢卸,歡迎評論走诞,或者直接在 GitHub 上給我們提問題:Build software better, together
參考
D-Bus introspection:Introspection - Using of D-Bus
YodaOS:YODAOS Project