引言
因为工作的上的原因又接触到了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
中的nodeIntegration
和contextIsolation
设置为true
,所以在preload.js
中增加了ipcRenderer
的中转函数,使用时在渲染进程中调用window.ipc即可
。
-
主进程给渲染进程发送消息
// 主进程给渲染进程发送消息 mainWindow.webContents.send('ping', ['ping', app.getPath('userData')]) // 渲染进程接收主进程传过来的数据,这里的ping要和主进程的ping对应, window.ipc.on('ping', (e, f) => { console.log(f) })
-
渲染进程给主进程发送消息
// 渲染进程给主进程发送消息 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/
- 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