-
-
Notifications
You must be signed in to change notification settings - Fork 262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sharing code between main and renderer processes #189
Comments
You are right. Due to the fact that each package technically built independently, there can be difficulties to share modules. In fact, I was thinking about this problem. But I did not find a solution. Technically you can import anything outside of the root directory. Example: // packages/shared/constants.ts
export const FOO = 'bar' // packages/renderer/src/App.vue
import {FOO} from '../../shared/constants' This works for me, provided that the shared files are easily tree-shaken and have no side effects. You must understand that such shared files will be imported and executed for each module independently. And this imposes some limitation:
You could explore how this repo works. As far as I know, the author used a plugin-based approach. Maybe this will help you. |
I'd just like to explain how I share type-safe IPC between render and main process. Unfortunately, I don't have a nice template repo like this to share, so I'll just show code fragments. First, I have a type IpcRequestResponse =
| { type: "FileAccess_OpenFile", request: null, response: TheFileData }
| { type: "FileAccess_SaveFile", request: { data: TheFileData, saveAs: boolean }, response: { saveCanceled: boolean }
| ... Each request or response defines both the request type and the response type ( I then define type MessageTypes<A> = A extends { type: infer T } ? T : never;
type IpcRequest<A, T> = A extends { type: T; request: infer R } ? R : never;
type IpcResponse<A, T> = A extends { type: T; response: infer R } ? R : never;
export type ProjectNameIpcMessageTypes = MessageTypes<IpcRequestResponse>;
export type ProjectNameIpcRequest<T extends ProjectNameIpcMessageTypes> = IpcRequest<
IpcRequestResponse,
T
>;
export type ProjectNameIpcResponse<T extends ProjectNameIpcMessageTypes> = IpcResponse<
IpcRequestResponse,
T
>; This automatically pulls out the message types, the request type for a specific message, and the response type for a specific message. You get compile errors if you try and get the request type of a message that does not exist. In type MessageHandlers = {
[key in ProjectNameIpcMessageTypes]: (
window: BrowserWindow,
request: ProjectNameIpcRequest<key>
) => Promise<ProjectNameIpcResponse<key>>;
};
const handlers: MessageHandlers = {
FileAccess_OpenFile: async (window, req) => {
const file = await dialog.showOpenDialog(window, { title: ..... });
...
return { theFileData: ... }
},
FileAccess_SaveFile: async (window, req) => { ... },
...
} What is nice is that you get a compile error if you forget one of the types ( Since the handlers are in main instead of preload, I need to bridge main and preload which I do using In const window = new BrowserWindow(...);
const { port1, port2 } = new MessageChannelMain();
port1.on("message", (msgEvt) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const message: { id: unknown; type: ProjectNameIpcMessageTypes; request: unknown } =
msgEvt.data;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
((handlers as any)[message.type](window, message.request) as Promise<any>)
.then((response) => {
port1.postMessage({
id: message.id,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
response: response,
});
})
.catch((err) => {
if (err instanceof Error) {
err = err.message;
}
port1.postMessage({
id: message.id,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
error: err,
});
});
});
port1.start();
window.webContents.on("did-finish-load", () => {
window.webContents.postMessage("projectname-init", "", [port2]);
}); Each message is an object containing an For The preload script is very short because all the work is done in main and the renderer. It just shuffles the projectnameInit message along to the renderer, passing along the port. This is the entierty of my const { ipcRenderer } = require("electron");
ipcRenderer.once("projectname-init", (evt, msg) => {
window.postMessage({ projectnameInit: true, msg }, "*", evt.ports);
}); Finally, the renderer contains code to send and receive messages over the port using the specific api types imported from // eslint-disable-next-line @typescript-eslint/no-explicit-any
const inFlight = new Map<number, (response: any) => void>();
let nextId = 0;
let resolvePort: (p: MessagePort) => void;
const portPromise: Promise<MessagePort> = new Promise((resolve) => (resolvePort = resolve));
function handleProjectInit(event: MessageEvent<{ projectnameInit: boolean }>) {
if (event.source === window && typeof event.data === "object" && event.data && event.data.projectnameInit) {
resolvePort(event.ports[0]);
event.ports[0].onmessage = (portMsg) => {
const msg = portMsg.data;
const handler = inFlight.get(msg.id);
if (handler) {
handler(msg);
}
};
window.removeEventListener("message", handleProjectInit);
}
}
window.addEventListener("message", handleProjectInit);
export function sendIpc<T extends ProjectNameIpcMessageTypes>(ty: T, request: ProjectNameIpcRequest<T>): Promise<ProjectNameIpcResponse<T>> {
const messageId = nextId;
nextId += 1;
return portPromise.then(
(port) =>
new Promise((resolve, reject) => {
inFlight.set(messageId, (response) => {
inFlight.delete(messageId);
if (response.error) {
reject(response.error);
} else {
resolve(response.response);
}
});
port.postMessage({
id: messageId,
type: ty,
request: request,
});
})
);
} Each mesage has an incrementing id and the The Finaly, |
@wuzzeb Thanks a lot. It works perfectly fine. |
@cawa-93 and @wuzzeb thanks for your detailed responses, excuse the delayed response. As I prepare to release my app into the wild, I've been thinking about security and I want to remove the |
@wuzzeb I might try using contextIsolation bridge instead in order to create typesafety for ipc stuff |
The main difference is the context bridge is synchronous while using message channels as I describe above is asynchronous and you can await the response. With my solution it is nice to be able to just I guess it depends on what kinds of things you are doing in the main process, if it is just things like file open, you probably don't care that the renderer process is frozen while the synchronous call to the main process happens. |
For sharing actual code I just use a "local" dependency. It's a little klunky because you need to do "yarn install" when you make changes to the common code, and it gets duplicated across the two parts of the code, but it's nice for things like shared string ID's. You can see it in action if you want right here kevinfrei/EMP@0c1af40 |
@kevinfrei such a simple and neat solution, thanks mate! |
Is your feature request related to a problem? Please describe.
This repository is posited as an alternative to electron-webpack as the library is now in "maintenance mode" as stated in readme.
In electron-webpack, is it possible to share resources between main and renderer processes. This allows for best practices such as avoiding the hard-coding of event name strings for IPC communication via constants. In this case, common code resources are located in a folder which is a sibling to main and renderer folders.
Describe the solution you'd like
This repo should include configuration that enables sharing of code between main and renderer builds.
Describe alternatives you've considered
I've attempted to use the solution posited in this issue but this causes typescript errors. Aliases present DX issues because one can't use regular import statements like in webpack and must resort to some kind of module resolver which is beyond the scope of someone who wants to get started on an electron project quickly and easily.
Another solution I've attempted: move index.html file directly inside packages folder, setting the PACKAGE_ROOT to be packages folder. For renderer, main, and preload folders, in vite config files, change build.lib.entry value to be "[folderName]/src/index.ts" and change index.html script src to be "./renderer/src/index.tsx". This does not fix the issue and the watch tasks errors out:
The text was updated successfully, but these errors were encountered: