The introduction

Due to work reasons, I came into contact with electron again. Actually, I developed a small tool using electron vue before, but as we all know, electron vue has not been updated for a long time. After searching a lot of information, I found that most of the manuals are based on Vue CLI Plugin Electron Builder to introduce Electron into Vue project. For some reason, I don’t want to do this via the Vue CLI Plugin Electron Builder method. I need to introduce Electron based on the existing project, so based on vue-CLI + Electron builder method.

First we have to have a VUE project

Install vuecli globally
  npm i @vue/cli -g
# create project
  vue create hello-word
# You can create vue2 or VUE3 based projects, depending on your needs
# Next, let's start our project
  npm run serve
A basic VUE page is now available in the web page
Copy the code

◉ Choose Vue Version ◉ Babel infection infection TypeScript infection Progressive Web App (PWA) Support ◉ Router infection infection of Vuex ◉ CSS pre-processors ◉ Linter/Formatter ◉ Unit Testing infection of E2E Testing? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, Router, CSS Pre-processors, Linter, Unit ? Choose a version of Vue.js that you want to start the project with 3.x ? Use history mode for router? (Requires proper server setup for index fallback in production) No ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass) ? Pick a linter / formatter config: Standard ? Pick additional lint features: Lint on save ? Pick a unit testing solution: Jest ? Where do you prefer placing config for Babel, ESLint, etc.? In package.json ? Save this as a preset for future projects? (y/N)Copy the code

Project transformation (written based on VUE3)

0. Install dependencies
  • The electron-store is used to store local data, which will be packaged and generated with a config.json file in the project

  • Element-plus UI framework (using elementUI for VUe2)

  • CPX is used in the scripts command in package to copy files after packaging

  • Cross-env environment variable

  • Electron (16.0.6)

  • Electron – Builder package tool

  • Node (14.17.3) Some installation errors may occur because the node version is too late.

      npm i electron-store element-plus -S # or NPM I electron-store element-uI-s
      npm i cpx cross-env electron electron-builder -D
      # If the installation is slow, please refer to the following command:
      npm i cpx cross-env electron electron-builder -D --registry=https://registry.npmmirror.com --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ --electron_mirror=https://npm.taobao.org/mirrors/electron/
      If taobao mirror does not specify the electron source, you can specify the electron version such as [email protected]
    Copy the code
1. Create a file in the root directoryvue.config.js, as follows:
  'use strict'
  const path = require('path')

  function resolve (dir) {
    return path.join(__dirname, dir)
  }

  // Distinguish the address to which the development environment's formal environment points
  const port = process.env.port || process.env.npm_config_port || 9521 // dev port

  module.exports = {
    publicPath: process.env.VUE_APP_PUBLIC_PATH,
    outputDir: 'build'.assetsDir: 'static'.lintOnSave: process.env.NODE_ENV === 'development'.productionSourceMap: false.devServer: {
      port: port,
      open: true.overlay: {
        warnings: false.errors: true
      },
      proxy: {
        '^/api': {
          target: `${process.env.VUE_APP_BASE_API}`.changeOrigin: true}}},configureWebpack: {
      // provide the app's title in webpack's name field, so that
      // it can be accessed in index.html to inject the correct title.
      resolve: {
        alias: {
          The '@': resolve('src'),
          rootpath: resolve('/'),
          assets: path.join(__dirname, 'src'.'assets')}}},chainWebpack: config= > { // Modify the webpack entry file. You need to create two entry JS files in the root directory
      config.entry('app').clear().add('./src/main.js')

      // it can improve the speed of the first screen, it is recommended to turn on preload
      config.plugin('preload').tap(() = >[{rel: 'preload'.// to ignore runtime.js
          // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
          fileBlacklist: [/\.map$/./hot-update\.js$/./runtime\.. *\.js$/].include: 'initial'}])// when there are many pages, it will cause too many meaningless requests
      config.plugins.delete('prefetch') config .when(process.env.NODE_ENV ! = ='development'.config= > {
            config
              .optimization.splitChunks({
                chunks: 'all'.cacheGroups: {
                  libs: {
                    name: 'chunk-libs'.test: /[\\/]node_modules[\\/]/,
                    priority: 10.chunks: 'initial' // only package third parties that are initially dependent
                  },
                  elementUI: {
                    name: 'chunk-elementUI'.// split elementUI into a single package
                    priority: 20.// the weight needs to be larger than libs and app or it will be packaged into libs or app
                    test: /[\\/]node_modules[\\/]_? element-plus(.*)/ // in order to adapt to cnpm
                  },
                  commons: {
                    name: 'chunk-commons'.test: resolve('src/components'), // can customize your rules
                    minChunks: 3.// minimum common number
                    priority: 5.reuseExistingChunk: true}}})// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
            config.optimization.runtimeChunk('single')})}}Copy the code
