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/cliOfficial templates are given; But the Vite piece is not provided, after all, the positioning is andwebpackUniversal 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 tonpmscriptsIt can also be simplernpm run electron
{
  "scripts": {
    "electron": "electron path/filename.js"}}Copy the code
  • 🤔 Think about it,npmOnly one start command is required for Vite and Electron, which adds up to two start commands hereconcurrentlyStart 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
    1. Electron should load a Vite startup development server in the development environment and launch a specific file in production
    2. 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 APIchild_process.spawn()Pull up
  • We will benpm scriptsMake a change so we know what the script does,Let’s rename it relative to the script above concurrentlyAlso 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
    1. Startup Timing Control – Listen Vite is started
    2. 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 toelectronexportipcRenderer
// 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

    1. Different coding styles. They all use itESModuleMixed withCommonJsIt’s not pretty
    2. require('xxxx')If no processing is done during packaging, it will not be Rollup processed.tsDocuments, 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.

  • 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 betterwebpackThe 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
  • invite.config.tsThe 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 forwardStep 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