Gloria 任务开发指南

有关 Gloria 的一切都是围绕着任务展开的, 每一个任务包含着它所需要执行的代码, 它们是 Gloria 最重要的部分. 这篇文章将完整地说明如何编写能够被 Gloria 执行的任务代码.

编程语言

JavaScript 是 Gloria 任务代码唯一支持的编程语言.

Gloria 作为一款 Chrome 扩展程序, 它本身就是用 JavaScript 开发的, 利用 JavaScript 作为动态语言的特性, 我们可以直接在 Gloria 里执行用户定义的 JavaScript 代码而不必遭受过多的性能损失.

除了性能上的优势, JavaScript 还足够流行, 它在 Stack Overflow 已经蝉联全球最受欢迎的开发技术多年, 是事实上的胶水语言, 这意味着大部分开发人员都应该已经接触过它, 假如你不了解 JavaScript, 或许是时候开始学习它了.

另外, 由于 Gloria 的任务代码需要访问的对象多为网络服务, 而网络服务在浏览器上必然要通过 JavaScript 以及其他 ECMAScript 方言运行, 使用 JavaScript 作为编程语言将有助于你利用访问的网站对象已有的代码, 同构是一个巨大的优势.

我想说的是: 没有任何一门编程语言, 在编写 Gloria 任务代码这件事上比 JavaScript 更适合.

从最简单的例子开始

Gloria 使用指南里我提供了一段可以被 Gloria 执行的代码, 我们就从解释这段代码开始说起.

commit({ message: `Hello, it is ${new Date()}.` })

这段代码首先创建了一个对象:

{
  message: `Hello, it is ${new Date()}.`
}

这个对象唯一的一个成员变量message, 被初始化为字符串'Hello, it is'加上当前的时间. 代码部分是通过 ECMAScript 6 的 template string 特性实现的, Gloria 在开发初期就没有考虑过在低版本的 Chrome 上运行, 所以在 Gloria 里你可以放下顾虑使用当前 Chrome 版本支持的新特性, 即便你想支持低版本的 V8 引擎所不支持的特性, 其中大部分也可以通过 Babel 等转译工具实现, 你是开发者, 一切应当受你控制.

创建完对象, 我们将这个对象作为参数调用了 commit 函数, 一切就结束了, Gloria 会定期弹出关于当前时间的通知.

状态无关的回调函数: commit

需要提前说明的是, 我们的任务代码是在一个基于 Web Worker 技术搭建的沙箱中执行, 这确保了多个任务代码会在不同的线程中独立执行, 各个线程不会互相影响, 同时也保证了 Gloria 自身的运行环境不会被任务代码污染.

由于 Web Worker 的特性, 当我们的任务代码想要返回点什么给 Gloria 时, 我们必须调用回调函数, 而这个回调函数就是 commit 函数.

commit 函数接收一个参数, 这个参数的类型可以是一个 Gloria Notification 对象, 也可以是一个由 Gloria Notification 对象组成的数组. 任务代码在执行过程中只能调用一次 commit 函数, 当该函数被调用, 其所在的 Web Worker 就会销毁自己, 调用 commit 就表示当前的任务代码执行完毕, 并将收集到的信息一次性返回给 Gloria 处理.

调用 commit 函数会将 Gloria Notification 返回给 Gloria, 但这并不意味着 Gloria 一定会将返回值以通知的形式呈现给用户. Gloria 的任务代码被刻意设定成状态无关的, 试想你要编写一个获取用户收件箱条目的任务, 你是要每一次返回所有的10个任务(这是该收件箱一次性能够展示内容的极限), 还是每次只返回其中新增的?

从直觉角度考虑, 返回新增的内容是正确的做法, 但在实践中我发现, 很多时候我们无法分辨由网络服务提供的内容新旧与否, 若是在任务代码里包含持久化, 或是管理状态的代码, 则会让代码的复杂度直线上升, 所以我决定让 Gloria 吸取函数式编程的思想, 让 Gloria 的任务代码保持简单——由 Gloria 决定哪些通知会被显示, 哪些应该被忽略, 这带来的直接影响便是为 Gloria 编写任务代码的开发者只需要完成用于获取信息的中间件代码, 剩下的一切都会由 Gloria 自动完成, 开发者无需关心状态.

决定是否推送消息的内部状态: stage

Gloria 内部决定是否显示一个通知的策略是非常简单的, 所有 commit 函数传来的 Gloria Notification 将被缓存进一个叫做 stage 的地方(你可能会觉得很熟悉, 是的, commit 和 stage 的命名就是抄自 Git). 在缓存进 stage 时, 程序将会对比 stage 里是否已经存在相似的 Gloria Notification 对象, 从而决定其是否应该被显示, 所以当 message 的内容和之前一样时, Gloria 就会认为这个 Gloria Notification 是已经存在过的旧消息, 不允显示.

