消息通信优化

This commit is contained in:
IndieKKY
2024-10-04 14:15:08 +08:00
parent c3b53a016c
commit 8b3bd44d59
14 changed files with 308 additions and 239 deletions

View File

@@ -0,0 +1,79 @@
import { MESSAGE_TARGET_EXTENSION } from '@/const'
class ExtensionMessage {
methods?: {
[key: string]: (params: any, context: MethodContext) => Promise<any>
}
debug = (...args: any[]) => {
console.debug('[Extension Messaging]', ...args)
}
init = (methods: {
[key: string]: (params: any, context: MethodContext) => Promise<any>
}) => {
this.methods = methods
/**
* Note: Return true when sending a response asynchronously.
*/
chrome.runtime.onMessage.addListener((event: MessageData, sender: chrome.runtime.MessageSender, sendResponse: (result: any) => void) => {
this.debug((sender.tab != null) ? `tab ${sender.tab.url ?? ''} => ` : 'extension => ', event)
// check event target
if (event.target !== MESSAGE_TARGET_EXTENSION) return
const method = this.methods?.[event.method]
if (method != null) {
method(event.params, {
event,
sender,
}).then(data => sendResponse({
success: true,
code: 200,
data,
})).catch(err => {
console.error(err)
let message
if (err instanceof Error) {
message = err.message
} else if (typeof err === 'string') {
message = err
} else {
message = 'error: ' + JSON.stringify(err)
}
sendResponse({
success: false,
code: 500,
message,
})
})
return true
} else {
console.error('Unknown method:', event.method)
sendResponse({
success: false,
code: 501,
message: 'Unknown method: ' + event.method,
})
}
})
}
broadcastMessage = async (ignoreTabIds: number[] | undefined | null, target: string, method: string, params?: any) => {
const tabs = await chrome.tabs.query({
discarded: false,
})
for (const tab of tabs) {
try {
if (tab.id && ((ignoreTabIds == null) || !ignoreTabIds.includes(tab.id))) {
await chrome.tabs.sendMessage(tab.id, {target, method, params})
}
} catch (e) {
console.error('send message to tab error', tab.id, e)
}
}
}
}
export default ExtensionMessage

View File

@@ -0,0 +1,121 @@
import { MESSAGE_TARGET_EXTENSION, MESSAGE_TARGET_INJECT } from '@/const'
import { PostMessagePayload, PostMessageResponse, startListening } from 'postmessage-promise'
class InjectMessage {
//类实例
methods?: {
[key: string]: (params: any, context: MethodContext) => Promise<any>
}
postMessageToApp?: (method: string, payload: PostMessagePayload) => Promise<PostMessageResponse>
debug = (...args: any[]) => {
console.debug('[Inject Messaging]', ...args)
}
/**
* @param sendResponse No matter what is returned, this method will definitely be called.
*/
messageHandler = (event: MessageData, sender: chrome.runtime.MessageSender | null, sendResponse: (response?: MessageResult) => void) => {
const source = sender != null ? ((sender.tab != null) ? `tab ${sender.tab.url ?? ''}` : 'extension') : 'app'
this.debug(`${source} => `, JSON.stringify(event))
// check event target
if (event.target !== MESSAGE_TARGET_INJECT) return
const method = this.methods?.[event.method]
if (method != null) {
method(event.params, {
event,
sender,
}).then(data => {
// debug(`${source} <= `, event.method, JSON.stringify(data))
return data
}).then(data => sendResponse({
success: true,
code: 200,
data,
})).catch(err => {
console.error(err)
let message
if (err instanceof Error) {
message = err.message
} else if (typeof err === 'string') {
message = err
} else {
message = 'error: ' + JSON.stringify(err)
}
sendResponse({
success: false,
code: 500,
message,
})
})
return true
} else {
console.error('Unknown method:', event.method)
sendResponse({
success: false,
code: 501,
message: 'Unknown method: ' + event.method,
})
}
}
init(methods: {
[key: string]: (params: any, context: MethodContext) => Promise<any>
}) {
this.methods = methods
// listen message from app
startListening({}).then(e => {
const { postMessage, listenMessage, destroy } = e
this.postMessageToApp = postMessage
listenMessage((method, params, sendResponse) => {
this.messageHandler({
target: MESSAGE_TARGET_INJECT,
method,
params,
}, null, sendResponse)
})
}).catch(console.error)
/**
* listen message from extension
* Attention: return true if you need to sendResponse asynchronously
*/
chrome.runtime.onMessage.addListener(this.messageHandler)
}
sendExtension = async <T = any>(method: string, params?: any): Promise<T> => {
return await chrome.runtime.sendMessage<MessageData, MessageResult>({
target: MESSAGE_TARGET_EXTENSION,
method,
params: params ?? {},
}).then((messageResult) => {
if (messageResult.success) {
return messageResult.data as T
} else {
throw new Error(messageResult.message)
}
})
}
sendApp = async <T>(method: string, params: any): Promise<T> => {
if (this.postMessageToApp != null) {
const messageResult = await this.postMessageToApp(method, params) as MessageResult | undefined
if (messageResult != null) {
if (messageResult.success) {
return messageResult.data as T
} else {
throw new Error(messageResult.message)
}
} else {
throw new Error('no response')
}
} else {
throw new Error('error: postMessageToApp is not initialized')
}
}
}
export default InjectMessage

