原文

项目中需要从百度图片和谷歌图片批量抓取一系列关键词的图片,而且需要是大图资源,不能是缩略图。在后端通过http请求直接拉取内容抓取,遇到下面两个问题:

有的大图地址是在前端通过脚本生成的,拉取页面内容之后无法直接得到大图地址
翻页请求并不是简单的pageindex++,拿到下一页内容。抓取第一页后边的内容也需要分析翻页请求链接组装,以及返回的数据如何解析。
这两个问题导致通过后端爬取大图列表十分困难。于是我想起了以前玩过的杂技——浏览器插件。通过javascript控制浏览器打开网页,搜索关键词,页面渲染完毕之后拿到大图地址,第一页拿完之后让页面滚动到底部,继续加载图片,and so on!直到拿到足够数量的图片。做完这个小工具,想着总结一下经验,加深点印象,免得以后某一天有需要再来做的时候一脸懵逼,于是抽时间慢慢写下这边文章记录一下我对浏览器插件的认识。

什么是chrome浏览器插件
地址栏右侧那些icon就是一个个浏览器插件。点击插件图标可以弹出插件窗口。我所理解的chrome浏览器插件功能有三大块:

弹出一个窗口,让用户执行操作,或者显示信息
向网页中注入脚本文件,执行某些功能
调用chrome提供的native api,执行浏览器tab页开关、窗口开关、文件下载等操作
如上图腾讯电脑管家的插件,就是用来提供广告过滤功能的。它的工作原理应该就是给需要过滤的网址插入一段脚本,来把页面上的广告标签干掉。

chrome插件开发
文档地址:https://developer.chrome.com/extensions/overview

代码模块
chrome插件完全由javascript、html、css开发,和上面的插件功能相对应,代码也可以分为三大模块:

popup 弹出窗口代码集合。弹窗UI通过html+css开发,弹窗中也可以引用js脚本来控制交互操作。每个弹窗都相当于一个独立的tab页,运行在其中的js脚本拥有一个独立的上下文。
inject.js。注入网页文件的脚本。需要注意的是,注入的脚本上下文也是独立的,它可以操作目标网页DOM,但是并不在目标网页脚本的上下文中。
background.js。插件后台脚本,拥有独立的上下文,且此上下文是唯一的,无论浏览器打开多少个tab页,background.js的上下文都不会变化,除非关闭浏览器。
这三块代码之间的关系我画了个图方便理解:

图中,黑色的部分代表chrome原生部分,其他的每一个方块都拥有一个独立的javascript上下文。

manifest.json
chrome插件有一个比较重要的配置文件,manifest.json,用来指定各个模块的代码文件名、插件权限、插件图标、inject脚本插入时机等

{
  "name": "imagefetcher",
  "version": "0.0.1",
  "manifest_version": 2,
  "description": "抓取图片网站大图文件",
  "background": { "scripts": ["dist/background.js"] },
  "icons": { "16": "icon.jpg",
    "48": "icon.jpg",
    "128": "icon.jpg" },
  "permissions": [
    "tabs","downloads",
    "http://*.baidu.com/",
    "http://*.google.com.hk/"
  ],
  "browser_action": {
    "default_icon": "icon.jpg" ,
    "default_title": "抓取图片",
    "default_popup": "index.html"
  },
  "content_scripts":[{
    "run_at":"document_end",
    "matches":["<all_urls>"],
    "js":["lib/jquery-2.0.0.min.js", "dist/inject.js"]
  }]
}

background指定background代码文件路径;content_scripts指定inject的脚本列表,以及注入的条件、注入时机;browser_action中的default_popup指定popup弹出的html文件路径;permissions指定能访问的网页或chrome提供的一些功能的权限;icons指定插件图标。

模块间通信
按照上面的结构,很容易可以联想到各个模块的分工:popup模块 的代码负责显示弹窗,让用户输入关键词,下发开始抓取指令;显示抓取进度;下发下载指令。inject.js负责分析网页的DOM,拿到大图资源链接,并翻页,直到获取足够数量的图片。background.js负责汇总各个网页抓取的结果,并将结果显示到弹窗中。由于这些脚本拥有各自的执行上下文,并不能通过直接调用函数的方式来通信,所以我们需要通过chrome提供的方式来进行模块间的通信。

与background的通信
在popup中或者inject中发出消息给background接收。

chrome.runtime.sendMessage({action:ACTION.START_FETCH,data:xxxx});

sendMessage函数还可以接受一个回调函数,处理收到消息之后处理的返回结果。 rome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback)

在background.js中监听消息:

chrome.runtime.onMessage.addListener(function(request, sender, sendRequest){
    var data=request.data;
    var fetch;
    if(data.tab_id){
        fetch=window.FETCH_ITEMS.getByTabId(data.tab_id);
    }
    //开始抓取消息,读取抓取队列的第一个,开始抓取,抓取完成之后继续读
    if(request.action==ACTION.START_FETCH){
        __fetch_list.push(data);
        readFetchList();
    }
    else if(request.action==ACTION.FETCH_PROGTRESS){
        fetch.urls=data.urls;
    }
    //抓取完成,修改fetch_item状态,若存在弹窗,通知弹窗刷新视图
    else if(request.action==ACTION.FETCH_SUCCESS){
        fetch.status=DOWNLOAD_STATUS.SUCCESS;
        fetch.urls=data.urls;
    }
});

与inject的通信
要指挥inject的脚本执行一些操作,必须给inject发消息,而inject是注入网页中的,所以发消息第一步必须先获取tab页的tabid,然后将消息发给特定的tab页。在background中发出消息:

chrome.tabs.getSelected(function(tab){
    chrome.tabs.sendMessage(tab.id, data, function(response) {
        console.log(response);
    });
});

inject中接收消息:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    var data=request.data;
    //开始抓取
    if(request.action==ACTION.START_FETCH){
        var site=SITES.getSite(data.site);
        if(!site){
            sendResponse({err:1,message:"未实现此网页抓取"});
            return;
        }
        sendResponse({err:0});
        fetcher=require('./fetchors/'+data.site);
        fetcher(data).then(function(urls){
            data.urls=urls;
            chrome.runtime.sendMessage({action:ACTION.FETCH_SUCCESS, data:data}); //发送给background
        }).done()
    }
});

调试
不能调试还写什么代码! chrome插件的三大模块也是可以调试的,只不过都藏在各种犄角旮旯里边,下面扒一扒怎么分别给他们打断点。

background
打开chrome://extensions/点“检查视图”后边的链接,就可以打开控制台了,在source里边打上断点,调试走起

popup
在插件图标上右键——审查弹出内容,打开控制台,在source里边打上断点,调试走起

inject
按F12打开被注入的页面的控制台,点Sources,点右侧中间的Content Scripts,就可以看到这个页面被那些插件注入了脚本了,根据名称找到自己的脚本,打上断点,调试走起

使用vue开发chrome插件
vue带来的好处
干掉DOM操作
我开发的chrome插件是一个用来完成图片下载任务的插件。抓取过程中,需要显示抓取进度,并可以进行删除下载,其中涉及很多DOM操作。使用vue可以减少大量的dom操作代码,这个不细讲,参见http://vuejs.org/guide/

干掉复杂的通信
这个是我觉得用vue开发chrome插件最有价值的部分了,前边介绍了插件几大模块之间的通信,需要调用chrome提供的接口进行频繁的发送消息和监听处理。通过

chrome.extension.getBackgroundPage()

可以拿到插件background脚本的window对象,注意这个background的执行上下文是只有一个的,所以在插件运行期间我们可以用它来存储各个tab页抓取回来的数据。在popup的脚本中:

var FETCH_ITEMS=chrome.extension.getBackgroundPage().FETCH_ITEMS;
var vm=new Vue({
    el: '#wrapper',
    data: {
        FETCH_ITEMS: FETCH_ITEMS
    },
    methods:{
    }
});

这样,将从各个tab页抓取回来的数据push到background.js暴露出来的一个对象中,popup弹出的网页中就可以实时显示抓取进度了,不需要在popup和background之间编写大量的通信代码。至于各个tab页和background之间的通信,可以使用上面chrome提供的通信方式,也可以自己拿到background的window对象暴露出的变量,再进行操作。这里并没有复杂的视图更新和用户操作,所以怎么通信都无所谓了~

代码结构
按照上面理解的结构,每个模块的代码集中到一起。另外,插件中也允许根据路径直接访问插件中的资源,其路径是“chrome-extension://[extensionId]/[resourceName]” 。select目录中存放的是一个用来筛选图片的页面代码,在popup页面中直接跳转到/select.html即可打开此页面。

插件开发完成打包之后,把这些资源放到一个文件夹中然后打开chrome://extensions/选择上面的目录即可看到地址栏右侧出现插件的图标。


初次在chrome插件开发中使用vue的时候,遇到了这样一个问题:模型更新了,视图始终不更新,然后打开调试界面后运行vm.$mount("#wrapper");,视图却更新了。百思不得其解,google之,发现chrome插件中有些javascript代码写法和正常环境中有所不同,幸好vue居然贴心的为这种情况准备了一个特殊的包,不然就前功尽弃了 :(这里总结了一个经验,用前端框架开发的过程中最好不要使用.min的包,打包好的代码里边会去掉一些警告的逻辑… 把.min包替换成非压缩包之后,看到了这个错误:根据提示,在vue的项目里边找到了一个叫CSP的分支https://github.com/vuejs/vue/tree/csp/dist看文档说明,果然就是为插件开发定制的啊! 没想到那么偏门的场合他们也有关注到!

Logo

前往低代码交流专区

更多推荐