当 commit 的参数处于不同类型时, Gloria 的行为会有少许变化, 在 Gloria 0.9.9 版本以前, 无论传入的是单个 Gloria Notification 对象, 还是由 Gloria Notification 组成的数组, 都会被当作"常规任务"处理, 在 Gloria 0.9.9 及之后的版本里, 新增了一种称作"观察任务"的任务类型, 传入单个 Gloria Notification 作为参数时会被作为"观察任务"处理.

注意, 在 Gloria 0.9.9 及之后的版本里, 一个任务应该明白自己的定位, 是作为返回值为数组的"常规任务", 还是作为返回值是单个 Gloria Notification 的"观察任务", 混合两种返回值会造成程序混乱.

常规任务

"常规任务"指的是 commit 参数为数组的任务, 在 Gloria 0.9.9 以前, 所有任务都会被视为常规任务, 单个 Gloria Notification 对象会被包装成只有一个对象的数组.

当你新建一个任务, 这个任务在第一次执行时会返回所有它能收集到的内容, 若是这些内容全部都被推送给用户, 将是一个非常恼人的事情. 所以 Gloria 的常规任务假设你在新建任务时, 之前的所有消息都是你已经读过的, 这样 Gloria 就只会推送新的消息, 而不会出现首次执行弹出一大堆消息的情况.

这个设计理念的具体实现形式, 便是 stage 对于第一次 commit 的结果会全部标记成已读状态, 不允许显示, 而当一个任务被创建时, 它的代码会被立刻执行一次, 这就是为什么当你创建任务时, 发现 Trigged 计数值已经变成了1, 却没有任何通知显示.

值得额外说明的是, 每一个"常规任务"的 stage 存在一个缓存数量上限, 当 stage 中缓存的 Gloria Notification 到达上限时, 它会删除掉旧的 Gloria Notification 为新的 Gloria Notification 腾出空间, 所以理论上同一个 Gloria Notification 是有可能被显示两次的, 请不要刻意造成这种状况. 由于这个上限值有可能在之后的版本里发生改变, 所以你也无需关注这个上限的具体值, 只要保证每一次的 Gloria Notification 不要更新太多新消息就行了, 如果一个消息源的更新频率高到突破了这个上限, 它很可能并不适合用 Gloria 进行通知.

观察任务

"观察任务"是 Gloria 0.9.9 版本中加入的新任务类型, 用于解决需要观察目标变化的场景: 比如某网站钱包的余额变化, 股票的价格变动等等("常规任务"由于不会显示重复的内容, 所以不适用于此场景).

当一个任务的 commit 参数为单个 Gloria Notification 时, 该任务将被视作"观察任务". 在这种任务类型下, stage 将只会记录上一个 Gloria Notification, 在有新的 Gloria Notification 提交时, 会将上一个 Gloria Notification 与之对比, 决定是否显示新的通知.

关于比较 Gloria Notification 不同的更多技术细节

你可能需要先阅读鸭子类型: Gloria Notification, 才能理解这些技术细节.

Gloria 原先是根据 Gloria Notification 的 MD5(title + message) 作为参考来判断其是否在 stage 中重复的. 由于 title 和 message 都是在弹出的通知中用户可见的属性, 不便于加入一些用于判定的变量, 所以从 Gloria 0.9.0 开始, 这个计算方法变成了 MD5(title + message + id), 你可以通过在 Gloria Notification 里加入 id 变量来避免被当作重复的值.

鸭子类型: Gloria Notification

一个 Gloria Notification 对象或是一个由 Gloria Notification 构成的数组是 commit 函数的唯一参数, 它将决定你的通知显示时的形式, 对任务代码而言非常重要.

实际上, Gloria Notification是 Chrome NotificationOptions 类型的定制版本, 其大多数成员与NotificationOptions是一致的, Gloria Notification 放宽了 NotificationOptions 对于其成员的一些强制要求, 但也有一部分 NotificationOptions 的成员在 Gloria Notification 中被禁止使用.

一个标准的 Gloria Notification 的结构是这样的:

{
  title: String
  message: String
  iconUrl: String
  imageUrl: String
  url: String
}

这里的每一个值都不是必要的, 它们都有着自己的默认值, 所以实际上它们的初始状态是这样的:

{
  title: '',
  message: '',
  iconUrl: 'assets/images/icon-128.png', // Gloria Icon
  imageUrl: undefined,
  url: undefined
}

你返回的值只需要包含其中的一部分, Gloria 会将你返回的对象和这个初始对象合并, 最后得出一个符合 NotificationOptions 标准的对象.

Gloria Notification 的成员将怎样决定通知的显示效果, 让我们来看一个实例:

它返回的 Gloria Notification 是这样的:

{
  title: "王老菊带你石油大亨02:大股东!",
  message: "市长同志跟我讲话, 说「都决定啦, 你来当大股东」, 我说另请高明吧. 我实在我也不是谦虚, 我一个挖石油的煤老板, 怎么到市里来了呢?但是呢, 市长同志讲「大家已经研究决定了」, 所以, 我就被坑了一万多块钱, 当下了这个股东. ",
  iconUrl: "http://i1.hdslb.com/bfs/face/b55e96895608e03c3435d018b708a705ccc2bda4.gif",
  imageUrl: "http://i0.hdslb.com/bfs/archive/6a41f5a2d59513b513f944d876c83089fc3d9cf1.jpg_320x200.jpg",
  url: "http://www.bilibili.com/video/av5750678/"
}

title, message 会决定标题和信息的文字内容, 要注意的是, message 的字号是比 title 要小一号的, 而且当 message 显示不下时, 会以省略号的形式结尾.

iconUrl 会决定通知左上角的图标, 这是 Chrome 强制规定的, 如果你的 Gloria Notification 不包含 iconUrl, 则会被显示成 Gloria 自己的图标.

imageUrl 会决定通知下方的大尺寸图片, 很多消息源里是找不到这样的图片的, 如果不包含 imageUrl, 显示图片的那一块区域会被取消. 如果你使用过 Chrome 的 NotificationOptions, 会知道显示带有图片的通知必须要修改通知的 type 参数为 "image", 我认为这相当多余, 所以在 Gloria 里, Gloria Notification 是鸭子类型的, 如果包含 imageUrl, type 将被自动修改成 "image", 这些事情你都不需要在 Gloria 里操心.

url 将决定点击这个通知后打开什么页面, 如果不包含 url, 通知也仅仅是变成点击不会打开页面而已, 不过我觉得大多数消息源还是需要一个 url 的. 从 Gloria 0.9.0 版本开始, 如果通知的 url 已经在 Chrome 里打开, 则会直接转到已经打开的标签页, 而不是创建一个新的标签页.

从 Gloria 0.9.0 开始, 你可以通过给 Gloria Notification 添加 id 属性来标志一个 Gloria Notification 的唯一性, id 属性会和 title、 message 一样用于去重计算, 详见stage- 更多技术细节.

另外, Gloria 会自动设置 NotificationOptions 的 contextMessage 值为发出该通知的任务名称和推送时间的信息.

由于 Chrome 的限制, 如果你的 title 内容过长, 则会变成两行, 使得 message 彻底消失:

当 title 的值为空字符串时, Chrome Notification 会去掉标题, 这样 message 就可以显示多行, 由于 message 的字号会略小于 title, 所以能够显示更多文字, 对于文本量较多的消息, 开发者应该只使用 message 进行显示:

现在你知道了如何创建一个通知, 如何将通知返回给 Gloria, 以及 Gloria 如何对待这些通知, 但也许你关注的不是这些, 而是该怎样访问 url, 取得你要的数据.

在创建网络请求这件事上, Gloria 用 fetch 取代了 XMLHttpRequest, Chrome 内置了 fetch 函数, 你可以在 Fetch API - Web API 接口 | MDNgithub/fetch: A window.fetch JavaScript polyfill. 找到 fetch 的使用方法.

Gloria 任务代码运行环境下的 fetch 与原本的 fetch 稍有不同, 所有通过 fetch 创建的请求, 都会自动加上目标 url 的 Cookie, 这样就能利用当前 Chrome 在目标网站上的登录状态, 轻松的获取到你想要的数据, 使得那些略显私人的通知提醒也成为了可能. 在 Gloria 运行环境下的 XMLHttpRequest 并没有被加上这个特性, 所以如果你不想要自带 Cookie, 用 XMLHttpRequest 就可以了, 我认为这个需求是相当小的.

这是一个用 fetch 获取当前登录 bilibili 的用户的订阅内容的任务代码, 代码非常简单, 供你参考:

fetch('http://api.bilibili.com/x/feed/pull?ps=10&type=0&pn=1')
.then(res => res.json())
.then(json => {
  let notifications = json.data.feeds.map(feed => {
    return {
      title: feed.addition.title
    , message: feed.addition.description
    , iconUrl: feed.source.avatar
    , imageUrl: feed.addition.pic
    , url: feed.addition.link
    }
  })
  commit(notifications)
})

当然, Chrome 原生的 fetch 目前也还存在少许问题, 比如 fetch 碍于 Promise 的设计无法取消请求, 受制于 w3c 标准使得所有自定义 header 的名称将在请求时变成小写(原因是 w3c 标准规定 header 名称大小写无关, 很多服务器做了错误的实现使得这一点变成相关的了, 进入 http2 时代后应该全部都会转为小写了). 我的选择是不去妥协 XHR, 也不去引入 SuperAgent 这类第三方库, 让 Chrome 和网站们自己解决这些问题, 作为任务代码的编写者, 你也可以尝试自己通过其他途径替代这些目前还存在问题的实现.

异步载入外部脚本 importScripts

熟悉 Web Worker 的开发者可能知道, Web Worker 的运行环境内置了一个 importScripts 函数用于同步载入外部脚本, 这意味着很强的扩展性, 但由于 Web Worker 的运行环境里是没有 window 这个 global 对象的, 很多外部脚本并不是针对这样的环境编写的, 于是强大的能力无从发挥, 有着强大的库却无法使用, 着实是一件令人难受的事情.

出于兼容性和主动缓存方面的考虑, 这个函数在 Gloria 的任务代码运行环境里被改造成了异步的, 而且会制造一个虚拟的 window 对象, 以便一些外部脚本可以正常执行. 调用这个异步的 importScripts 会返回一个 Promise 对象, Promise.then的回调函数所接收到的第一个参数就是载入的外部脚本的返回值.

我强烈推荐开发者使用 webpack 打包自己需要的外部脚本, 在使用 webpack 的情况下, 你甚至可以使用一些本来为 Node.js 环境编写的模块, 比如 cheeriojs/cheerio. 要知道在 Web Worker 下是没有 DOM 的, 所以当你要分析一个 HTML 网页时, 你会很需要一个像 cheerio 这样的模块.

从仓库 Clone 代码, 用npm install安装依赖, 用 webpack 打包代码:

webpack --target=web --entry=./index.js --output-filename=./cheerio-bundle.js --module-bind=json

生成的 cheerio-bundle.js 就是那个可以直接用在 importScripts 上的外部脚本, 接着你可以像这样使用它, 非常优雅:

Promise.all([
  importScripts('http://cdn.blackglory.me/cheerio-bundle.js')
, fetch('https://www.zhihu.com/noti7/stack/vote_thank?limit=10').then(res => res.json())
])
.then(([cheerio, { msg }]) => {
  let $ = cheerio.load(msg)
    , notifications = []
  $('.zm-noti7-content-item').each((i, el) => {
    let notification = {
      iconUrl: 'https://pic1.zhimg.com/2e33f063f1bd9221df967219167b5de0_m.jpg'
    , message: $(el).text().trim().replace(/\n/g, '')
    , url: ((base, href) => {
        if (!href.startsWith('http')) {
          if (href.startsWith('/')) {
            return `${base}${href}`
          } else {
            return `${base}/${href}`
          }
        }
        return href
      })('http://www.zhihu.com', $(el).find('a.question_link, a.post-link').attr('href'))
    }
    notifications.push(notification)
  })
  commit(notifications)
})

importScripts 会主动缓存你加载的外部脚本, 在第一次执行之后, 你的代码就可以以本地访问的速度载入该外部脚本.

注意: Gloria 的 importScripts 只支持单个参数, 与 Web Worker 的调用方式有所不同, 此处是我的设计失误, 日后可能会在新版本的 Gloria 代码里对其进行修改, 新版本将独立于旧版本代码, 所以不会产生兼容性问题, 请放心使用 importScripts 函数.

当然, 自己上传打包好的外部依赖是一件相当麻烦的事, 所以 Gloria 内置了一些常用的模块, 你可以通过importScripts('gloria-utils')加载这些内部模块, 并通过ES6的解构赋值特性, 从中方便的提取你想要的模块. 关于 gloria-utils 的详细说明, 在 gloria-utils 的 Github 页可以找到: gloria-utils: Chrome extension Gloria task utils.

如果你需要的模块在 gloria-utils 里没有提供, 又不想每次都手动上传一次打包好的模块 你可以通过我的另一个项目 webpack-online 建立一个简易的在线 webpack 打包服务. 关于 webpack-online 的详细说明, 在 webpack-online 的 Github 页可以找到: webpack-online: webpack online service.

运行时长

在 0.13.0 版本中增加了对任务代码运行时长的限制, 单个 Gloria 任务的执行时间最多为 1 分钟, 超时执行会被强制中断.

调试

Gloria 在"高级"面板提供了一些方便开发者调试的功能, 你可以在这里测试你的任务代码, 测试代码的执行结果不会经过 stage, 所以一旦有 notification 被 commit, 你可以直接看到通知效果. 如果你的任务代码有同步的错误(比如语法错误), 错误将会显示, 但 Gloria 的任务代码大多数是异步代码, 如果你的异步代码产生了错误, 那么只能在扩展程序的"背景页"看到错误输出.


现在你已经了解如何编写任务代码, 但在编写过程中可能还会有一些困惑, 我整理了一些任务代码编写时的常见问题, 希望对你有帮助.

results matching ""

    No results matching ""