往日星尘

Celestial Echoes of Yesteryears

浏览器插件开发简易指南0

最近想参与一个开源项目,用于 moji 词典的点击查询日语单词,比官方的用的顺手一些。用着还不错但发现年久失修。奇怪的是 xlog 的 rss 不能和 ai 总结结合在一起,这下要凑下字数了。

本篇只展示 Vite/Webpack+React 在 Chrome 浏览器中的使用:

你可以在 Chrome 浏览器地址栏键入chrome://extensions/来查看你现在安装的所有插件,

另外请保证右上角的开发者模式是打开的:

image

💻 开发环境#

一般#

IDE: Visual Studio Code
React: v18+
Vite: v3+
Vite: v4.2.0
Webpack: v5.76.2

我使用了pnpm来管理本机的所有依赖,如果没有或者使用其他包管理工具的可以自行替换pnpm为对应命令!

其他配合食用的#

  • TailwindCSS
  • TypeScript

📦打包工具 (Module Bundler)#

原生开发 (VanillaJS)#

直接参考官方给的这些 demo 即可:

Webpack#

Vite (推荐)#

如何使用这个库?clone 后安装包依赖pnpm install,然后启动开发模式:

pnpm dev: 这是开发模式
pnpm build: 这是生产模式

以上命令生成的代码都会在 build 目录下,因此只需要到chrome://extension目录载入一下这个目录就能边开发边看结果了:

image

📑官方文档#

https://developer.chrome.com/docs/extensions/

👟前置准备#

生命周期#

  • onInstalled: 安装、更新或重新加载插件触发
  • onSuspend: 插件即将被挂起触发
  • onSuspendCanceled: 插件被挂起取消时触发
  • onUpdateAvailable:插件可更新时(可以用于提醒用户)触发
  • onStartup: Chrome 启动并加载扩展时触发
  • onConnect 事件: 插件与 Chrome 的另一部分(例如内容脚本)建立连接时,将触发
  • onConnectExternal:当来自外部应用程序(例如本机应用程序)的连接建立时触发
  • onMessage: 当从 Chrome 的另一部分(例如内容脚本)接收到消息时,将触发 onMessage 事件

如何在代码中使用它?

chrome.runtime.XX XX 为上述对应的一个生命周期名称,如:

chrome.runtime.onInstalled.addListener((details) => {
  console.log('Extension installed:', details.reason);
});

插件配置 manifest.json#

从这点就有很多和 manifest v2 版本很不同的地方,比较重要的是 manifest v2 的直接修改 don 机制被替换成 service worker,大意是不用他的时候该 service worker 会被 chrome 选择性屏蔽,但我们仍然可以通过其他方式操纵 dom。

详细解释: https://developer.chrome.com/docs/extensions/mv3/service_workers/

一个不太复杂的 manifest 配置文件长这样

{
	"manifest_version": 3,
	"name": "Life Helper",
	"version": "1.0.0",
	"description": "The things we must know in our life.",
	"icons": {
		"16": "icon.png",
		"48": "icon.png",
		"128": "icon.png"
	},
	"action": {
		"default_popup": "popup.html",
		"default_title": "Popup"
	},
	"permissions": [
		"scripting",
		"bookmarks"
	],
	"background": {
		"service_worker": "background.js"
	},
	"options_page": "options.html"
}

manifest.json的格式请:https://developer.chrome.com/docs/extensions/mv3/manifest/

我们需要理解的只有

  • permissions 开启的权限就对应了你在 background.js 能够调用的 api,如bookmarks说明你可以用 chrome.runtime 下面的方法

  • actions 主要规定弹出层窗口的位置名称等

    • popup 点击插件图标弹出的页面
      image
  • background 对应 service_worker 的承载对象,可以理解为插件后端 / 服务端

  • contentScript 前端脚本的位置,这里直接对应 react 的程序入口 (app.tsx->index.tsx),之后所有逻辑 & 样式 (popup, options, newTab) 都参照 react 的组织形式即可

开发流程#

各模块的分工如下:

image

简单来说,所有之前交由后端的工作都由background来完成,所有之前交由前端的都由contentScript负责的options, newTab, popup来完成。

这也意味着,当你想做状态管理时,也能很好地遵循原子性原则组织各自的状态(M)和样式(V)、类型(M)等。

