Разработка плагинов для браузера с помощью Vue

Vue.js

предисловие

Плагины для браузеров — это относительно нишевая область применения в области внешнего интерфейса.Chrome 插件. существуетМагазин плагинов Chromeмного有趣опять таки实用изChrome 插件,Напримерoctotree(показать дерево кода github),Adblock Plus(блокировка рекламы) и др.

Автор сейчас на связиChrome 插件Также есть год разработки, который изначально используется командой原生js+jqueryСпособ разработки плагинов, а затем рассмотреть возможность использования Vue для рефакторинга плагинов, основные причины:

  • Плагины становятся все более функциональными
  • Нативная разработка неэффективна
  • Нет модулей, не прост в обслуживании
  • Технологический стек команды смещен в сторону Vue.

Поэтому данная статья призвана поделиться мнением автора.基于vue-cli开发浏览器插件опыт инженерной практики и部分功能的思考与实现, разобратьсяVue开发插件的有关知识предоставляя想尝试浏览器插件开发разработчикVue开发插件маленькая идея. Если вы еще не знакомы с浏览器插件开发, пожалуйста, используйтеэта статьяПоймите основы разработки плагинов (вы прочитали эту статью по умолчанию), а затем попрактикуйтесь в разработке плагинов Vue.

проектирование

Модернизация vue.config.js

Основные файлы в плагине: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(Это также может быть веб-пакет) Структура упакованного файла соответствует приведенной выше структуре, мы определяем ее следующим образом.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)функция, которая значительно повышает эффективность нашего кода разработки и отладки. Затем нам нужно сделать это для отладки плагина:

  1. Откройте Google Chrome扩展程序страница
  2. Чтобы включить режим разработчика, выберите加载已解压的扩展程序, добавьте файл плагина, плагин запустится
  3. Сохранить после смены кода
  4. Вернитесь на панель плагинов, чтобы обновить плагин и загрузить последний код.
  5. Перейдите на целевую страницу и обновите страницу (content scriptsЭто обязательно), смотрите изменения

Видно, что весь процесс отладки громоздкий и повторяющийся, автор использовалgithubРешение проблемы с горячим обновлением (если есть решение получше, дайте знать), причина, по которой оно называется热刷新, потому что это вызывает обновление страницы, а не совсем热替换(без обновления страницы), наш процесс отладки после его использования выглядит так:

  1. Откройте Google Chrome扩展程序страница
  2. Чтобы включить режим разработчика, выберите加载已解压的扩展程序, добавьте файл плагина, плагин запустится
  3. Сохранить после смены кода
  4. Перейдите на целевую страницу, целевая страница автоматически обновится, и изменения будут просмотрены после завершения обновления.

热刷新В основном это поможет нам выполнить следующие задачи:

  • Плагин загружает последний код
  • Целевая страница автоматически принудительно обновляется (дляcontent scripts), примените последний код

热刷新Реализация составляет всего более 50 строк кода, а принцип таков:

  1. существуетbackgroundДобавьте логику кода (используяbackgroundФункции, которые могут быть активны в фоновом режиме в течение длительного времени)
  2. пройти черезchrome.runtime.getPackageDirectoryEntryПолучите каталог файлов плагина и отслеживайте изменения файлов
  3. Рекурсивно отсортировать все файлы, а затем вернуть имена файлов этих файлов плюс время последней модификации, чтобы сформировать массив и вернуть
  4. согласно с文件名加上上次修改时间изменения, чтобы решить, обновлять ли страницу, а затем передатьsetTimeoutМетод прерывистого рекурсивного мониторинга изменений файла
  5. Механизм обновления черезchrome.tabs.queryНайдите текущую страницу (текущая активная вкладка), выполнитеchrome.tabs.reloadПринудительное обновление страницы

热刷新дефект:

  • Автоматически обновлять текущую активную страницу браузера. Если текущая активная страница не является вашей целевой страницей обновления, вам также необходимо обновить целевую страницу вручную.
  • Если вы долго не открывали браузер после смены кода, последний код может не загрузиться, нужно вручную загрузить плагин и обновить страницу

Упаковка плагина

Открыть谷歌扩展程序页面Будуvue-cliУпакованные файлы упакованы.Первый пакет создаст файл в корневом каталоге проекта.插件私钥(для различения плагинов) иcrxФайл (формат файла рабочей среды подключаемого модуля, который по сути является ZIP-файлом, но Google вставил настраиваемые личные поля, такие как описание подключаемого модуля, идентификатор подключаемого модуля, ключ и т. д.) ---Закрытый ключ плагина и ссылка на crx, мы можем использоватьcrx(упакован как пакет npm crx) для сотрудничества插件私钥Плагины могут быть упакованы как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;

Шаги вставки:

  1. пройти с входящим компонентомextendСоздайте конструктор, который будет создавать экземплярvmвернуть
  2. Пройдите по целевому селектору
  3. пройти черезcreateElementгенерироватьdivВставить в целевой дом
  4. передачаvmПример$mountсмонтировать целевой дом

Затем вставьте наш компонент на странице:

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? пожалуйста, посмотриздесьК пониманиюcontent scriptsа такжеinjected-scriptразница

    Хотя окончательная реализация состоит всего из нескольких строк кода, предоставляемая функциональность очень мощная.

    Этот метод также имеет тот недостаток, что его можно применять только дляajaxЦелевая страница запроса, если целевая страница используетfetchзапрос, этот метод недействителен. можно включить черезservice workerспособ достиженияfetchМониторинг запросов (я не пробовал).

конец

Плагины имеют множество разрешений, и разработчики могут использовать эти функции для предоставления расширенных функций. Автор поместил шаблон плагина разработки Vue вgithubНа, если это поможет вам, добро пожаловатьstar