最近开始基于electron-vue做一个桌面应用,因为是独立做的项目,也是我首次从搭建环境开始自己做项目开发,踩了很多坑,总结一下一些值得记住的东西吧。
简介
首先简单介绍一下Electron吧。
当用Electron启动一个应用,会创建一个主进程。这个主进程负责与你系统原生的GUI进行交互并为你的应用创建GUI(在你的应用窗口),所以你能把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器。
Electron-Vue集成了vue-cli脚手架,项目环境同常规的vue项目大致相同。
安装
作为vue-cli
的一个模板,可以直接使用以下命令搭建1
2npm install -g vue-cli
vue init simulatedgreg/electron-vue my-project
跨域
在后台没有设置Access-Control-Allow-Origin
的情况下,浏览器对于跨域请求会出错1
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9080' is therefore not allowed access.
我们可以在前台解决这个问题,取消浏览器对于非同源请求的限制,在初始化Electron的BrowserWindow模块中配置这样一个参数:1
2
3
4// /src/main/index.js
mainWindow = new BrowserWindow({
webPreferences: {webSecurity: false},
})
webSecurity
是什么意思呢?顾名思义,他是设置web安全性,如果参数设置为 false,它将禁用相同地方的规则 (通常测试服), 并且如果有2个非用户设置的参数,就设置 allowDisplayingInsecureContent
和 allowRunningInsecureContent
的值为true。 (webSecurity的默认值为true)
allowDisplayingInsecureContent表示是否允许一个使用 https的界面来展示由 http URLs 传过来的资源。默认false。
allowRunningInsecureContent表示是否允许一个使用 https的界面来渲染由 http URLs 提交的html,css,javascript。默认为 false。
build空白
npm run build:win32
打包出来一片空白,几经波折之后在webpack.renderer.config.js
中发现了以下代码1
2
3
4
5
6plugins: [
……
nodeModules: process.env.NODE_ENV !== 'production'
? path.resolve(__dirname, '../node_modules')
: false
]
一脸懵比。。原来是node_modules
没有加载上,于是就有了解决办法
- 打包之前修改环境为production
- 修改代码为
1
nodeModules: path.resolve(__dirname, '../node_modules')
全屏显示
1 | import { app, globalShortcut, BrowserWindow } from 'electron' |
- 通过设置
win.setFullScreen(true)
全屏显示,ESC
退出全屏 - 配置项
autoHideMenuBar: true
用来隐藏菜单栏,这样设置的话按Alt
键会显示菜单,想要完全隐藏的话,可以设置win.setMenu(null)
注册快捷键
global-shortcut
模块可以便捷的设置(注册/注销)各种自定义操作的快捷键,例如上面的ESC
退出全屏,包含以下函数:
1.globalShortcut.register(accelerator, callback)
accelerator
‘accelerator’callback
Function
快捷方式使用 register 方法在 globalShortcut 模块中注册, 即:1
2
3
4
5
6
7
8const {app, globalShortcut} = require('electron')
app.on('ready', () => {
// Register a 'CommandOrControl+Y' shortcut listener.
globalShortcut.register('CommandOrControl+Y', () => {
// Do stuff when Y and either Command/Control is pressed.
})
})
2.globalShortcut.isRegistered(accelerator)
查询 accelerator 快捷键是否已经被注册过了,将会返回 true(已被注册) 或 false(未注册).
3.globalShortcut.unregister(accelerator)
注销全局快捷键 accelerator.
4.globalShortcut.unregisterAll()
注销本应用注册的所有全局快捷键.
axios封装
目的:
- 设置请求头
- 数据处理
- 添加loading
- 返回值验证
1 | import Vue from 'vue' |
这里使用的是axios提供的interceptors
拦截器方法,对request和response进行了拦截。
request: 添加loading,修改url,token,Content-Type,我在用户登录时,将token存在了localStorage中,并使用qs模块对post请求发送的数据进行了处理。
response: 移除loading,当token过期时,退出登录,这里的退出功能使用了vuex来实现,在根组件监听onlineStatus
的变化,当值为false时登出。
。
最后将axios注册在Vue实例的原型上,可以直接通过this.$http
来调用。1
this.$http.get('/list').then(res => {}).catch(() => {});
I18n多语言设置
项目要求使用多语言,我选择使用了vue-i18n,配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import Vue from 'vue'
import VueI18n from 'vue-i18n'
import enLocale from 'element-ui/lib/locale/lang/en'
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'
Vue.use(VueI18n);
let locale = localStorage.getItem('locale');
export const i18n = new VueI18n({
locale: locale || 'zh-CN', // set locale
silentTranslationWarn: true,
messages: {
'zh-CN': require('./zh-CN').default,
'en': require('./en').default
}
});
i18n.mergeLocaleMessage('zh-CN', zhLocale);
i18n.mergeLocaleMessage('en', enLocale);
项目使用了Element-ui
,所以需要导入语言包,后来发现Element-ui
有方法导入多语言,就不需要再用mergeLocaleMessage
进行合并,在main.js
中引入时这样使用1
2
3
4
5
6// 注意这里的i8n是上面语言配置文件导出的VueI18n实例
import { i18n } from './lang'
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
});
我选择将语言的配置存放在localStorage
中,在每次切换语言后修改localStorage
,记录当前所选择的语言,切换语言时调用1
2this.$i18n.locale = 'en';
localStorage.setItem('locale', 'en');
当然,在配置完成之后,还需要在main.js
中实例化Vue的时候引入1
2
3
4
5
6
7
8
9import { i18n } from './lang';
new Vue({
components: { App },
router,
store,
i18n,
template: '<App/>'
})
以中文文件为例,为了方便管理,在’zh-CN文件夹下引入各个模块的语言文件1
2
3
4
5
6
7
8
9
10
11
12
13// index.js
export default {
'table': require('./table').default,
'order': require('./order').default,
'indent': require('./indent').default,
'member': require('./member').default,
'report': require('./report').default,
'login': require('./login').default
}
// login.js
export default {
'title': '登录'
}
使用方法:1
<div>{{ $t('login.title') }}</div> //登录
在组件中,可以使用
this.$i18n
访问对象
登录超时
项目要求无操作1分钟后退出登录,退出前有消息提示(这里需要吐槽一下1分钟这个时间),话不多说,直接贴代码,我把登录超时验证的逻辑放在了与login.vue
同级的full.vue
根文件上,除登陆页之外的所有页面都注册在它的子路由下。1
2
3
4
5<template>
<div class="full-page" @mousemove="onMouseMove">
<router-view></router-view>
</div>
</template>
1 | <script type="text/javascript"> |
其实道理很简单,给根页面绑定mousemove事件,记录上次操作的时间,然后启动定时器,判断当前时间差,超时之后弹出消息提示,这时候已经清掉了储存的token,已经算是退出登录了,在message box的回掉中退出到登陆页。
一定要清定时器!一定要清定时器!一定要清定时器!
Eslint 从入门到放弃
之前提到Electron-Vue使用了vue-cli脚手架来搭建项目环境,在使用vue-cli安装时,可以选择直接安装eslint,如果想安装eslint到其他项目1
2npm install -g eslint
eslint --init
全局安装eslint之后,在项目文件夹执行命令生成.eslintrc配置文件即可,下面来大致介绍一下
- env: 脚本运行环境,如brower、node环境变量、es6环境变量、mocha环境变量等
- globals: 额外的全局变量
- rules: 开启规则和发生错误时报告的等级
规则的错误等级有三种:1
2
30或'off':关闭规则。
1或'warn':打开规则,并且作为一个警告(并不会导致检查不通过)。
2或'error':打开规则,并且作为一个错误 (退出码为1,检查不通过)。
默认配置的eslint有很多特(sang)别(xin)贴(bing)心(kuang)的地方,就不一一赘述了,我在开发的时候关闭了一些自己不能忍的规则1
2
3
4
5
6
7
8
9
10
11
12
13
14'rules': {
"no-unused-vars": [2, {
// 允许声明未使用变量
"vars": "local",
// 参数不检查
"args": "none"
}],
// 关闭语句强制分号结尾
"semi": [0],
//空行最多不能超过100行
"no-multiple-empty-lines": [0, {"max": 100}],
//关闭禁止混用tab和空格
"no-mixed-spaces-and-tabs": [0],
}
由于Eslint不允许使用未声明的变量,因此在使用全局变量的时候会出现no-undef
的报错1
'axios' is not defined
几番尝试之后发现这种东西似乎是关不掉的,于是只能在globals
中添加允许的全局变量1
2
3
4globals: {
__static: true,
axios: true
}
对于使用webstrom / phpstrom的童鞋,想体验eslint的酸爽的话,需要手动开启vip体验
Preferences -> Languages & Frameworks -> JavaScript -> Code Quality Tools -> Eslint -> Enable (勾选) -> Apply -> OK
当然,在使用vue-cli脚手架不小心安装了eslint的时候,直接在webpack的config配置文件中删除即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15module: {
rules: [
// {
// test: /\.(js)$/,
// enforce: 'pre',
// exclude: /node_modules/,
// use: {
// loader: 'eslint-loader',
// options: {
// formatter: require('eslint-friendly-formatter')
// }
// }
// },
}
}
Better-Scroll滚动封装
因为是应用于pos机的桌面应用,很多地方就要考虑内容的滚动,这里使用了‘better-scroll’;
具体的使用方法见官方文档,我在使用的时候并没有像官方文档那样封装scroll组件,因为项目不需要那么复杂的功能和配置,当然,也进行了简单的封装,用‘mixin’的形式调用方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// scroll.js
import BScroll from 'better-scroll';
export default{
data () {
return {
scroll: null
}
},
methods: {
initScroll (dom = this.$refs.wrapper, option) {
this.$nextTick(() => { //等待dom元素加载完成
if (!this.scroll) {
this.scroll = new BScroll(dom, option || {
scrollbar: {
fade: true,
interactive: false
}
});
this.scrollPullDown();
} else {
this.scroll.refresh(); //刷新
}
})
},
scrollPullDown () { //下拉事件
if (this.pullDown) {
this.scroll.on('scrollEnd', ({x, y}) => {
if (y === this.scroll.maxScrollY) {
this.onPullDown();
}
})
}
}
}
}
在配置option的时候只添加了滚动条,没有其他的配置,在vue文件中使用scroll时1
2
3
4// html
<div class="scroll-wrapper" ref="wrapper">
<div class="content"></div>
</div>
1 | // js |
注意设置wrapper样式
1 | .scroll-wrapper{ |
定义的scrollPullDown
方法,为滚动对象绑定了一个下拉事件,当滚动区域下拉至底部时触发,我在这里把他应用于表格的分页加载,因为是桌面应用,没有使用传统的分页器,而是使用下拉来加载下一页使用,需要设置pullDown: true
以及onPullDown
方法使用。
最后发现better-scroll并不是适用于触屏PC,弃用了。。由于electron自带chrome,所以直接通过css修改了滚动条样式,使用原生滚动
1 | .scroll-wrapper{ overflow: auto; } |
Table简易封装
上文提到了,将table结合scroll,实现下拉刷新。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import scroll from './scroll';
export default{
mixins: [
scroll
],
data () {
return {
tableData: [],
pullDown: true
}
},
mounted () {
this.queryTableData();
},
methods: {
queryTableData () {
if (this.resource) {
this.$http.post(this.resource, this.parms).then(res => {
this.tableData = this.tableData.concat(res.data.data)
this.initScroll('.el-table__body-wrapper');
})
}
},
onPullDown () {
this.parms.page++;
this.queryTableData();
}
}
}
贴上简易版本的代码,依旧使用mixin的方法,在组件中导入后,配置resource请求地址和params请求参数,绑定tableData为表格数据,下拉时请求下一页数据。
在组件中也可以通过调用queryTableData
方法刷新表格。
上面提到了弃用了better-scroll,这里自己写了一个
touchend
方法来控制刷新表格
1 | touchStart (ev) { |
在el-table
绑定事件,判断触摸滑动的位移等于内容差且滑动的垂直距离大于50px,前者是为了判断是否滑到底,后者是为了限制滑动最短距离,判断完成后加载下一页。
还有一种封装方式,将表格作为一个组件,内容作为slot插入进去,这样在将配置项传入组件之后就可以使用了,在表格配件较多情况下,感觉这种方法相比mixins更方便。
Sass预设
相信用过bootstrap4的都会对它的样式预设印象深刻,特别是盒模型的样式预设,对于我这种习惯使用的人来说,已经是爱不释手了,为了减少项目打包的负担,没有使用bootstrap,就自己使用sass写了一套,发布成了一个npm包‘ly-sass’,部分代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// margin padding setting
$directions: (l: left, r: right, t: top, b: bottom);
@for $index from 0 to 10 {
.p-#{$index} {
padding: $index * 0.5rem !important;
}
.m-#{$index} {
margin: $index * 0.5rem !important;
}
@each $key,$value in $directions {
.p#{$key}-#{$index} {
padding-#{$value}: $index * 0.5rem !important;
}
.m#{$key}-#{$index} {
margin-#{$value}: $index * 0.5rem !important;
}
}
}
@for $index from -1 to 101 {
.w-#{$index}{
width: #{$index}% !important;
}
.h-#{$index}{
height: #{$index}% !important;
}
.font-#{$index}{
font-size: #{$index}px !important;
}
}
使用时类似1
2<div class='m-0 pt-2 ml-1'></div> // margin: 0; padding-top: 1rem; margin-left: 0.5rem;
<div class='w-50 h-100 font-20'></div> // width: 50%; height: 100%; font-size: 20px;
这只是其中比较有代表性的,具体的代码就不贴了,其他的预设都类似这种写法。
读卡器
由于项目需要刷卡登录,刷卡识别会员卡等功能,所以外连了一个读卡器跟扫码枪,而且是功能最基础的那种,甚至没有任何的事件或者回调。。在读卡的时候会在光标处打出识别的卡号。
想要用这么个玩意实现刷卡登录。。。em。。苦思冥想好几天之后,发现这个读卡器是可以呗keydown事件捕获的,那么问题就在于,如何去区分扫码和正常的按键盘,最后的实现代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33export let cardReader = {
data () {
return {
keyList: [], // 记录字符集
keyTime: null, // 记录上次键盘时间
card_sn: null
}
},
mounted () {
this.initCardReader();
},
methods: {
initCardReader () {
let vm = this;
window.onkeydown = e => {
if (e.timeStamp - vm.keyTime > 50) {
vm.keyList = [];
vm.card_sn = null;
}
vm.keyTime = e.timeStamp;
vm.keyList.push(e.key);
}
}
},
watch: {
keyList: function (val) {
if (val.length === 10) { // 识别10位会员号之后执行回调
this.card_sn = val.join('');
this.readerCallback();
}
}
}
};
通过比较两次keydown事件的时间差,如果相隔时间超过50毫秒,清空keyList数组,在长度达到10位时(会员卡号为10位),执行回调readerCallback
。
由于用mixin的方式引入
cardReader
方法,在使用时发现当同一页面中有不同组件(dialog)中加载方法时,由于键盘事件是注册在window上的,会导致多个组件中只有一个能用,解决方式是在每个dialog弹出时重新调用initCardReader
Iconfont图标
很早就听说过‘Iconfont’的大名,这次自己当家作主的项目,终于有了应用的机会,在实际开发一段时间之后,觉得这个东西是真的好用啊,不需要导入更多图标库,不需要再为了找图标浪费时间,话不多说,下面来介绍一下这个犀利的图标库吧。
在打开Iconfont网站之后,登录用户名(github账号即可),查询到你想要的图标,点击加入购物车,
然后点右上角的购物车打开,创建项目并添加图标到项目中,然后进入到项目页面
在这里就可以看到项目图标库代码以及图标的代码,复制上面的代码,和iconfont的字体样式一同加入到项目中,这里列举三种使用方法
在线字体库
直接复制图标库代码使用,项目开发过程中方便修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@font-face {
font-family: iconfont;
src: url('//at.alicdn.com/t/font_592912_662zcis5mcu6usor.eot'); /* IE9*/
src: url('//at.alicdn.com/t/font_592912_662zcis5mcu6usor.eot?#iefix') /* IE6-IE8 */format('embedded-opentype'),
url('//at.alicdn.com/t/font_592912_662zcis5mcu6usor.woff') format('woff'), /* chrome, firefox*/
url('//at.alicdn.com/t/font_592912_662zcis5mcu6usor.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('//at.alicdn.com/t/font_592912_662zcis5mcu6usor.svg#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont{
font-family: iconfont !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-webkit-text-stroke-width: 0.2px;
-moz-osx-font-smoothing: grayscale;
}
引用css文件
在项目代码的Font Class
选项中可以查看路径1
<link rel="stylesheet" type="text/css" href="at.alicdn.com/t/font_611773_gnzu317r1jvvaemi.css">
引入图标文件
在项目图标库定义完成之后,可以直接使用项目代码中的字体地址下载字体文件,引入使用1
2
3
4@font-face {
font-family: iconfont;
src: url(assets/font/iconfont.ttf) // electron只需要兼容chrome即可
}
对于使用webpack的项目,在打包之后会存在字体文件缺失的情况,需要下载字体文件并且按照webpack的引用规则引入文件。
ok,这样就完成iconfont的设置了,复制上面的图标代码使用就可以了1
<i class="iconfont"></i>
每次添加新图标都需要更新图标库代码