2. Environment variables (cannot be obtained in electron after packaging, but used for web packaging)
  • The new env. Development,. The env. Production file

  • .env.development

      ENV = 'development'
      NODE_ENV = 'development'
      # base api
      VUE_APP_BASE_API = '/'
      VUE_APP_PUBLIC_PATH = '/'
    Copy the code
  • .env.production

      ENV = 'production'
      NODE_ENV = 'production'
      # base api
      VUE_APP_BASE_API = '/'
      VUE_APP_PUBLIC_PATH = '/'
    Copy the code
3. ElectronFile entry
  • Create app folder and index.js,preload. Js, utils. Js file in root directory as follows:

    . ├ ─ ─ app │ ├ ─ ─ index. The js │ ├ ─ ─ preload. Js │ └ ─ ─ utils. JsCopy the code
  • index.js

      const { app, Menu, BrowserWindow, dialog } = require('electron')
      const path = require('path')
      const info = require('.. /package.json')
      const process = require('process')
      const Utils = require('./utils')
    
      Utils.ipcOn()
      // Platform decision
      const platform = require('os').platform()
      const isMac = platform === 'darwin'
    
      const winURL = process.env.NODE_ENV === 'development' ? 'http://localhost:9521' : `file://${path.resolve(__dirname, '.. /.. /app.asar/build/')}/index.html`
    
      // Keep a global reference of the window object, if you don't, the window will
      // be closed automatically when the JavaScript object is garbage collected.
      let mainWindow
    
      function createWindow () {
        const clearObj = {
          storages: ['appcache'.'filesystem'.'indexdb'.'localstorage'.'shadercache'.'websql'.'serviceworkers'.'cachestorage']}const template = [
          ...(isMac ? [{
            label: app.name,
            submenu: [{type: 'separator' },
              { label: 'service'.role: 'services' },
              { type: 'separator' },
              { label: 'hide'.role: 'hide' },
              { label: 'Hide other'.role: 'hideothers' },
              { type: 'separator' },
              { label: 'exit'.role: 'quit'}]}] : []), {label: 'view'.submenu: [{label: 'reload'.role: 'reload' },
              { label: 'Force reload'.role: 'forcereload' },
              { label: 'Developer Tools'.role: 'toggledevtools' },
              {
                label: 'Clear cached data'.accelerator: 'CmdOrCtrl+Shift+Delete'.click: (item, focusedWindow) = > {
                  if (focusedWindow) {
                    focusedWindow.webContents.session.clearStorageData(clearObj)
                  }
                }
              }
            ]
          },
          {
            label: 'other'.submenu: [{label: 'about'.click: () = > {
                  dialog.showMessageBox({
                    title: 'test'.message: 'test'.detail: `Version: ${info.version}`.type: 'info'})}}, {label: 'ping'.click: () = > {
                  mainWindow.webContents.send('ping'['ping', app.getPath('userData')])
                }
              }
            ]
          }
        ]
    
        mainWindow = new BrowserWindow({
          frame: true.width: 1220.height: 650.minWidth: 1220.minHeight: 650.center: true.resizable: true.show: false.webPreferences: {
            / / the parameters can be found in the document here at https://www.electronjs.org/zh/docs/latest/api/browser-window
            autoplayPolicy: 'no-user-gesture-required'.nodeIntegration: true.contextIsolation: true.preload: path.join(__dirname, './preload')
          }
        })
    
        mainWindow.center()
        // and load the index.html of the app.
        mainWindow.loadURL(winURL)// Here is the entry to load the renderer
    
        // Emitted when the window is closed.
        mainWindow.on('closed'.function () {
          // Dereference the window object, usually you would store windows
          // in an array if your app supports multi windows, this is the time
          // when you should delete the corresponding element.
          const currentWindow = BrowserWindow.getFocusedWindow()
          if (currentWindow === mainWindow) {
            mainWindow = null
          }
          mainWindow = null
        })
    
        mainWindow.once('ready-to-show'.() = > {
          mainWindow.show()
        })
    
        const menu = Menu.buildFromTemplate(template)
        Menu.setApplicationMenu(menu)
    
        if (platform === 'darwin') {
          mainWindow.excludedFromShownWindowsMenu = true}}// 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 OS X 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 () {
        console.log('main process activate')
        // On OS X 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 (mainWindow === null) {
          createWindow()
        }
    
        if (mainWindow) {
          mainWindow.show()
        }
      })
    
    Copy the code
  • The contextBridge method is used to mount the object on the window object, i.e. : Preload the specified script before running any other scripts on the page. Regardless of whether the page is integrated with Node, this script can access all Node APIS. The script path is the absolute path of the file. When Node Integration is closed, the preloaded script reintroduces node’s global reference flag from global scope

      const { contextBridge, ipcRenderer } = require('electron')
    
      contextBridge.exposeInMainWorld(
        '_platform',
        process.platform
      )
    
      /** * The communication method is mounted to the window object * used in the renderer process: *  */
      contextBridge.exposeInMainWorld('ipc', {
        send: (channel, ... args) = >ipcRenderer.send(channel, ... args),invoke: (channel, ... args) = >ipcRenderer.invoke(channel, ... args),on: (channel, listener) = > {
          ipcRenderer.on(channel, listener)
        }
      })
    
    Copy the code
  • Utils.js utility functions

      const { app, dialog, ipcMain, shell } = require('electron')
      const Store = require('electron-store')
      const store = new Store()
      const info = require('.. /package.json')
      const path = require('path')
    
      const Utils = {
        ipcOn: () = > {
          ipcMain.on('open-url'.(event, url) = > {
            shell.openExternal(url)
          })
    
          ipcMain.on('about'.(event) = > {
            dialog.showMessageBox({
              title: 'test'.message: 'test'.detail: `Version: ${info.version}`})})// The generic method is used to save local data
          ipcMain.on('saveStore'.(event, { storeName, val }) = > {
            store.set(storeName, val)
          })
    
          // The generic method is used to read local data
          ipcMain.on('getStore'.(event, { storeName, callBackName }) = > {
            console.log(storeName, callBackName)
            event.sender.send(callBackName, { [storeName]: store.get(storeName) })
          })
        }
      }
      module.exports = Utils
    
    Copy the code
