js-rpc2 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -355,4 +355,56 @@ async function postPortMessage() {
355
355
  let ret = await rpc.hello('6667');
356
356
  console.info("ret from rpc:", ret);
357
357
  }
358
+ ```
359
+
360
+ ## chrome extensions
361
+ ```js
362
+ import {createRpcServerChromeExtensions, createRpcClientChromeExtensions} from 'js-rpc2/src/lib.js'
363
+
364
+ // background.js
365
+ export class RpcBackgroundApi {
366
+ /**
367
+ * @param {(progress: string, date: Date) => Promise<void>} cb
368
+ */
369
+ async callback(cb) {
370
+ console.info('callback in background', 'cb')
371
+ for (let i = 0; i < 10; i++) {
372
+ await sleep(1000)
373
+ await cb(`from background index is ${i}`, new Date())
374
+ }
375
+ return 'over!'
376
+ }
377
+ }
378
+ createRpcServerChromeExtensions({ chrome, key: 'rpc-popup->background', extension: new RpcBackgroundApi(), })
379
+ createRpcServerChromeExtensions({ chrome, key: 'rpc-content-scripts->background', extension: new RpcBackgroundApi(), })
380
+
381
+
382
+ // content_scripts.js
383
+ export class RpcContentScriptsApi {
384
+ async callBackgroundWithCallback(name, callback) {
385
+ console.info('name', name)
386
+ return await rpcContentScriptBackground.callback(callback)
387
+ }
388
+ }
389
+ createRpcServerChromeExtensions({ chrome, key: 'rpc-popup->content-script', extension: new RpcContentScriptsApi() })
390
+ /** @type{RpcBackgroundApi} */
391
+ export const rpcContentScriptBackground = createRpcClientChromeExtensions({ chrome, key: 'rpc-content-scripts->background' })
392
+
393
+
394
+ // popup.js
395
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
396
+ /** @type{RpcContentScriptsApi} */
397
+ export const rpcPopupContentScripts = createRpcClientChromeExtensions({ chrome, key: 'rpc-popup->content-script', tabId: tab.id, })
398
+ /** @type{RpcBackgroundApi} */
399
+ export const rpcPopupBackground = createRpcClientChromeExtensions({ chrome, key: 'rpc-popup->background' })
400
+
401
+ let resp = await rpcPopupContentScripts.callBackgroundWithCallback("name", async (progress, date) => {
402
+ console.info('at popup callback ',progress, date)
403
+ })
404
+ console.info("over!", resp)
405
+
406
+ let resp = await rpcPopupBackground.callback(async (progress, date) => {
407
+ console.info('at popup callback ',progress, date)
408
+ })
409
+ console.info("over!", resp)
358
410
  ```
@@ -0,0 +1,133 @@
1
+ # Chrome Extensions RPC 通信使用说明
2
+
3
+ ## 概述
4
+
5
+ 本项目使用 `js-rpc2` 库实现在 Chrome 扩展各组件间的远程过程调用(RPC)通信,支持在 popup、content scripts 和 background 之间进行双向通信。
6
+
7
+ ## 项目结构
8
+
9
+ ### 组件说明
10
+
11
+ - **background.js**: 后台脚本,提供核心服务接口
12
+ - **content_scripts.js**: 内容脚本,运行在网页上下文中,可作为通信中继
13
+ - **popup.js**: 扩展弹窗界面,用户交互入口
14
+
15
+ ## 核心实现
16
+
17
+ ### Background 脚本
18
+
19
+ ```javascript
20
+ import {createRpcServerChromeExtensions} from 'js-rpc2/src/lib.js'
21
+
22
+ export class RpcBackgroundApi {
23
+ /**
24
+ * 带回调的示例方法
25
+ * @param {(progress: string, date: Date) => Promise<void>} cb - 回调函数
26
+ */
27
+ async callback(cb) {
28
+ for (let i = 0; i < 10; i++) {
29
+ await sleep(1000)
30
+ await cb(`from background index is ${i}`, new Date())
31
+ }
32
+ return 'over!'
33
+ }
34
+ }
35
+
36
+ // 创建RPC服务端点
37
+ createRpcServerChromeExtensions({
38
+ chrome,
39
+ key: 'rpc-popup->background',
40
+ extension: new RpcBackgroundApi()
41
+ })
42
+
43
+ createRpcServerChromeExtensions({
44
+ chrome,
45
+ key: 'rpc-content-scripts->background',
46
+ extension: new RpcBackgroundApi()
47
+ })
48
+ ```
49
+
50
+ ### Content Scripts 脚本
51
+
52
+ ```javascript
53
+ import {createRpcServerChromeExtensions, createRpcClientChromeExtensions} from 'js-rpc2/src/lib.js'
54
+
55
+ export class RpcContentScriptsApi {
56
+ async callBackgroundWithCallback(name, callback) {
57
+ return await rpcContentScriptBackground.callback(callback)
58
+ }
59
+ }
60
+
61
+ // 创建RPC服务端点供popup调用
62
+ createRpcServerChromeExtensions({
63
+ chrome,
64
+ key: 'rpc-popup->content-script',
65
+ extension: new RpcContentScriptsApi()
66
+ })
67
+
68
+ // 创建RPC客户端连接到background
69
+ export const rpcContentScriptBackground = createRpcClientChromeExtensions({
70
+ chrome,
71
+ key: 'rpc-content-scripts->background'
72
+ })
73
+ ```
74
+
75
+ ### Popup 脚本
76
+
77
+ ```javascript
78
+ import {createRpcClientChromeExtensions} from 'js-rpc2/src/lib.js'
79
+
80
+ // 获取当前活动标签页
81
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
82
+
83
+ // 创建RPC客户端
84
+ export const rpcPopupContentScripts = createRpcClientChromeExtensions({
85
+ chrome,
86
+ key: 'rpc-popup->content-script',
87
+ tabId: tab.id
88
+ })
89
+
90
+ export const rpcPopupBackground = createRpcClientChromeExtensions({
91
+ chrome,
92
+ key: 'rpc-popup->background'
93
+ })
94
+
95
+ // 使用示例
96
+ let resp = await rpcPopupContentScripts.callBackgroundWithCallback("name", async (progress, date) => {
97
+ console.info('at popup callback', progress, date)
98
+ })
99
+ console.info("over!", resp)
100
+
101
+ let resp2 = await rpcPopupBackground.callback(async (progress, date) => {
102
+ console.info('at popup callback', progress, date)
103
+ })
104
+ console.info("over!", resp2)
105
+ ```
106
+
107
+ ## 通信模式
108
+
109
+ ### 直接通信
110
+ Popup → Background
111
+ ```javascript
112
+ rpcPopupBackground.callback(callbackFunction)
113
+ ```
114
+
115
+ ### 间接通信
116
+ Popup → Content Script → Background
117
+ ```javascript
118
+ rpcPopupContentScripts.callBackgroundWithCallback(name, callbackFunction)
119
+ ```
120
+
121
+ ## 使用方法
122
+
123
+ 1. 在各组件中导入相应的 RPC 创建函数
124
+ 2. 定义服务接口类
125
+ 3. 使用 `createRpcServerChromeExtensions` 创建服务端点
126
+ 4. 使用 `createRpcClientChromeExtensions` 创建客户端连接
127
+ 5. 通过客户端实例调用远程方法
128
+
129
+ ## 注意事项
130
+
131
+ - 确保 manifest.json 中正确配置了各组件的权限和通信策略
132
+ - 注意处理异步回调的生命周期管理
133
+ - 建议为所有 RPC 接口添加适当的错误处理机制
@@ -0,0 +1,138 @@
1
+ # Electron RPC 通信使用说明
2
+
3
+ ## 概述
4
+
5
+ 本文档介绍如何在 Electron 应用中使用 `js-rpc2` 库实现主进程与渲染进程之间的 RPC 通信。通过 MessagePort 机制,实现跨进程的远程方法调用。
6
+
7
+ ## 项目结构
8
+
9
+ ### 组件说明
10
+
11
+ - **main.js**: Electron 主进程,提供核心服务接口
12
+ - **preload.js**: 预加载脚本,建立主进程与渲染进程的通信桥梁
13
+ - **renderer.js**: 渲染进程,运行在浏览器环境中
14
+ - **usage.js**: 使用示例,展示如何调用 RPC 方法
15
+
16
+ ## 核心实现
17
+
18
+ ### 主进程 (main.js)
19
+
20
+ ```javascript
21
+ import { ipcMain } from 'electron'
22
+ import { createRpcServerElectronMessagePort } from 'js-rpc2/src/lib.js'
23
+
24
+ class AppApi {
25
+ asyncLocalStorage = new AsyncLocalStorage()
26
+
27
+ async hello(param) {
28
+ return 'wertyuioiuytre ' + param
29
+ }
30
+ }
31
+
32
+ // 监听来自渲染进程的 port 消息
33
+ ipcMain.on('port', (event) => {
34
+ const port = event.ports[0]
35
+ // 创建 RPC 服务端点
36
+ createRpcServerElectronMessagePort({
37
+ port,
38
+ rpcKey: '',
39
+ extension: new AppApi()
40
+ })
41
+ port.start()
42
+ })
43
+ ```
44
+
45
+ ### 预加载脚本 (preload.js)
46
+
47
+ ```javascript
48
+ import { ipcRenderer } from 'electron/renderer'
49
+
50
+ // 监听来自渲染进程的消息
51
+ window.onmessage = (/** @type {MessageEvent} */ event) => {
52
+ // 验证消息来源并转发给主进程
53
+ if (event.origin == location.origin && event.data != 'port') {
54
+ ipcRenderer.postMessage('port', null, [event.ports[0]])
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### 渲染进程 (renderer.js)
60
+
61
+ ```javascript
62
+ import { createRpcClientMessagePort } from 'js-rpc2/src/lib.js'
63
+
64
+ // 创建消息通道
65
+ const channel = new MessageChannel();
66
+
67
+ // 向主进程发送端口
68
+ window.postMessage("port", location.origin, [channel.port1]);
69
+
70
+ // 创建 RPC 客户端
71
+ let rpc = createRpcClientMessagePort({
72
+ port: channel.port2,
73
+ rpcKey: ""
74
+ });
75
+ ```
76
+
77
+ ### 使用示例 (usage.js)
78
+
79
+ ```javascript
80
+ async function postPortMessage() {
81
+ // 调用远程方法
82
+ let ret = await rpc.hello('6667');
83
+ console.info("ret from rpc:", ret);
84
+ }
85
+ ```
86
+
87
+ ## 通信流程
88
+
89
+ 1. 渲染进程创建 `MessageChannel` 并通过 `postMessage` 将 `port1` 发送到预加载脚本
90
+ 2. 预加载脚本验证消息来源并将端口转发给主进程
91
+ 3. 主进程接收端口并创建 RPC 服务端点
92
+ 4. 渲染进程使用 `port2` 创建 RPC 客户端
93
+ 5. 客户端可以调用主进程中定义的服务方法
94
+
95
+ ## 使用方法
96
+
97
+ ### 1. 主进程设置
98
+
99
+ 在主进程中定义服务类并创建 RPC 服务:
100
+
101
+ ```javascript
102
+ class AppApi {
103
+ async yourMethod(params) {
104
+ // 实现业务逻辑
105
+ return result;
106
+ }
107
+ }
108
+
109
+ ipcMain.on('port', (event) => {
110
+ const port = event.ports[0]
111
+ createRpcServerElectronMessagePort({
112
+ port,
113
+ rpcKey: '',
114
+ extension: new AppApi()
115
+ })
116
+ port.start()
117
+ })
118
+ ```
119
+
120
+ ### 2. 渲染进程调用
121
+
122
+ 在渲染进程中建立连接并调用远程方法:
123
+
124
+ ```javascript
125
+ const channel = new MessageChannel();
126
+ window.postMessage("port", location.origin, [channel.port1]);
127
+ let rpc = createRpcClientMessagePort({ port: channel.port2, rpcKey: "" });
128
+
129
+ // 调用远程方法
130
+ let result = await rpc.yourMethod(params);
131
+ ```
132
+
133
+ ## 注意事项
134
+
135
+ - 确保在 Electron 的安全策略下正确配置 `contextIsolation` 和 `sandbox` 选项
136
+ - 预加载脚本中的消息验证是安全通信的关键
137
+ - MessagePort 需要在两端都启动后才能正常通信
138
+ - 异步方法调用需要正确处理 Promise 返回值
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-rpc2",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "js web websocket http rpc",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -21,7 +21,10 @@
21
21
  "web",
22
22
  "http",
23
23
  "websocket",
24
- "rpc"
24
+ "rpc",
25
+ "electron",
26
+ "chrome",
27
+ "extensions"
25
28
  ],
26
29
  "author": "yuanliwei",
27
30
  "license": "MIT",
package/src/lib.js CHANGED
@@ -518,7 +518,7 @@ export function createRpcServerHelper(param) {
518
518
  decode.readable.pipeTo(new WritableStream({
519
519
  async write(buffer) {
520
520
  let asyncLocalStorage = param.extension.asyncLocalStorage
521
- asyncLocalStorage.enterWith(param.context)
521
+ asyncLocalStorage?.enterWith(param.context)
522
522
  if (param.async) {
523
523
  rpcRunServerDecodeBuffer(param.extension, writer, buffer, param.logger).catch(console.error)
524
524
  } else {
@@ -832,3 +832,119 @@ export function createRpcClientMessagePort(param) {
832
832
  }
833
833
  return createRPCProxy(helper.apiInvoke)
834
834
  }
835
+
836
+ /**
837
+ * @param {string} text
838
+ * @returns
839
+ */
840
+ export function base64decode(text) {
841
+ const _tidyB64 = (/** @type {string} */ s) => s.replace(/[^A-Za-z0-9\+\/]/g, '')
842
+ const _unURI = (/** @type {string} */ a) => _tidyB64(a.replace(/[-_]/g, (m0) => m0 == '-' ? '+' : '/'))
843
+ text = _unURI(text)
844
+ return new Uint8Array(globalThis.atob(text).split('').map(c => c.charCodeAt(0)))
845
+ }
846
+
847
+ /**
848
+ * @param {Uint8Array<ArrayBuffer>} buffer
849
+ * @returns
850
+ */
851
+ export function base64encode(buffer, urlsafe = false) {
852
+ let b64 = globalThis.btoa(String.fromCharCode(...new Uint8Array(buffer)))
853
+ if (urlsafe) {
854
+ b64 = b64.replace(/=/g, '').replace(/[+\/]/g, (m0) => m0 == '+' ? '-' : '_')
855
+ }
856
+ return b64
857
+ }
858
+
859
+ /**
860
+ * @import {chrome as Chrome} from './types.js'
861
+ * @template T
862
+ * @param {{
863
+ * chrome:Chrome;
864
+ * key: string;
865
+ * extension: ExtensionApi<T>
866
+ * logger?:(msg:string)=>void;
867
+ * }} param
868
+ */
869
+ export function createRpcServerChromeExtensions(param) {
870
+ const chrome = param.chrome
871
+ const actionMap = new Map()
872
+ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
873
+ if (request.action === param.key) {
874
+ let tabId = sender.tab?.id
875
+ let { keyServer, keyClient } = request.data
876
+ let helper = createRpcServerHelper({
877
+ rpcKey: '', extension: param.extension, async: true, logger: param.logger,
878
+ })
879
+ let writer = helper.writable.getWriter()
880
+ actionMap.set(keyServer, (/** @type {Uint8Array<ArrayBuffer>} */ data) => {
881
+ writer.write(data)
882
+ })
883
+ helper.readable.pipeTo(new WritableStream({
884
+ async write(chunk) {
885
+ let resp = null
886
+ if (tabId) {
887
+ resp = await chrome.tabs.sendMessage(tabId, { action: keyClient, data: base64encode(chunk), })
888
+ } else {
889
+ resp = await chrome.runtime.sendMessage({ action: keyClient, data: base64encode(chunk), })
890
+ }
891
+ if (!resp) {
892
+ actionMap.delete(keyServer)
893
+ }
894
+ }
895
+ }))
896
+ sendResponse(true)
897
+ }
898
+ let write = actionMap.get(request.action)
899
+ if (write) {
900
+ write(base64decode(request.data))
901
+ sendResponse(true)
902
+ }
903
+ })
904
+ }
905
+
906
+ /**
907
+ * @param {{
908
+ * chrome:Chrome;
909
+ * key:string;
910
+ * tabId?: number;
911
+ * }} param
912
+ */
913
+ export function createRpcClientChromeExtensions(param) {
914
+ const chrome = param.chrome
915
+ let helper = createRpcClientHelper({ rpcKey: '' })
916
+ let writer = helper.writable.getWriter()
917
+ !(async () => {
918
+ let keyServer = guid()
919
+ let keyClient = guid()
920
+ chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
921
+ if (request.action === keyClient) {
922
+ sendResponse(true)
923
+ await writer.write(base64decode(request.data))
924
+ }
925
+ })
926
+ helper.readable.pipeTo(new WritableStream({
927
+ async write(chunk) {
928
+ if (param.tabId) {
929
+ await chrome.tabs.sendMessage(param.tabId, { action: keyServer, data: base64encode(chunk) })
930
+ } else {
931
+ await chrome.runtime.sendMessage({ action: keyServer, data: base64encode(chunk) })
932
+ }
933
+ }
934
+ }))
935
+ let response = null
936
+ try {
937
+ if (param.tabId) {
938
+ response = await chrome.tabs.sendMessage(param.tabId, { action: param.key, data: { keyServer, keyClient }, })
939
+ } else {
940
+ response = await chrome.runtime.sendMessage({ action: param.key, data: { keyServer, keyClient }, })
941
+ }
942
+ } catch (error) {
943
+ throw new Error(`Failed to establish RPC connection: ${error.message}`)
944
+ }
945
+ if (!response) {
946
+ throw new Error('RPC server did not respond - connection failed')
947
+ }
948
+ })()
949
+ return createRPCProxy(helper.apiInvoke)
950
+ }
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
- export type ExtensionApi<T> = { asyncLocalStorage: AsyncLocalStorage<T> } & object;
3
+ export type ExtensionApi<T> = { asyncLocalStorage?: AsyncLocalStorage<T> } & object;
4
4
 
5
5
 
6
6
  export type RPC_TYPE_CALL = 0xdf68f4cb
@@ -52,6 +52,11 @@ export declare namespace Electron {
52
52
 
53
53
  const NodeEventEmitter: typeof import('events').EventEmitter;
54
54
 
55
+ interface MessageEvent {
56
+ data: any;
57
+ ports: MessagePortMain[];
58
+ }
59
+
55
60
  class MessagePortMain extends NodeEventEmitter {
56
61
 
57
62
  // Docs: https://electronjs.org/docs/api/message-port-main
@@ -88,4 +93,42 @@ export declare namespace Electron {
88
93
  start(): void;
89
94
  }
90
95
 
91
- }
96
+ }
97
+
98
+ export declare namespace chrome {
99
+ namespace events {
100
+ interface Event<T extends (...args: any) => void> {
101
+ addListener(callback: T): void;
102
+ }
103
+ }
104
+ namespace tabs {
105
+ interface Tab {
106
+ id?: number | undefined;
107
+ }
108
+ interface MessageSender {
109
+ tab?: chrome.tabs.Tab;
110
+ }
111
+ export interface MessageSendOptions {
112
+ }
113
+ export function sendMessage<M = any, R = any>(
114
+ tabId: number,
115
+ message: M,
116
+ options?: MessageSendOptions,
117
+ ): Promise<R>;
118
+ interface MessageOptions {
119
+ includeTlsChannelId?: boolean | undefined;
120
+ }
121
+ }
122
+ namespace runtime {
123
+ interface MessageSender {
124
+ tab?: chrome.tabs.Tab;
125
+ }
126
+ const onMessage: events.Event<
127
+ (message: any, sender: MessageSender, sendResponse: (response?: any) => void) => void
128
+ >;
129
+ function sendMessage<M = any, R = any>(message: M, options?: MessageOptions): Promise<R>;
130
+ interface MessageOptions {
131
+ includeTlsChannelId?: boolean | undefined;
132
+ }
133
+ }
134
+ }