基于vue3.0开发chrome浏览器插件

2,374 阅读15分钟

1. 前沿

公司承接了一个文档审核得项目,开发得项目需要嵌入在客户的网页系统中,能够爬虫到客户的页面数据和附件中的文档信息,然后基于这些去完成合同得审核,并能展示文档合同的审核的结果,和审核这个任务的流程信息,同时又能基于系统页面中的信息去控制文档是否自动审核,基于这种现实需求,我们客户端选择了chrome插件的开发。 Chrome 扩展(通常也叫插件)也是软件程序,使用 Web(HTML, CSS, and JavaScript)技术栈开发。允许用户自定义 Chrome 浏览体验。开发者可以通过增加特效或功能来优化体验。Chrome插件提供了很多实用API供我们使用,包括但不限于:

  • 书签控制
  • 下载控制
  • 窗口控制
  • 标签控制
  • 网络请求控制,各位事件监听
  • 自定义原生菜单
  • 完善的通信机制

2. 核心概念

2.1 manifest.json

这是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的一个配置文件,必须放在根目录。其中,manifest_versionnameversion3个是必不可少的,descriptionicons是推荐的。 下面是一些常见的配置项,均有中文注释。


{ // 清单文件的版本,这个必须写,而且必须是2 
"manifest_version": 2,
// 插件的名称 
"name": "demo", 
// 插件的版本
"version": "1.0.0", 
// 插件描述 
"description": "简单的Chrome扩展demo", 
// 图标,一般偷懒全部用一个尺寸的也没问题 
"icons": { "16": "img/icon.png", "48": "img/icon.png", "128": "img/icon.png" }, 
// 会一直常驻的后台JS或后台页面 
"background": {
// 2种指定方式,如果指定JS,那么会自动生成一个背景页 
"page": "background.html"
//"scripts": ["js/background.js"] 
},
// 浏览器右上角图标设置,browser_action、page_action、app必须三选一 
"browser_action": { 
"default_icon": "img/icon.png", 
// 图标悬停时的标题,可选 
"default_title": "这是一个示例Chrome插件",
"default_popup": "popup.html"
}, 
// 当某些特定页面打开才显示的图标 
"page_action":{
"default_icon": "img/icon.png",
"default_title": "我是pageAction",
"default_popup": "popup.html" 
},
 // 需要直接注入页面的JS 
"content_scripts":
[{ //"matches": ["http://*/*", "https://*/*"],
// "<all_urls>" 表示匹配所有地址 
"matches": ["<all_urls>"], 
// 多个JS按顺序注入 
"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
// JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式 
"css": ["css/custom.css"],
// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle 
"run_at": "document_start" 
}], 
// 权限申请 
"permissions": 
[ 
"contextMenus", // 右键菜单
"tabs", // 标签
"notifications", // 通知
"webRequest", // web请求 
"webRequestBlocking",
"storage", // 插件本地存储 
"http://*/*", // 可以通过executeScript或者insertCSS访问的网站"
]

2.2 content-scripts

是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还可以包括css的),借助content-scripts我们可以实现通过配置的方式轻松向指定页面注入JS和CSS(如果需要动态注入,可以参考下文),最常见的比如:广告屏蔽、页面CSS定制,等等。 示例配置:

"content_scripts":
[{ //"matches": ["http://*/*", "https://*/*"],
// "<all_urls>" 表示匹配所有地址 
"matches": ["<all_urls>"], 
// 多个JS按顺序注入 
"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
// JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式 
"css": ["css/custom.css"],
// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle 
"run_at": "document_start" 
}]

2.3 background

是一个常驻的后台页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。

background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS
配置中,background可以通过page指定一张网页,也可以通过scripts指定一个JS,chrome会自动为这个JS生产一个默认的网页:

{ 
// 会一直常驻的后台JS或后台页面
"background": { 
// 2种指定方式,如果指定JS,那么会自动生成一个背景页 
"page": "background.html" 
"scripts": ["js/background.js"]
}
}