4. package.jsonModify the
  • Scripts object Modification

      "scripts": {
        "serve": "vue-cli-service serve"."electron": "electron ./app/"."dev": "cross-env NODE_ENV=development electron ./app/"."build": "cross-env NODE_ENV=production vue-cli-service build"."electron:copy": "cpx ./app/**/*.js ./build"."pack:mac": "npm run build && npm run electron:copy && electron-builder --mac"."pack:win": "npm run build && npm run electron:copy && electron-builder --win"."pack:all": "npm run build && npm run electron:copy && electron-builder --win && electron-builder --mac"."test:unit": "vue-cli-service test:unit"."lint": "vue-cli-service lint"
      }
    Copy the code
  • The build object is added to configure the electron build packaging parameters

      "build": {
        "extraMetadata": {
          "main": "build/index.js"
        },
        "extraResources": [{"from": "resources/"."to": ". /"}]."productName": "test"."appId": "com.test.app"."files": [
          "build/**/*"]."mac": {
          "icon": "./resources/icons/icon.icns"."artifactName": "${productName}_setup_${version}.${ext}"
        },
        "dmg": {
          "sign": false."artifactName": "${productName}_setup_${version}.${ext}"
        },
        "win": {
          "icon": "./resources/icons/icon.ico"."artifactName": "${productName}_setup_${version}.${ext}"."target": [{"target": "nsis"."arch": [
                "ia32"]]}},"linux": {
          "icon": "build/icons"
        },
        "nsis": {
          "allowToChangeInstallationDirectory": true."oneClick": false."artifactName": "${productName}_setup_${version}.${ext}"
        },
        "directories": {
          "buildResources": "assets"."output": "release"}},Copy the code

