扩大程序是基于事件的程序,用于批改或加强 Chrome 浏览体验,如果此时你想构建一个 Chrome 扩大程序,并致力寻找一篇涵盖 Chrome 扩大程序的整个构思、构建和启动过程的文章,这里有一个综合指南,可帮忙您实现整个过程。

本文分几个局部解说 Chrome 扩大开发的全流程以及高级应用技巧,实用用最新的 v3 版本,心愿这能够节俭您浏览和综合来自多篇文章的信息的工夫,具体内容请参阅官网文档。

为什么是V3?

  1. 更高的安全性、隐衷性和性能
  2. 反对 service workers 和 promises
  3. 取得更疾速的代码审核
  4. Chrome 网上利用店不再承受 Manifest V2 扩大

为什么应用 Chrome 扩大程序?

  1. 开发门槛低

    简直反对任意的技术栈,对于前端入门用户如果想开发一个简略的用于加强用户体验的插件性能,例如高亮关键字、减少黑夜模式等会比开发传统相似性能的网站和挪动利用成本低的多,即便你不会应用 React 或者 Vue 你也能够通过 Jquery 又或者是原生 Javascript 去实现它。

  2. 覆盖范围广

    Chrome 在市场份额上以很大的劣势击败了其余浏览器,因而,优先开发 Chrome 扩大是获取下载量和流量最好的入口。插件部署后,所有 Chrome 用户都能够在Chrome 网上利用店下载你的扩大程序。

  3. u can do whtever u want

    在你的扩大程序中,你能够不必为浏览器的同源策略担心,你能够在你想要的任意网站中"侵入"你本人的代码,你能够像 React devTools 那样加强你本人的调试器,你甚至能够在你的扩大程序中治理他人的扩大程序(如防钓鱼,防沉迷等)。

Chrome Extension 架构组成

manifest.json