2.4 popup

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。popup可以包含任意你想要的HTML内容,并且会自适应大小。可以通过default_popup字段来指定popup页面,也可以调用setPopup()方法。

{ 
"browser_action": 
{ 
"default_icon": "img/icon.png",
// 图标悬停时的标题,可选 
"default_title": "这是一个示例Chrome插件", 
"default_popup": "popup.html" 
}
}

acdc68c51b616bcce8c00c166b9faa1.png

2.5 injected-script

指的是通过DOM操作的方式向页面注入的一种JS,它是为了解决content-script的“缺陷”,也就是无法访问页面中的JS,虽然它可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-script中的代码(包括直接写onclickaddEventListener2种方式都不行),通过动态加载js的方式往原网页中注入inject.js。inject.js就如同原网页自己加载js一样,拥有对原网页dom操作,js操作的全部功能。 注入方式:

// 向页面注入JS 
function injectCustomJs(jsPath) {
jsPath = jsPath || 'js/inject.js'; 
var temp = document.createElement('script'); 
temp.setAttribute('type', 'text/javascript'); 
// 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js 
temp.src = chrome.extension.getURL(jsPath); 
temp.onload = function() { 
// 放在页面不好看,执行完后移除掉 
this.parentNode.removeChild(this); 
};
document.head.appendChild(temp); }

3. 5种类型的js对比

Chrome插件的JS主要可以分为这5类:injected scriptcontent-scriptpopup jsbackground jsdevtools js

3.1 权限对比

JS种类可访问的APIDOM访问情况JS访问情况直接跨域
injected-script和普通JS无任何差别,不能访问任何扩展API可以访问可以访问不可以
content-script只能访问 extension、runtime等部分API可以访问不可以不可以
popup-js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
background-js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
devtools-js只能访问 devtools、extension、runtime等部分API可以可以不可以

3.2 调用方式对比

JS类型调试方法
injected script直接普通的F12即可
content-script打开Console,如图切换
popup-jspopup页面右键审查元素
background插件管理页点击背景页即可
devtools-js暂未找到有效方法

4. 消息通信

4.1 相互通信

注:--表示不存在或者无意义,或者待验证。

injected-scriptcontent-scriptpopup-jsbackground-js
injected-script--window.postMessage----
content-scriptwindow.postMessage--chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
popup-js--chrome.tabs.sendMessage chrome.tabs.connect--chrome.extension. getBackgroundPage()
background-js--chrome.tabs.sendMessage chrome.tabs.connectchrome.tabs.sendMessage chrome.tabs.connect--
devtools-jschrome.devtools. inspectedWindow.eval--chrome.runtime.sendMessagechrome.runtime.sendMessage

4.2 通信介绍

4.2.1 popup和background

popup可以直接调用background中的JS方法,也可以直接访问background的DOM:

// background.js 
function test() { alert('我是background!'); } 
// popup.js 
var bg = chrome.extension.getBackgroundPage(); 
bg.test(); // 访问bg的函数 
alert(bg.document.body.innerHTML); // 访问bg的DOM

background访问popup如下(前提是popup已经打开):

var views = chrome.extension.getViews({type:'popup'}); 
if(views.length > 0) 
{ 
console.log(views[0].location.href);
}

4.2.2 popup或者bg向content主动发送消息

background.js或者popup.js:

function sendMessageToContentScript(message, callback) 
{ 
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 
chrome.tabs.sendMessage(tabs[0].id, message, function(response) { 
if(callback) callback(response); 
});
});
} 
sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response) { console.log('来自content的回复:'+response); });

content-script.js接收:


chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension"); 
if(request.cmd == 'test') 
alert(request.value); 
sendResponse('我收到了你的消息!');
});

4.2.3 content-script主动发送消息给后台

content-script.js:

function sendBackground(flag) {

      chrome.runtime.sendMessage(
        {
          greeting: "你好,我是content-script呀,我主动发消息给后台!",
          type: "content-script",
        },
        function (response) {
          sendVerifyTask(response, flag);
        }
      );
    }

background-script.js


// 监听来自content-script的消息
chrome.runtime.onMessage.addListener (function (request, sender, sendResponse) {
  chrome.tabs.query (
    {
      active: true,
      currentWindow: true,
    },
    tabs => {
      console.log (tabs);
      let destUrl = '';
      try {
        destUrl = tabs[0].url;
      } catch (err) {
        console.log (err.message);
      }
      if (destUrl !== '') {
        chrome.cookies.getAll (
          {
            domain: destUrl.split ('/')[2].split (':')[0] || '',
          },
          cookies => {
            console.log (cookies);
            const cookieArr = [];
            cookies.map (cookie => {
              if (cookie.expirationDate) {
                cookieArr.push ({
                  ...cookie,
                  expirationDate: resolveData (cookie.expirationDate),
                });
              } else {
                cookieArr.push ({
                  ...cookie,
                });
              }
            });
            console.log (cookieArr);
            const fileInfo = {
              cookie: JSON.stringify (cookieArr),
              pageUrl: destUrl,
            };
            if (sendResponse && typeof sendResponse === 'function') {
              sendResponse (fileInfo);
            }
          }
        );
      }
    }
  );
  return true;
});


4.2.4 inject-script和content-script

content-script和页面内的脚本(injected-script自然也属于页面内的脚本)之间唯一共享的东西就是页面的DOM元素,有2种方法可以实现二者通讯:

1.可以通过window.postMessagewindow.addEventListener来实现二者消息通讯;

2.通过自定义DOM事件来实现;

第一种方法(推荐)injected-script中:

window.postMessage({"test": '你好!'}, '*');

content script中:

window.addEventListener("message", function(e) { console.log(e.data); }, false);

第二种方法 inject-script中:

var customEvent = document.createEvent('Event'); customEvent.initEvent('myCustomEvent', true, true); function fireCustomEvent(data) { 
hiddenDiv = document.getElementById('myCustomEventDiv');
hiddenDiv.innerText = data hiddenDiv.dispatchEvent(customEvent); 
} fireCustomEvent('你好,我是普通JS!');

content-script.js中

var hiddenDiv = document.getElementById('myCustomEventDiv'); 
if(!hiddenDiv) { 
hiddenDiv = document.createElement('div'); 
hiddenDiv.style.display = 'none';
document.body.appendChild(hiddenDiv); 
} 
hiddenDiv.addEventListener('myCustomEvent', function() { 
var eventData = document.getElementById('myCustomEventDiv').innerText; console.log('收到自定义事件消息:' + eventData); });

4.3 长连接和短链接

其实上面已经涉及到了,这里再单独说明一下。Chrome插件中有2种通信方式,一个是短连接(chrome.tabs.sendMessagechrome.runtime.sendMessage),一个是长连接(chrome.tabs.connectchrome.runtime.connect)。

短连接的话就是挤牙膏一样,我发送一下,你收到了再回复一下,如果对方不回复,你只能重新发,而长连接类似WebSocket会一直建立连接,双方可以随时互发消息。

短连接上面已经有代码示例了,这里只讲一下长连接。 popup.js:

getCurrentTabId((tabId) => { 
var port = chrome.tabs.connect(tabId, {name: 'test-connect'}); port.postMessage({question: '你是谁啊?'}); 
port.onMessage.addListener(function(msg) { 
alert('收到消息:'+msg.answer); 
if(msg.answer && msg.answer.startsWith('我是')) 
{ port.postMessage({question: '哦,原来是你啊!'});
} }); 
});

content-script.js:

// 监听长连接 
chrome.runtime.onConnect.addListener(function(port) { 
console.log(port); 
if(port.name == 'test-connect') { 
port.onMessage.addListener(function(msg) { 
console.log('收到长连接消息:', msg); 
if(msg.question == '你是谁啊?') 
port.postMessage({answer: '我是你爸!'}); 
}); } });