At this point, the basic construction of the project framework is completed

5.resourcesdirectory

Can be used to store some extra files, when packaging will be stored in the root directory

6. Operation project
  • Render pages (Web side)

      npm run serve
    Copy the code
  • Electron(PC端)

    In the development environment, you need to start the rendering page first and then run Electron because the development environment points to the local localhost address

      npm run dev
    Copy the code
  • Final directory structure

    . ├ ─ ─ the README. Md ├ ─ ─ app │ ├ ─ ─ index. The js │ ├ ─ ─ preload. Js │ └ ─ ─ utils. Js ├ ─ ─ Babel. Config. Js ├ ─ ─ package - lock. Json ├ ─ ─ Package. The json ├ ─ ─ public │ ├ ─ ─ the favicon. Ico │ └ ─ ─ index. The HTML ├ ─ ─ resources │ └ ─ ─ the ICONS │ ├ ─ ─ icon. The icns │ ├ ─ ─ icon. Ico │ └ ─ ─ icon. PNG ├ ─ ─ the SRC │ ├ ─ ─ App. Vue │ ├ ─ ─ assets │ │ └ ─ ─ logo. The PNG │ ├ ─ ─ components │ │ └ ─ ─ the HelloWorld. Vue │ ├ ─ ─ main. Js │ ├ ─ ─ the router │ │ └ ─ ─ index. The js │ └ ─ ─ views │ ├ ─ ─ the About the vue │ └ ─ ─ Home. Vue ├ ─ ─ tests │ └ ─ ─ unit │ └ ─ ─ example. Spec. Js └ ─ ─ vue.config.jsCopy the code

The main process communicates with the renderer

The communication principle is based on preload to transfer, So when creating BrowserWindow, set nodeIntegration and contextIsolation in webPreferences to true, so add ipcRenderer transfer function in preload. Call window.ipc in the renderer process.

  1. The main process sends a message to the renderer

      // The main process sends a message to the renderer
      mainWindow.webContents.send('ping'['ping', app.getPath('userData')])
      // The renderer receives data from the main process, where the ping corresponds to the ping of the main process.
      window.ipc.on('ping'.(e, f) = > {
        console.log(f)
      })
    Copy the code
  2. The renderer sends a message to the main process

      // The renderer sends a message to the main process
      window.ipc.send('ping'.'https://www.baidu.com')
      // The main process receives data from the renderer
      ipcMain.on('ping'.(event, url) = > {
        console.log(url)
        // called if you want to reply to the renderer message here
        event.sender.send('pong'.'response success')})// The renderer needs to receive this response:
      window.ipc.on('pong'.(e, f) = > {
        console.log(f) // response success
      })
    Copy the code