通信#

通信在这里并没有明确的方向性,甚至也没有文件、模块等限制,需要就用。

但根据我们先前规定各模块大致的工作范畴,作为前端在通讯时最好也是来沟通各组件的样式、API 调用、用户事件,而后端在通讯时优先考虑更好地组织数据、构建 API 或执行外部请求。

基本格式如下:

// 发送消息,并回调
chrome.runtime.sendMessage({greeting: "hello"})
  .then(response => console.log(response.farewell))
  .catch(error => console.error(error));

// 接收消息,并回调
chrome.runtime.onMessage.addListener(function(request, sender) {
  console.log(sender.tab ?
              "from a content script:" + sender.tab.url :
              "from the extension");
  if (request.greeting == "hello") {
    return Promise.resolve({farewell: "goodbye"});
  }
});

消息发送和接受可以筛选 tab id,并指定类型,假设我们想从 popup 往 background 发送消息,并指定当前活动标签页:

全局状态管理和插件数据存储#

快使用 Redux Toolkit!

时至今日,原生的 Redux 不再被推荐,新的 Redux Toolkit 也可以帮助我们更好结合现有模块作出分工,有类似pinia的用法:

我们首先初始化整体的,根据先前我们的分工,这里最好在后端(即 background 模块)来完成初始化和服务的构建。

//store.ts
import { configureStore } from '@reduxjs/toolkit';
import { slice1, slice2 } from './slices';

export default configureStore({
  reducer: {
    slice1: slice1.reducer,
    slice2: slice2.reducer,
  },
});
export default store;

对于各自的 slice,定义如下:

import { createSlice } from '@reduxjs/toolkit';

// 定义初始值
const initialState1 = {
  value: 0,
};

// 定义第一个slice
export const slice1 = createSlice({
  name: 'slice1',
  initialState: initialState1,
  reducers: {
    increment1: state => {
      state.value += 1;
    },
    decrement1: state => {
      state.value -= 1;
    },
  },
});
export default slice1.reducer;

和 chrome.storage 结合保证状态持久#

这里需要安装两个库redux-persist-storage-chromeredux-persist,然后做如下改动:

store.ts中改动如下:

import { configureStore } from '@reduxjs/toolkit'
import { chromeStorage } from 'redux-persist-storage-chrome';
import { persistReducer } from 'redux-persist';
import { slice1, slice2 } from './slices';

const persistConfig = {
	key: 'chrome-extension-extended',
	storage: chromeStorage,
}


export const store = configureStore({
  reducer: {
    slice1: persistReducer(persistConfig, slice1.reducer),
    slice2: persistReducer(persistConfig, slice2.reducer),
  },
})

export default store;

调试#

如果你使用了 HMR 或者其他形式的热重载, 应该要关心的就只是 background 或 contentScript 本身发出的消息。

  • 监视 background 脚本的方法:

chrome://extensions/页的插件程序直接点击 service worker,会弹出单独的调试窗口

image

  • 监视 options, popup 或者 new tab 等页面:

直接在对应的页面右键->inspect ,会弹出单独的调试窗口

总结#

这篇给完全没接触过浏览器插件的小伙伴做一个快速指南,其实我也是刚入门,不过好在官方给了很多 sample,网上也有大量的现成教程和 boilderplates 来上手。

预告#

下篇会尝试研究 chrome-extension-boilerplate-react-vite 项目的 rollup 和 ws 结合过程。(这个已经不属于插件开发的范畴了)

下篇会研究其他浏览器的插件与拓展其他 boilderplate 在 Vue3 上的使用。

(05/08 更新:)

antfu 的浏览器 extension 模板很好用。但在 firefox 上行不通,问题在于 web-ext 且Firefox 浏览器对 manifestV3 的支持仍不稳定

(06/01 更新:)

经评论区 Diy.God 推荐了浏览器拓展专用的 SDK (用来帮助整合资源和打包的引擎,这样前端写写写就好了)尝试下发现很好用,plasmo 。可以覆盖上面提到所有 boilerplate template 的功能 le,推荐!

🧩参考#

https://dev.to/anobjectisa/how-to-build-a-chrome-extension-new-manifest-v3-5edk

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。