5. 其他补充

5.1 动态注入或者是执行js

虽然在backgroundpopup中无法直接访问页面DOM,但是可以通过chrome.tabs.executeScript来执行脚本,从而实现访问web页面的DOM(注意,这种方式也不能直接访问页面JS)。

示例manifest.json配置:

{ 
"name": "动态JS注入演示",
... ,
"permissions": [ "tabs", "http://*/*", "https://*/*" ],
... }

JS:

// 动态执行JS代码 chrome.tabs.executeScript(tabId, 
{code: 'document.body.style.backgroundColor="red"'});
// 动态执行JS文件 
chrome.tabs.executeScript(tabId, {file: 'some-script.js'});

5.1 动态注入css

示例manifest.json配置:

"content_scripts": [
{
"css": ["css/content.css","css/chunk-vendors.26f1403c.css"]
}]

动态执行css代码:

// 动态执行CSS代码,TODO,这里有待验证 
chrome.tabs.insertCSS(tabId, {code: 'xxx'}); 
// 动态执行CSS文件 chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});

6. vue3.0创建项目

6.1 创建项目

使用 vue-cli 创建 vue3.x 版本的 vue 项目 vue create my-vue3-plugin

.



├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   └── main.js
├── package.json
├── babel.config.js
├── README.md

6.2 修改文件目录

因为我们要开发 chrome 插件项目,而这种生成的 vue 项目里面的文件夹和文件很多我们不需要,所以我们需要修改文件目录:

  • assets 文件中创建 images 文件夹

  • src 文件夹下 创建 background、content、plugins、popup、utils 文件夹

  • background 文件夹下创建 main.js

  • content 文件夹下创建 components 文件夹和 main.jscomponents 文件夹下创建 app.vue

  • plugins 文件夹下创建 inject.js、manifest.json 文件

  • popup 文件夹下创建 components 文件夹 main.jsindex.htmlcomponents 文件夹下创建 app.vue

  • vue.config.jsvue 项目的打包、运行、等的配置文件,我们需要生成插件项目,这个文件需要创建并且自行配置

  • 删除多余的文件,我们插件里面目前只有 需要一个 popup 页面,不需要 外部的 app.vue 和 组件

  • 自己的插件 icon,按照 16 * 16、48 * 48、128 * 128 三个尺寸

  • 删除 public 文件夹里面的 index.html以及public

  • 在background文件夹下创建 background.js 文件

  • 创建services文件夹

  • 创建style文件夹

文件层级目录如下:

├── src
│ ├── assets
│ │ ├── images
│ │ ├── icon128.png
│ │ ├── icon16.png
│ │ └── icon48.png
│ │
│ ├── background
│ │ └── main.js
│ ├── content
│ │ ├── components
│ │ ├── app.vue
│ │ └── main.js
│ ├── main.js
│ ├── plugins
│ │ ├── inject.js
│ │ └── manifest.json
│ ├── popup
│ │ ├── components
│ │ │ └── app.vue
│ │ ├── index.html
│ │ └── main.js
│ └── utils
|
├── .env
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── vue.config.js

6.2 配置vue.config.js

npm install copy-webpack-plugin@6.0.2 -D

vue.config.js配置如下:

const CopyWebpackPlugin = require ('copy-webpack-plugin');
const path = require ('path');

// 复制文件到指定目录
const copyFiles = [
  {
    from: path.resolve ('src/plugins/manifest.json'),
    to: `${path.resolve ('dist')}/manifest.json`,
  },
  {
    from: path.resolve ('src/assets'),
    to: path.resolve ('dist/assets'),
  },
  {
    from: path.resolve ('src/plugins/inject.js'),
    to: path.resolve ('dist/js'),
  },
];

// 复制插件
const plugins = [
  new CopyWebpackPlugin ({
    patterns: copyFiles,
  }),
];