Go ahead and modify the above code and make a demo page

  • Main.js is modified to introduce element

      import { createApp } from 'vue'
      import App from './App.vue'
      import ElementPlus from 'element-plus'
      import 'element-plus/dist/index.css'
      import router from './router'
    
      createApp(App).use(router).use(ElementPlus, { size: 'small' }).mount('#app')
    Copy the code
  • The HelloWorld. Vue

    <template> <div class="hello"> <div class="mb10"> <el-input class="mr10 w40" v-model="saveStoreName" placeholder="storeName"></el-input> <el-input class="mr10 w40" v-model="saveStoreVal" placeholder="storeVal"></el-input>  <el-button class="mr10 w10" @click="saveStore">saveStore</el-button> </div> <div class="mb10">showGetStore:{{showGetStore}}</div> <div class="mb10"> <el-input class="mr10 w40" v-model="getStoreName" placeholder="getStoreName"></el-input> <el-input class="mr10 w40" v-model="getStoreCallBackName" placeholder="getStoreCallBackName"></el-input> <el-button class="mr10 w10" @click="getStore">getStore</el-button> </div>  <div class="mb10"> <el-button class="mr10 w10" @click="about">about</el-button> <el-button class="mr10 w40" {{fromMainDate}} </div> </div> </div> </template> <script> const ipc = window.ipc export default { name: 'HelloWorld', props: { msg: String }, data () { return { saveStoreName: '', saveStoreVal: '', getStoreName: '', getStoreCallBackName: '', showGetStore: '', fromMainDate: '' } }, created () { try { ipc.on('ping', (e, f) => { this.fromMainDate = f }) } catch (error) { } }, methods: { openBaidu () { try { ipc.send('open-url', 'https://www.baidu.com') } catch (error) { } }, about () { try { ipc.send('about') } catch (error) { } }, saveStore () { try { ipc.send('saveStore', { storeName: this.saveStoreName, val: this.saveStoreVal }) } catch (error) { } }, getStore () { try { ipc.send('getStore', { storeName: this.getStoreName, callBackName: this.getStoreCallBackName }) ipc.on(this.getStoreCallBackName, (e, f) => { console.log(f) this.showGetStore = f }) } catch (error) { } } } } </script> <! -- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="scss"> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } .mr10 { margin-right: 10px; } .mb10 { margin-bottom: 10px; } .w40 { width: 40%; } .w10 { width: 10%; } </style>Copy the code

    NPM run serve, NPM run dev run the project to see the effect

packaging

NPM Run pack: MAC or NPM Run pack: Win

Package If you encounter slow download, you can pre-download the tool to the specified directory, the path is as follows:


# electron
Linux: $XDG_CACHE_HOME or ~/.cache/electron/
MacOS: ~/Library/Caches/electron/
Windows: $LOCALAPPDATA/electron/Cache or ~/AppData/Local/electron/Cache/

# electron-builder
Linux: $XDG_CACHE_HOME or ~/.cache/electron-builder/
MacOS: ~/Library/Caches/electron-builder/
Windows: $LOCALAPPDATA/electron-builder/Cache or ~/AppData/Local/electron-builder/Cache/
Copy the code

Win reference here

  • The same is true for macs, but the path is different
Electron - builder ├ ─ ─ nsis │ ├ ─ ─ nsis - 3.0.4.2 │ └ ─ ─ nsis - resources - 3.4.1 track ├ ─ ─ winCodeSign │ └ ─ ─ winCodeSign - server └ ─ ─ wine └ ─ ─ wine - 4.0.1 - MAC electron ├ ─ ─ SHASUMS256. TXT - 5.0.8 ├ ─ ─ SHASUMS256. TXT - v16.0.7. TXT ├ ─ ─ ├── electron-v16.0.6-win32-ia32.zip chromeDriver - V16.0.6-Win32 -ia32.zipCopy the code

DEMO address point I direct electron-vue2 || electron-vue3

reference

  • electronjs
  • Electron + vite + vue3 + TS builds an APP template