快速搭建一个基于vuecli+Electron的工程目录

1,751 阅读5分钟

引言

因为工作的上的原因又接触到了electron,之前其实使用electron-vue开发过一个小工具,但是众所周知electron-vue已经很久不更新了。 然后查找了不少资料,发现现在大部分指南都是基于Vue CLI Plugin Electron Builder来将Electron引入vue工程中的。 出于一些原因,我并不想通过Vue CLI Plugin Electron Builder方法来实现。我需要已有项目的基础下引入Electron,那么就基于vue-cli + electron-builder的方法。

首先我们得有一个vue项目

# 全局安装vuecli
  npm i @vue/cli -g
# 创建项目
  vue create hello-word
# 这里可以创建基于vue2或者vue3的项目都可以,看个人需求
# 接下来,启动一下我们的项目
  npm run serve
# ok 在网页中已经看到了一个基本的vue页面了

 ◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◉ Unit Testing
 ◯ 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)

项目改造(基于vue3来写的)

0. 安装依赖
  • electron-store 用于存储本地数据,打包生成后会在项目中有个config.json文件

  • element-plus ui框架(基于vue2的话可以用elementUI)

  • cpx 用于打包后复制文件到目录 package中的scripts命令中用到

  • cross-env 环境变量

  • electron (16.0.6)

  • electron-builder 打包工具

  • node (14.17.3) 这里要注意下有时候安装时会报错可能是node版本高了。

      npm i electron-store element-plus -S # 或者 npm i electron-store element-ui -S
      npm i cpx cross-env electron electron-builder -D
      # 如果安装缓慢的话 可以参考如下命令(指定了淘宝源,electron下载地址,node-sass下载地址)
      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/
      # 如果淘宝镜像没有指定的electron源的话 可以指定electron版本 如:electron@16.0.6
    
1. 在根目录下新建一个文件 vue.config.js,内容如下:
  'use strict'
  const path = require('path')

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

  // 区分开发环境的正式环境指向的地址
  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: {
          '@': resolve('src'),
          rootpath: resolve('./'),
          assets: path.join(__dirname, 'src', 'assets')
        }
      }
    },
    chainWebpack: config => { // 修改webpack打包的入口文件。需要在根目录建两个对应入口js文件
      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')
          }
        )
    }
  }
2. 环境变量(打包后electron里是获取不到的,为了web端打包的时候使用的)
  • 新建.env.development,.env.production文件

  • .env.development

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

      ENV = 'production'
      NODE_ENV = 'production'
      # base api
      VUE_APP_BASE_API = './'
      VUE_APP_PUBLIC_PATH = './'
    