// 页面文件
const pages = {};
// 配置 popup.html 页面
const chromeName = ['popup'];

chromeName.forEach (name => {
  pages[name] = {
    entry: `src/${name}/main.js`,
    template: `src/${name}/index.html`,
    filename: `${name}.html`,
  };
});

module.exports = {
  pages,
  productionSourceMap: false,
  // 配置 content.js background.js
  configureWebpack: {
    entry: {
      content: './src/content/main.js',
      background: './src/background/main.js',
    },
    output: {
      filename: 'js/[name].js',
    },
    plugins,
  },
  // 配置 content.css
  css: {
    extract: {
      filename: 'css/[name].css',
    },
  },
  chainWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.output.filename ('js/[name].js').end ();
      config.output.chunkFilename ('js/[name].js').end ();
      config.module
        .rule ('fonts')
        .test (/\.(ttf|otf|eot|woff|woff2)$/)
        .use ('file-loader')
        .loader ('file-loader')
        .tap (options => {
          options = {
            limit: 10000,
            name: '/fonts/[name].[ext]',
          };
          return options;
        })
        .end ();
    }
  },
};

6.3 配置plugins文件夹下的插件配置文件manifest.json

{
	"manifest_version": 2,
	"name": "chrome-audit-plugin",
	"description": "安徽电信文档审核chrome插件",
	"version": "1.0.5",
	"browser_action": {
		"default_title": "chrome-audit-plugin",
		"default_icon": "assets/images/icon48.png",
		"default_popup": "popup.html"
	},
	"permissions": [
		"contextMenus",
		"cookies",
		"tabs",
		"notifications",
		"webRequest",
		"webRequestBlocking",
		"storage",
		"http://*/*",
		"https://*/*"
	],
	"background": {
		"scripts": ["js/chunk-vendors.js", "js/background.js"]
	},
	"icons": {
		"16": "assets/images/icon16.png",
		"48": "assets/images/icon48.png",
		"128": "assets/images/icon128.png"
	},
	"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
  "homepage_url": "https://www.baidu.com",
	"content_scripts": [
		{
			"matches": ["http://*.mss.ctc.com/*","http://oa.iflytek.com/*","http://172.31.97.59:8095/*","http://gcfz-ah.mss.ctc.com:81/*"],
			"css": ["css/content.css","css/chunk-vendors.26f1403c.css"],
			"js": ["js/chunk-vendors.js", "js/content.js"],
      "img":["img/ionicons.a2c4a261.svg"],
			"run_at": "document_end"
		}
	],
	"web_accessible_resources": ["js/inject.js","fonts/**.*","img/**.*","assets/images/*.png","assets/images/*.gif"]
}

6.4 修改content里面的main.js文件

main.js中引入inject.js

injectJsInsert ();
function injectJsInsert () {
  document.addEventListener ('readystatechange', () => {
    const injectPath = 'js/inject.js';
    const script = document.createElement ('script');
    script.setAttribute ('type', 'text/javascript');
    script.src = chrome.extension.getURL (injectPath);
    document.body.appendChild (script);
  });
}

main.js中引入app.vue插入新建的DOM中

injectJsInsert ();
function joinContent (element) {
  const div = document.createElement ('div');
  div.id = 'joinContentApp';
  document.body.appendChild (div);
  const app = createApp (element);
  // app.use (store);
  app
    .component ('Select', Select)
    .component ('DatePicker', DatePicker)
    .component ('Option', Option)
    .component ('Input', Input)
    .component ('Button', Button)
    .component ('Tabs', Tabs)
    .component ('TabPane', TabPane)
    .component ('Table', Table)
    .component ('Message', Message)
    .component ('Modal', Modal)
    .component ('Icon', Icon)
    .component ('Menu', Menu)
    .component ('MenuItem', MenuItem)
    .component ('Page', Page)
    .component ('Tooltip', Tooltip);
  app.mount ('#joinContentApp');
}

6.5 解决启动时的报错和页面不出现的报错

a. npm run build 打包构建报错 错误如下:

 Building for production... ERROR Failed to compile with 1 error 
 error in ./src/content/main.js 
 Module Error (from ./node_modules/thread-loader/dist/cjs.js): /Users/guoqiankun/work/chromePlugin/my-vue3-plugin/src/content/main.js 21:16 
 error 'chrome' is not defined no-undef ✖ 1 problem (1 error, 0 warnings) 
 ERROR Build failed with errors. 
 error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

全局没有配置chrome,在.eslintrc.js中配置

globals:{
    chrome:true
}

b.打包构建成功之后,没有出现content的页面内容和样式效果,需要在plugins的manifest.json下引入生成打包生成的js和css文件

"content_scripts": [
		{
			"css": ["css/content.css","css/chunk-vendors.26f1403c.css"],
			"js": ["js/chunk-vendors.js", "js/content.js"],
		}
	],

c.由于每次打包的js文件名会包含hash值,每次manifest.json都需要重新引入,造成极大的不便,所以在打包构建时去掉hash的生成,每次生产的js都是稳定的名字。 修改vue.config.js配置如下:

configureWebpack: {
    entry: {
      content: './src/content/main.js',
      background: './src/background/main.js',
    },
    output: {
      filename: 'js/[name].js',
    },
    plugins,
  },

6.6 热加载配置

浏览器加载的插件,每次都是打包生产之后的文件,当在开发阶段需要频繁的调试代码时,这就需要不断的去打包,不利于开发效率的提升,需要引入热加载的方式,只要代码改变会自动的去重新更新之前打包的文件内容并能自动的去刷新浏览器,这样每次修改代码后就能查看最新的代码效果,极大的提升了开发的效率和开发体验。由于要主动更新浏览器,通过上面的核心概念我们知道要想操作浏览器,我们需要在bacrground.js中去开发代码逻辑, 要开发一个热更新逻辑,我们需要如下步骤:

  1. 引入npm run watch的命令,watch命令将编译组件然后监视文件并在其中一个更改时重新编译
  2. 获取插件目录,监听文件变化,代码如下:
// 观察文件改动
const watchChanges = (dir, lastTimestamp) => {
  timestampForFilesInDirectory(dir).then(timestamp => {
    // 文件没有改动则循环监听watchChanges方法
    if (!lastTimestamp || lastTimestamp === timestamp) {
      setTimeout(() => watchChanges(dir, timestamp), 1000); // retry after 1s
    } else {
      // 强制刷新页面
      reload();
    }
  });
};
// 加载文件

const filesInDirectory = dir =>
  new Promise(resolve =>
    dir.createReader().readEntries(entries => {
      Promise.all(
          entries
          .filter(e => e.name[0] !== '.')
          .map(e =>
            e.isDirectory ? filesInDirectory(e) : new Promise(resolve => e.file(resolve))
          )
        )
        .then(files => [].concat(...files))
        .then(resolve);
    })
  );
// 遍历插件目录,读取文件信息,组合文件名称和修改时间成数据
const timestampForFilesInDirectory = dir =>
  filesInDirectory(dir).then(files =>
    files.map(f => f.name + f.lastModifiedDate).join()
  );
const hotReload = () => {
  window.chrome.management.getSelf(self => {
    if (self.installType === 'development') {
      // 获取插件目录,监听文件变化
      window.chrome.runtime.getPackageDirectoryEntry(dir => watchChanges(dir));
    }
  });
};

3.强制刷新当前页面

// 刷新当前活动页
const reload = () => {
  window.chrome.tabs.query({
      active: true,
      currentWindow: true
    },
    tabs => {
      // NB: see https://github.com/xpl/crx-hotreload/issues/5
      if (tabs[0]) {
        window.chrome.tabs.reload(tabs[0].id);
      }
      // 强制刷新页面
      window.chrome.runtime.reload();
    }
  );
};

