一种开发 Chrome 扩展程序的新姿势 admin 2023-02-13 15:33:02 篇首语:本文由小编为大家整理,主要介绍了一种开发 Chrome 扩展程序的新姿势相关的知识,希望对你有一定的参考价值。 我们在日常工作和生活中经常会使用到各种各样的 Chrome 扩展程序,比如 1Password 能自动帮你填充密码,Adblock 能帮你拦截广告,又或者是开发时经常会使用的 React Developer Tools,Redux DevTools 等等。这些扩展程序对效率的提升是巨大的。在日常开发过程中,我们也可以尝试开发自己的扩展程序,来辅助提升团队的开发体验和效率。本文首先会展示一个实际开发扩展程序的例子,从而让读者体会到当前开发时存在的问题,并基于此提出解决方案。如果你已经对如何开发一个扩展程序很熟悉了,那么直接看下文中【新的开发方式】部分,或者直接浏览对应的框架—— browser-extension-kit(https://github.com/alibaba/browser-extension-kit)即可。)分发到对应的模块中,由这些模块处理具体的业务逻辑,整个流程和 Redux 很像:这里使用了 rxjs,很适合处理类似消息这样的场景,当然不用也行,这不是本文的重点import fromEventPattern, merge from "rxjs";import module1Processer, observable1 from "./module1";import module2Processer, observable2 from "./module2";// 为了保存不同执行环境发来的消息,考虑到每个 port 生成时机、消息流向等不同,不得不保存外部变量let devtoolPort: chrome.runtime.Port | undefined;let contentScript: chrome.runtime.Port | undefined;let pageScriptPort: chrome.runtime.Port | undefined;// 以下 2 个 fromEventPattern,分别处理内部、外部消息fromEventPattern( handler => chrome.runtime.onConnect.addListener(handler), handler => chrome.runtime.onConnect.removeListener(handler)).subscribe(port => if (port.name === "devtool") devtoolPort = port; else //... port.onMessage.addListener(message => switch (message.type) // 分发消息到具体的 module case module1: module1Processer() case module2: module2Processer() );)fromEventPattern( handler => chrome.runtime.onConnectExternal.addListener(handler), handler => chrome.runtime.onConnectExternal.removeListener(handler)).subscribe(port => if (port.name === "devtool") devtoolPort = port; else //... //...)// 有些功能需要整合多个 module 内的数据,放在这里处理merge(observable1, observable1).pipe(...).subscribe(...)而在 UI 部分(如 popup )中,不出意外肯定需要调用 background 的能力,并且监听来自 background 的消息。如果使用 React 来写 UI 的话,我们可能会将这部分功能包装到顶层 Context 里,向下传递:import createContext, useState, useRef from "react";// 手动建立连接const port = chrome.runtime.connect( name: BACKGROUND_MESSAGE_NAME );const defaultStore: Store = // 功能 1 的 state module1: ..., // 功能 2 的 state module2: ..., // 类似 Redux 中的 dispatch,用来传递消息,发送事件 dispatch: // 功能 1 的 action module1: (payload) => port.postMessage( type: MessageType.emit, payload: ...payload, domain: "module1" ); , // ... ;export const Context = createContext(defaultStore);const ContextProvider = () => const [store, setStore] = useState(defaultStore); const storeRef = useRef(defaultStore); useEffect(() => port.onMessage.addListener(msg => // 手动监听事件,根据不同的事件更新不同的 state switch (msg.type) case MessageType.storeChanged: storeRef.current = ...storeRef.current, [msg.payload.domain]: msg.payload.store, ; setStore(storeRef.current); break; default: break; ); // eslint-disable-next-line react-hooks/exhaustive-deps , []); return ( </Context.Provider> );;export default ContextProvider;可以看到,在这种代码组织方式下,一旦功能变的复杂起来,会变的很难维护。而每当新增一个功能模块时,都少不了一大堆模板代码(监听消息并派发、UI 中初始化对应 store 等)。类比一下的话,我们在写原生 Redux 时不爽的地方,就是这里会感到不爽的地方。存在的问题通过刚才的例子,我们可以看出,在开发 Chrome 扩展程序时,处理不同执行环境下的消息流转是一件很麻烦的事。事实上,执行环境还有很多:backgroundpopupcontent-scriptpage-scriptdevtools这些环境之间互相隔离,通信只能依靠 message。在实际开发过程中,我们遇到了 2 类问题:框架能力缺失:社区中没有针对开发扩展程序的最佳实践,自然也就没有对应的框架能力,导致每个项目、模块之间重复建设或者逻辑混乱,例如如下这些常用模块或功能:监控埋点日志鉴权、用户信息消息机制实际经验缺失:由于不是经常会开发到的场景,在初次上手时踩坑是在所难免的,例如:content-script 插入带 iframe 的页面时执行环境错乱(this 指向问题,意外创建多实例等)Chrome API 在不同执行环境下可用性差异大这里需要特别强调一下消息机制的问题,这是目前开发扩展程序面临的最大的问题。Chrome 扩展程序本身提供了基于postMessage的消息传递机制,在实际开发中遇到如下问题:不同执行环境之间,需要区分内部消息和外部消息(如 page-script 和 background 通信),他们的 API 存在差异建立连接的顺序:主动建立连接的一方应该是生命周期较短的一方,但是生命周期在不同的场景下可能完全相反,例如 devtool 和 page-script 之间,完全取决于 devtool 的打开关闭时机并不是任意 2 个执行环境之间都可以直接建立连接,例如 page-script -> content-script -> background -> devtool,当扩展程序慢慢变的复杂起来之后,消息传递路径不明确,排查困难message 传递方式有多种,需要根据实际情况区分使用,例如 page-script -> content-script,即可以使用 chrome 的 API(需要 background 中转),也可以直接使用 window.postMessage单条 message 最大支持的数据大小是有限的,这在大部分开发场景下不是问题,但是如果扩展程序承载的功能对此有要求,那就比较麻烦了message 序列化:同样是一些特殊场景下凸显的问题,消息只能传递可被序列化的数据,导致该场景下需要认真考虑序列化方案,否则带来严重的性能问题,例如:循环引用内存占用嵌套类型(object/Map/Set/Regexp/Funtion)新的开发方式扩展程序中不同执行环境相互隔离这一点无法改变,但是,在每个执行环境中,开发中应该专注于自己的逻辑,不用关心事件注册、逻辑派发、公共方法调用等这些通用能力,这些东西都应该交给框架去做。总体上来说,新的方式下,业务逻辑通过继承特定的类,获取到父类上的公共方法,这些公共方法中包装好了关于消息、日志等的逻辑,可供直接使用。基于这个想法,我们可以设计出新的编写形式:module1/background.tsimport rxjs from "rxjs";import Background from "browser-extension-kit/background";import subject, observable from "browser-extension-kit";export default class MyBackground extends Background constructor() super(); // 对 myObservable1$ 额外的处理逻辑 this.myObservable1$.subscribe(data => // do something with data ); // 订阅消息 this.on("messageID", message => // ... ); // 接受外界传来的 id 为 "uniqueID" 的消息,并且自动转化为 mySubject 的下一个值 @subject("uniqueID") private mySubject = new rxjs.Subject<number>(); // 通过 @observable.popup 来声明当 myObservable1$ 发出一个值时,自动通过消息传递给 popup // 这个消息的默认 id 为 "MyBackground::myObservable1$",可自定义 @observable.popup private myObservable1$ = rxjs.from(...).pipe( rxjs.shareReplay(1) ); // 当多个环境都需要订阅这个消息时,可以使用 @observable(["background", "popup"]) @observable(["background", "popup"]) private myObservable2$ = rxjs.concat( rxjs.from(...), this.mySubject ).pipe( rxjs.shareReplay(1) );module1/popup.tsx在 popup、devtools 等环境中,通常都是一个 React 组件,这种情况下,框架提供了相应的 hooks 来帮助实现逻辑:import React from "react";import useMessage, usePostMessage from "browser-extension-kit/popup";const App = () => const active = useMessage("MyBackground::active$", null); const port = usePostMessage(); const toggleActive = useCallback(() => port.background("MyBackground::active", !active); , [port]); return ( active: active click me</button> p> );;export default App;实现方式可以猜到,如果要使用上述新的开发方式,核心是要在各个基类中内置处理消息的逻辑,background 负责所有消息的接受和转发,这一点依然不变,对外只暴露处理好的消息即可:首先,每当 content-script/page-script/devtools/popup 加载时,框架会自动向 background 建立连接,每一个 context 下,只会建立一次连接,连接建立后的 port 会存储在中心化的 port hub 内针对每个 port,系统会自动对其订阅消息,这里的 port 和 port 内的 message,全部由框架接管,不对外暴露有的时候,开发者希望向某个 context 发送消息,但是对应的 context 还没有加载,考虑到这种情况,框架内部使用 rxjs 的 ReplaySubject 来中转 port 产生的 message,这样保证当一个 context 加载后,可以立即获取到加载前向这个 context 发送的消息background 还会生成一个 dispatcher,用来分发消息。由于所有的消息都汇集到 background 中,dispatcher 会检查每个消息的接受方,如果是 background ,则直接派发给对应的处理者,如果是其他 context,则调用 port 将该消息传递给对应的 context最后,每个 context 初始化时,也会生成一个 controller,这个 controller 订阅 ReplaySubject, 用来真正处理属于自己的 message总结事实上,在这种方案下,所有的消息都会由 background 中转一下,在一定程度上造成了性能损失,这也算是在简洁和性能之间的一种取舍。而对于 devtools 和 popup 这 2 个 UI 的部分,目前只考虑了在 React 下的场景。此外,虽然监控、埋点、鉴权等也是较为通用的功能,但是不同的业务也存在着不同的诉求,所以这一部分无法集成到框架中,需要业务自行实现。如果符合你的口味,可以直接安装体验一下:npm i browser-extension-kit -S// oryarn add browser-extension-kit更多使用方式和 API 可在 https://github.com/alibaba/browser-extension-kit 查看,如果感兴趣,欢迎一起交流。关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向 以上是关于一种开发 Chrome 扩展程序的新姿势的主要内容,如果未能解决你的问题,请参考以下文章 进程间通信--system V共享内存 云原生使用Dockerfile制作openGauss镜像 您可能还会对下面的文章感兴趣: 相关文章 浏览器打不开网址提示“ERR_CONNECTION_TIMED_OUT”错误代码的解决方法 如何安装ocx控件 VMware的虚拟机为啥ip地址老是自动变化 vbyone和EDP区别 linux/debian到底怎么重启和关机 苹果平板键盘被弄到上方去了,如何调回正常? 机器学习常用距离度量 如何查看kindle型号