文章目录
概要
提示:最近在electron项目中使用serialport+sqlite3的时候遇到的各种问题再此做个记录
准备工作
首先你要设置electron的镜像源不然你很有可能在后面的某些步骤上出错
npm config set registry https://registry.npmmirror.com
npm config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/
然后win+R打开cmd 输入.npmrc回车
会出现一个这样的文件
在里面添加
disturl=https://registry.npmmirror.com/-/binary/node
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
operadriver_cdnurl=https://registry.npmmirror.com/-/binary/operadriver/
phantomjs_cdnurl=https://registry.npmmirror.com/phantomjs
SASS_BINARY_SITE=https://registry.npmmirror.com/mirrors/node-sass/
这都是各种踩坑后总结出来的
PS:另外node_gyp、msvs_version、python这几项在下面会单独说。折腾我一两天
1.项目创建
网上有个比较火的脚手架electron-vue,github 上 12.2k 的 star,大家应该都听说过或者使用过,但现在我们不使用它,electron-vue是vue-cli2.0的版本,现在都已经出道 4.0 了,再者electron-vue已经很久没有更新,我们可以使用 vue 最新的脚手架加上插件vue-cli-plugin-electron-builder来搭建项目,项目结构也更加清晰明了。如果对脚手架electron-vue感兴趣可去 这里
vue create vue-electron
// 自定义创建
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
// 这里选择自定义创建,使用babel(语法编译器)、Router(路由)、CSS Pre-processors(css预处理器)、Linter / Formatter(代码风格、格式校验):
? Please pick a preset: Manually select features
? Check the features needed for your project:
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router
◉ Vuex
◉ CSS Pre-processors
❯◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
` 注意:router这里不要使用history模式,原因是electron的项目不能使用history模式 输入n,进入下一步:`
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) n
// css预处理器这里选择Sass
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
Sass/SCSS (with dart-sass)
❯ Sass/SCSS (with node-sass)
Less
Stylus
// 选择ESLint代码检查工具,我这里选择的是“ESLint + Airbnb config”,强烈建议大家一定要在项目内使用ESLint,一直用一直爽,懂的自然懂……该有的规范还是要遵循的:
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config:
ESLint with error prevention only
❯ ESLint + Airbnb config
ESLint + Standard config
ESLint + Prettier
// 选择什么时候执行ESLint检查,我这里两个都选了,保存时检查“Lint on save”,向仓库提交代码时也检查“Lint and fix on commit”,不符合规范的代码我们不允许提交到仓库:
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Airbnb
? Pick additional lint features:
◉ Lint on save
❯◉ Lint and fix on commit
// 这里是询问 babel, postcss, eslint这些配置是单独的配置文件还是放在package.json 文件中,这里我们选择“In dedicated config files”单独放置:
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
In package.json
// 这里是最后的操作,是否保存该配置,如果你下次需要使用相同的配置,那么可以选择 yes,我这选择 no:
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) no
执行完以上操作,剩下的我们等待项目下载依赖包,vue项目初始化就算搞定了。
cd vue-electron-notes进入项目跑一遍npm run serve,这个时候没什么问题就已经成功运行起来了!
2.在项目内集成Electron
进入我们项目的根目录,我们执行以下命令来安装插件
vue add vue-cli-plugin-electron-builder
这里选择版本^12.0.0:
✔ Successfully installed plugin: vue-cli-plugin-electron-builder
? Choose Electron Version (Use arrow keys)
^10.0.0
^11.0.0
❯ ^12.0.0
等安装完毕后会重新构建项目架构,在src目录下生成background.js文件,并且还新增了启动命令,如下图:
到这里其实还有一个小问题,插件安装的Electron是 12.0 版本的,但官方最新的版本其实已经到了31.x.x了,为了后续我们可以使用最新的api,我们需要执行命令更新一下版本:
npm uninstall electron
2.1安装最新的electron版本
npm install -S electron
npm install -S @electron/remote
2.2运行程序
npm run serve
看到这个就说明成功了
2.3 解压dist_electron/win-unpacked/resources/app.saar文件(重要)
npm install -g asar
//进入app.asar所在目录,使用以下命令
asar e app.asar ./
2.4 electron配置,background.js:
//background.js
//文件头部,引用增加ipcMain用于通信
import { app, protocol, BrowserWindow, shell, Menu } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
const isDevelopment = process.env.NODE_ENV !== 'production'
const winURL = process.env.NODE_ENV === 'development' ? `http://localhost:${process.env.PORT}` : `app://./index.html`
const loadingURL = process.env.NODE_ENV === 'development' ? `http://localhost:${process.env.PORT}/loader.html` : `app://./loader.html`
var loadWindow = null
var mainWindow = null
async function createWindow () {
// Create the browser window.
//由于渲染进程中electron.remote已废弃,需要手动引入,并在每一个browserwindow中启用remote
require('@electron/remote/main').initialize()
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: true, //是否window自带的关闭最小化等按钮
show: false,
useContentSize: false,
resizable: true, //禁止改变主窗口尺寸
maximizable: true, // 是否可以最大化
webPreferences: {
webSecurity: false, // 安全策略
enableRemoteModule: true, // 可以使用remote
nodeIntegration: true, // 渲染层可以使用node
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
}
})
require("@electron/remote/main").enable(mainWindow.webContents);
if (process.env.NODE_ENV !== 'development') {
createProtocol('app')
}
mainWindow.loadURL(winURL)
mainWindow.webContents.once('dom-ready', () => {
mainWindow.show()
mainWindow.webContents.openDevTools({ mode: 'detach' })
if (process.env.NODE_ENV !== 'development'){
}
loadWindow.destroy()
})
mainWindow.on('closed', () => {
mainWindow = null
})
createMenu()
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
}
// 设置菜单栏
function createMenu () {
// darwin表示macOS,针对macOS的设置
if (process.platform === 'darwin') {
const template = [
{
label: 'App Demo',
submenu: [
{
role: 'about'
},
{
role: 'quit'
}]
}]
let menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
} else {
// windows及linux系统
Menu.setApplicationMenu(null)
}
}
function loadindWindow () {
loadWindow = new BrowserWindow({
width: 400,
height: 600,
frame: false,
show: false,
backgroundColor: '#222',
transparent: true,
resizable: false,
webPreferences: { experimentalFeatures: true }
})
if (process.env.NODE_ENV !== 'development') {
createProtocol('app')
}
loadWindow.loadURL(loadingURL)
loadWindow.webContents.once('dom-ready', () => {
loadWindow.show()
})
setTimeout(() => {
createWindow()
}, 3000)
loadWindow.on('closed', () => {
loadWindow = null
})
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) loadindWindow()
})
app.on('ready', async () => {
// if (isDevelopment && !process.env.IS_TEST) {
// // Install Vue Devtools
// try {
// await installExtension(VUEJS_DEVTOOLS)
// } catch (e) {
// console.error('Vue Devtools failed to install:', e.toString())
// }
// }
loadindWindow()
})
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// 如果获取失败,说明已经有实例在运行了,直接退出
app.quit();
}
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
**这里我用了一个启动加载动画方法为 loadindWindow (loadingURL)为启动时要展示的路径 文件位置级代码如下
**
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
html {
height: 100%;
min-height: 100%;
overflow: hidden;
user-select: none;
-webkit-app-region: no-drag;
}
html body {
background-size: 163px;
font: 14px/21px Monaco, sans-serif;
color: #999;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
-moz-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
text-size-adjust: 100%;
height: 100%;
min-height: 100%;
margin: 0px;
}
html body h4 {
margin: 0;
}
.scene {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background: #222;
position: relative;
}
.loader {
position: relative;
width: 15em;
height: 15em;
background: linear-gradient(-225deg, #ff3cac 0%, #562b7c 52%, #2b86c5 100%);
border-radius: 50%;
animation: spin 0.5s linear infinite;
}
span {
position: absolute;
width: 100%;
height: 100%;
border-radius: inherit;
background: inherit;
}
span:nth-child(1) {
filter: blur(5px);
}
span:nth-child(2) {
filter: blur(10px);
}
span:nth-child(3) {
filter: blur(25px);
}
span:nth-child(4) {
filter: blur(50px);
}
span::after {
position: absolute;
content: "";
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
background: #222;
border-radius: inherit;
}
.text {
position: absolute;
opacity: 0;
animation: breath 3s ease-in-out infinite;
}
@keyframes breath {
from {
opacity: 0.05;
}
50% {
opacity: 1;
}
to {
opacity: 0.05;
}
}
@keyframes spin {
to {
transform: rotate(1turn);
}
}
</style>
</head>
<body>
<div class="scene">
<div class="loader">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="text">正在准备资源中...</div>
</div>
</body>
</html>
2.4 vue.config.js:
const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir);
}
// const BASE_URL = process.env.NODE_EVN === 'production' ? './' : './'
const port = process.env.PORT || 8080
const { dependencies } = require('./package.json')
module.exports = {
transpileDependencies: true,
// publicPath: BASE_URL,
lintOnSave: process.env.NODE_ENV === 'development',
// 这里写你调用接口的基础路径,来解决跨域,如果设置了代理,那你本地开发环境的axios的baseUrl要写为 '' ,即空字符串
devServer: {
// proxy: 'localhost:3000'
port,
open: false
},
chainWebpack(config) {
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
},
pluginOptions: {
electronBuilder: {
chainWebpackMainProcess: (config) => {
config.output.filename('background.js');
},
customFileProtocol: "./",
nodeIntegration: true,
enableRemoteModule: true,
externals: [
...Object.keys(dependencies || {})
],
builderOptions: {
// "appId": "com.example.app",
// "productName": "xxxxxx",//项目名,也是生成的安装文件名
copyright: "Copyright © 2021",//版权信息
// directories: {
// output: "build"//输出文件路径
// },
win: {//win相关配置
// "icon": "./src/assets/icon.ico",//图标,当前图标在根目录下,
target: [
{
target: "nsis",//利用nsis制作安装程序
arch: [
"x64",//64位
// "ia32"//32位
]
}
]
},
nsis: {
oneClick: false, // 是否一键安装
allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
allowToChangeInstallationDirectory: true, // 允许修改安装目录
// "installerIcon": "./src/assets/icon.ico",// 安装图标
// "uninstallerIcon": "./src/assets/icon.ico",//卸载图标
// "installerHeaderIcon": "./src/assets/icon.ico", // 安装时头部图标
createDesktopShortcut: true, // 创建桌面图标
createStartMenuShortcut: true,// 创建开始菜单图标
// "shortcutName": "mine", // 图标名称
},
}
},
},
// 设为false打包时不生成.map文件
productionSourceMap: true
}
3.安装Serialport
npm i serialport
安装electron-rebuild(用来重新编译serialport包)
sqlite3也需要它来重新编译
npm install --save-dev electron-rebuild
node-serialport是一个让node可以访问电脑串口设备的原生模块,需要编译,也是出问题最多的一个环节。编译需要做一些准备工作。
1. 安装gyp命令 npm install -g node-gyp
(如果有可以跳过);
2. 安装Visual C ++构建环境 Visual Studio构建工具网上很多说要安装2015或者2017,其实没有影响安装最新的2022也可以
3. 安装Python 2.7~3.10.0之间的任意版本也有人说必须2.7其实也没有影响,我这里安装的3.10.0
4. 在第一节准备工作的时候.npmrc文件中的node_gyp
、msvs_version
、python
这三个参数就是为了解决这里安装gyp、Visual C ++和Python 后的问题
执行.\node_modules\.bin\electron-rebuild.cmd 提示“Rebuild Complete”表示执行成功
.\node_modules\.bin\electron-rebuild.cmd
√ Rebuild Complete
4.安装sqlite3
如果你按照前面的步骤一步一步到这里了的话到了,接下来按照下面的步骤做就应该不会有问题
1. npm i sqlite3@5.1.6
2. npm install -g node-pre-gyp
安装完成后一定要看自己的sqlite3依赖包里面是否含有
\node_modules\sqlite3\lib\binding\napi-xxx-xxx-unknown-x64的文件,如果没有请重新安装低版本的sqlite3
安装完成后尝试执行以下命令
// 进入依赖根目录
1. cd ./node_modules/sqlite3
// 重新构建运行文件
2. node-pre-gyp install --fallback-to-build
3. node-pre-gyp rebuild //或者 node-pre-gyp rebuild --disturl=https://npmmirror.com/mirrors/node
完成后回到项目的根目录依次执行以下命令
1. npm i
2. npm run postinstall
3. .\node_modules\.bin\electron-rebuild.cmd
完成后就可以使用serialport以及sqlite3了
打包问题
具体参考:打包采坑问题汇总
无法使用require引用问题
当你在项目中使用require引用一些模块(比如jQuery),会导致Uncaught TypeError: $ is not a function
这个问题是的直接原因是require无法引用,根本原因node中的require覆盖了webpack的require,这就导致了打包失败
解决办法使用window.require代替
例如:
const { SerialPort } = window.require(“serialport”);
总结
路漫漫其修远兮 多看文档 多研究