YodaOS 中是如何生成 API 的?

489 阅读4分钟

在 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…

参考