Fresh to chrome extension

这篇笔记涉及的项目叫做Haruka,是我最近在写的一个玩具,由于日后面向的是公司内部的使用(如果被使用的话……orz),所以不会开源。不过中间chrome插件的部分个人感觉相当有趣,单独抽出来做一个简化版本用来讨论也挺好的。

这个简化版本会做这种事: 对一段鼠标勾选的内容点右键,会出现一个标记按钮,点击后这段内容会被记录下来。在某个页面点击插件图标,会列出在这个页面曾经标记过的内容。

实现很简单,所以不打算在文章中列出一坨一坨的代码,反正你们也不会看,主要想写的是思路和一些坑。另外,虽然本篇叫做fresh,不过如果您真的没写过js的话,应该会有些难度才对菊苣退散!

项目的名字来源于樱Trick窝是脑残粉!,与春香阁下无关。

manifest.json

所有的chrome插件都是由这个文件开始的(manifest文件涉及的项请参阅这个),来看一下我们关心什么。

mainfest_version,name,description,version,这几项您看看就好了。

permissions: 这一项是权限。所谓权限,是指可以调用的chrome接口。比如 tabscookiescontextMenus……这些接口,都是高权限的接口,必须指定。指定了字段之后,就可以在popup以及background中去调用这些接口了。

content_scripts: 这是需要注入到页面的js文件,只有注入了页面,才能对页面生效。

background: background通常不会显示出来,它的生命周期和浏览器生命周期一样,您可以类比为一个守护进程。很多时候,其实我们并不需要background。说起来,刚开始瞎折腾的时候,我以为只有background才能访问高权限接口来着,其实不是。那么,什么时候background才是必须的呢?看官方文档中的这句话 — a single long-running script to manage some task or state。不过我的话,习惯于把实现放在background,而仅仅在popup中留一个接口,后面会说到,这种方式有坑……另外,新版文档推荐用event page去代替这玩意,我是没有研究过啦。

browser_action: 用来指定弹出页面的图标和页面内容。

获取标记内容与右键菜单

这应该是最简单的一步,因为直接调用chrome的接口就成。

chrome.contextMenus.create 这个接口可以在您右键的菜单项中增加一项。

var mark = chrome.contextMenus.create({
    "title":"标记它!",
    "contexts":["selection"],
    "onclick":markHandler
});

不过您应该注意到了,contextMenus是高权限接口,我们不能写在content_script里面,事实上,它只能被写在background里面。

被标记的内容可以通过markHandler这个回调函数的参数得到.

function markHandler(info,tab){
     var sText = info.selectionText;
     alert(sText);
}

持久化内容

这部分应该是我时间花最久的部分,主要是弯路走太多……

说说我最开始是怎么考虑这个问题的。作为一个js方面毫无经验的新手,对于持久化这部分,理所当然会使用文件作为储存的介质。而事实上,最开始我甚至不知道html5提供的File API(即使知道也不行,后面也会说),那时候我正在看犀牛书,犀牛书的第20章谈到了客户端储存。我看了看书中谈到的方法,决定把每个页面标记的内容写到对应页面的cookie中去。一方面,cookie本质上和文件很相似,另一方面,cookie支持的大小足足有4k,对于任意一个页面中的标记,大体上容量是足够的。另外,犀牛书上给出了一个cookieStorage的实现,相当鼓舞人心。

虽然上述思路看上去简明易懂,但是实现起来还是很麻烦的,最关键的问题是跨域。就cookie而言,是不能跨域访问的,但是现在的问题是,我需要在popup中把当前页面cookie中储存的数据解析后显示,这就涉及了页面间的通信问题。

并且,虽然和本次的项目无关,但是当时我是打算任意一个页面都可以读取其他页面的标记的,所以还有一个汇总的问题。

chrome.cookies.getAll可以获取到一个tab页中所有的cookie,然后,只需要获取需要的cookie就好。

如果需要汇总的话,更麻烦。当时的做法是我利用localStorage,在background中重头实现了一个用于储存的麻烦玩意(算类么……)。这东西封装了解析,序列化,储存,获取数据等方法。

但是太麻烦了。

由于页面通信机制的存在,上面的操作都不是那么方便,大多数时候,都需要通过发送消息,接收消息这样的方式进行参数的传递,代码相当不可靠并且难以维护。

基于上面的方式,我实现了第一版代码,然后在部门做了讲解和演示而且居然能跑。

cookie不好用,那么html5里面提供的File API会好用么?结论是不行,插件里面file api调用是很受限的(That is the only way to download files to disk and it is limited)。

那么怎么办呢?