{   "name": "__MSG_extName__", // 国际化语法,或默认去根目录下找_locales.en(对应的语言包).message.extName   "version": "1.0.0",   "description": "__MSG_extDescription__", // 同name   "icons": {    '16': 'src/assets/icons/icon16.png',    '32': 'src/assets/icons/icon32.png',    '48': 'src/assets/icons/icon48.png',    '128': 'src/assets/icons/icon128.png'    },"background": {      "persistent": false, // 放弃后盾脚本继续流动的惟一状况是扩大应用chrome.webRequest API 来阻止或批改网络申请。webRequest API 与非持久性后盾页面不兼容。默认状况下,"persistent"设置为 true。      "scripts": [ "background.js" ]    },      "content_scripts": [ {      "js": [ 'src/content/ethers/address.tsx' ],      "matches": [ "*://etherscan.io/address/*", "*://*.bscscan.com/address/*" ]   } ],"web_accessible_resources": [    {      "matches": ['<all_urls>'],      "resources": ['src/assets/images/*.png']    }  ],  "action": {    "default_popup": 'src/popup/popup.html',    "default_icon": {      '16': 'src/assets/icons/icon16.png',      '32': 'src/assets/icons/icon32.png',      '48': 'src/assets/icons/icon48.png',      '128': 'src/assets/icons/icon128.png'    }  },  "permissions": ['storage', 'webNavigation', 'webRequest'], // 没有用到的权限不要增加,否则审核过不了  "host_permissions": [    '*://explorer.btc.com/*',    '*://etherscan.io/*',    '*://cn.etherscan.com/*',    '*://polygonscan.com/*',    '*://*.bscscan.com/*',    '*://snowtrace.io/*',    '*://optimistic.etherscan.io/*',    '*://arbiscan.io/*',    '*://ftmscan.com/*',    '*://cronoscan.com/*',    '*://*.moonscan.io/*',    'https://*.blocksec.com/*', // 本人的业务申请api域名,这样才不会跨域    'https://explorer.api.btc.com/*' // 应用webRequest监听申请信息,被监听的域名也要配置,否则不失效  ] }

配置清单有几个留神点:

  1. 以最小权限束缚插件,permissions 字段如果写了你利用中没用到(或者不必要)的权限,在提交审核时会被回绝。留神 tabs 绝大部分API是不须要申请这个 tabs 权限的。
  2. 如果波及跨域申请,须要在 host_permissions 外面配置域名,当然你也能够用 <all_urls> ,更好的重视用户隐衷的做法是捕风捉影,用到哪些列哪些,因为这个配置文件会存在用户的硬盘上,这会引起用户担心。
  3. 如果波及 webRequest 拦挡申请,比方监听用户翻页了(其实就是监听 getMore 的接口申请实现了),须要把你要监听的申请域名配置在 permissions
  4. background 中放弃后盾脚本继续流动的惟一状况是扩大应用 chrome.webRequest API 来阻止或批改网络申请。webRequest API 与非持久性后盾页面不兼容。默认状况下,"persistent"设置为 true
  5. 无论是 matchesresources 还是 host_permissions 都能够用通配符形容。
  6. 插件中用到的动态资源都须要在 web_accessible_resources 中配置。
  7. content_scripts 中的资源是依照你列表中的程序加载到页面中的,请留神依赖关系的先后顺序。

background.js

后盾脚本大部分工夫都处于休眠状态,并且蕴含仅在某些浏览器事件产生时才触发脚本的侦听器。 后盾脚本会贯通你插件利用的全生命周期,所以在这里个别用于监听 Popup 或者 content script 的事件,例如网络申请等。

import { chromeEvent } from '@common/event'import { reloadCurrentTab, isNil } from '@common/utils'import commonApi from '@common/api'import { REFRESH } from '@common/constants'/** refresh current page (usually user change the settings) */chromeEvent.on(REFRESH, () => {  reloadCurrentTab()})chromeEvent.on('custom-event-name', async params => {  try {    const { success, data, msg } = await commonApi.getXxx(params)    return {      success: success,      data: data,      message: msg    }  } catch (error) {    /** external exception */    return { success: false, data: error, message: 'error' }  }})chrome.webRequest.onCompleted.addListener(  async details => {    const { url, tabId } = details        // do something  },  { urls: [] })

popup

用户界面,点击浏览器扩大图标后展现的UI元素。

<img src="https://picgo-cloudimg.oss-cn-hangzhou.aliyuncs.com/img/Xnip2022-11-19_09-14-32.jpg" style="zoom:30%;" />

开发popup与你开发一个失常的webapp时没有任何区别,惟一须要留神的是打包的时候须要把popup.html配置到entry和output以失常拜访到这个页面,开发时目录构造相似以下所示:

content script

内容脚本读取和批改网页。它们是用 Javascript 编写的,并在网页加载时执行。例如,MetaDock 的内容脚本局部性能用于替换*scan页面中的address显示标签,如下所示:

<img src="https://picgo-cloudimg.oss-cn-hangzhou.aliyuncs.com/img/202211190931705.png" alt="image-20221119093113672" style="zoom:50%;" />

"run_at" 用于配置脚本执行机会,默认是 document_idle,此时 DOM 曾经挂载实现。另外两个可配置选项值是 document_start (dom挂载前)和 document_end (dom挂载实现后立马执行,此时其余图像和框架等子资源可能并没有加载实现)。

"all_frames" 字段容许扩大指定是否应将 JavaScript 和 CSS 文件注入到合乎指定 URL 要求的所有框架中,还是仅注入到选项卡中最顶层的框架中。

如果有内容脚本与宿主页面的通信需要请应用 window.postMessage

const port = chrome.runtime.connect();window.addEventListener("message", (event) => {  // We only accept messages from ourselves  if (event.source != window) {    return;  }  if (event.data.type && (event.data.type == "FROM_PAGE")) {    console.log("Content script received: " + event.data.text);    port.postMessage(event.data.text);  }}, false);
document.getElementById("theButton").addEventListener("click", () => {  window.postMessage({ type: "FROM_PAGE", text: "Hello from the webpage!" }, "*");}, false);

options

配置页面,在清单中配置此项会在插件邮件菜单中多一个选项的 item,options 页面与 popup 开发模式没什么区别,不多做介绍。

{  "name": "My extension",  ...  "options_ui": {    "page": "options.html",    "open_in_tab": false  },  ...}

其余业务页面

插件能够有本人域下的页面,开发这些页面跟开发多页利用的流程统一,比方你要在插件中新增一个隐衷策略页面,目录构造应该相似以下:

配置 entryoutput,以下以 viterollupOptions 为例。

rollupOptions: {  input: {    policy: 'src/pages/PrivacyPolicy/index.html'  }}

通过 chrome-extension://fkhgpeojcbhixxxxxliepkpcgcoo/src/pages/PrivacyPolicy/index.html 关上此页面。

下图摘自Google 的指南,阐明了各种文件之间的交互。发送和接管音讯是文件之间通信的要害办法,用于协调整个扩大的性能。

Chrome API

Chrome.runtime

  1. chrome.runtime.sendMessage:它容许您向事件侦听器发送一条音讯,容许不同脚本之间的交互(无奈发送到内容脚本)
  2. chrome.runtime.onMessage.addListener:监听并在从扩大过程/另一个脚本接管到音讯时触发
  3. chrome.runtime.getURL:获取插件的资源门路,个别门路往往是以 chrome-extension://fkhgpeojcbhixxxxxliepkpcgcoo 结尾,fkhgpeojcbhixxxxxliepkpcgcoo 是插件ID,这个个别不会变,在利用中你不必去保护这个ID即可取得资源的残缺门路。个别获取图片等资源时能够用chrome.runtime.getURL('/src/assets/images/logo.png')
  4. chrome.runtime.openOptionsPage:容许用户通过提供选项页面来自定义扩大的行为。用户能够通过右键单击工具栏中的扩大图标而后抉择选项或导航到扩大治理页面chrome://extensions,找到所需的扩大,单击详细信息,而后抉择选项链接来查看扩大的选项。

Chrome.tabs

如果您的扩大程序与浏览器选项卡无关,则您须要此 API。

  1. chrome.tabs.get:获取无关任何指定选项卡的详细信息(例如 URL、题目、ID、是否处于活动状态)。如果您只想在用户拜访某些网站时触发操作(例如,如果您的扩大程序是特定于网站的),这将很有用。
  2. chrome.tabs.getCurrent : 获取以后标签的详细信息
  3. chrome.tabs.sendMessage:将音讯发送到指定选项卡的内容脚本,并在发送回响应时运行可选的回调
  4. chrome.tabs.create:创立一个新标签页(你能够指定一个 URL)
  5. chrome.tabs.reload:刷新选项卡页面
  6. chrome.tabs.query:获取选项卡相干

chrome.webRequest

应用chrome.webRequestAPI 察看和剖析流量并拦挡、阻止或批改运行中的申请。

生命周期中每个勾子都能被监听到。更多细节请参考文档。

开发调试

插件中的页面如 popupoptions ,还有后盾脚本等调试工具是跟你以后浏览器的调试工具(F12)独立的,如果须要调试元素和网络申请等控制台信息,请在 popup 面板上右键查看,留神:每从新开一个 tab 都须要从新执行上述步骤。

对于 content script 热更新调试问题整顿了以下两个计划,都有尝试,举荐第二种计划。

  1. webpack 版本解决方案(简单插件开发过程不稳固,提早高)

    • 配置 webpack server,将 bundle 写到磁盘。
    • 通过 webpack plugin 裸露 compiler 对象。
    • 为 webpack server 减少中间件,拦挡 reload 申请,转化为 SSE,compiler 注册编译实现的钩子,在回调函数中通过 SSE 发送音讯。
    • chrome extension 启动后,background 与 webpack server 建设连贯,监听 reload 办法,收到 server 的告诉后,执行 chrome 自身的 reload 办法,实现更新。
  2. CRXJS Vite 插件(举荐)

CRXJS Vite 插件应用技巧

额定的 HTML 页面

扩大程序蕴含您无奈在清单中申明的网页是很常见的。例如,您可能心愿在用户登录后更改弹出窗口或在用户装置扩大程序时关上欢送页面。此外,像 React Developer Tools 这样的开发工具扩大不会在清单中申明它们的查看器面板。

给定以下文件构造和清单,index.html并将src/panel.html在开发期间可用,但在生产构建中不可用。咱们能够在vite.config.ts

.├── vite.config.ts├── manifest.json├── index.html└── src/    ├── devtools.html    └── panel.html
// manifest.json{  "manifest_version": 3,  "version": "1.0.0",  "name": "example",  "devtools_page": "src/devtools.html"}
// vite.config.jsimport { resolve } from 'path';import { defineConfig } from 'vite';import { crx } from '@crxjs/vite-plugin';import manifest from './manifest.json';export default defineConfig({  build: {    rollupOptions: {      // add any html pages here      input: {        // output file at '/index.html'        welcome: resolve(__dirname, 'index.html'),        // output file at '/src/panel.html'        panel: resolve(__dirname, 'src/panel.html'),      },    },  },  plugins: [crx({ manifest })],  });

应用动静清单文件

设想一下,如何将 manifest.json 中的版本号跟 package.json 中的版本对立?

Vite 插件提供了一个defineManifest与 Vite 性能相似的defineConfig性能,并提供了 IntelliSense,能够轻松地在构建时扩大你的清单。

// manifest.config.tsimport { defineManifest } from '@crxjs/vite-plugin'import { version } from './package.json'const names = {  build: 'My Extension',  serve: '[INTERNAL] My Extension'}// import to `vite.config.ts`export default defineManifest((config, env) => ({  manifest_version: 3,  name: names[env.command],  version,}))

图标和公共资源

你能够参考清单中的公共文件。如果 CRXJS 在其中没有找到匹配的文件,它将查找绝对于Vite 我的项目根目录的文件并将资产增加到输入文件中。

你能够将这些动态资源对立搁置在 public 目录中治理。

Web Accessible Resources

你每次应用一个图片都要手动去更新到清单,这个过程可能会让咱们感觉比拟繁琐。咱们正在应用构建工具,那么为什么要做不必要的手动工作呢?当你将图像导入内容脚本时,CRXJS 会自动更新清单。✨

import logoPath from './logo.png'const logo = document.createElement('img')logo.src = chrome.runtime.getURL(logo)

动静内容脚本

亲测门路解析没问题,然而我去 executeScript 次门路的脚本时没有反馈,兴许是版本bug,兴许是我应用有误,请自行判断

可能会遇到一个场景:比方你在 background.js 监听某个网络申请,命中后想执行一遍你的某个 content script,这时你须要在 executeScript 中写脚本的门路能力正确执行,然而你并不知道打包后的脚本门路(打包后的门路你并不关怀或者文件名是通过哈希解决的),这时候动静内容脚本就派上用场了。

CRXJS 应用惟一的导入查问来指定导入指向内容脚本。当导入名称以查问结尾时?script,默认导出是内容脚本的输入文件名。

import scriptPath from './content-script?script'chrome.action.onClicked.addListener((tab) => {    chrome.scripting.executeScript({    target: { tabId: tab.id },    files: [scriptPath]  });});

应用技巧

  1. 举荐应用 fetch 进行网络申请,能够应用ky包缩小累赘,另外 axios 我记得在插件开发中有额定的须要配置或者坑。
  2. 所有工夫监听包含申请举荐在 background.js 中对立解决,应用音讯通信传递后果到其余脚本/页面中。
  3. 消息传递如果是其余 -> background.js 能够应用 chrome-extension-coreEvent 进行传递,敌对的反对了 typescript,应用形式大抵如下:

    // common/event.js// 在全局的event文件中治理所有的事件定义,包含参数束缚import { Event } from 'chrome-extension-core'import { SCOPE } from '@common/constants'import type { PostXXXParams } from '@common/api/types'import type { REFRESH, GET_XXX } from '@common/constants/event'type EventInfo = {  [REFRESH]: boolean  [GET_XXX]: PostXXXParams}export const chromeEvent = new Event<EventInfo>(SCOPE)
    // content scriptconst res = await chromeEvent.emit<typeof GET_XXXX, Response>(  GET_XXX,  {    name: 'ghostwang'  })
    // background.jschromeEvent.on(GET_XXX, async params => {  try {    const { success, data, msg } = await commonApi.getXxx(params)    return {      success: success,      data,      message: msg    }  } catch (error) {    return { success: false, data: error, message: 'error' }  }})
  4. background.js 被动给其余发消息,留神必须要解决异步,不然会在插件面板报错(尽管这个谬误可能不影响你的性能)

    // 后盾脚本发送逻辑chrome.tabs.sendMessage(tabId, EXECUTE_XXX_SCRIPT, function () {  /** 留神:以下代码不要删 */  if (!chrome.runtime.lastError) {    // 如果你有任何回应  }})// 内容脚本监听逻辑chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {  if (message === EXECUTE_XXX_SCRIPT) {    run()    sendResponse()  }})
  5. 长久化存储能够封装自定义的 hooks 并实现 监听逻辑。

    import { useCallback, useEffect, useState } from 'react'import { store, defaultValue, type StorageInfo } from '@src/store'/** * 长久化存储store */export default function useStore<Key extends keyof StorageInfo>(  key: Key): [StorageInfo[Key], (newValue: StorageInfo[Key]) => Promise<void>] {  const [value, setValue] = useState<StorageInfo[Key]>(defaultValue[key])  useEffect(() => {    const getStore = async () => {      const currentStoreValue = await store.get(key)      setValue(currentStoreValue)    }    const storeWatcher = (      data: Record<keyof StorageInfo, chrome.storage.StorageChange>    ) => {      if (data[key]) {        const changedData = data[key]        setValue(changedData.newValue)      }    }    store.addWatcher(storeWatcher)    getStore()    return () => {      store.removeWatcher(storeWatcher)    }  }, [key])  const setStore = useCallback(    (newValue: StorageInfo[Key]) => {      return store.set(key, newValue)    },    [key]  )  return [value, setStore]}
    import type { WatcherCallback } from 'chrome-extension-core'import { useEffect } from 'react'import type { StorageInfo } from '@src/store'import { store } from '@src/store'export default function useStoreWatcher(  callback: WatcherCallback<StorageInfo>,  deps?: (keyof StorageInfo)[]) {  useEffect(() => {    const storeWatcher = (      data: Record<keyof StorageInfo, chrome.storage.StorageChange>    ) => {      const dataKeys = Object.keys(data)      if (!deps || deps.some(val => dataKeys.includes(val))) {        callback(data)      }    }    store.addWatcher(storeWatcher)    return () => {      store.removeWatcher(storeWatcher)    }  }, [callback, deps])}
  6. 内容脚本加载字体等资源比拟耗时,因为自身执行机会可能就是在宿主网页资源加载后,所以尽量避免用 iconfont ,应用小图片代替会更快。
  7. 插件的本地化无奈提供动静切换的性能,所以如果要做国际化,还得写两套,插件反对的国际化性能实用于插件市场展现你的插件利用的名称和形容信息,popup 等页面须要通过 i18next 实现动静切换语言,所以,你的目录构造应该相似:

Chrome 内置的国际化目录必须在根目录下,能够配合 vitevite-plugin-files-copy 插件实现:

CopyPlugin({  patterns: [    {      from: './src/_locales/chrome',      to: 'dist/_locales'    }  ]})

写在最初

举荐一个专门为 Web3 用户服务的 Chorme 浏览器插件 —— MetaDock

产品性能:
装置插件后,在大家拜访 EtherscanBscScanBTC.com 等区块链浏览器时,提供多项加强性能,间接展现在页面中。目前已实现地址标签、危险评分,资金流向图,合约代码下载,疾速在 Phalcon 中关上交易,跳转到多款区块链浏览器等性能。

隐衷爱护:
MetaDock 有严格隐衷爱护,只会在指定的区块链浏览器网站上运行(能够在配置中敞开),不会在其余网站上运行,不会上传自定义信息。请大家放心使用 :)