Electron-Vue项目记录

    最近开始基于electron-vue做一个桌面应用,因为是独立做的项目,也是我首次从搭建环境开始自己做项目开发,踩了很多坑,总结一下一些值得记住的东西吧。

简介

    首先简单介绍一下Electron吧。
    当用Electron启动一个应用,会创建一个主进程。这个主进程负责与你系统原生的GUI进行交互并为你的应用创建GUI(在你的应用窗口),所以你能把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器。
    Electron-Vue集成了vue-cli脚手架,项目环境同常规的vue项目大致相同。

安装

作为vue-cli的一个模板,可以直接使用以下命令搭建

1
2
npm 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个非用户设置的参数,就设置 allowDisplayingInsecureContentallowRunningInsecureContent的值为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
6
plugins: [
……
nodeModules: process.env.NODE_ENV !== 'production'
? path.resolve(__dirname, '../node_modules')
: false
]

一脸懵比。。原来是node_modules没有加载上,于是就有了解决办法

  • 打包之前修改环境为production
  • 修改代码为
    1
    nodeModules: path.resolve(__dirname, '../node_modules')

全屏显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { app, globalShortcut, BrowserWindow } from 'electron'

mainWindow = new BrowserWindow({
height: 768,
width: 1024,
autoHideMenuBar: true,
webPreferences: {
webSecurity: false
}
})

mainWindow.setFullScreen(true)
// mainWindow.setMenu(null)

globalShortcut.register('ESC', () => {
mainWindow.setFullScreen(false);
})
  • 通过设置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
    8
    const {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封装

目的:

  1. 设置请求头
  2. 数据处理
  3. 添加loading
  4. 返回值验证
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
37
38
39
40
41
42
43
44
45
46
47
48
49
import Vue from 'vue'
import axios from 'axios'
import qs from 'qs'
import store from '../../store'
import {Loading} from 'element-ui'

let loading;

// Add a request interceptor
axios.interceptors.request.use(function (config) {
showLoading();
config.url = 'http://xxxxxx' + config.url;
config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
config.headers['Authorization'] = localStorage.getItem('token');
if (config.method === 'post') {
config.data = qs.stringify({
...config.data
})
}
return config
}, function (error) {
return Promise.reject(error)
})

// Add a response interceptor
axios.interceptors.response.use(function (response) {
hideLoading();
if (response.data.error_code === '002') {
store.commit('_setOnlineStatus', false)// sign out
} else {
return response
}
}, function (error) {
hideLoading();
return Promise.reject(error)
})

function showLoading () {
loading = Loading.service({
fullscreen: true,
lock: false,
text: 'Loading...'
})
}
function hideLoading () {
loading.close()
}

Vue.http = Vue.prototype.$http = axios

这里使用的是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
18
import 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
2
this.$i18n.locale = 'en';
localStorage.setItem('locale', 'en');

当然,在配置完成之后,还需要在main.js中实例化Vue的时候引入

1
2
3
4
5
6
7
8
9
import { 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
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
37
38
39
40
41
42
43
44
45
46
47
<script type="text/javascript">
import moment from 'moment';
export default {
data () {
return {
lastTime: null, // 上次操作的时间
showMessage: true // 防止重复弹出message box
}
},
created () {
if (!localStorage.getItem('token')) { // 登录超时后刷新页面也会退出
this.onExit();
}
},
mounted () {
this.checkTime();
},
methods: {
onMouseMove () { // 记录上次操作的时间
this.lastTime = moment();
},
checkTime () {
this.lastTime = moment();
let timer = setInterval(() => {
let time = moment() - moment(this.lastTime);
if (time > 60000 && this.showMessage) { // 时间差大于一分钟
localStorage.removeItem('token');
this.showMessage = false;
this.$alert(this.$t('login.errors.overtime_text'), this.$t('login.errors.overtime'), {
confirmButtonText: this.$t('confirm'),
closeOnClickModal: true,
callback: () => {
this.showMessage = true;
clearInterval(timer);
this.onExit();
}
})
}
}, 5000)
},
onExit () {
localStorage.removeItem('token');
this.$router.push('/login');
}
}
}
</script>

其实道理很简单,给根页面绑定mousemove事件,记录上次操作的时间,然后启动定时器,判断当前时间差,超时之后弹出消息提示,这时候已经清掉了储存的token,已经算是退出登录了,在message box的回掉中退出到登陆页。

一定要清定时器!一定要清定时器!一定要清定时器!

Eslint 从入门到放弃

之前提到Electron-Vue使用了vue-cli脚手架来搭建项目环境,在使用vue-cli安装时,可以选择直接安装eslint,如果想安装eslint到其他项目

1
2
npm install -g eslint
eslint --init

全局安装eslint之后,在项目文件夹执行命令生成.eslintrc配置文件即可,下面来大致介绍一下

  • env: 脚本运行环境,如brower、node环境变量、es6环境变量、mocha环境变量等
  • globals: 额外的全局变量
  • rules: 开启规则和发生错误时报告的等级

规则的错误等级有三种:

1
2
3
0或'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
4
globals: {
__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
15
module: {
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// js
import scroll from '../../assets/js/scroll';
export default {
mixins: [
scroll
]
……
methods: {
queryData () {
axios.post('url', {}).then(() => {
this.initScroll();
}).catch(() => {
this.initScroll();
})
}
}
}

注意设置wrapper样式

1
2
3
4
5
6
.scroll-wrapper{
overflow: hidden;
position: relative(滚动条定位)
margin: 0 -15px;(空出滚动条的位置)
padding: 0 15px;
}

定义的scrollPullDown方法,为滚动对象绑定了一个下拉事件,当滚动区域下拉至底部时触发,我在这里把他应用于表格的分页加载,因为是桌面应用,没有使用传统的分页器,而是使用下拉来加载下一页使用,需要设置pullDown: true以及onPullDown方法使用。

最后发现better-scroll并不是适用于触屏PC,弃用了。。由于electron自带chrome,所以直接通过css修改了滚动条样式,使用原生滚动

1
2
3
4
.scroll-wrapper{ overflow: auto; }
::-webkit-scrollbar-track-piece{ background-color: $white-color; }
::-webkit-scrollbar{ width:5px; }
::-webkit-scrollbar-thumb{ background-color: rgba(0,0,0,.6); border-radius: 3px }

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
30
import 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
2
3
4
5
6
7
8
9
10
11
touchStart (ev) {
this.touchStartSite = ev.touches[0].pageY;
},
touchEnd (ev) {
let endSite = ev.changedTouches[0].pageY;
let wrapper = document.getElementsByClassName('el-table__body-wrapper')[0];
let content = document.getElementsByClassName('el-table__body')[0];
if (wrapper.scrollTop === (content.clientHeight - wrapper.clientHeight) && (this.touchStartSite - endSite) > 50) {
this.onPullDown();
}
},

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
33
export 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">&#xe649;</i>

每次添加新图标都需要更新图标库代码

文章目录
  1. 1. 简介
  2. 2. 安装
  3. 3. 跨域
  4. 4. build空白
  5. 5. 全屏显示
  6. 6. 注册快捷键
  7. 7. axios封装
  8. 8. I18n多语言设置
  9. 9. 登录超时
  10. 10. Eslint 从入门到放弃
  11. 11. Better-Scroll滚动封装
  12. 12. Table简易封装
  13. 13. Sass预设
  14. 14. 读卡器
  15. 15. Iconfont图标
    1. 15.1. 在线字体库
    2. 15.2. 引用css文件
    3. 15.3. 引入图标文件
|