以上就是整个热更新的代码逻辑,然后在background.js调用即可

6.7 contentbackground之间的组件通信

由于我们需要操作插件页面中的某个按钮,然后去获取浏览器中的一些cookie信息,这个功能就需要有content组件向background发起请求,由background建立起对content发起消息的监听,监听到content.js发过来的消息后,调用chrome本地的一些api去获取当前浏览器窗口信息,拿到当前的窗口信息再去获取指定域名下的cookie信息,并将这个cookie信息回传给content.js,由content.js去做业务开发。

  1. content发起消息请求,代码如下:
//向background发送通信
    function sendBackground(flag) {

      chrome.runtime.sendMessage(
        {
          greeting: "你好,我是content-script呀,我主动发消息给后台!",
          type: "content-script",
        },
        function (response) {
          sendVerifyTask(response, flag);
        }
      );
    }
  1. background.js建立监听,代码如下:
chrome.runtime.onMessage.addListener (function (request, sender, sendResponse) {
  chrome.tabs.query (
    {
      active: true,
      currentWindow: true,
    },
    tabs => {
      console.log (tabs);
    }
  );
  return true;
});
  1. 获取浏览器的底层cookie信息,代码如下:
//先获取当前窗口的tab
 chrome.tabs.query (
    {
      active: true,
      currentWindow: true,
    },
    tabs => {
      console.log (tabs);
      let destUrl = '';
      try {
        destUrl = tabs[0].url;
      } catch (err) {
        console.log (err.message);
      }
      //基于当前当前窗口tab的地址去获取指定的cookie信息
      if (destUrl !== '') {
        chrome.cookies.getAll (
          {
            domain: destUrl.split ('/')[2].split (':')[0] || '',
          },
          cookies => {
            console.log (cookies);
            sendResponse (cookies);
          }
        );
      }
    }
  );

6.7 具体开发过程中的问题记录

  1. 引入iview组件库fonts字体图标文件,字体图标不出现的问题: 不仅需要在manifest.jsoni的引入外部资源中配置,代码如下:
"web_accessible_resources": ["fonts/**.*"]

当css文件在引入的字体图标文件路径要以绝对路径的形式去引入,这个绝对路径就是以浏览器的id形式去引入字体图像文件资源,@@extension_id代表着该插件的ID,可以在扩展内部用这个字符串构造URL用于访问某些资源,绝对路径有三部分组成 url=chrome-extension://__MSG_+插件ID+打包的资源相对路径代码示例如下:

@font-face {
  font-family: Ionicons;
  src: url(chrome-extension://__MSG_@@extension_id__/fonts/ionicons.143146fa.woff2)
      format("woff2"),
    url(chrome-extension://__MSG_@@extension_id__/fonts/ionicons.99ac3308.woff)
      format("woff"),
    url(chrome-extension://__MSG_@@extension_id__/fonts/ionicons.d535a25a.ttf)
      format("truetype"),
    url(chrome-extension://__MSG_@@extension_id__/img/ionicons.a2c4a261.svg#Ionicons)
      format("svg");
  font-weight: 400;
  font-style: normal;
}

2.css中引入外部图片资源也要以浏览器插件绝对路径的方式去引入,代码参照如下:

background-image: url(chrome-extension://__MSG_@@extension_id__/assets/images/logo.gif);

3.组件之间发起消息通信,回调方法不执行的问题

组件之间发起消息通信,在监听函数内部发起回调后,这个回调方法没有执行导致接受不到传过来的参数,原来是在监听消息的方法中,没有return true ,具体代码示例如下:

chrome.runtime.onMessage.addListener (function (request, sender, sendResponse) {
  chrome.tabs.query (
    {
      active: true,
      currentWindow: true,
    },
    tabs => {
      console.log (tabs);
      sendResponse(tabs);
    }
  );
  `return true`;
});

6.8 效果展示

效果图.png