基于环信Web Vue3 Demo使用electron快速打包生成桌面端应用
前言
一直以来都有听说利用electron
可以非常便捷的将网页应用快速打包生成为桌面级应用,并且可以利用electron提供的API调用原生桌面API一些高级功能,于是这次借着论证环信Web端SDK是否可以在electron生成的桌面端正常稳定使用,我决定把官方新推出的webim-vue3-demo,打包到桌面端,并记录一下这次验证的过程以及所遇到的问题以及解决方式。
前置技能
- 拥有良好的情绪自我管理,能够在遇到棘手问题时不一拳给到键盘。
- 拥有较为熟练的水群能力,能够在遇到问题时,主动向技术群内参差不齐的群友们抛出自己的问题。
- 【重要】拥有较为熟练的搜索引擎使用能力。
- 能够看到这篇文章,那说明以上能力你已完全具备。
测试流程记录
第一步、准备工作
- 克隆vue3 Demo项目到本地 环信vue3-demo源码地址
- 在编辑器内打开此项目并执行
yarn install
安装项目相关npm依赖。 - 在此项目目录下打开终端请敲下
yarn add electron
,从而在该项目中安装electron。 - 安装一些依赖工具
wait-on
以及cross-env
wait-on
是一个 Node.js 包,它可以用于等待多个指定的资源(如 HTTP 资源、TCP 端口或文件)变得可用。它通常用于等待应用程序的依赖项准备好后再启动应用程序。例如,您可以使用wait-on
等待数据库连接、消息队列和其他服务就绪后再启动您的应用程序。这样可以确保您的应用程序在尝试使用这些资源之前不会崩溃。
cross-env
是一个npm
包,它的作用是在不同平台上设置环境变量。在不同操作系统中,设置环境变量的方式是不同的。例如,在 Windows 中使用命令set NODE_ENV=production
设置环境变量,而在 Unix/Linux/Mac 上则需要使用export NODE_ENV=production
命令。
此时可能会进入到漫长的等待阶段,第一、这个包本身就比较大,第二、相信大家都懂由于网络原因导致,并且有可能进行会经历几次
TIMOUT
安装失败。此时就需要心平气和,且有耐心的进行改变镜像地址
、科学进行上网
,WIFI切换为移动流量
多去重试几次,相信道友你总会成功过的。有如下输出则应该为安装成功。
第二步、项目目录增加electron文件
在项目增加electron文件时我们需要扩展一部分知识从而了解为什么创建创建这个目录,并在该目录下增加
main.js
文件的作用。当然如果觉得不需要可以直接略过。
主进程与渲染进程的概念
在 Electron 中,主进程和渲染进程是两个不同的概念。主进程是 Electron 应用程序的核心,它运行在一个 Node.js 实例中,并管理应用程序的生命周期、窗口创建和销毁、与底层操作系统进行交互等。主进程还可以通过 IPC(进程间通信)机制与渲染进程进行通信。 渲染进程则是应用程序的 UI 界面所在的进程。每个 Electron 窗口都有其自己的渲染进程。渲染进程是一个 Chromium 渲染引擎实例,它运行在一个仅包含 Web API 的环境中。渲染进程负责渲染 HTML、CSS 和 JavaScript,并处理来自用户的输入事件,同时通过 IPC 机制与主进程进行通信。 由于渲染进程只能访问 Web API 而不能直接访问 Node.js API,因此如果需要在渲染进程中使用 Node.js API,就需要通过 IPC 机制向主进程发出请求,由主进程代为执行并将结果返回给渲染进程。
主进程与渲染进程分别应该写在哪?
在 Electron 应用程序中,主进程通常写在名为
main.js
或者index.js
的 JavaScript 文件中,这个文件是应用程序的入口点。而渲染进程则通常写在 HTML 文件和其引入的 JavaScript 文件中。在一个 Electron 窗口中,可以通过调用webContents
对象的loadURL
方法来加载一个 HTML 文件,其中包含了渲染进程所需的代码和资源。该 HTML 文件中的 JavaScript 代码将运行在对应的渲染进程中,可以通过 Electron 提供的一些 API 和 Web API 来进行与用户界面相关的操作 需要注意的是,在 Electron 中,由于主进程和渲染进程是不同的 Node.js 实例,因此它们之间并不能直接共享变量或者调用函数。如果想要实现主进程和渲染进程之间的通信,必须使用 Electron 提供的 IPC 机制,通过发送消息的方式来进行进程间通信。
有些electron文件目录下preload.js的作用
在 Electron 中,
preload.js
文件是一个可选的 JavaScript 文件,用于在渲染进程创建之前加载一些额外的脚本或者模块,从而扩展渲染进程的能力。preload.js 文件通常存放在与主进程代码相同的目录下。
preload.js 的实际运用主要有以下几个方面:
- 托管 Node.js API:preload.js 中可以引入 Node.js 模块,并将其暴露到 window 对象中,从而使得在渲染进程中也能够使用 Node.js API,避免了直接在渲染进程中调用 Node.js API 带来的安全风险。
- 扩展 Web API:preload.js 中还可以定义一些自定义的函数或者对象,然后将它们注入到 window 对象中,这样在渲染进程中就可以直接使用它们了,而无需再进行额外的导入操作。
- 进行一些初始化操作:preload.js 文件中的代码会在每个渲染进程的上下文中都运行一遍,在这里可以进行一些初始化操作,比如为页面添加一些必要的 DOM 元素、为页面注册事件处理程序等。
需要注意的是,preload.js 文件中的代码运行在渲染进程的上下文中,因此如果 preload.js 中包含一些恶意代码,那么它很可能会危及整个渲染进程的安全性。因此,在编写 preload.js 文件时,一定要格外小心,并且仅引入那些你信任的模块和对象。
1、 添加electron文件
- 此时项目目录
2、 electron下新建main.js
示例代码如下:
const { app, BrowserWindow } = require('electron')
const path = require('path')
const NODE_ENV = process.env.NODE_ENV
app.commandLine.appendSwitch('allow-file-access-from-files')
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 980,
height: 680,
fullscreen: true,
skipTaskbar: true,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js')
}
})
if (NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:9001/')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadURL(
`file://${path.join(__dirname, '../dist/index.html')}`
)
}
}
// 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.whenReady().then(() => {
createWindow()
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
3、 electron下新建preload.js
,示例代码如下:
此文件为可选文件
//允许vue项目使用 ipcRenderer 接口, 演示项目中没有使用此功能
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('ipcRender', ipcRenderer)
4、修改package.json
,当前示例代码如下:
- 修改
"main"配置
,将其指向为"main": "electron/main.js"
- 增加一个针对electron启动的
"scripts"
,"electron:dev": "wait-on tcp:3000 && cross-env NODE_ENV=development electron ./"
当前项目配置如下所示
{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./",
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
},
}
第三步、本地启动起来验证一下
- 启动运行原vue项目
这里启动项目至端口号9001,跟上面electron/main.js
mainWindow.loadURL(' http://localhost:9001/')
是可以对应上的,也就是electron运行起来将会加载此服务地址。
yarn run dev
- 新开一个终端执行,输入下方命令启动electron
执行下面命令
yarn run electron:dev
可以看到自动开启了一个electron页面
并且经过测试验证登录没有什么问题。
第四步、尝试打包并验证打包出来的安装包是否可用。
1、安装electron-builder
该工具为electron打包工具库
终端执行下面命令安装electron-builder
yarn add electron-builder --dev
2、package.json 配置打包脚本命令以及设置打包个性化配置项
参考配置如下
具体配置项作用请参考官网文档,下面有些配置也是CV大发过来的,没有具体深入研究。
{
"name": "webim-vue3-demo",
"version": "0.1.0",
"private": true,
"main": "electron/main.js",
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:dev": "wait-on tcp:9001 && cross-env NODE_ENV=development electron ./",
"electron:build": "rimraf dist && vue-cli-service build && electron-builder",
"electron:build2": "electron-builder"
},
"dependencies": {
"@vueuse/core": "^8.4.2",
"agora-rtc-sdk-ng": "^4.14.0",
"axios": "^0.27.2",
"benz-amr-recorder": "^1.1.3",
"core-js": "^3.8.3",
"easemob-websdk": "^4.1.6",
"element-plus": "^2.2.5",
"nprogress": "^0.2.0",
"pinyin-pro": "^3.10.2",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"cross-env": "^7.0.3",
"electron": "^24.3.1",
"electron-builder": "^23.6.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.51.0",
"sass-loader": "^12.6.0",
"wait-on": "^7.0.1"
},
"build": {
"productName": "webim-electron",
"appId": "com.lvais",
"copyright": "2023@easemob",
"directories": {
"output": "output"
},
"extraResources": [
{
"from": "./src/assets",
"to": "./assets"
}
],
"files": [
"dist/**/*",
"electron/**/*"
],
"mac": {
"artifactName": "${productName}_${version}.${ext}",
"target": [
"dmg"
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}_${version}.${ext}"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true
},
"linux": {}
}
}
3、开始build
- 先这样
build 原始vue项目
yarn run build
- 再那样
build electron项目
yarn run electron:build
可能会进入漫长的等待,但是不要慌,可能与网络关系比较大,需要耐心等待。
打包成功之后可以看到有一个output文件夹的生成,打开之后可以选择双击打开软件验证看下是否可以正常开启应用
正常开启页面的话,证明没有问题,如果遇到了问题,下方会有一些我遇到的问题,可以作为参考。
令人痛苦的问题汇总
问题一、打包后页面空白,并且出现类似(Failed to load resource: net::ERR_FILE_NOT_FOUND)报错
问题简述:发现只有在打包之后的electron应用,启动后存在页面空白,dev情况下正常。
解决手段之一:
经排查,更改vue.config.js
中publicPath
的配置为'./'
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
devServer: {
host: 'localhost',
port: 9001
// https:true
},
publicPath: './',
chainWebpack: (config) => {
//最小化代码
config.optimization.minimize(true)
//分割代码
config.optimization.splitChunks({
chunks: 'all'
})
}
})
原因打包后的应用electron会从相对路径开始找资源,所以经过此配置可以所有资源则开始从相对路径寻找。
默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 `https://www.my-app.com/`。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 `https://www.my-app.com/my-app/`,则设置 `publicPath` 为 `/my-app/`。
这个值也可以被设置为空字符串 (`''`) 或是相对路径 (`'./'`),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。
解决手段之二:
经过一顿操作之后发现仍然还是空白,并且打开控制台看到页面可以正常加载资源文件,但是index.html返回此类错误:
We're sorry but XXX doesn't work properly without JavaScript
,经过查找发现可以通过修改路由模式来解决,经过测试确实有效。
参考文章为:juejin.cn/post/714362…
修改后的代码示例:
const router = createRouter({
//改为#则可以直接变更路由模式
history: createWebHistory('#'),
routes
})
问题二、
问题简述:页面展示正常后,调用登录发现出现下图报错
解决方式:经发现原来是发起axios请求环信置换连接token接口的时候,协议的获取是通过
window.location.protocol
来获取的,那么打包之后的协议为file:
那么这时发起的请求就会变更为以file协议发起的请求,那么修改这里的逻辑,判断如果为file协议则默认走http协议发起请求,示例代码如下:
import axios from 'axios'
const defaultBaseUrl = '//a1.easemob.com'
console.log('window.location.protocol', window.location.protocol)
// create an axios instance
const service = axios.create({
withCredentials: false,
// baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
baseURL: `${
window.location.protocol === 'file:'
? 'https:'
: window.location.protocol
}${defaultBaseUrl}`,
// withCredentials: true, // send cookies when cross-domain requests
timeout: 30000, // request timeout
headers: { 'Content-Type': 'application/json' }
})
// request interceptor
service.interceptors.request.use(
(config) => {
// do something before request is sent
return config
},
(error) => {
// do something with request error
console.log('request error', error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
(response) => {
const res = response.data
const code = response.status
// if the custom code is not 20000, it is judged as an error.
if (code >= 400) {
return Promise.reject(new Error(res.desc || 'Error'))
} else {
return res
}
},
(error) => {
if (error.response) {
const res = error.response.data // for debug
if (error.response.status === 401 && res.code !== '001') {
console.log('>>>>>无权限')
}
if (error.response.status === 403) {
res.desc = '您没有权限进行查询和操作!'
}
return Promise.reject(res.desc || error)
}
return Promise.reject(error)
}
)
export default service
参考资料
特别鸣谢两位道友文章非常有用,可以作为参考:
转载自:https://juejin.cn/post/7241057772623888440