Electron — это фреймворк рабочего стола, основанный на языке JS. Его базовый контейнер представлений представляет собой окно, содержащее ядро Chromium, называемое BrowserWindow. Для более сложных проектов, если вам нужно встроить сторонние бизнес-страницы в окно, есть три решения на выбор: BrowserView, тег webView и Iframe.
Принципы реализации этих четырех типов контейнеров представлений различны, а также методы связи с основным процессом, главным окном и другими родственными окнами. Описания в официальных документах (начиная с версии Electron20) относительно разбросаны. В этой статье основное внимание уделяется выяснению их соответствующих характеристик и методов связи, а также даны рекомендуемые режимы упаковки для справки разработчиков.
Процесс рендеринга Electron основан на Chromium. На следующем рисунке показано иерархическое разделение контейнеров представлений в официальной документации Chromium.
Концепция, наиболее тесно связанная с Electron, — это веб-контент, который эквивалентен независимому контексту рендеринга. В Chrome при каждом добавлении вкладки создается независимый веб-контент. Они могут загружать разные URL-адреса и независимы друг от друга.
В Electron, когда мы создаем объект базового окна, мы можем получить WebContents по его ссылке.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
win.loadFile('index.html')
console.log(win.webContents)
Это объект EventEmitter, через который можно отправлять межпроцессные сообщения и отслеживать события, отправленные другими процессами. Это основа встроенной связи ipc в Electron.
Кроме того, Electron также предоставляет среду предварительной загрузки изолированного контекста для каждого объекта веб-контента и выполняет в ней сценарий предварительной загрузки, указанный разработчиком. Он запускается до того, как средство рендеринга загрузит страницу, имеет доступ как к интерфейсу DOM, так и к среде Node.js, а также может предоставлять привилегированные интерфейсы средству рендеринга через интерфейс contextBridge.
Поскольку объекты межпроцессного взаимодействия ipcMain и ipcRenderer, инкапсулированные Electron, основаны на API среды nodejs, по соображениям безопасности обычно необходимо отключить разрешения узла процесса рендеринга в производственной среде (установить nodeIntegration значение false), чтобы предотвратить повреждение операционной системы вредоносными сценариями. Но в этом случае связь между основным процессом и процессом рендеринга станет затруднительной.
Сценарий предварительной загрузки предоставляет нам компромисс. Мы можем установить канал связи в контексте изоляции, а затем предоставить ограниченные интерфейсы контексту рендеринга для использования в бизнесе. И выполните некоторую проверку и фильтрацию параметров при открытии интерфейса, чтобы предотвратить попадание незаконных сценариев в основной процесс.
Поэтому, хотя некоторые официальные демо-версии напрямую вводят ipcRenderer в процесс рендеринга, в производственной среде нам следует стараться избегать этого. Во всех приведенных ниже демонстрационных кодах ipcRenderer должен быть объектом, отфильтрованным с помощью проверки предварительной загрузки, а не исходным объектом узла.
// Откройте доступ к объекту, к которому обращается процесс рендеринга, или вы можете изменить его на псевдоним.
contextBridge.exposeInMainWorld('ipcRenderer', {
send: async (channel: string, ...args: any) => {
// Вы можете проверить и отфильтровать легальность бизнеса здесь.
ipcRenderer.send(channel, ...args);
},
invoke: async (channel: string, ...args: any) => {
// Вы можете проверить и отфильтровать легальность бизнеса здесь.
return await ipcRenderer.invoke(channel, ...args);
},
});
Поверх веб-контетов работает несколько фреймов. Мы можем просмотреть все объекты фреймов окна в основном процессе. Если в окне открыт инструмент разработчика или загружен тег iframe, объекты фреймов будут добавлены. У каждого веб-контента есть mainFrame, который является основным объектом, загружаемым непосредственно окном.
this.win.webContents.on('did-frame-finish-load',(event, isMainFrame, frameProcessId, frameRoutingId)=>{
// Это событие будет срабатывать после загрузки каждого кадра.
console.log("aaaaaa did-frame-finish-load", isMainFrame, frameProcessId, frameRoutingId);
// Просмотрите все объекты фрейма в окне и сравните идентификатор маршрутизации, чтобы определить текущий фрейм и распечатать его основную информацию.
this.win.webContents.mainFrame.frames.forEach(frame => {
if(frame.routingId === frameRoutingId){
const url = new URL(frame.url)
console.log("URL-адрес, загруженный текущим фреймом ", url);
}
})
})
Frame также имеет ряд свойств и перехватчиков жизненного цикла, но он не является EventEmitter и не может через него взаимодействовать с другими процессами. Если необходимо обмениваться сообщениями между кадрами, необходимо принять обходное решение, которое мы объясним позже.
BrowserWindow — это базовая единица области просмотра в Electron. Созданная и запланированная основным процессом, BrowserWindow эквивалентна независимому процессу Chrome.
1. Связь между BrowserWindow и основным процессом
Связь между основным процессом и формой необходима практически для всех предприятий. Electron официально предоставляет инкапсуляцию на основе IpcMain и IpcRenderer. Поскольку в официальном документе это описано очень четко, код здесь не будет указан, а будет только диаграмма. использовал для обобщения.
Вызов основного процесса из окна разделен на два режима: отправка и вызов. Первый — это односторонняя отправка, которая подходит для сценариев, где возвращаемое значение не связано с выполнением определенных операций. что эквивалентно одному возврату и является асинхронным. Официально также предусмотрен интерфейс синхронного вызова sendSync, но это приведет к блокировке процесса, поэтому постарайтесь не использовать его в реальном бизнесе.
Из основного процесса в окно вам необходимо использовать метод отправки веб-контента для его отправки. Официальный обеспечивает только инкапсуляцию односторонних вызовов. Это может быть связано с тем, что основной процесс выполняется в фоновом режиме и не имеет представления. , поэтому основной процесс обычно не вызывает. Он инициируется заранее и зависит от сцены, возвращаемой процессом рендеринга. Однако, если в реальном бизнесе действительно есть потребность, вы также можете указать уникальный идентификационный идентификатор при отправке. После завершения процесса рендеринга отправьте идентификатор и смоделируйте его посредством двух сообщений. Тот же эффект.
Поскольку основой связи ipc является веб-контент, а контекстная информация рендеринга не может напрямую обмениваться между двумя независимыми окнами, требуется помощь основного процесса. Если количество запросов невелико, то не будет большой проблемой, если основной процесс будет пересылать их каждый раз. Однако если запросов много, учитывая проблемы с производительностью многооконных приложений, лучше всего установить прямую связь между окнами.
Есть два способа сделать это:
Этот метод поддерживает передачу webContentsId в качестве цели отправки и отправку его в определенный контекст рендеринга. С его помощью мы можем добиться прямой связи от окна к окну, но сначала нам нужно получить webContentsId другого окна через основной процесс.
// Окно
const targetId = await ipcRenderer.invoke(“GetIWindowBId”) //Основной процесс должен прослушать это событие через ipcMain и вернуть идентификатор окна B
ipcRenderer.sendTo(targetId, «CrossWindow», «Окно A отправляет в окно B»)
// окно Б
ipcRenderer.on("CrossWindow",(event,...params)=>{
console.log("CrossWindow Request from ",event.senderId,...params) // окно Бмогу поставитьsenderIdзапиши это,и отправлять через него сообщения Окно
ipcRenderer.sendTo(event.senderId, «CrossWindow», «Окно B отправляет в окно A»)
})
Как только оба окна узнают webContentsId друг друга, они смогут свободно отправлять сообщения (имя события может быть указано произвольно).
MessagePort не является возможностью, предоставляемой Electron, но основан на веб-стандартном API MDN, что означает, что его можно создать непосредственно в процессе рендеринга. В то же время Electron предоставляет реализацию на стороне nodejs, поэтому ее также можно создать в основном процессе.
// в процессе рендеринга
const messageChannel = new MessageChannel();
console.log(messageChannel.port1);
console.log(messageChannel.port2);
// в основном процессе
import { MessageChannelMain } from 'electron';
const messageChannel = new MessageChannelMain();
console.log(messageChannel.port1);
console.log(messageChannel.port2);
Объекты порта, созданные с обеих сторон, симметричны по возможностям. Объекты, созданные основным процессом, могут быть переданы.
win.webContents.postMessage('port', null, [port1])
Метод отправляется в BrowserWindow, а одноименное событие нужно отслеживать на стороне окна.
ipcRenderer.on('port', e => {})
Получите объект e.ports[0] и сохраните его.
Основной процесс должен только распределить порты по окнам A и B. После того, как каждое из двух окон содержит порт1 и порт2, они могут обмениваться данными через них.
Подробные коды см. в официальной документации: https://www.electronjs.org/docs/latest/tutorial/message-ports
Похоже, что MessagePort не так удобен, как sendTo. Для простой оконной связи обычно достаточно sendTo.
Но самая большая разница между ним и ipcRenderer.sendTo заключается в том, что последний основан на WebContents, поэтому можно использовать только объекты с webContents, но messagePort является веб-стандартом и также применим к webWorker или iframe, что означает, что мы можем напрямую создавать Окно/канал связи между основным процессом и рабочим или iframe окна B. Это очень удобная возможность в определенных бизнес-сценариях. В более поздней части введения в iframe будут даны практические рекомендации.
BrowserView также является независимым контейнером представлений, созданным основным процессом. Его можно встроить в другое окно BrowserWindow и загрузить другой URL-адрес. Он чем-то похож на Iframe, но работает на более низком уровне, чем iframe, и имеет независимый веб-контент.
В принципе, создание BrowserView эквивалентно добавлению вкладки в браузере Chrome. Окно может встраивать несколько BrowserViews, а координаты смещения относительно главного окна можно указать при их создании. Если вам необходимо встроить стороннюю подстраницу в бизнес-окно, использование BrowserView может обеспечить независимость подстраницы и не влиять на работу главной страницы.
const win = new BrowserWindow({ width: 800, height: 600 })
const view = new BrowserView()
win.setBrowserView(view)
view.setBounds({ x: 0, y: 0, width: 300, height: 300 }) // Определяет положение представления относительно главного окна.
view.webContents.loadURL('https://electronjs.org') //представление также имеет независимый объект webContents
Но у BrowserView также есть ограничения. Поскольку он создается основным процессом и «прикрепляется» к окну хоста, его среда рендеринга полностью независима и находится вне дерева DOM хост-страницы, а это означает, что после создания другие элементы хоста страница Ни одна из них не может быть отображена на ней, установив z-index.
Поскольку BrowserView имеет независимый веб-контент и может монтировать сценарии загрузки, его статус на уровне связи ipc точно такой же, как и у BrowserWindow. Мы можем таким же образом обмениваться с ним сообщениями непосредственно в основном процессе, не пересылая его через хост. Различные BrowserViews также могут взаимодействовать друг с другом через sendTo.
Поскольку контекст BrowserView полностью независим, он не может напрямую взаимодействовать с главной страницей. Когда ему необходимо обмениваться сообщениями с главной страницей, ему также необходимо использовать метод «окно-окно» для обмена webContentsid или MessagePort. Это самая большая разница между ним и традиционным iframe встроенной страницы.
Я считаю, что каждый веб-разработчик знаком с концепцией Iframe. Он не имеет ничего общего с платформой Electron. Это встроенный тег, который поставляется со стандартом DOM браузера, а также является самым простым встроенным решением. В Electron iframe не имеет webContents, но существует в виде фрейма под содержимым главной страницы.
Метод связи с хост-страницей — это знакомый postMessage, который является полноценным веб-стандартом и не будет здесь подробно описываться.
Поскольку iframe не имеет независимого веб-контента и не может напрямую установить соединение с основным процессом, проще всего перенаправить его через страницу хоста. Сначала используйте postMessage для отправки всех запросов на внешний уровень, а затем отправьте их на него. основной процесс через ipcRenderer, чтобы получить результат. Затем отправьте его обратно в iframe.
Хотя это и возможно, это все равно довольно сложно реализовать, и каждый запрос требует второго обмена данными, что также повлияет на производительность при большом количестве запросов.
Как упоминалось ранее, характеристики messageChannel реализуются симметрично на стороне рендеринга и на стороне узла. Тогда мы можем использовать страницу хоста в качестве «посредника» и выполнять только обмен портами, а затем позволить основному процессу и iframe взаимодействовать напрямую через него. порт.
// основной процесс
this.win.once('ready-to-show', () => {
const { port1, port2 } = new MessageChannelMain()
this.win.webContents.postMessage('sendPort', null, [port1])
port2.start();//Обратите внимание, что здесь start должен быть вызван один раз, иначе сообщение останется в ожидании без запуска обратного вызова.
// useport2 отправляет сообщения в iframe, а также может получать сообщения из iframe.
port2.on('message',(event)=>{
console.log("основной Процесс Получил сообщение от iframe",event.data);
})
setTimeout(()=>{
port2.postMessage("основной Сообщение iframe от процесса ");
},5000)
})
// Хост-страница
ipcRenderer.on('sendPort', event => {
const port2 = event.ports[0]
const iframe = document.querySelector("iframe");
// Обратите внимание: если родительское окно и iframe являются междоменными, для второго параметра должно быть установлено значение *.
iframe.contentWindow.postMessage("sendPortToIframe", '*', [port2]);
})
// внутри iframe
let messagePort;
window.addEventListener("message", function (event) {
messagePort = event.ports[0];
// монитор宿主сообщение от,Сохраните порт,Вы можете напрямую общаться с основным процесс передан
messagePort.onmessage = function (event) {
console.log('iframe получатьосновной процесссообщение от',event)
};
// использовать portДаватьосновной Процесс Отправить сообщение
messagePort.postMessage('iframeДаватьосновной Процесс Отправить сообщение');
});
Видно, что в процессе установления соединения задействованы три роли, но хост-странице нужно только один раз перенаправить порт, и тогда вы сможете выйти из нее, не беспокоясь о связи между iframe и основным процессом.
После практики автора приведенный выше код может нормально работать на основе версии Electron20. Однако время создания iframe не обязательно соответствует моменту готовности к показу окна хоста. Это также может произойти при более позднем переключении определенного маршрута. Соответственно, время нового messageChannel также должно быть скорректировано. процесс все еще немного громоздкий.
А поскольку в iframe нет сценария предварительной загрузки, аналогичного предварительной загрузке, для завершения эти коды инициализации необходимо внедрить в код подбизнеса, а сотрудничество в области межбизнес-разработки также становится более проблематичным.
Как видно из предыдущей статьи, BrowserView и iframe имеют свои собственные ограничения. Первый из них не зависит от потока документов хоста, не может следовать правилам макета хост-страницы и не может охватывать некоторые глобальные всплывающие окна и плавающие слои. очень сложно использовать большие ограничения. Последний не имеет независимой операционной среды, а наладить связь с другими процессами затруднительно, и это легко может отразиться на работе хост-страницы.
<webview> Тег представляет собой компромисс между двумя механизмами.,это и<iframe>TagТакой же,Может быть встроен в документооборот Хост-страницы.,Но у него есть независимые веб-контенты, такие как BrowserView.,И поддерживает установку частных сценариев загрузки.
<!DOCTYPE html>
<html lang="">
<body>
<div id="drag-area">webviewтест</div>
<webview
id="testWebview"
src="file:///xxxx/embedpage.html?subBusinessType=someBusiness"
style="width: 400px; height: 480px; position: absolute; top: 0; left: 0; z-index: 1000"
preload="file:///xxxx/testpreload.js"
></webview>
</body>
<script>
const webview = document.getElementById('testWebview');
</script>
</html>
Объект веб-просмотра, который мы получаем через API-запрос dom, будет захвачен Electron и заменен теневым Dom. Это HTMLElement, но он также имеет функцию EmittEvent и может использоваться как webContents.
Уведомление,Electronвнутри<webview>tagоснован наchrome Приложение разработано в соответствии со стандартами. Поскольку Chrome отказался от последнего, разработчики Electron не могут гарантировать доступность последующих версий.
Но поскольку это очень удобно, все же стоит попробовать использовать контроль версий. Если в будущем от него действительно откажутся, его также можно будет перенести обратно в iframe в качестве альтернативы понижению версии.
потому что选中из<webview>Объект имеетsendметод,Эквивалент ipcRenderer.send,использовать Он может генерировать события непосредственно из окна хоста внутрь веб-просмотра.,Внутренне его необходимо отслеживать через ipcRenderer.on.
// От хоста к веб-просмотру
// принимающая сторона
webview.send("HostToWebview","hello webview")
// веб-просмотр сторона
ipcRenderer.on("HostToWebview",(event,...params)=>{
console.log("from host:",...params) })
});
Напротив, внутри Webview вы можете отправлять события через ipcRenderer.sendToHost, а получать и обрабатывать их на главной странице, добавляя прослушиватель событий для ipc-message к объекту webview.
// От веб-просмотра до хостинга
// веб-просмотр сторона
ipcRenderer.sendToHost("WebviewToHost","hello host")
// принимающая сторона
webview.addEventListener("ipc-message", (event) => {
console.log("from webview:", event.channel, event.args);
});
Как и в случае с принципами, упомянутыми выше, вызовы ipcRenderer на стороне веб-просмотра должны быть ограничены предварительной загрузкой, чтобы избежать прямого предоставления собственных объектов контексту рендеринга.
我们知道<webvw>TagЕсть независимостьwebConentsиз,иметь в видуосновной процесс可以直接и它通信,Но здесь есть что-то особенное,Он управляется окном хоста. процессе рендерингавнутри创建из,Поэтому, когда он будет создан,основной процесс并Нет知道它из存在,Сначала необходимо отправить уведомление.
Обратите внимание, что в отличие от iframe процесс уведомления может выполняться в собственной предварительной загрузке веб-просмотра без необходимости пересылки со стороны хост-страницы.
// веб-просмотр сторона(обычно вpreloadвнутри)
// Отправить запрос на регистрацию,subBusinessType может быть строкой, идентифицирующей тип бизнеса.,удобныйосновной процессразличать,Его также можно опустить.
ipcRenderer.invoke("webviewRegister", subBusinessType)
// мониторосновной процесс发来из事件
ipcRenderer.on(“MainToWebview”,, (event, ...params) => {
console.log("Событие, полученное от процесса",...params)
})
// основной процесс
// Обрабатывать запросы на регистрацию
ipcMain.handle('webviewRegister', (event, subBusinessType:SubBusinessType) => {
// ProcessId и FrameId получаются посредством события, которые используются в качестве идентификаторов для последующих событий отправки.
// Уведомление,Причина, по которой необходим идентификатор процесса,Это потому чтоwebviewи Хост-страница跨域из情况下,Оба работают в разных процессах,Необходимо передать [processId, FrameId] двоичная пара для идентификации и не может быть опущена.
console.log('webview регистрирует тип subBussiness', subBusinessType, event.processId, event.frameId);
const processId = event.processId;
const frameId = event.frameId; // Получите отправителя (объект webContents веб-просмотра) и отправьте его.
event.sender.sendToFrame([processId, frameId],“MainToWebview”,“helloWebview”);
})
Заметили в этом волшебство? Объект webContents веб-представления можно получить непосредственно через свойство sender события, без необходимости получать его через объект win узла.
Таким образом,<webview>就и窗体解藕了,Когда мы представляем третьих лиц Суббизнесиз时候,основной процесс Нетиспользовать关心具体是哪个窗口внутри嵌入了<webview>Этикетка,Просто заботьтесь о самом бизнесе,Примите соответствующие меры. Решение iframe не может этого сделать.
<webview>Есть еще одно преимущество,Процесс регистрации можно выполнить в скрипте предварительной загрузки.,Сценарий предварительной загрузки поддерживается головной компанией. Код суббизнеса перед загрузкой,我们就可以建立好иосновной процесс之间из通道,и положить Суббизнес Нужно настроитьиспользоватьиз接口,Инкапсулирован в форму, похожую на jsApi.,подвергается воздействию контекста рендеринга,и无需入侵Суббизнесиз任何代码,还可以考虑Нет同Суббизнесиз公共接口复использовать,Архитектурно он гораздо элегантнее iframe.
Общий механизм связи показан на рисунке.
Упомянутые выше методы связи необходимо инкапсулировать, чтобы они были более удобными в реальном бизнесе. На основе новой версии проекта QQ, в котором я недавно участвовал, я поделюсь некоторым опытом инкапсуляции канала IPC между окном и основным процессом.
这внутри以采использовать<webview>Tag嵌入из业务窗口иосновной процессиз通信为例(其他из容器对象原理类似),Существует два основных принципа инкапсуляции:
1. Изолируйте среду выполнения
Как упоминалось ранее, в целях безопасности приложения (во избежание атак путем внедрения сценариев и т. д.) мы должны запретить бизнесу напрямую использовать собственные объекты ipc. По этой причине нам необходимо изолировать среду выполнения на уровне упаковки, чтобы избежать этого. прямое воздействие бизнес-кода.
2. Изолируйте основные детали
Бизнес-сторона обычно не заботится о деталях установления канала, а лишь надеется получить данные и выполнить команды. Мы надеемся сделать связь IPC максимально простой и удобной, чтобы облегчить понимание и использование бизнес-стороны.
Прежде всего, нам необходимо уточнить требования. Когда существует несколько предприятий, какие из них являются общими, а какие частными для бизнеса. Мы используем базовый класс для размещения общих частей, а подклассы наследуют базовый класс для обеспечения. частные части.
// основной процесс
class baseApiHelper{
public handlers = {
// Предположим, что запись журнала является обычным явлением.использоватьизapi
writelog(ctx:IpcWebviewCtx, logType:string, ...info:any){
loggerService.log('[+'+ctx.subBusinessType+’+]’,…info);
},
}}
class SomeBusinessApiHelper extends BaseApiHelper{
public handlers = {
...super.handlers, // 继承自基类из通использоватьapi
openFile:async (ctx:IpcWebviewCtx,...params:any)=>{
// Опустить конкретную реализацию
return 'mock openFile done';
}
}
}
type IpcWebviewCtx = {
subBusinessType:SubBusinessType,
processId:number,
frameId:number,
}
Среди них IpcWebviewCtx — это определенный нами тип контекста, включающий идентификатор типа подслужбы, идентификатор процесса и идентификатор кадра отправителя, чтобы функция-обработчик могла выполнять специальную обработку для различных служб.
Каждый помощник — это синглтон, которым можно управлять с помощью какой-либо среды внедрения зависимостей или просто создать его и экспортировать. Короче говоря, его можно использовать как синглтон.
Далее мы реализуем проходиспользоватьиз注册事件,Выполнять привязку после запуска приложения,Любое последующее Суббизнес<webview>был создан,Будет запущен процесс регистрации.
Чтобы облегчить управление, мы собираем идентификатор суббизнеса и его идентификатор отправителя как имя частного канала контейнера, регистрируем для него функцию прослушивания, получаем имя вызываемого метода, добавляем контекст и затем передаем его функции-обработчику для обработка.
// основной процесс
// Обработка глобальных событий регистрации веб-просмотра
ipcMain.handle('webviewRegisterSubBussiness', (event, subBusinessType:SubBusinessType) => {
console.log('webview регистрирует тип subBussiness', subBusinessType, event.processId, event.frameId);
const processId = event.processId;
const frameId = event.frameId;
const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;
// Получите Помощника, соответствующего бизнесу
const helper:any = ipcWebviewContainer.get(subBusinessType);
// Обработайте метод вызова из определенного веб-представления, добавьте контекст и назначьте его соответствующему помощнику.
ipcMain.handle(channelName, async (event: IpcMainInvokeEvent, eventName: string, ...payload)=>{
if(helper.handlers[eventName]){
return await helper.handlers[eventName]({
subBusinessType,
processId:processId,
frameId:frameId
} as IpcWebviewCtx,
...payload);
}
return Promise.reject("ipc hanlder not found")
})
// Обработайте метод отправки из определенного веб-представления, добавьте контекст и назначьте его соответствующему помощнику.
ipcMain.on(channelName, (event: IpcMainInvokeEvent, eventName: string, ...payload)=>{
if(helper.handlers[eventName]){
helper.handlers[eventName]({
subBusinessType,
processId:processId,
frameId:frameId
} as IpcWebviewCtx,
...payload);
}else{
console.warn("ipc hanlder not found")
}
})
return Promise.resolve(true)
});
На стороне процесса рендеринга, после запуска сценария предварительной загрузки, мы отправляем событие webviewRegisterSubBussiness в основной процесс и предоставляем вызывающему объекту контекст рендеринга. Вызовите ipcApi.invoke или ipcApi.send напрямую в бизнесе, чтобы выполнить соответствующий метод.
// webview preload
// Получите бизнес-тип страницы по URL-адресу (или любым другим способом)
const subBusinessType = parseQuery(location.search).subBusinessType;
const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;
let registerIpcPromiseReslover = ()=>{};
const registerIpcPromise = new Promise((resolve) => {
registerIpcPromiseReslover = resolve;
});
ipcRenderer.invoke("webviewRegisterSubBussiness", subBusinessType).then(res=>{
registerIpcPromiseReslover();
});
contextBridge.exposeInMainWorld('ipcApi',{
invoke: async (cmd,...params)=>{
await registerIpcPromise;
console.log("call ipcApi invoke ",cmd)
return await ipcRenderer.invoke(channelName,cmd,...params);
},
send: async (cmd,...params)=>{
await registerIpcPromise;
console.log("call ipcApi send ",cmd)
ipcRenderer.send(channelName,cmd,...params);
},
})
Когда необходимо вызвать подбизнес, просто используйте ipcApi для объекта окна.
// Суббизнес
window.ipcApi.send('writelog','info', ‘hello IPC’);
const res = await window.ipcApi.invoke('openFile’, somefileName)
Обратите внимание, что здесь создается RegisterIpcPromise. Это связано с тем, что событие регистрации достигает основного процесса асинхронно. В крайних случаях, если вызывает бизнес-код, основному процессу требуется некоторое время. API сразу после его запуска, возможно, что основной процесс не завершил регистрацию, и в это время вызов может завершиться неудачно. Чтобы избежать этой ситуации, мы используем объект-промис, чтобы заставить запросы вызова и отправки ждать, а затем отменяем их после завершения регистрации, чтобы гарантировать, что все вызовы могут быть обработаны правильно.
Далее обрабатываются уведомления, выданные основным процессом.
Чтобы отправлять уведомления дочерним предприятиям, точка триггера должна находиться в определенном модуле основного процесса. Мы предоставляем триггер модулю, чтобы он мог получить соответствующий триггер через тип дочернего бизнеса и инициировать событие.
Мы также инкапсулируем триггер в baseApiHelper и используем Map для его обслуживания. Это должно быть совместимо с ситуацией, когда у дочернего бизнеса есть несколько экземпляров (конечно, в реальных бизнес-сценариях таких ситуаций не должно быть много и их можно упростить). как уместно)
// основной процесс
class baseApiHelper{
private emiiterMap = new Map<string,Function>;
public handlers = {
……
}
public addEmtter(key:string,emitFunc:Function){
this.emiiterMap.set(key, emitFunc);
}
public removeEmtter(key:string){
this.emiiterMap.delete(key);
}
public emitEvent(eventName:string, ...params:any){
this.emiiterMap.forEach((emitFunc)=>{
emitFunc(eventName,...params);
})
}
}
При регистрации подбизнеса мы собираем объект-отправитель отправителя и помещаем его в emiiterMap (по-прежнему демонстрационный код выше, опуская повторяющиеся части)
// основной процесс
// Обработка глобальных событий регистрации веб-просмотра
ipcMain.handle('webviewRegisterSubBussiness', (event, subBusinessType:SubBusinessType) => {
console.log('webview регистрирует тип subBussiness', subBusinessType, event.processId, event.frameId);
const processId = event.processId;
const frameId = event.frameId;
const channelName = 'ipc-webview-'+subBusinessType+'-'+frameId;
const helper:any = ipcWebviewContainer.get(subBusinessType);
// Обработайте метод вызова из определенного веб-представления, добавьте контекст и назначьте его соответствующему помощнику.
……
// Обработайте метод отправки из определенного веб-представления, добавьте контекст и назначьте его соответствующему помощнику.
……
// Добавьте temitter к помощнику, и компания сможет отправлять события в определенное веб-представление через помощника.
helper.addEmtter(processId+'-'+frameId, (eventName:string, ...params:any)=>{
event.sender.sendToFrame([processId, frameId],channelName, eventName, ...params);
})
return Promise.resolve(200)
});
На стороне подбизнеса просто зарегистрируйте прослушиватель для события отправителя и последовательно запускайте бизнес-прослушиватели.
// webview preload
const eventCbMap = {}
ipcRenderer.on(channelName, (event, eventName, ...params) => {
eventCbMap[eventName]?.forEach(cb=>{
cb(...params);
})
})
contextBridge.exposeInMainWorld('ipcApi',{
on:(eventName, cb)=>{
console.log("Мониторинг регистрации страниц", eventName)
if(!eventCbMap[eventName]){
eventCbMap[eventName] = []
}
eventCbMap[eventName].push(cb);
},
……
}
Таким образом устанавливается канал. В модуле, который должен генерировать событие, пока получен соответствующий помощник, может быть запущен эмиттер. Бизнес также может привязать прослушиватель через ipcApi.on и получать уведомления.
// основной процесс Любой бизнес-модуль
const someBusinessApiHelper = ipcWebviewContainer.get<SomeBusinessApiHelper>(SubBusinessType.SomeBusiness);
someBusinessApiHelper.emitEvent('helloIPC',`основной процесс发Даватьwebview`);
Конечно, все зарегистрированные события должны обеспечивать логику удаления. Вы можете вернуть объект удаления в конце функции регистрации, чтобы отменить регистрацию прослушивателя.
основной процессиз也emitterТакже необходимо быть в<webview>Удалить после завершения жизненного цикла,Вы можете выбратьwebviewизbeforeunload事件внутри Даватьосновной Процесс Отправить запрос на удаление,И очистите объект-эмиттер в соответствующем помощнике.,Конкретная логика здесь повторяться не будет.
Таким образом, ipc-инкапсуляция подбизнеса завершена. Вам нужно только договориться о том, какие возможности необходимы, и разработчик реализует их в основном процессе. Суббизнес можно вызвать через ipcApi в собственном коде. не заботясь о деталях.
Последний пункт,потому что<webview> Тег может быть создан с помощью сценария процесса рендеринга, а атрибут preload указывает на локальный сценарий. В целях безопасности мы должны перехватить событие will-attach-webview, проверить параметры и указать, что разрешены только наши собственные. необходимо смонтировать, чтобы избежать злонамеренного вмешательства со стороны сторонних сценариев. Вы также можете ограничить некоторые действия в веб-просмотре, например отключить перенаправление и т. д. Подробности см. в официальной документации Electron.
В этой статье представлены характеристики четырех контейнеров представлений в Electron и соответствующие им методы связи ipc.
Все три подпредставления имеют схожие функции и могут использоваться для внедрения сторонних сервисов. При реальном использовании вы можете выбрать наиболее подходящее решение в зависимости от бизнес-сценария.