electron-vue-next
This repository contains the starter template for using vue-next with the latest electron.
I started to learn electron & vue by the great project electron-vue. This project is also inspired from it.
Holpfully, you generally learn how to use rollup & its plugin API from this project 😃
Features
- Electron 11
- Follow the security guide of electron, make renderer process a browser only environment
- Using electron-builder to build
- Empower vue-next and its eco-system
- Using vite which means develop renderer process can be blazingly fast!
- Using vuex 4.0 with strong type state, getters, and commit
- Using vue-router-next
- Using eslint with Javascript Standard by default
- Built-in TypeScript Support
- NodeJS
worker_threads
workflow out-of-box- Don't need to worry the worker threads bundle/build when you use it.
- Preload scripts build workflow out-of-box
- No need to worry about the bundle/build of electron preload script.
- Built-in support for preload reload in DEV mode. Changing preload scripts won't restart the whole electron app
- Github Action with Github Release is out-of-box
- Auto bump version in package.json and generate CHANGELOG.md if you follow the Conventional Commits
- Detail how this work described in Release Process section
- Integrate VSCode well
- Support debug .ts/.vue files in main/renderer process by vscode debugger
- Detail see Debug section
- Multiple Windows Support
- Can add a new window for App easily. See Add a New Window section
- vue-devtool support
- Run npm run postinstall to install extensions
- Support vue-router-next and vuex 4 with new UI
Quick Start
You should use npm init
to create the project from template: npm init electron-vue-next
.
Once you're done, you can run following commands:
# Install dependencies
npm install
# Will start vite server, rollup devserver, and electron to dev!
npm run dev
# OPTIONAL. Will compile the main and renderer process to javascript and display output size
npm run build
# OPTIONAL. Will compile all and output an unpacked electron app. You can directly
npm run build:dir
# Will compile all and build all products and ready to release
npm run build:production
Config Your Project and Build
Once you install your project, you should change the package base info in package.json, and also the build information in build.base.config.js.
Project Structure
File Tree
Your workspace should looks like
your-project
├─ scripts all dev scripts, build script directory
├─ extensions temp folder of the vue-devtools extension
├─ build build resource and output directory
│ └─ icons/ build icon directory
├─ dist compiled output directory
├─ src
│ ├─ main
│ │ ├─ services/ services to access the network or files
│ │ ├─ workers/ multi-thread scripts using nodejs worker_threads
│ │ ├─ dialog.ts the ipc handler to support dialog API from renderer process
│ │ ├─ global.ts typescript global definition
│ │ ├─ index.dev.ts the development rollup entry
│ │ ├─ index.ts real electron start-up entry file
│ │ └─ logger.ts a simple logger implementation
│ ├─ preload
│ │ ├─ index.ts the preload entry
│ │ └─ another.ts another preload entry
│ ├─ renderer
│ │ ├─ assets/ assets directoy
│ │ ├─ components/ all vue components
│ │ ├─ hooks/ hooks or composition API
│ │ ├─ router.ts vue-router initializer
│ │ ├─ store.ts vuex store initializer
│ │ ├─ App.vue entry vue file imported by index.ts
│ │ ├─ index.css entry css file for vite
│ │ ├─ index.html entry html file for vite
│ │ └─ index.ts entry script file for vite
│ └─ shared shared folder can be access from both main and renderer side
│ ├─ store/ vuex store definition
│ └─ sharedLib.ts an example file that can be access from both side
├─ static/ static resource directory
├─ .eslintrc.js
├─ .gitignore
├─ package.json
└─ README.md
assets, static resources, build resources... what's the difference?
The assets is only used by the renderer process (in-browser display), like picture or font. They are bundled by vite/rollup. You can directly import
them in .vue/.ts
files under renderer directory. The default assets are in renderer/renderer/assets
The static resources are the static files which main process wants to access (like read file content) in runtime vie file system. They might be the tray icon file, browser window icon file. The static folder is at static.
The build resources are used by electron-builder
to build the installer. They can be your program icon of installer, or installer script. Default build icons are under build/icons.
Notice that your program icon can show up in multiple place! Don't mixup them!
- In build icons, of course you want your program has correct icon.
- In static directory, sometime you want your program has tray which require icon in static directory.
- In assets, sometime you want to display your program icon inside a page. You need to place them in the assets!
Main and Renderer Processes
Quote from electron official document about main and renderer processes. The main process is about
- The Main process creates web pages by creating BrowserWindow instances. Each BrowserWindow instance runs the web page in its Renderer process. When a BrowserWindow instance is destroyed, the corresponding Renderer process gets terminated as well.
- The Main process manages all web pages and their corresponding Renderer processes.
And the renderer process is about
- The Renderer process manages only the corresponding web page. A crash in one Renderer process does not affect other Renderer processes.
- The Renderer process communicates with the Main process via IPC to perform GUI operations in a web page. Calling native GUI-related APIs from the Renderer process directly is restricted due to security concerns and potential resource leakage.
Commonly, the main process is about your core business logic, and renderer side act as a data consumer to render the UI. Though, this is not absolutly right. Someone thinks the main process should not do any job other than communicating with system API (by electron API), since once the main process is blocked, the whole app will be blocked (not responsed). So if you have some really CPU heavy job, you definitly should not put them in the main process (Most of IO job in nodejs are async, that's fine). Maybe you can use the nodejs worker_thread module to put them into another thread, so it won't make the whole app not responsable.
So this design is depend on your core business type. If it's a IO heavy job, it's fine to put them in main process. If it's a CPU heavy job, you need to consider not to put them much in main process.
Following the security guideline of electron, in this boilerplate, the renderer process does not have access to nodejs module by default. The electron provide the preload
options in webPreferences
. This boilerplate provides one simple way to solve this problem, wrapping your core logic into Service
.
The Service
is a type of class defined under the src/main/services
. All the public method can be access by the renderer process. It's the bridge between the main and renderer. You can look at Service for the detail.
Notice that this is really a simple/trivial solution which derives from my personal electron project (much complex case). It only shows a possibility. Real life software might need more modifications on it!
NPM Scripts
npm run dev
Start the vite dev server hosting the renderer webpage with hot reloading. Start the rollup server hosting the main process script. It will auto reload the electron app if you modify the source files.
npm run build
Compile both main
and renderer
process code to production, located at dist
npm run build:production
It will compile both processes, and then run electron-builder
to build your app into executable installer or zip. The build config is defined in scripts/build.base.config.js.
npm run build:dir
It will compile both processes, and it will run electron-builder
to build only the directoy version of the production electron app, which for example, for windows x64, it's located at build/win-unpacked
.
This will much faster than npm run build:production
. So you can use it to quick testing the production app.
npm run lint
Run eslint to report eslint error.
npm run lint:fix
Run eslint to fix and report eslint error.
Development
Due to the project is following the security guideline. It does not allow the renderer to access node by default. The Service is a simple solution to isolate renderer logic and the core logic with full nodejs module access. See this section if you want to directly use node modules in renderer process.
Service
A Service lives in a class in src/main/services
. It should contain some of your core logic with file or network access in main process. It exposes these logic to renderer process. You call the hook useService('NameOfService')
to use it in renderer side.
The concept of service is totally optional. This is a design for security. If you think this is redundent and not fit with your program design, you can just remove it.
Create a new Service
Add a file to the /src/main/services
named BarService.ts
export default class BarService extends Service {
async doSomeCoreLogic() {
// perform some file system or network work here
}
}
And you need to add it to the interface Services
in src/main/services/index.ts
.
import { BarService } from './BarService'
export interface Services {
// ... other existed services
BarService: BarService
}
Then, add it to the initializeServices
in src/main/index.ts
async function initializeServices(logger: Logger) {
initialize({
// ...other services
BarService: new BarService(logger)
})
}
And this is ready to be used in renderer process by useService('BarService')
. See Using Service in Renderer.
Using Other Service in a Service
If you need to use other Service
, like FooService
. You need to @Inject
decorator to inject during runtime.
export default class BarService extends Service {
@Inject('FooService')
private fooService: FooService
async doSomeCoreLogic() {
const result = await this.fooService.foo()
// perform some file system or network operations here
}
}
Using Service in Renderer
You can directly access all the async methods in a service class by useService('nameOfService')
Here is an example in About.vue, using the BaseService
.
<template>
<div>
<img alt="Vue logo" src="../assets/logo.png" />
<div>Electron Version: {{ version }} </div>
<div>Appdata Path: {{ path }} </div>
<div>Running Platform: {{ platform }} </div>
</div>
</template>
<script lang=ts>
import { defineComponent, reactive, toRefs } from 'vue'
import { useService } from '../hooks'
export default defineComponent({
setup() {
const { getBasicInformation } = useService('BaseService')
const data = reactive({
version: '',
path: '',
platform: ''
})
getBasicInformation().then(({ version, platform, root }) => {
data.version = version
data.path = root
data.platform = platform
})
return {
...toRefs(data)
}
}
})
</script>
Remove Service Infra
If you don't like Service design, you just easily remove it by
- Remove the whole
src/main/services
directory - Remove the import line
import { initialize } from './services'
and initialization lineinitialize(logger)
insrc/main/index.ts
Static Resource
Place all your static resources under the static
folder.
To use them in the main process, you just need to import them by /@static/<filename>
.
For example, we have a logo.png
in static folder, and we want to get it path in runtime:
import logoPath from '/@static/logo.png' // this is the absolute path of the logo
The plugin manage this is the scripts/rollup.static.plugin.js
.
Preload Script
No matter you use the Service design or not, you will need to care about the preload script of the BrowserWindow
.
If you don't know what is the preload script. You can read it in electron document about BrowserWindow, and also the security guideline.
In this template, we have already setup the build script for preload. You can see all the preloads under /src/preload
.
You must put the preload script under that folder. If you want to use it in main process. You can just import them by import preloadPath from '/@preload/<your-preload-file>'
For example, if you add a new preload script named /src/preload/my-preload.ts
, you can refer it while creating the BrowserWindow
:
import myPreloadPath from '/@preload/my-preload'
new BrowserWindow({
webPreferences: {
preload: myPreloadPath,
}
})
The rollup config of preload is located at the rollup.config.js
.
The preload script in dist
will be built like dist/<name>.preload.js
.
The plugin manage this process is located at scripts/rollup.preload.plugin.js
.
Hooks or Composable in Renderer Process
One great feature of vue 3 is the composition-api. You can write up some basic piece of logic and compose them up during the setup functions. Currently, these hooks
are placed in /src/renderer/hooks
by default.
Take the example from vue composition api site, you have such code in /src/renderer/hooks/mouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
You'd better to export this mouse.ts
to /src/renderer/hooks/index.ts
// other exports...
export * from './mouse.ts'
Then in the vue
file you can import all hooks by the alias path
<template>
...template content
</template>
<script lang=ts>
import { defineComponent } from 'vue'
import { useMousePosition } from '/@/hooks'
export default defineComponent({
setup() {
const { x, y } = useMousePosition()
// other logic
return { x, y }
}
})
</script>
Electron API in Renderer Process
The boilplate exposes several electron APIs by default. You can access them by useShell
, useClipboard
, useIpc
and useDialog
. These are provided by static/preload.js
script. If you remove the preload during the creation of this BrowserWindow, this won't work.
import { defineComponent } from 'vue'
import { useShell } from '/@/hooks'
export default defineComponent({
setup() {
const shell = useShell() // this is equivalence to the import { shell } from 'electron' normally
// the shell object type definition works normally
}
})
The only exception is the useDialog
. You can only use async
functions in it as the API call goes through IPC and it must be async
.
Dependencies
If you adding a new dependency, make sure if it's using any nodejs module, add it as external
in the package.json
. Otherwise, the vite will complain about "I cannot handle it!".
{
// ...other package.json content
"dependencies": {
// ...other dependencies
"a-nodejs-package": "<version>"
},
"external": [
// ...other existed excluded packages
"a-nodejs-package" // your new package
],
// ...rest of package.json
}
The raw javascript dependencies are okay for vite.
Native Dependencies
If you want to use the native dependencies, which need to compile when install, usually, you need node-gyp to build, the electron-builder
will rebuild it upon your electron for you. Normally you don't need to worry much. Notice that if you are in Windows, you might want to install windows-build-tools to install the compile toolchain.
Dependencies Contains Compiled Binary
If you want to use the dependencies containing the compiled binary, not only you should adding it to vite exclude
, you should also take care about the electron-builder config. See the Build section for detail. The development process won't affect much by it.
New Window
- Add a new html file under the
src/renderer
- Reference some typescript/javascript file in you new added html file
- Add a code block in your
src/main/index.ts
to control the creation of this window
For example, you just added a side.html
under the src/renderer
. You need to add such controller code in index.ts
:
import preload from '/@preload/index'
// This function should be called once app is ready
function createANewWindow() {
// this part is the same as before, modify it as your wish
const win = new BrowserWindow({
height: 600,
width: 300,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false
}
})
// __windowUrls.side is pointing to the real url of `side.html`
win.loadURL(__windowUrls.side)
}
The scripts/vite.config.js
will automatically scan all html files under the src/renderer
. So, you do not need to touch the any vite/rollup config files. But, if you want more customization, you can refer the official vite document about the multi-page app!
Worker Threads
If you want to use worker_threads in main process, you need separately load the Worker
script. The template already setup the build/bundle process of the Worker
script. Normally, you do not need to modify this build process.
You just need to import the worker script by ending a ?worker
query in import.
If you have a new worker file named src/main/workers/sha256.ts
, you can access it in main process like:
import createSha256Worker from './workers/sha256?worker'
import { Worker } from 'worker_threads'
const worker: Worker = createSha256Worker(/* options */)
The worker thread files are built together with the normal main process code. They are under the same config in rollup.config.js
.
In dist
, The worker script will be compiled as dist/<name>.worker.js
.
Debugging
This is really simple. In vscode debug section, you will see three profiles:
- Electron: Main (attach)
- Electron: Renderer (attach)
- Electron: Main & Renderer (attach)
{
"version": "0.2.0",
"configurations": [
{
"name": "Electron: Main (attach)",
"type": "node",
"request": "attach",
"cwd": "${workspaceFolder}",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"smartStep": true,
"sourceMaps": true,
"protocol": "inspector",
"port": 5858,
"timeout": 20000
},
{
"name": "Electron: Renderer (attach)",
"type": "chrome",
"request": "attach",
"port": 9222,
"webRoot": "${workspaceFolder}",
"timeout": 15000
},
],
"compounds": [
{
"name": "Electron: Main & Renderer (attach)",
"configurations": ["Electron: Main (attach)", "Electron: Renderer (attach)"]
}
]
}
The name should be clear. The first one attach to main and the second one attach to renderer (required vscode chrome debug extension). The third one is run the 1 and 2 at the same time.
You should first run npm run dev
and start debugging by the vscode debug.
Option: Using Node Modules in Renderer Process
By default, the renderer process environment is just a raw front-end environment. You cannot use any nodejs module here. (Use service alternative)
If you just want to use node modules in electron renderer/browser side anyway, you can just enable the nodeIntegration
in BrowserWindow creation.
For example, you can enable the main window node integration like this:
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
webPreferences: {
preload: join(__static, 'preload.js'),
nodeIntegration: true // adding this to enable the node integration
}
})
Build
The project build is based on electron-builder. The config file is majorly in scripts/build.base.config.js. And you can refer the electron-builder document.
Compile
The project will compile typescript/vue source code by rollup into javascript production code. The rollup config for main process is in rollup.config.js. It will output the production code to dist/index.js
.
Notice that by default, this project's rollup config won't bundle the nodejs dependencies used in main process. As the rollup is based on esm, it has a hard time to resolve some circular dependencies problems, which can happen frequently in some nodejs package (e.g. electron-updater, Webpack can handle these kind of problem though). Once you put them into the external
in package.json
, they will be packed in the node_modules
in output asar. Just let you know that this is not like webpack, bundling them into your index.js
.
The config to compile renderer process is in vite.config.js. It will compile the production code into dist/renderer/*
.
Speed Up Compile
If you feel the compile time is too long, this is majorly caused by typecheck for main process code.
You can set the wait to typescript plugin to false in rollup.config.js
to skip type check to dev.
pluginTypescript({
tsconfig: [join(__dirname, '../src/main/tsconfig.json'), join(__dirname, '../src/preload/tsconfig.json')],
wait: false // add this to not wait the type error result
}),
You will still see the typecheck result, but the build won't be prevent if the typecheck failed.
If you still think the compile is slow, you can just remove this plugin from rollup.config.js
Exclude Files
Normally, once you correctly config the dependencies
in Development section, you should not worry to much about the build. But some dependencies contains compiled binary. You might want to exclude them out of the unrelated OS builds.
For example, 7zip-min:
Since it using the 7zip-bin
which carry binary for multiple platform, we need to correctly include them in config. Modify the electron-builder build script build.base.config.js
asarUnpack: [
"node_modules/7zip-bin/**/*"
],
Add them to asarUnpack
to ensure the electron builder correctly pack & unpack them.
To optimize for multi-platform, you should also exclude them from files
of each platform config build.config.js
mac: {
// ... other mac configs
files: [
"node_modules/7zip-bin/**/*",
"!node_modules/7zip-bin/linux/**",
"!node_modules/7zip-bin/win/**"
]
},
win: {
// ... other win configs
files: [
"node_modules/7zip-bin/**/*",
"!node_modules/7zip-bin/linux/**",
"!node_modules/7zip-bin/mac/**"
]
},
linux: {
// ... other linux configs
files: [
"node_modules/7zip-bin/**/*",
"!node_modules/7zip-bin/win/**",
"!node_modules/7zip-bin/mac/**"
]
},
Release
The out-of-box github action will validate each your PR by eslint and run npm run build
. It will not trigger electron-builder to build production assets.
For each push in master branch, it will build production assets for win/mac/linux platform and upload it as github action assets. It will also create a pull request to asking you to bump version and update the changelog.
It using the conventional-commit. If you want to auto-generate the changelog, you should follow the conventional commit guideline.
If the bump version PR is approved and merged to master, it will auto build and release to github release.
If you want to disable this github action release process, just remove the .github/workflows/build.yml file.
AutoUpdate Support
This boilerplate include the electron-updater as dependencies by default. You can follow the electron-builder guideline to implement the autoUpdate process.