浏览器插件是前端领域比较小众的应用范畴,我们所说的浏览器插件指的就是Chrome 插件
。在Chrome 插件市场上有非常多有趣
又实用
的Chrome 插件
,比如octotree(显示github代码树)、Adblock Plus(拦截广告)等。
目前笔者接触Chrome 插件
开发也有一年时间,最初团队中使用原生js+jquery
的方式开发插件,后来考虑使用Vue重构插件,主要原因在于:
因此本篇文章旨在分享笔者在基于vue-cli开发浏览器插件
的工程化实践经验以及部分功能的思考与实现
,在整理Vue开发插件的有关知识
的同时提供给想尝试浏览器插件开发
的开发者Vue开发插件
的一点思路。如果你还未熟悉浏览器插件开发
,请先借助这篇文章了解插件开发的基础知识(本篇文章默认你已认真读完),再进行Vue开发插件的实践。
插件中必不可少的文件是manifest.json
(必须放在项目根目录),我们知道package.json
是项目的基本配置文件,那manifest.json
就是chrome 插件
中最重要的配置文件。这个文件记录插件里background
、content_scripts
、browser_action
等配置的相关规则和文件摆放位置。
假如有这样一个manifest.json
文件:
{
"manifest_version": 2,
"name": "vue-chrome-extension",
"description": "基于vue的chrome插件",
"version": "1.0.0",
"browser_action": {
"default_title": "vue-chrome-extension",
"default_icon": "assets/logo.png",
"default_popup": "popup.html"
},
"permissions": [
"webRequestBlocking",
"notifications",
"tabs",
"webRequest",
"http://*/",
"https://*/",
"<all_urls>",
"storage",
"activeTab"
],
"background": {
"scripts": ["js/background.js"]
},
"icons": {
"16": "assets/logo.png",
"48": "assets/logo.png",
"128": "assets/logo.png"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"content_scripts": [
{
"matches": [
"https://*.baidu.com/*"
],
"css": [
"css/content.css"
],
"js": [
"js/content.js"
],
"run_at": "document_end"
}
],
"web_accessible_resources": ["fonts/*", "inject.js"]
}
复制代码
manifest.json
定义了插件的目录文件结构,上面配置对应这样的结构:
.
├── assets
│ └── logo.png
├── css
│ └── content.css
├── inject.js
├── js
│ ├── background.js
│ └── content.js
├── manifest.json
└── popup.html
复制代码
因此我们必须改造vue.config.js
文件,让Vue-cli
(也可以是webpack)打包后的文件结构与上面结构一致,我们这样定义vue.config.js
:
代码过长,点击查看
const CopyWebpackPlugin = require("copy-webpack-plugin");
const ZipWebpackPlugin = require("zip-webpack-plugin");
const path = require("path");
// 只需要复制的文件
const copyFiles = [
{
from: path.resolve("src/chrome/manifest.json"),
to: `${path.resolve("dist")}/manifest.json`
},
{
from: path.resolve("src/assets"),
to: path.resolve("dist/assets")
},
{
from: path.resolve("src/chrome/inject.js"),
to: path.resolve("dist")
}
];
// const plugins = [];
const plugins = [
new CopyWebpackPlugin({
patterns: copyFiles
})
];
// 生产环境打包dist为zip
if (process.argv.includes("--zip")) {
plugins.push(
new ZipWebpackPlugin({
path: path.resolve("./"),
filename: "dist.zip"
})
);
}
// 配置页面
const pages = {};
/**
* popup 和 devtool 都需要html文件
* 因此 chromeName 还可以添加devtool
*/
const chromeName = ["popup"];
chromeName.forEach(name => {
pages[name] = {
entry: `src/${name}/index.js`,
template: `src/${name}/index.html`,
filename: `${name}.html`
};
});
module.exports = {
pages,
// 生产环境是否生成 sourceMap 文件
productionSourceMap: false,
configureWebpack: {
// 多入口打包
entry: {
content: "./src/content/index.js",
background: "./src/chrome/background/index.js"
},
output: {
filename: "js/[name].js"
},
plugins
},
css: {
extract: {
filename: "css/[name].css"
}
},
chainWebpack: config => {
config.resolve.alias.set("@", path.resolve("src"));
// 处理字体文件名,去除hash值
const fontsRule = config.module.rule("fonts");
// 清除已有的所有 loader。
// 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
fontsRule.uses.clear();
fontsRule
.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
.use("url")
.loader("url-loader")
.options({
limit: 1000,
name: "fonts/[name].[ext]"
});
}
};
复制代码
配置好vue.config.js
后再package.json
加入脚本:
"scripts": {
"serve": "vue-cli-service build --watch",
"build": "vue-cli-service build"
},
复制代码
到这里就可以进行插件的开发工作了,npm run serve
和npm run build
分别提供了开发
和生产
的命令。
vue
和react
都提供了模块热替换(hmr)
的功能,这大大的提高我们开发调试代码的效率。那我们调试插件需要这样操作:
扩展程序
页面加载已解压的扩展程序
,加入插件文件,插件就开始运行了content scripts
需要这样操作),查看改动可以看到整个调试过程比较繁琐且重复,笔者使用了github上热刷新的解决方案(如果有更好的方案请告知),之所以称它为热刷新
,是因为它会强制刷新页面,并不是真正意义上的热替换
(不刷新页面),使用它后我们的调试过程是这样:
扩展程序
页面加载已解压的扩展程序
,加入插件文件,插件就开始运行了热刷新
主要会帮我们做这几个工作:
content scripts
),应用最新代码热刷新
的实现也就50多行代码,其原理就是:
background
加入代码逻辑(利用background
能长时间活动在后台的特点)chrome.runtime.getPackageDirectoryEntry
获取插件的文件目录,监听文件变化文件名加上上次修改时间
的变化来决定是否刷新页面,再通过setTimeout
间歇性递归监听文件变化的方法chrome.tabs.query
找到当前页(当前活动标签页),执行chrome.tabs.reload
强制刷新页面热刷新
缺陷:
打开谷歌扩展程序页面
将vue-cli
打包后的文件打包,第一次打包会在项目根目录生成一个插件私钥
(用于区分插件)和crx
文件(插件生产环境的文件格式,本质是ZIP文件,只是谷歌插入了自定义的私有字段,如,插件描述,插件ID,密钥等)---插件私钥和crx参考,我们可以使用crx(打包成crx的npm包)配合插件私钥
可以将插件打包成crx
文件。我们在项目中加入这样一个脚本:
// src/scripts/crx.js
const fs = require("fs");
const path = require("path");
const manifest = require(path.resolve(__dirname, "../chrome/manifest.json"));
const ChromeExtension = require("crx");
const crxName = `${manifest.name}-v${manifest.version}.crx`;
const crx = new ChromeExtension({
privateKey: fs.readFileSync(path.resolve(__dirname, "../../dist.pem"))
});
crx
.load(path.resolve(__dirname, "../../dist"))
.then(crx => crx.pack())
.then(crxBuffer => {
fs.writeFile(crxName, crxBuffer, err =>
err
? console.error(err)
: console.log(`>>>>>>> ${crxName} <<<<<<< 已打包完成`)
);
})
.catch(err => {
console.error(err);
});
复制代码
在package.json
加入我们添加的脚本:"build:crx": "npm run build && node src/scripts/crx.js"
使用build:crx
命令能把vue-cli
打包后的文件再打包成一个crx
文件,提高了打包的效率。
上面主要围绕修改Vue-cli项目
、热刷新调试
、自动打包
等工程化的几个方面展开阐述,接下来主要分享下项目中几个通用的解决方案。
content scripts
主要往目标页面插入我们的js,这些脚本通常是插入我们的dom。例如:
这是某网盘的插件(该插件目前已失效,这里只是展示),该插件在页面上插入黑框标注的按钮,这就是content scripts
的作用。
回到vue
项目中笔者封装了一个通用的将Vue组件转为真实dom
的插入方法
import Vue from "vue";
function insert(component, insertSelector = "body") {
insertDomFactory(component, insertSelector);
}
function insertDomFactory(component, insertSelector) {
const vm = generateVueInstance(component);
generateInsertDom(insertSelector, vm);
}
// 将createElement生成的元素插入到目标dom中,再将vue实例挂载到上面
function generateInsertDom(insertSelector, vm) {
// 待插入的dom
const insertDom = document.querySelectorAll(insertSelector);
insertDom.forEach(item => {
const insert = document.createElement("div");
insert.id = "insert-item";
item.appendChild(insert);
vm.$mount("#insert-item");
});
}
// 生成Vue实例
function generateVueInstance(component) {
const insertCon = Vue.extend(component);
return new insertCon();
}
export default insert;
复制代码
插入步骤为:
extend
生成构造器,将实例化后的的vm
返回createElement
生成一个div
插入到目标dom上vm
实例$mount
挂载目标dom接下来把我们的组件插入到页面上:
import App from "./App/App.vue";
import insert from "@/utils/insert";
insert(App);
复制代码
上面的插入方法都是通过new Vue
的方式生成,那页面上可能会存在多个Vue根实例,组件(除非父子组件)间就不能用props/$emit
通信,我们可以引入mixin
,配合vuex
将store
混合到全局Vue
上(当然还可以使用event bus
)
// store mixin
import store from "@/store";
export default {
beforeCreate() {
this.$store = store;
}
};
复制代码
全局混合
import Vue from "vue";
Vue.mixin(stroe);
复制代码
现在每个Vue
组件都有了访问store
的能力,可以基于vuex
进行通信。
笔者的插件项目中某个需求需要获取到原页面上某接口返回的数据,类似抓取数据的功能,提供三种解决方案:
devtools
devtools
的权限非常大,只有devtools
可以访问chrome.devtools api
,开启devtools
可以监听网页中接口的请求,vue-devtools插件就是通过该方式开发
我们这样开启devtools
:
// 创建一个Panel
// 这里配置F12面板里的标签页
chrome.devtools.panels.create(
// title
"vue-chrome-extension",
// iconPath
null,
// pagePath
"panel.html"
);
// 打印错误日志
const log = args =>
chrome.devtools.inspectedWindow.eval(`
console.log(${JSON.stringify(args)});
`);
// 注册回调,每一个http请求响应后,都触发该回调
chrome.devtools.network.onRequestFinished.addListener(async (...args) => {
try {
const [
{
// 请求的类型,查询参数,以及url
request: { url },
// 该方法可用于获取响应体
getContent
}
] = args;
if (url.indexOf("xxxx") === -1) {
const content = await new Promise(res => getContent(res));
// 发送请求内容
chrome.runtime.sendMessage({ content });
}
} catch (err) {
log(err.stack || err.toString());
}
});
复制代码
devtools
页面中获取到接口响应实体后再将内容发送出去,具体的模块通信可以看这里。
缺点:需要开启F12
重发请求
因为使用插件的用户在目标页处在登录状态,我们就可以利用登录状态(cookie
)来拷贝目标接口地址,再通过请求重发获取响应内容,我们可以这样实现:
import axios from "@/utils/axios";
// 根据自定义请求头判断是否需要重发
function isRequestSelf(headers) {
return headers.some(header => header.name === "X-No-Rerequest");
}
// 使用后台请求
const installRequest = () => {
chrome.webRequest.onBeforeSendHeaders.addListener(
async function(details) {
if (!isRequestSelf(details.requestHeaders)) {
const res = await axios.request({
method: details.method,
url: details.url,
// 添加自定义请求头,区分页面和插件请求,防止循环请求
headers: {
"X-No-Rerequest": "true"
}
});
// 后续可以将响应实体转发出去,与其他模块进行通信
}
},
{ urls: ["https://www.baidu.com/*"] },
["blocking", "requestHeaders"]
);
};
export default installRequest;
复制代码
缺点:重发请求需要消耗性能
注入js,替换ajax对象(推荐)
笔者遇到的情况非常严苛:
content scripts
,devtools
方式要打开F12,用户是开发者也许能够理解,但对普通用户肯定会影响到插件使用体验重发请求
方式,但目标网站中的目标接口安全措施做的非常完美:请求url中有一个随机参数,这个参数由鼠标位置
、时间戳
、页面高度
等参数合成,可以说独一无二。虽然在网上找了解出该参数的方法,但重发请求后,返回的内容与原请求响应内容不一致(也就是说该接口的内容是随机返回的)。前两种方式对笔者的实际情况不适用,笔者从`请求拦截`到`请求替换`的思路中找到最终的解决方案。我们可以这样实现:
```
// inject.js
let oldXHR = window.XMLHttpRequest;
function filterUrl(url) {
return url.indexOf("baidu.com") !== -1;
}
function newXHR() {
let realXHR = new oldXHR();
realXHR.onload = function() {
// 发送搜索列表页数据
if (filterUrl(realXHR.responseURL)) {
window.postMessage({ data: realXHR.responseText }, "*");
console.log(`这是onload函数请求的文本:${realXHR.responseText}`);
}
};
return realXHR;
}
window.XMLHttpRequest = newXHR;
复制代码
```
这种方式是使用`injected-script`,原理是先缓存页面中原`ajax`请求对象,在原`ajax`对象上添加`onload`方法,监听请求完成的回调,再将目标接口的响应实体通过相应的通信方法发送出去。
在`content scripts`中将`injected-script`插入到页面上
```
// content.js
injectJS();
function injectJS() {
document.addEventListener("readystatechange", () => {
const injectPath = "inject.js";
const temp = document.createElement("script");
temp.setAttribute("type", "text/javascript");
// 获得的地址类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
temp.src = chrome.extension.getURL(injectPath);
document.body.appendChild(temp);
});
}
复制代码
```
为什么不用`content scripts`?请看[这里](https://link.juejin.cn/?target=https%3A%2F%2Fwww.cnblogs.com%2Fliuxianan%2Fp%2Fchrome-plugin-develop.html%23injected-script "https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#injected-script")了解`content scripts`与`injected-script`的区别
虽然最终的实现方式只有寥寥几行代码,但提供的功能非常强大。
这样的方式也有缺点,就是只能适用于`ajax`请求的目标页面,若目标页面使用[fetch](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FFetch_API%2FUsing_Fetch "https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch")请求,这种方式则无效。可以通过开启[service worker](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FService_Worker_API "https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API")方式实现`fetch`请求监听(笔者没有尝试过)。
插件拥有的权限非常多,开发者可以利用这些特性提供丰富的功能。笔者将Vue开发插件的模板放到了github上,若对你有帮助,欢迎star✨
原网址: 访问
创建于: 2022-01-15 17:06:57
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论