preface
- Vite is a build tool for the next few years, and it’s worth trying to integrate it in every scenario
- Electron is a standard desktop development tool for the front end, which has no official scaffolding and is not integrated with any framework
@vue/cli
Official templates are given; But the Vite piece is not provided, after all, the positioning is andwebpack
Universal build tools like that
Even Vue doesn’t integrate 🖖 so let’s try to do that
According to Vue’s integration style in Vite, the Electron piece should be written as a plug-in!
Note 📢
- This assumes that you know a little about how Vite works, and there are plenty of articles about it online
- Also assume you’ve used or played Electron; Get started is very simple, directly look at the official website
- All the code of the project can be directly used for production in the electron vue-Vite.
Directory structure design
· ├ ─ ─ script project scripts directory ├ ─ ─ the SRC | ├ ─ ─ the main Electron main process code | ├ ─ ─ preload Electron preload directory | ├ ─ ─ render Electron - both Vite rendering process Code | ├ ─ ─ vite. Config. Ts vite configuration fileCopy the code
Vite. Config. Ts configuration
- Electron supports the full NodeJs API rendering process will inevitably be used
- Vite based Rollup build project, so we modify the Rollup part of the configuration, output CommonJs format
- The directory structure is different from the default structure provided by Vite and needs to be configured to work as expected
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { join } from 'path'
export default defineConfig((env) = > ({
plugins: [
vue(), // Enable Vue support].root: join(__dirname, 'src/render'), // Point to the renderer directory
base: '/'.// static resource loading location in index.html
build: {
outDir: join(__dirname, 'dist/render'),
assetsDir: ' '.// Relative path loading problem
rollupOptions: {
output: {
format: 'cjs'.// configure Rollup to package output in CommonJs format
},
external: ['electron'].// Tell Rollup not to pack Electron}},optimizeDeps: {
exclude: ['electron'].// Tell Vite not to convert the electron module
},
// Other configuration omitted...
}))
Copy the code
Start Script Analysis
- Let’s start with the conclusion – Electron’s startup behavior is almost identical to NodeJs’s –
Executable program
+Entrance to the file
#NodeJs installed in the global directory
node path/filename.js
#Project directory installation at Electron
node_modules/.bin/electron path/filename.js
Copy the code
- If we design the Electron’s startup to
npm
的scripts
It can also be simplernpm run electron
{
"scripts": {
"electron": "electron path/filename.js"}}Copy the code
- 🤔 Think about it,
npm
Only one start command is required for Vite and Electron, which adds up to two start commands hereconcurrently
Start Vite and Electron at the same time
{
"scripts": {
"dev": "concurrently \"npm run vite\" \"npm run electron\""."vite": "vite"."electron": "electron path/filename.js"}}Copy the code
Looks good!
- But let’s think about the Electron starting problem
- Electron should load a Vite startup development server in the development environment and launch a specific file in production
- In this case, the Electron waits for Vite to start
Startup script Design
- We need to monitor the startup of Vite and pull the Electron. In this case, we will consider monitoring the port through rotation training
- After Vite starts up Electron we use the NodeJs child PROCESS API
child_process.spawn()
Pull up - We will be
npm scripts
Make a change so we know what the script does,Let’s rename it relative to the script aboveconcurrently
Also add some command-line arguments for a more console friendly output
{
"scripts": {
"dev": "concurrently -n=vue,ele -c=green,blue \"npm run dev:vue\" \"npm run dev:ele\""."dev:vite": "vite"."dev:electron": "node -r ts-node/register script/build-main --env=development --watch"}}Copy the code
- Since we want to control the Electron startup, we will write a separate script (script/build-main.ts) to control the Electron startup, including the following two function points
- Startup Timing Control – Listen Vite is started
- Main process code is developed using typescript – compiled and packaged with Rollup
script/build-main.ts
import { join } from 'path'
import { get } from 'http'
import { spawn, ChildProcess } from 'child_process'
import { watch } from 'rollup'
import minimist from 'minimist'
import electron from 'electron'
import options from './rollup.config'
import { main } from '.. /package.json'
/** * 1. Listen to vite start */
function waitOn(arg0: { port: string | number; interval? :number; }) {
return new Promise(resolve= > {
const { port, interval = 149 } = arg0
const url = `http://localhost:${port}`
// Send requests to the Vite server through timer rotation
const timer: NodeJS.Timer = setInterval(() = > {
get(
`http://localhost:${port}`.// Point to the Vite development server
res= > {
clearInterval(timer)
resolve(res.statusCode)
}
)
}, interval)
})
}
/** * 2. Control the Electron startup time and compile typescript */
waitOn({ port: '3000' }).then(msg= > {
// Parse command line arguments to NPM script
const argv = minimist(process.argv.slice(2))
// Loads the rollup configuration
const opts = options(argv.env)
// Vite starts in listening mode Rollup compile the Electron main process code
const watcher = watch(opts)
let child: ChildProcess
watcher.on('event'.ev= > {
if (ev.code === 'END') {
// Ensure that only one Electron program is started
if (child) child.kill()
// Pull the Electron program using the NodeJs subprocess capability
child = spawn(
// Here electron is essentially just a string; Absolute path to the Electron executable
electron as any.// Specify the Electron main process entry file; The path to the output file after Rollup compilation
[join(__dirname, `.. /${main}`)] and {stdio: 'inherit'})}})Copy the code
script/rollup.config
import { builtinModules } from 'module'
import { join } from 'path'
import { RollupOptions } from 'rollup'
import nodeResolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import json from '@rollup/plugin-json'
/** node.js builtins module */
const builtins = () = > builtinModules.filter(x= > !/^_|^(internal|v8|node-inspect)\/|\//.test(x))
export default (env = 'production') = > {const options: RollupOptions = {
input: join(__dirname, '.. /src/main/index.ts'),
output: {
file: join(__dirname, '.. /dist/main/index.js'),
format: 'cjs'.// Use CommonJs modularity
},
plugins: [
nodeResolve(), // support package lookup under node_modules
commonjs(), // Support CommonJs module
json(), // Support for importing JSON files
typescript({
module: 'ESNext'./ / support the typescript}),].external: [
// Package to avoid built-in modules. builtins(),'electron',]}return options
}
Copy the code
At this point, the project should be running; However, Electron, nodeJs-related apis are not yet available
Here, some copywriting in app. vue and HelloWorld. Vue was simply changed without logical modification. I’m not going to post the code
Join the Electron API
- Rendering and main process communication is a very common feature; I try to
electron
exportipcRenderer
// src/render/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { ipcRenderer } from 'electron'
console.log('ipcRenderer:', ipcRenderer)
createApp(App).mount('#app')
Copy the code
- An error! By default, direct import syntax will be compiled by Rollup
- In fact in
'electron'
In the Electron environment is oneBuilt-in moduleYou can try this code out in the console
Note that the Electron related package is not introduced here to ensure that the project can run
require.resolve('electron')
"electron" // Will output
Copy the code
- Since Electron already supports the full NodeJs API we might as well just write it in code
// src/render/main.ts
import { createApp } from 'vue'
import App from './App.vue'
- import { ipcRenderer } from 'electron'
+ const { ipcRenderer } = require('electron')
console.log('ipcRenderer:', ipcRenderer)
createApp(App).mount('#app')
Copy the code
The project did work, and we can use this method to further validate other modules
Plug-in design analysis
-
It’s a safe bet that this one will work! (Development period only); But there are two problems with this
- Different coding styles. They all use it
ESModule
Mixed withCommonJs
It’s not pretty require('xxxx')
If no processing is done during packaging, it will not be Rollup processed.ts
Documents, there are big god know how to deal with this situation, please point out the little brother)
If you’re importing packages in node_modules, that’s bad; So require(‘ electronic-store ‘) will print it as is; The package will not find the ‘electronic-store’ module when it opens.
- Different coding styles. They all use it
-
We know that the ESModule script will get an error when running during development, but we should write it anyway;
Wouldn’t we have the best of both worlds if we converted ESModule to NodeJs built-in CommonJs at the moment of execution? We can even convert any package that is related to the NodeJs API. After all, node_modules is the repository of the NodeJs package in the development project root directory.
Analysis so far, we should move handwriting plug-in; Let plug-ins automate their completion – ESModule to CommonJs
vitejs-plugin-electron
- See the official website for the Vite plugin tutorialVitejs. Dev/guide/API – p…Personally, I think it’s better
webpack
The plugins over there are easier to write. - To support parameter passing for future extension, we make it a Function and return a plug-in
- Code handling this, we need an AST tool to help –
yarn add acorn
import * as acorn from 'acorn'
import { Plugin as VitePlugin } from 'vite'
const extensions = ['.js'.'.jsx'.'.ts'.'.tsx'.'.vue'] // File suffixes to be processed
export interfaceEsm2cjsOptions { excludes? :string[] // The module to be converted
}
export default function esm2cjs(options? : Esm2cjsOptions) :VitePlugin {
const opts: Esm2cjsOptions = {
// By default we convert the electron and electric-store modules
excludes: [
'electron'.'electron-store',],... options }return {
name: 'vitejs-plugin-electron'.// The name is the plug-in name
transform(code, id) {
const parsed = path.parse(id) // Resolve the path of the import module, id is the full path of the import file
if(! extensions.includes(parsed.ext))return // Process only the file suffixes that need to be processed
const node: any = acorn.parse(code, { // Use Acorn to parse ESTree
ecmaVersion: 'latest'.// Specify that the es module is resolved according to the latest standard
sourceType: 'module'.// Specify parsing by module
})
let codeRet = code
node.body.reverse().forEach((item) = > {
if(item.type ! = ='ImportDeclaration') return // Skip non-import statements
if(! opts.excludes.includes(item.source.value))return // Skip modules that do not convert
/** * The following const declarations are used to determine how to write import */
const statr = codeRet.substring(0, item.start)
const end = codeRet.substring(item.end)
const deft = item.specifiers.find(({ type }) = > type= = ='ImportDefaultSpecifier')
const deftModule = deft ? deft.local.name : ' '
const nameAs = item.specifiers.find(({ type }) = > type= = ='ImportNamespaceSpecifier')
const nameAsModule = nameAs ? nameAs.local.name : ' '
const modules = item.
specifiers
.filter((({ type }) = > type= = ='ImportSpecifier'))
.reduce((acc, cur) = > acc.concat(cur.imported.name), [])
/** * Here we begin to convert according to the various import syntax */
if (nameAsModule) {
// import * as name from
codeRet = `${statr}const ${nameAsModule} = require(${item.source.raw})${end}`
} else if(deftModule && ! modules.length) {// import name from 'mod'
codeRet = `${statr}const ${deftModule} = require(${item.source.raw})${end}`
} else if (deftModule && modules.length) {
// import name, { name2, name3 } from 'mod'
codeRet = `${statr}const ${deftModule} = require(${item.source.raw})
const { ${modules.join(', ')} } = ${deftModule}${end}`
} else {
// import { name1, name2 } from 'mod'
codeRet = `${statr}const { ${modules.join(', ')} } = require(${item.source.raw})${end}`}})return codeRet
},
}
}
Copy the code
- in
vite.config.ts
The use ofvitejs-plugin-electron
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vitejs-plugin-electron'
export default defineConfig((env) = > ({
plugins: [
vue(),
electron(),
],
// Other configuration omitted...
}))
Copy the code
- Run the project again
-
It ‘s Worked! 🎉 🎉 🎉
-
Ok, the plugin is ready to use; The Rollup configuration can be integrated into vitejs-plugin-electron to make the viet.config. ts file smaller and clearer.
-
And the packaging part, I won’t show you… Specific code is not demonstrated, their own pull code to see 🚀
-
vitejs-plugins-electron
conclusion
- Vite personally thinks it is a good solution, after all, packaging tools will be introduced to the stage of history; Vite took another step forward
Step 0.5
- The Electron integration is just one example, and from a case to writing a plug-in, you’ll have a better understanding of Vite design and ideas
- Finally, what can not stand in the objective point of view to wait, we need to take the initiative to build