我重新审视了代码,发现所有的复杂度都是由于需要在页面中储存数据以及在页面间传递数据造成的,而如果使用ajax,把数据的储存和解析放在后端的服务器上,所有的操作看作对对应url的请求就好。这样,数据和逻辑就会相当清晰。

事实上也是如此。我用flask在本地搭了一个服务器用于处理请求,background中实现了send和get方法,很少几行就搞定了。

js部分

function send(url,content){
    var request = new XMLHttpRequest();
    request.open("POST","http://localhost:9978/"+url);
    request.setRequestHeader("Content-Type","text/plain;charset=UTF-8");
    request.send(content);
}

function get(url,callback){
    var request = new XMLHttpRequest();
    request.open("GET",url);
    request.onreadystatechange = function(){
        if(request.readyState === 4 && request.status === 200){
            var type = request.getResponseHeader("Content-Type");
            if(type.match(/^text/))
                callback(request.responseText);
        }
    };
    request.send(null);
}

python部分

@app.route("/save",methods=['POST'])
def save():
    json_str = request.data
    localStorage.store(json_str)
    return "success"

@app.route("/get")
def get():
    domin = request.args.get("href")
    return localStorage.query(domin)

python中的localStorage是我自己实现的一套很简单的用于储存的玩意,单纯只是不想使用数据库而已……思路是使用python的字典作为储存介质,key为各页面的域名,value为该页面中标记过的内容。由于完全是字符串,所以我使用marshal进行序列化 —- marshal是所有序列化里面速度最快的。localstorage会每5分钟写入一次文件,为了不阻塞当前进程,采取的方法是另开一个线程负责写文件这个操作。

localStorage稍微长了点,所以我放在gist里面去了,有兴趣请戳这里

页面间的通信

前面也多次提到了这点,虽然很麻烦不过还是很简单的。顺便一说,我认为js最强大也是最恶心的就是回调,应该是我太弱了……

为什么需要页面间通信?

因为我们的插件,涉及了三个页面,分别是popup,background以及当前您正在浏览的页面。

其中比较特殊的,popup中可以通过chrome.extension.getBackgroundPage()直接获取到background页面中的所有全局变量和函数。

但是,没有类似的函数可以让popup取得当前浏览页面的全局变量和函数。比如说,我可以在当前页面中很容易的获取到页面的href,并储存起来var href = window.location.href,但是,popup不能直接访问到这个变量。

事实上,要获取到href这个变量,我们只能通过发送事件和接受事件这样的机制去获取。

基本流程是这样: 发送方发送一个消息 -> 接受方的handler处理后返回结果 -> 发送方的回调函数获取到值,并使用。

很简单是么?

但是不对,请考虑一下,发送方怎么知道应该发送到哪里去呢?

发送方需要知道接收方所在页面的tab id。

怎么获取tab id?chrome.tabs.query可以获取到所有打开的tabs的id,其中,tabs[0]就是当前正在浏览的tab。

自此,好像所有的问题都解决了。下面是一个简单的例子:

background

function giveMeUrl(tab){
    chrome.tabs.sendMessage(tab.id,"giveMeUrl",function(data){
        var url = data.url;
        alert(url);
    });
}

content_script

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if(request == "giveMeUrl"){
        sendResponse({url:window.location.href});
    }
});

很完美不是么?

看,我们的background里面发送请求,似乎只需要在popup里面调用giveMeUrl,稍微改改alert(url) —- 在giveMeUrl里面添加一个href变量,在回调函数中赋值,最后添加一个return href,唔,这样就能返回值了吧?

这种方案?

function giveMeUrl(tab){
    var href = ""
    chrome.tabs.sendMessage(tab.id,"giveMeUrl",function(data){
        var url = data.url;
        href = url;
    });
    return href;
}

不对,js的实现是异步的,上面的函数,会总是返回"“,因为回调的执行是在这整个函数执行完了之后。

请注意,异步编程中,return永远是靠不住的。

为了获得这个返回值,我做过一些现在看起来很傻的尝试,比如,在return前面加while(!href)。很明显,我失败了。由于js是单进程模型,上面的while语句会直接阻塞住整个页面的进程,所以压根不会有任何结果。

那么怎么办。

其实推荐的就是直接在popup里面去写发送的逻辑,不过非要在background中去写的话,那就应该在回调函数中给popup发消息,附带上需要传递的参数 —- 这不是没事找事嘛!

ok,说完了上面这些个东西,基本上,主要的技术点就没什么了,感觉很简单对不对?

阅读到这里的您辛苦了。

以上。

准备喝红茶打炉石的AS。