preface
Browser plugins are a relatively niche category in the front-end field, and when we say browser plugins, we mean Chrome plugins. There are plenty of interesting and useful Chrome add-ons on the market, from Octotree (displays the Github code tree) to Adblock Plus(blocks ads).
At present, THE author has been engaged in Chrome plug-in development for one year. At first, the team used the way of native JS +jquery to develop the plug-in, and later considered using Vue reconstruction plug-in. The main reasons are as follows:
- Plug-ins are becoming more and more powerful
- Native development is inefficient
- No modules, not easy to maintain
- Team technology stack skews Vue
Therefore, this article aims to share the author’s engineering practice experience and thinking and implementation of some functions in developing browser plug-in based on VUe-CLI, and provide some ideas for developers who want to try browser plug-in development while collating the relevant knowledge of VUE plug-in development. If you are not already familiar with browser plug-in development, please use this article to understand the basics of plug-in development (assuming you have read this article carefully) before getting into the practice of Vue plug-in development.
Project engineering
Transform the vue. Config. Js
The essential file in the plugin is manifest.json(which must be placed at the root of the project). We know that package.json is the basic configuration file for the project, so manifest.json is the most important configuration file in the Chrome plugin. This file records the rules and file placement of background, content_scripts, browser_action, and other configurations in the plugin.
Suppose you have a manifest.json file like this:
{
"manifest_version": 2."name": "vue-chrome-extension"."description": Vue based Chrome plugin."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"]}Copy the code
Manifest.json defines the directory file structure for the plug-in, and the configuration above corresponds to this structure:
. ├ ─ ─ assets │ └ ─ ─ logo. The PNG ├ ─ ─ CSS │ └ ─ ─ content. CSS ├ ─ ─ inject. Js ├ ─ ─ js │ ├ ─ ─ background. Js │ └ ─ ─ content. js ├ ─ ─ The manifest. Json └ ─ ─ popup. HTMLCopy the code
Therefore, we have to modify the vue.config.js file to make the vue-CLI (or webpack) packed file structure consistent with the above structure. We define vue.config.js as follows:
Code is too long, click to view
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 }) ]; Plugins.push (new ZipWebpackPlugin({path: path.resolve("./"), filename: "dist.zip" }) ); } // const pages = {}; /** * Const chromeName = ["popup"]; /** * const chromeName = ["popup"]; chromeName.forEach(name => { pages[name] = { entry: `src/${name}/index.js`, template: `src/${name}/index.html`, filename: `${name}.html` }; }); Module. exports = {pages, // whether the production environment generates sourceMap file productionSourceMap: false, configureWebpack: {// multientry package 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")); // Handle font file names, remove hash values const fontsRule = config.module.rule("fonts"); // Clear all existing loaders. // If you do not do this, the following loader will be appended to the existing loader for this rule. fontsRule.uses.clear(); fontsRule .test(/\.(woff2? |eot|ttf|otf)(\? . *)? $/i) .use("url") .loader("url-loader") .options({ limit: 1000, name: "fonts/[name].[ext]" }); }};Copy the code
Configure vue.config.js before adding package.json to the script:
"scripts": {
"serve": "vue-cli-service build --watch"."build": "vue-cli-service build"
},
Copy the code
From here, you can develop the plug-in. NPM Run Serve and NPM Run Build provide commands for development and production, respectively.
Hot refresh
Both Vue and React provide module hot replacement (HMR) functionality, which greatly improves the efficiency of developing and debugging code. So we need to debug the plug-in like this:
- Open Google Chrome
add-in
page - To open developer mode, select
Load the unzipped extension
Add the plug-in file and the plug-in starts running - Save after changing the code
- Go back to the plugins panel to refresh the plugin and load the latest code
- Refresh page to target page (
content scripts
This is required) to view the changes
Can see the whole debugging process is cumbersome and repeat, the author used a lot on hot flush solution (if you have a better plan please let us know), call it hot flush, because it will be forced to refresh the page, not real hot replacement (not refresh the page) and use it after our debugging process is like this:
- Open Google Chrome
add-in
page - To open developer mode, select
Load the unzipped extension
Add the plug-in file and the plug-in starts running - Save after changing the code
- Go to the target page, the target page automatically refresh, refresh to view the changes
The hot refresh will mainly help us do the following:
- The plug-in loads the latest code
- The target page is automatically forced to refresh (for
content scripts
), apply the latest code
Hot flush is implemented in just over 50 lines of code, and it works like this:
- in
background
Add code logic (leveragebackground
Can be active in the background for a long time) - through
chrome.runtime.getPackageDirectoryEntry
Gets the plug-in’s file directory and listens for file changes - Recursively sort out all the files, and then the file name of these files plus the last time to modify the array returned
- According to the
File name plus last modified time
Changes to decide whether to refresh the page again throughsetTimeout
Intermittent recursive method of listening for file changes - The refresh mechanism is through
chrome.tabs.query
Find the current page (current active TAB) and executechrome.tabs.reload
Forcing a page refresh
Hot refresh defects:
- Automatically refresh the active page of the current browser, or if the current active page is not your target page, you need to manually refresh the target page
- If the browser is not opened for a long time after code changes, the latest code may not be loaded. You need to manually load the plug-in and refresh the page
The plug-in package
Open Google Extensions page to package vue-CLI packaged files. The first package will generate a plug-in private key (used to distinguish plug-ins) and CRX file (plug-in production environment file format, essentially is ZIP file, but Google inserted custom private fields, such as plug-in description, plug-in ID, Key, etc.)– plugin private key and CRX reference, we can use CRX (packaged into CRX NPM package) with plugin private key to package plug-in into CRX file. We added this script to the project:
// 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}<<<<<<< has been packaged)); }) .catch(err= > {
console.error(err);
});
Copy the code
Add the script we added to package.json :”build: CRX “: “NPM run build && node SRC /scripts/crx.js”
You can use the build: CRX command to package vue-CLI files into a CRX file, which improves the packaging efficiency.
Adding basic features
The above mainly focuses on the modification of VUe-CLI project, hot refresh debugging, automatic packaging and other engineering aspects of the exposition, the next mainly share some common solutions in the project.
Insert method
Content scripts mainly insert our JS into the target page, and these scripts usually insert our DOM. Such as:
This is a plugin for a web disk (now defunct and only shown here) that inserts a black-boxed button on the page, which is what Content Scripts does.
Back in the VUE project, I encapsulated a generic insertion method for converting VUE components into real DOM
import Vue from "vue";
function insert(component, insertSelector = "body") {
insertDomFactory(component, insertSelector);
}
function insertDomFactory(component, insertSelector) {
const vm = generateVueInstance(component);
generateInsertDom(insertSelector, vm);
}
// Insert the element generated by createElement into the target DOM and mount the vue instance to it
function generateInsertDom(insertSelector, vm) {
// Dom to be inserted
const insertDom = document.querySelectorAll(insertSelector);
insertDom.forEach(item= > {
const insert = document.createElement("div");
insert.id = "insert-item";
item.appendChild(insert);
vm.$mount("#insert-item");
});
}
// Generate a Vue instance
function generateVueInstance(component) {
const insertCon = Vue.extend(component);
return new insertCon();
}
export default insert;
Copy the code
Insert steps as follows:
- Pass with the incoming component
extend
Generate the constructor to instantiate thevm
return - Traverse the target selector DOM
- through
createElement
To generate adiv
Insert into the target DOM - call
vm
The instance$mount
Mount the target DOM
Next we insert our component into the page:
import App from "./App/App.vue";
import insert from "@/utils/insert";
insert(App);
Copy the code
The above insert methods are generated as new Vue. There may be multiple Vue root instances on the page. Components (except parent and child components) cannot communicate with each other using props/$emit. Blend stores into global Vue with Vuex (you can also use event Bus, of course)
// store mixin
import store from "@/store";
export default {
beforeCreate() {
this.$store = store; }};Copy the code
The global hybrid
import Vue from "vue";
Vue.mixin(stroe);
Copy the code
Each Vue component now has the ability to access the Store and communicate based on VUEX.
The request for
In the author’s plug-in project, a requirement needs to obtain the data returned by an interface on the original page, which is similar to the function of capturing data. Three solutions are provided:
-
devtools
Only DevTools can access the Chrome. Devtools API. If you enable DevTools, you can listen for interface requests on web pages
We start DevTools like this:
// Create a Panel // Configure the tabs in the F12 panel chrome.devtools.panels.create( // title "vue-chrome-extension".// iconPath null.// pagePath "panel.html" ); // Prints error logs const log = args= > chrome.devtools.inspectedWindow.eval(` console.log(The ${JSON.stringify(args)}); `); // Register a callback that is triggered after each HTTP request is responded to chrome.devtools.network.onRequestFinished.addListener(async(... args) => {try { const[{// Request type, query parameters, and URL request: { url }, // This method can be used to get the response body getContent } ] = args; if (url.indexOf("xxxx") = = =- 1) { const content = await new Promise(res= > getContent(res)); // Send the requested contentchrome.runtime.sendMessage({ content }); }}catch(err) { log(err.stack || err.toString()); }});Copy the code
The devTools page obtains the interface response entity and then sends the content out. The specific module communication can be seen here.
Disadvantages: F12 needs to be enabled
-
Retransmission request
Because the user using the plug-in is in the login state on the target page, we can use the login state (cookie) to copy the address of the target interface, and then obtain the response content through request resending. We can achieve this:
import axios from "@/utils/axios"; // Determine whether the request needs to be resent based on the custom request header function isRequestSelf(headers) { return headers.some(header= > header.name === "X-No-Rerequest"); } // Use background requests const installRequest = (a)= > { chrome.webRequest.onBeforeSendHeaders.addListener( async function(details) { if(! isRequestSelf(details.requestHeaders)) {const res = await axios.request({ method: details.method, url: details.url, // Add a custom request header to distinguish between page and plug-in requests and prevent circular requests headers: { "X-No-Rerequest": "true"}});// The response entity can then be forwarded out to communicate with other modules}}, {urls: ["https://www.baidu.com/*"[]},"blocking"."requestHeaders"]); };export default installRequest; Copy the code
Disadvantages: Resending requests consumes performance
-
Inject JS instead of Ajax objects (recommended)
The situation I encountered was harsh:
- Plug-in projects are based on
content scripts
.devtools
The way to open F12, the user is the developer may be able to understand, but for ordinary users will certainly affect the plug-in experience - use
Retransmission request
But the target interface security measures in the target site do it perfectly: the request URL has a random parameter, which is determined byThe cursor position
,The time stamp
,Height of the page
Isoparametric synthesis is unique. Although the method to find this parameter is found on the Internet, the content returned by the request is inconsistent with the original response (that is, the content of the interface is returned randomly).
The first two methods are not applicable to the actual situation of the author, the author found the final solution from the idea of request interception to request replacement. We can do this:
// inject.js let oldXHR = window.XMLHttpRequest; function filterUrl(url) { return url.indexOf("baidu.com")! = =- 1; } function newXHR() { let realXHR = new oldXHR(); realXHR.onload = function() { // Send search list page data if (filterUrl(realXHR.responseURL)) { window.postMessage({ data: realXHR.responseText }, "*"); console.log(Here is the text of the onload function request:${realXHR.responseText}`); }};return realXHR; } window.XMLHttpRequest = newXHR; Copy the code
This approach uses injected- Script. The principle is to cache the original Ajax request object of the page, add onload method to the original Ajax object, listen for the completed callback of the request, and then send the response entity of the target interface through the corresponding communication method.
Inject injected-script into the page in Content scripts
// content.js injectJS(); function injectJS() { document.addEventListener("readystatechange", () = > {const injectPath = "inject.js"; const temp = document.createElement("script"); temp.setAttribute("type"."text/javascript"); / / get the address of the similar: chrome - the extension: / / ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject. Js temp.src = chrome.extension.getURL(injectPath); document.body.appendChild(temp); }); } Copy the code
Why not use Content Scripts? See here for the difference between Content Scripts and injected- Script
The final implementation is only a few lines of code, but it provides a lot of power.
The downside of this approach is that it only works with the target page of an Ajax request and does not work if the target page uses a FETCH request. Fetch request listening can be implemented by enabling the service worker mode (I didn’t try this).
- Plug-in projects are based on
The end of the
Plug-ins have so many privileges that developers can leverage these features to provide rich functionality. The author put the template of Vue development plug-in on Github, if you can help, welcome to star✨