The guides and issues are scattered, I summarized all together to start the journey.
Two computers for testing:
- Mac - MacBook Pro (Retina, 13-inch, Early 2015): macOS High Sierra (Version 10.13.6), English.
- Win - ThinkPad T430: Windows 10 professional, Chinese.
Setup steps:
- Install node.js from https://nodejs.org/
- Mac: Terminal, Win: Node.js command prompt. input command 'node -v' or 'node --version' to show the version of node.js and make sure the installation is successful. Installations below are global(-g) visible. If you want to limit to a specific project, add '--save-dev' before the package.
- 'npm install -g create-react-app' to install React (https://reactjs.org/).
- 'npm install -g typescript' to install TypeScript (https://www.typescriptlang.org/). Step 3 and 4 together command 'npx create-react-app my-app --template typescript'.
- 'npm install -g electron' to install Electron (https://www.electronjs.org/).
- 'npm install -g electron-builder' for packaging Electron application.
- 'npm install -g concurrently' for concurrent commands support.
- 'npm install -g wait-on' for step by step commands support. On Mac, you may be failed because of 'Missing write access permission'. Please access to https://flaviocopes.com/npm-fix-missing-write-access-error/ to fix it.
Configuration steps:
- Mac starts from the user's home folder. Mac & Win both use 'cd' to change directory.
- 'npx create-react-app my-app --template typescript' create an application named 'my-app' (change it to your project name) with typescript support. Use 'cd my-app' to the project folder. Setting up ESLint on VS Code with Airbnb JavaScript Style Guide: https://travishorn.com/setting-up-eslint-on-vs-code-with-airbnb-javascript-style-guide-6eb78a535ba6
- 'npm start' will launch the project and open a browser window to load URL 'http://localhost:3000/'. It works then the React works.
- Create an 'electron.ts' under the 'public' folder. Please check the sample codes below.
- Create a 'preload.js' under the 'public' folder. Please check the sample codes below.
- Edit 'src/App.tsx', add codes to communicate with Electron main process. Please check the sample codes (App.tsx, IBridge.ts, IAppData.ts, ElectronBrideg.ts) below.
- Edit 'package.json', add lines:
"main": "public/electron.ts", -- Electron initialization script
"homepage": "./", -- set it to fix possible ‘Failed to load resource’ errors
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron": "concurrently \"set BROWSER=none&&npm start\" \"wait-on http://localhost:3000&&set ELECTRON_ENV=development&&electron .\"", -- Win
"electron": "concurrently \"export BROWSER=none&&npm start\" \"wait-on http://localhost:3000&&export ELECTRON_ENV=development&&electron .\"", -- Mac
-- set or export ELECTRON_ENV to pass production environment variable
"package": "npm run build&&electron-builder build"
},
"build": {
"appId": "***",
"copyright": "***",
"productName": "***",
"files": [
"build/",
"public/electron.ts",
"!node_modules/"
],
"mac": {
"icon": "public/logo512.png",
"category": "public.app-category.utilities"
-- Need to provide signing details
-- https://www.electron.build/configuration/mac
},
"win": {
"icon": "public/logo512.png",
"target": [
"nsis",
"msi"
]
}
}
-- 'npm install' to fix possilbe dependencies or links issues.
-- Remove "react-scripts": "3.4.1" to avoid react-builder tricky error.
-- Add to improve security when load URLs (not file://).
- use 'npm run electron' in development mode. Please run 'npm run build' once before; use 'npm run package' to build the application. You can press 'Ctrl + C' to exit node.js process and run a new command.
Sample codes:
- electron.ts:
// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const isDev = process.env.ELECTRON_ENV?.trim() == 'development'
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
show: false,
webPreferences: {
contextIsolation: true,
preload: path.join(app.getAppPath(), './build/preload.js')
}
})
// Show the window after its ready
mainWindow.once('ready-to-show', () => {
// Show the window
mainWindow.show()
// Maximize the window
mainWindow.maximize()
// Not allowed to resize
// mainWindow.resizable = false
})
// Hide the application menu
mainWindow.setMenuBarVisibility(false)
// and load the app.
if (isDev)
mainWindow.loadURL('http://localhost:3000/')
else
mainWindow.loadFile(`${path.join(app.getAppPath(), './build/index.html')}`)
// Open the DevTools.
// Use 'Ctrl + Shift + I' instead
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
ipcMain.on('app', (event, arg) => {
event.reply('app-reply', { name: app.name, version: app.getVersion(), path: app.getAppPath() })
})
- preload.js:
const {
contextBridge,
ipcRenderer
} = require("electron")
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
"appRuntime", {
send: (channel, data = null) => {
ipcRenderer.send(channel, data);
},
subscribe: (channel, listener) => {
const subscription = (event, ...args) => listener(...args);
ipcRenderer.on(channel, subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
}
}
}
)
- App.tsx:
import React, {useState} from 'react'
import logo from './logo.svg'
import 'antd/dist/antd.css'
import './App.css'
import appRuntime from "./etsoo/bridges/ElectronBridge"
import IAppData from "./etsoo/bridges/IAppData";
function App() {
const [appData, setAppData] = useState('Loading...')
if(appRuntime != null) {
appRuntime.subscribe('app-reply', (data:IAppData) => {
setAppData(data.name + ': ' + data.version)
})
appRuntime.send('app')
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<p>
App: {appData}
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
)
}
export default App
- etsoo/bridges/IBridge.ts:
/**
* IBridge unsubscribe type
*/
type IBridgeUnsubscribe = () => void
/**
* IBridge subscribe listener type
*/
type IBridgeListener = (...args: any[]) => void
/**
* window.external calls bridge interface
*/
export interface IBridge {
/**
* Send data to the host process with an unique channel
* @param channel an unique channel
* @param data Data to send
*/
send(channel: string, data?: any):void
/**
* Subscribe to the host process
* @param channel an unique channel
* @param listener callback listener
*/
subscribe(channel: string, listener: IBridgeListener):IBridgeUnsubscribe
}
- etsoo/bridges/IAppData.ts:
/**
* Bridge App Data interface
*/
export default interface IAppData {
/**
* Application name
*/
name: string,
/**
* Application version
*/
version: string,
/**
* Application path
*/
path: string
}
- etsoo/bridges/ElectronBridge.ts:
import { IBridge } from './IBridge'
/**
* Electron bridge class
* copes with preload.js, contextBridge.exposeInMainWorld
* BrowserWindow.webPreferences, contextIsolation: true
*/
const appRuntime = (window as any).appRuntime as IBridge
export default appRuntime
Top comments (0)