几个月前,我的长期自由软件合作伙伴 Don Marti 联系我,谈论了一个关于 WebExtension 的想法。WebExtensions 是一个非常酷的浏览器扩展新标准,Mozilla 和 Chrome 团队(以及 Opera、Edge 和许多其他主要浏览器)正在共同开发。WebExtensions API 允许您使用与实现任何其他网站相同的 JavaScript 和 HTML 方法编写插件。
Don 的想法 基本上是使用新的 WebExtensions API 构建一个文本分析工具包。此工具包允许您监控各种浏览器活动和资源(历史记录、书签等),然后让您使用文本分析模块来发现您自己浏览历史记录中的模式。这个想法是扭转广告商对我们习以为常的日常浏览活动所进行的复杂分析。大型公司正在使用先进的技术来模拟用户行为并控制他们收到的内容,以便操纵诸如用户在系统上花费的时间或他们看到的广告等结果。如果我们为用户提供使用其自身浏览数据进行此操作的工具,他们将更有机会了解自己的行为,并更清楚地认识到外部系统何时试图操纵他们。
另一个主要目标是提供一个使用新 WebExtensions API 的有良好文档记录的示例。我阅读的关于 WebExtensions 的内容越多,我越意识到它们代表了将 Web 浏览智能“推向边缘”的改变游戏规则。可以使用 WebExtensions 完成各种分析和自动化,从而有可能让这些工具在任何流行的现代 Web 浏览器上使用。我唯一看到的缺失之处是缺乏一种围绕这些 Web 内容分析“配方”进行轻松协作的方法。我建议我们创建一个 WordPress 插件,该插件将提供一个用于共享分类的 RESTful 接口,并且“FilterBubbler”的基本计划由此诞生。
我们的初始原型是一个概念验证,它使用了极其基本的 HTML 弹出窗口和 贝叶斯分类器。此版本证明我们可以根据手动加载的语料库提供有用的网页内容分类,但很明显,我们需要额外的工具才能获得“消费者”的感觉。在我们可以开始添加重要的功能(如远程配方服务器、分类显示和配置屏幕)之前,我们显然需要对我们的基础架构做出一些决定。在本文中,我将介绍我们为提供现代 UI 环境所做的努力以及在使用 WebExtensions 时带来的挑战。
React/Redux
当 Facebook 于 2013 年发布该工具作为自由软件时,React 框架 及其相关的 Flux 实现席卷了 HTML UI 世界。React 最初于 2011 年作为 Facebook 本身新闻提要显示基础结构的一部分部署。从那时起,该库已在 Instagram、Netflix、AirBnB 和许多其他流行服务中得到使用。该工具围绕一种称为 Flux 的策略展开,该策略严格定义了应用程序中更新状态的方式。
Flux 是一种策略,而不是实际的实现,并且有许多库提供了其功能。当今最流行的库之一是 Redux。Redux 的核心价值是应用程序状态的简化通用视图。因为应用程序只有一个状态,所以一系列动作事件产生的行为是完全确定性和可预测的。这使得您的应用程序更容易推理、测试和调试。本文档无法全面讨论 React 和 Redux 背后的概念,因此如果您刚刚开始,我建议您阅读 Redux 入门资料 或查看 Dan Ambramov 在 Egghead 上的 优秀的入门课程。
与 WebExtensions 集成
深入研究 WebExtensions 框架,遇到的第一个障碍之一是 UI 弹出窗口和配置页面上下文与后台上下文是分开的。每次打开和关闭 UI 显示时都会重新创建 UI 上下文的 state。UI 上下文和后台脚本上下文之间的通信是通过消息传递架构实现的。
FilterBubbler 扩展的 state 将存储在后台上下文中,但我们需要将该 state 绑定到弹出窗口和配置页面上下文中的 UI 元素。Alexander Ivantsov 的 Redux-WebExt 项目 为此问题提供了一种解决方案。他的工具在 UI 和后台页面之间提供了一个带有代理的抽象层。代理使 UI 似乎可以直接访问 Redux store,但实际上它将操作转发到后台上下文,然后将 reducer 生成的结果 state 修改发送回 UI 上下文。
操作映射
我花了一些时间才让 Redux-WebExt 桥接工作起来。在 UI 上下文中运行的 React 组件认为它们正在与 Redux store 交谈;事实上,这是一个与后台上下文交换消息的 facade。您认为正要发送到 reducer 的操作对象实际上被序列化为消息,发送到后台上下文,然后解包并传递到 store。一旦 reducer 完成其 state 修改,生成的 state 就会被打包并发送回代理,以便它可以更新 UI 对等体的 state。
Redux-WebExt 在此过程的中间放置了一个映射表,允许您修改来自前端的操作事件如何传递到 store。在某些情况下(例如异步操作),您确实需要此映射来分离无法序列化为消息对象的操作(如回调函数)。
在某些情况下,这可能是一个仅复制来自 UI 操作事件的数据的直接映射,例如 FilterBubbler 的 store.js 中的此映射
actions[formActionTypes.TOUCH] = (data) => {
return { type: formActionTypes.TOUCH, ...data };
}
或者,您可能需要将该 UI 操作映射到完全不同的内容,例如此映射仅调用后台 store 中可用的异步函数
actions[UI_REQUEST_ACTIVE_URL] = requestActiveUrl;
简而言之,请注意映射器!我花了几个小时才弄清楚它的用途。如果您想像我们一样在扩展中使用 React/Redux,那么理解这一点至关重要。
这种安排使得可以使用标准的 React/Redux 工具,而只需进行最少的更改和配置。现有的用于表单处理和其他主要 UI 任务的复杂库可以插入 WebExtension 环境,而无需了解底层基于消息的连接。我们已经集成的其中一个示例工具是 Redux Form,它提供了一个完整的系统来管理表单输入,并提供验证和其他您期望在现代开发工作中使用的服务。
确定我们可以使用主要的 UI 工具包而无需从头开始后,我们的下一个关注点是如何让界面看起来更好。Google 的 Material Design 是一种流行的视觉外观标准,React 平台拥有流行的 Material UI,它将 Google 标准实现为一组 React/Redux 组件。这使我们能够创建外观良好的 UI 弹出窗口和配置屏幕,而无需开发新的 UI 工具包。
获取 thunk
我们需要执行的一些操作是基于回调的,这使得它们成为异步操作。在 React/Redux 模型中,这会带来一些问题。操作生成器函数和 reducer 旨在在被调用时立即完成其工作。诸如在操作生成器中提供对 store 的访问并在回调中调用 dispatch 的解决方案被认为是一种 反模式。解决此问题的一种流行方法是 Redux-Thunk 中间件。将 Redux-Thunk 添加到您的应用程序非常简单,您只需在创建 store 时将其传递进去即可。
import thunk from 'redux-thunk'
const store = createStore(
reducers,
applyMiddleware(thunk))
安装 Redux-Thunk 后,您将获得一种新的操作生成器样式,其中您向 store 返回一个函数,该函数稍后将传递一个 dispatch 函数。这种控制反转允许 Redux 在对异步操作与队列中的其他操作进行排序方面保持控制权。例如,以下函数请求当前选项卡的 URL,然后调度请求以在 UI 中设置结果
export function requestActiveUrl() {
return dispatch => {
return browser.tabs.query({active: true}, tabs => {
return dispatch(activeUrl(tabs[0].url));
})
}
}
activeUrl() 函数看起来更典型
export function activeUrl(url) {
return {
type: ACTIVE_URL,
url
}
}
由于 WebExtensions 跨越多个不同的上下文并与异步消息传递通信,因此像 Redux-Thunk 这样的工具是必不可少的。
调试 WebExtensions
调试 WebExtensions 会带来一些新的挑战,这些挑战的工作方式因您使用的浏览器而异。无论您使用哪个浏览器,第一个主要区别在于扩展的后台上下文没有可见页面,必须专门选择进行调试。让我们逐步了解如何在 Firefox 和 Chrome 上开始此过程。
Firefox
在 Firefox 上,您可以通过在浏览器的 URL 字段中输入“about:debugging”来访问您的扩展。此页面将允许您使用“加载临时加载项”按钮加载未打包的扩展(或者您可以使用方便的 web-ext 工具,该工具允许您从命令行启动该过程)。在此处按下“调试”按钮将为您的扩展程序打开一个源调试器。对于 FilterBubbler,我们正在使用灵活的 webpack 构建工具来利用最新的 JavaScript 功能。Webpack 使用 babel 转译器 将新的 JavaScript 语言功能转换为与当前浏览器兼容的代码。这意味着浏览器运行的源代码与其原始代码有很大不同。请务必从调试器的首选项菜单中选择“显示原始源”选项,否则您的代码看起来会非常陌生!
选择后,您应该会看到更符合您期望的内容
从这里,您可以设置断点并执行所有其他您期望的操作。
Chrome
在 Chrome 上,基本上都是同一个想法,只是 UI 中有一些细微的差别。首先,您将转到主菜单,向下钻取到“更多工具”,然后选择“扩展程序”
这将带您到扩展程序列表页面。
“检查视图”部分将允许您为您的后台脚本打开调试器。
Firefox 调试器在一个地方显示所有后台和前台活动,而 Chrome 环境的工作方式略有不同。前台 UI 视图通过右键单击 WebExtension 的图标并选择“检查弹出窗口”选项来激活。
从那里开始,事情就非常符合您的预期了。如果您编写过 JavaScript 应用程序并使用过浏览器的内置功能,那么您应该会发现这些东西很熟悉。
分类材料
随着我们所有新基础设施到位以及一个可用的调试器,我们又回到了为 FilterBubbler 添加功能的正轨上。我们的原型目标之一是提供配方将在其中运行的 API。FilterBubbler 配方的主要成分是
一个或多个来源:来源在给定的 URL 上提供分类事件流。原型提供了一个简单的来源,该来源将在浏览器切换到特定页面时发出分类请求。其他可能的来源可能包括扫描社交网络或新闻提要以获取内容的来源、电子邮件消息流或用户浏览器历史记录的一部分。
一个分类器:分类器获取来自来源的内容,并返回至少一个带有强度值的分类标签。分类器可能会返回标签和强度值对的数组。如果数组为空,则系统假设分类器无法生成匹配项。
一个或多个语料库:FilterBubbler 语料库提供了一个带有标签和强度值的 URL 列表。标签和强度值用于训练分类器。
一个或多个接收器:接收器是分类事件的目标。原型包含一个简单的接收器,它将给定的分类器连接到 UI 小部件,该小部件在 WebExtensions 弹出窗口中显示分类。其他可能的接收器可以为某些分类标签匹配生成传出电子邮件,或者接收器可以将 URL 写入以分类标签命名的书签文件夹中。
也许图表会有帮助。以下配置可以告诉您您当前正在查看的页面是“很棒”还是“愚蠢”!
传递知识
这些安排的配置称为“配方”,可以加载到您的本地配置中。配方使用简单的 JSON 格式定义,如下所示
{
“recipe-version”: “0.1”,
“name”: “My Recipe”,
“classifier”: “naive-bayes”,
“corpora”: [
“http://mywpblog.com/filterbubbler/v1/corpora/fake-news”,
“http://mywpblog.com/filterbubbler/v1/corpora/ufos”,
“http://mywpblog.com/filterbubbler/v1/corpora/food-blog”
],
“source”: [ “default” ],
“sink”: [ “default” ]
}
以上简单的演示代码片段可以帮助用户区分假新闻、UFO目击事件和美食博客(在某些城市,这比你想象的更成问题)。目前,分类器、来源和接收器必须是提供的实现之一,并且在初始原型中不能动态加载。在接下来的文章中,我们将扩展此功能,并描述这些活动在 WebExtensions 环境中带来的挑战。
参考文献
GitHub 上的 FilterBubbler: https://github.com/filterbubbler/filterbubbler-web-ext
基于 React 的 Material UI: http://www.material-ui.com/
Redux WebExt: https://github.com/ivantsov/redux-webext
Redux Form: http://redux-form.com/6.7.0/
Mozilla 浏览器 polyfill: https://github.com/mozilla/webextension-polyfill
关于 Ean Schuessler
我帮助企业使用自由软件创建美观而强大的解决方案。我是 Debian 项目的长期参与者,并参与了 Debian 社会契约的创建,该契约启发了开源定义。我曾担任软件公共利益组织的主席,该组织持有财产并为 Debian 和其他自由软件开发工作提供服务。我积极参与基于自由软件的 Java 开发。我相信开源 ERP 的前景,并且是 Apache OFBiz 项目的长期参与者。我在各种会议上发表过演讲,包括 JavaOne、Apachecon、OSCON 和巴西的 FISL。最近,我访问了美国西南部各地的 Java 用户组,讨论 WebExtensions 和同构应用程序。