3. Electron 文件入口
  • 根目录下创建app文件夹以及index.js,preload.js, utils.js文件,结构如下:

      .
      ├── app
      │   ├── index.js
      │   ├── preload.js
      │   └── utils.js
    
  • 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()
      // 平台判定
      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: '服务', role: 'services' },
              { type: 'separator' },
              { label: '隐藏', role: 'hide' },
              { label: '隐藏其他', role: 'hideothers' },
              { type: 'separator' },
              { label: '退出', role: 'quit' }
            ]
          }] : []),
          {
            label: '视图',
            submenu: [
              { label: '重新加载', role: 'reload' },
              { label: '强制重新加载', role: 'forcereload' },
              { label: '开发者工具', role: 'toggledevtools' },
              {
                label: '清除缓存数据',
                accelerator: 'CmdOrCtrl+Shift+Delete',
                click: (item, focusedWindow) => {
                  if (focusedWindow) {
                    focusedWindow.webContents.session.clearStorageData(clearObj)
                  }
                }
              }
            ]
          },
          {
            label: '其他',
            submenu: [
              {
                label: '关于',
                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: {
            // 这里的参数可以在文档中找到 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)// 这里是加载渲染程序的入口
    
        // 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()
        }
      })
    
    
  • preload.js (在页面运行其他脚本之前预先加载的指定的脚本) 这里其实主要是利用preload来将一些通信的方式加载进渲染页面contextBridge方法可以将对象挂载在window对象上,即:在渲染页面执行window.ipc就能调用 官方解释是: 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入node的全局引用标志

      const { contextBridge, ipcRenderer } = require('electron')
    
      contextBridge.exposeInMainWorld(
        '_platform',
        process.platform
      )
    
      /**
      * 通信方法挂载到window对象上
      * 在渲染进程中使用:
      * <script>
      * window.ipc.on('pong', (e, f) => console.log(e, f))
      * window.ipc.send('ping', val)
      * </script>
      */
      contextBridge.exposeInMainWorld('ipc', {
        send: (channel, ...args) => ipcRenderer.send(channel, ...args),
        invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
        on: (channel, listener) => {
          ipcRenderer.on(channel, listener)
        }
      })
    
    
  • utils.js 工具函数

      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}`
            })
          })
    
          // 通用方法用于保存本地数据
          ipcMain.on('saveStore', (event, { storeName, val }) => {
            store.set(storeName, val)
          })
    
          // 通用方法用于读取本地数据
          ipcMain.on('getStore', (event, { storeName, callBackName }) => {
            console.log(storeName, callBackName)
            event.sender.send(callBackName, { [storeName]: store.get(storeName) })
          })
        }
      }
      module.exports = Utils
    
    
4. package.json 修改
  • scripts 对象修改

      "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"
      }
    
  • 新增build对象 用于配置electron-build打包参数

      "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"
        }
      },
    

到这里的话工程框架基本搭建完成了

5.resources目录

可以用来存放一些额外文件,打包的时候会存放再根目录下

6. 运行工程
  • 渲染页面(web端)

      npm run serve
    
  • Electron(PC端)

    开发环境下要先启动渲染页面再执行Electron,因为开发环境下指向的地址是本地的localhost地址

      npm run dev
    
  • 最终目录结构

      .
      ├── README.md
      ├── app
      │   ├── index.js
      │   ├── preload.js
      │   └── utils.js
      ├── babel.config.js
      ├── package-lock.json
      ├── package.json
      ├── public
      │   ├── favicon.ico
      │   └── index.html
      ├── resources
      │   └── icons
      │       ├── icon.icns
      │       ├── icon.ico
      │       └── icon.png
      ├── src
      │   ├── App.vue
      │   ├── assets
      │   │   └── logo.png
      │   ├── components
      │   │   └── HelloWorld.vue
      │   ├── main.js
      │   ├── router
      │   │   └── index.js
      │   └── views
      │       ├── About.vue
      │       └── Home.vue
      ├── tests
      │   └── unit
      │       └── example.spec.js
      └── vue.config.js
    

主进程和渲染进程通信

通信原理是基于preload来进行中转的,所以创建BrowserWindow时要将webPreferences中的nodeIntegrationcontextIsolation设置为true,所以在preload.js中增加了ipcRenderer的中转函数,使用时在渲染进程中调用window.ipc即可

  1. 主进程给渲染进程发送消息

      // 主进程给渲染进程发送消息
      mainWindow.webContents.send('ping', ['ping', app.getPath('userData')])
      // 渲染进程接收主进程传过来的数据,这里的ping要和主进程的ping对应,
      window.ipc.on('ping', (e, f) => {
        console.log(f)
      })
    
  2. 渲染进程给主进程发送消息

      // 渲染进程给主进程发送消息
      window.ipc.send('ping', 'https://www.baidu.com')
      // 主进程接收渲染进程传过来的数据
      ipcMain.on('ping', (event, url) => {
        console.log(url)
        // 如果这里想要回复渲染进程消息则调用
        event.sender.send('pong', 'response success')
      })
    
      // 渲染进程需要接收这个回复的话,需要:
      window.ipc.on('pong', (e, f) => {
        console.log(f) // response success
      })
    

继续修改一下上面的代码,做一个demo页面

  • main.js改造,引入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')
    
  • 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" @click="openBaidu">在默认浏览器中打开百度</el-button>
          </div>
          <div class="mb10">
            fromMainDate:{{fromMainDate}}
          </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>
    

    npm run serve,npm run dev 运行下项目就可以看到效果了

打包

打包的时候如果需要锁定electron的版本,可以将package.json中的 "electron": "^16.0.6",改为"electron": "16.0.6"。 去掉^

npm run pack:mac 或者 npm run pack:win

打包如果遇到下载缓慢的话,可以预下载工具到指定目录,路径如下:


# 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/

win参考这里

  • mac 同理 只是路径存放不一样
electron-builder
├── nsis
│   ├── nsis-3.0.4.2
│   └── nsis-resources-3.4.1
├── winCodeSign
│   └── winCodeSign-2.6.0
└── wine
    └── wine-4.0.1-mac
    
electron
├── SHASUMS256.txt-5.0.8
├── SHASUMS256.txt-v16.0.7.txt
├── chromedriver-v16.0.7-darwin-x64.zip
├── electron-v16.0.6-darwin-x64.zip
└── electron-v16.0.6-win32-ia32.zip

DEMO地址点我直达 electron-vue2 || electron-vue3

参考