View File

@@ -0,0 +1,42 @@
import { MESSAGE_TARGET_EXTENSION } from '@/const'
import { injectWaiter } from './useMessageService'
import { useCallback } from 'react'
const useMessage = () => {
const sendExtension = useCallback(async <T = any>(method: string, params?: any) => {
return await chrome.runtime.sendMessage<MessageData, MessageResult>({
target: MESSAGE_TARGET_EXTENSION,
method,
params: params ?? {},
}).then((messageResult) => {
if (messageResult.success) {
return messageResult.data as T
} else {
throw new Error(messageResult.message)
}
})
}, [])
const sendInject = useCallback(async <T = any>(method: string, params?: any) => {
// wait
const postInjectMessage = await injectWaiter.wait()
// send message
const messageResult = await postInjectMessage(method, params) as MessageResult | undefined
if (messageResult != null) {
if (messageResult.success) {
return messageResult.data as T
} else {
throw new Error(messageResult.message)
}
} else {
throw new Error('no response')
}
}, [])
return {
sendExtension,
sendInject
}
}
export default useMessage

View File

@@ -0,0 +1,86 @@
import {useCallback, useEffect} from 'react'
import {
MESSAGE_TARGET_APP,
MESSAGE_TARGET_EXTENSION,
MESSAGE_TARGET_INJECT,
} from '@/const'
import {callServer, PostMessagePayload, PostMessageResponse} from 'postmessage-promise'
import {Waiter} from '@kky002/kky-util'
const debug = (...args: any[]) => {
console.debug('[App Messaging]', ...args)
}
let postInjectMessage: (method: string, params: PostMessagePayload) => Promise<PostMessageResponse> | undefined
export const injectWaiter = new Waiter<typeof postInjectMessage>(() => ({
finished: postInjectMessage != null,
data: postInjectMessage
}), 100, 15000)
const useMessageService = (methods?: {
[key: string]: (params: any, from: string, context: MethodContext) => boolean
}) => {
const messageHandler = useCallback((method: string, params: any, from: string, context: any): boolean => {
const handler = methods?.[method]
if (handler != null) {
return handler(params, from, context)
}else {
debug('unknown message method: ', method)
return false
}
}, [methods])
// connect to inject
useEffect(() => {
let destroyFunc: (() => void) | undefined
const serverObject = {
server: window.parent, // openedWindow / window.parent / window.opener;
origin: '*', // target-window's origin or *
}
const options = {}
callServer(serverObject, options).then(e => {
const { postMessage, listenMessage, destroy } = e
postInjectMessage = postMessage
destroyFunc = destroy
listenMessage((method, params, sendResponse) => {
debug('inject => ', method, params)
const success = messageHandler(method, params, MESSAGE_TARGET_INJECT, {})
sendResponse({
success,
code: success ? 200 : 500
})
})
debug('message ready')
}).catch(console.error)
return () => {
destroyFunc?.()
}
}, [messageHandler])
const extensionMessageCallback = useCallback((event: MessageData, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => {
debug((sender.tab != null) ? `tab ${sender.tab.url??''} => ` : 'extension => ', JSON.stringify(event))
// check event target
if (!event || event.target !== MESSAGE_TARGET_APP) return
messageHandler(event.method, event.params, MESSAGE_TARGET_EXTENSION, {
sender
})
}, [messageHandler])
// listen for message
useEffect(() => {
chrome.runtime.onMessage.addListener(extensionMessageCallback)
return () => {
chrome.runtime.onMessage.removeListener(extensionMessageCallback)
}
}, [extensionMessageCallback])
}
export default useMessageService