分类
extension

使用 Chrome extension 操作 CodeMirror

我们知道,浏览器扩展(Chrome extension)可以通过 content script 在目标网页里执行代码。它和目标页面的 JS 处于不同的沙箱,但是共用一套 DOM。所以,content script 无法访问目标页面的 JS 变量,但是可以修改目标页面的 DOM、侦听 DOM 上的事件、或者触发 DOM 事件。

在这种情况下,要操作 CodeMirror 的时候会遇到一些问题。为了性能考虑,CodeMirror 不会把所有代码都渲染出来,而是会按需渲染。比如代码总共 1k 行,但是当前窗口里只能看到 40 行,那么 CodeMirror 就会渲染当前的 40 行,和前后大概各 20 行(方便快速滚屏),总计约 80 行。

换言之,如果无法接触到目标页面的 JS 环境,单纯从 DOM 里无法获取到全部代码,也很难修改 CM 里的代码。于是我们就需要一些特别的技巧——这些技巧在操作其它类似的类库时也会用到,所以写篇博客分享下。

解决方案:向目标网页注入 <script>

此时,我们可以通过创建 <script> 的方式向目标网页注入一段 JS。这个 JS 的执行环境也是目标网页,所以可以访问到目标网页 环境下的 JS 变量。

CodeMirror 会把实例添加到容器节点上,所以我们可以先找到容器节点,即 div.CodeMirror;然后访问它的 CodeMirror 属性找到 CM 实例;最后进行操作。

为了方便 content script 和 CodeMirror 交互,我们必须搭建一个桥梁,也就是下面这段代码:

const cms = document.getElementsByClassName('CodeMirror')
for (const dom of cms) {
  // 这个 textarea 被 CodeMirror 视图所取代,是隐藏的。我们利用它传递代码
  const textarea = dom.previousElementSibling;
  const cm = dom.CodeMirror;
  // 给 CodeMirror 添加清空功能,可以通过在 dom 上广播事件触发
  dom.addEventListener('clear', () => {
    cm.setValue('');
  });
  // `changes` 是 CodeMirror 的事件,在代码改变时触发,这里我们通过侦听它,并且将代码同步到 `textarea` 的方式,方便 content script 访问
  cm.on('changes', cm => {
    if (texture.value === cm.getValue()) {
      return;
    }
    textarea.value = cm.getValue();
  });

  // 为初始化
  textarea.value = cm.getValue();
}

因为我执行 content script 的时机总在页面完成初始化之后,此时所有 CodeMirror 都已就位。如果你要在别的时间执行,可以做一些调整。

分类
js

诡异的Chrome插件事件机制

最近尝试开发Chrome插件,自然使用JQ来当基础。结果遇到一个问题:

我使用“程序注入(Programmatic injection)”的方式执行代码,试图实现自动填写表单的功能。因为目标网页的表单比较智能,前面的选项会影响后面选项的内容,所以必须在val(value)之后广播change事件来触发后面表单内容的填充。

结果失败了。直接在浏览器里使用控制台运行代码,没有问题,所以代码本身应该正确;确认需要的JS文件已经一一加载。于是我开始了漫长而挫折的调试之路。多次失败之后,我打开《英雄无敌三》换换心情……玩了一会儿游戏意外退出了,而我灵机一动,会不会是JQ广播事件的问题?在控制台调试没有问题,但是在content script里就不行,很可能是chrome把两段代码放在彼此隔离的环境中运行导致的。于是我放弃直接$(selector).change(),改用原生的dispatchEvent,结果,运行成功!

于是我猜,应该是JQ和Chrome的插件机制稍有冲突。可能以后做插件的话,还是Closure Library好些吧。

PS:写插件的好处是不用考虑兼容性,朝着Chrome写就行了。