原文链接:vue 全家桶 +Electron+Quasar 框架快速构建跨平台应用_quasar 框架 build electron-CSDN 博客
vue 全家桶 +Electron+Quasar 框架快速构建跨平台应用
最近在科研和横向中遇到不少 GUI 开发的需求,这些项目中,快速、低成本的构建跨平台 GUI 应用是重中之重,典型的解决方案有 Qt 和基于 Web 的方案等。鉴于笔者的需求主要是呈现一些科研图表或视频流,并无复杂的交互逻辑,故选择基于 Web 的方案比较合适。具体的,根据之前的使用经验,选择 vue 来做界面逻辑,vue 的生态比较完善,有很多 GUI 框架可用,并选择 Electron 打包桌面端。
本文记录学习和使用 vue 全家桶 +Electron+Quasar 框架快速构建跨平台应用的一些心得。
先导知识
JS+HTML+CSS 三件套
MDN web docs (强烈推荐)
廖雪峰 JS 教程
w3school HTML 教程
w3school CSS 教程
阮一峰的网络日志
前端构建工具
Electron
Electron 官方文档
electron-api-demo:了解 electron 特性的一个良好选择。
electron-builder 打包见解
vue.js
UI 组件框架
Vuetify UI 库:优秀 vue 组件库,受 vue 官方推荐
element UI 库 :饿了么前端团队推出的组件库
Quasar UI 库:十分优秀的多平台 UI 解决方案
本文使用 Quasar 框架提供 UI 支持。Quasar 框架是近两年新发展起来的全栈 UI 框架,组件非常全面,强大,迭代热度很高。
常用 js 库
axios:基于 promise 的 HTTP 库
FFmpeg 相关:
lowdb JSON 数据库,适合用于在本地存储小数据
环境配置
基于 vue-cli(废弃)
安装 Node.js 环境,安装包下载。
为 npm 包管理器更换华为源(或淘宝源),加速国内访问速度:
npm config set registry https://mirrors.huaweicloud.com/repository/npm/ npm config set disturl https://mirrors.huaweicloud.com/nodejs/ npm config set electron_mirror https://mirrors.huaweicloud.com/electron/
全局安装 vue-cli 工具,该工具为新建 vue 工程提供了极大的便利。
npm install -g @vue/cli
命令行键入“vue ui”启动 vue-cli 的可视化界面:
使用默认配置新建工程。
等待 CLI 工具完成依赖下载和资源配置,随后自动进入项目控制台:
下面通过插件方式安装 Quasar UI 库,Quasar 已经维护了对 vue-cli-plugin 的支持,非常方便,但官方推荐使用 Quasar-cli 取而代之。在 plugins 中搜索并安装 vue-cli-plugin-quasar。
添加成功后,编译一下检查配置是否成功。
若一切正常,则可看到 demo 界面:
用相同的方法添加 electron-builder 插件。
electron 插件会自动配置两个新的任务选项用于编译打包:
首次执行 electron:build 的时候会从 GitHub 下载所需的二进制依赖文件,由于众所周知的原因,下载龟速,经常失败。对此可以通过手动下载依赖文件来解决,见这里。
所需的依赖文件可从这里和这里快速下载(ps. 赞一下华为云提供的镜像服务)。
顺利执行打包后,可在【项目路径】/dist_electron 下找到打包好的程序。
至此,借助 vue-cli 工具,我们得以快速完成了程序框架的配置。
基于 quasar-cli(推荐)
本节使用 wsl2 提供的 ubuntu18.04 为运行环境。
基础环境配置
quasar 框架推荐使用 quasar-cli 构建应用程序,quasar-cli 类似 vue-cli,但与 quasar 框架结合更紧密,省去了很多繁琐的配置。鉴于在 windows 上安装 quasar-cli 套件生成的工程的依赖文件时会通过 node-gyp 编译部分依赖包,存在一些兼容性问题,故笔者转而使用 Linux 环境进行开发,通过 WSL2 提供 ubuntu-18.04 环境。参考这里在 win10 上配置 WSL2 和 node 环境。完成 node 环境配置后,换 npm 源,安装 cli,建立一个 demo 工程:
npm config set registry https://registry.npm.taobao.org npm install -g @quasar/cli quasar create <folder_name>
按提示完成工程配置,最后,quasar-cli 会执行 npm install 安装依赖文件。
启动开发服务器,即可看到 demo 工程:
quasar dev
electron 环境配置
除了对基础的单页面应用开发的支持,quasar-cli 还提供使用 electron 和 Cordova 打包 PC 端和移动端混合应用的能力。如若需添加 electron 打包功能,执行:
quasar mode add electron
添加 electron 打包组件,并执行 quasar dev -m electron 启动开发服务器。
注意,由于 WSL2 不包含 GUI 部分,故在打包 electron 时会遇到一些依赖问题(缺少 so 包),只需按照报错信息使用 apt install 对应的包即可。此外,针对 GUI 程序无法显示的问题,我们可以通过 Xserver 查看 electron 程序的 GUI 界面。使用自带 Xserver 的客户端访问 WSL(如 mobaxterm),将如下内容填入~/.bashrc,使得 WSL2 的图形界面通过 Xserver 桥接:
export DISPLAY=$(awk '/nameserver / {print $2; exit}' /etc/resolv.conf 2>/dev/null):0 export LIBGL_ALWAYS_INDIRECT=1
之后再执行 quasar dev -m electron 便可查看 electron 应用。
若需打包 windows 平台,则需要额外安装 wine 组件。笔者按 wine 官网的标准方式安装,未能成功,后参照此贴安装成功,推测安装失败的原因可能与 WSL2 有关。其步骤如下:
sudo apt-get purge *wine* sudo snap remove wine sudo snap update wine-platform-* grep -Ril "wine" /etc/apt sudo dpkg --add-architecture i386 wget -nc https://dl.winehq.org/wine-builds/winehq.key sudo apt-key add winehq.key sudo apt-add-repository 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' sudo apt update sudo apt upgrade sudo apt --fix-broken install sudo apt autoremove --purge sudo apt upgrade wget https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_18.04/Release.key sudo apt-key add Release.key sudo apt-add-repository 'deb https://download.opensuse.org/repositories/Emulators:/Wine:/Debian/xUbuntu_18.04/ ./' sudo apt update sudo apt install libfaudio0 libasound2-plugins:i386 -y sudo apt install --install-recommends winehq-stable -y
icongenie 图片生成工具安装
按文档进行安装时,出现报错,原因在于其中 sharp 包的依赖 libvips 安装失败。手动安装 libvips,直接使用 apt 安装后无效,故对 libvips 进行编译安装
在 https://github.com/libvips/libvips/releases 下载源代码,按照 https://libvips.github.io/libvips/install.html 进行编译、安装。
开发工具配置
推荐使用 VScode 通过 wsl-remote 插件直接访问 wsl 容器,并安装 Vetur 插件对.vue 单文件组件提供支持,安装 Vue VSCode Snippets 加速开发,安装 Eslint 提供静态代码分析,安装 prettier 格式化和工具配合 Eslint 提供代码格式化。
打开项目文件夹/.vscode/settings.json,添加对项目格式化引擎等选项的配置(仅对本工程生效):
{ "editor.formatOnPaste": true, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true }, "javascript.format.insertSpaceBeforeFunctionParenthesis": true, "javascript.format.placeOpenBraceOnNewLineForControlBlocks": false, "javascript.format.placeOpenBraceOnNewLineForFunctions": false, "typescript.format.insertSpaceBeforeFunctionParenthesis": true, "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false, "typescript.format.placeOpenBraceOnNewLineForFunctions": false, "vetur.format.defaultFormatter.html": "prettyhtml", "vetur.format.defaultFormatter.js": "prettier-eslint" }
vscode 的 prettier 插件和 eslint 插件的配置方法见:here。
项目工程结构
其中比较重要的是如下几个目录:
- /quasar.conf.js 填写 quasar 框架的配置信息,如,当要使用 quasar 的对话框插件,需在该文件中添加配置项。
- /src/router 填写 vue router 的路由规则
- /src/layouts 存放描述 layouts 的.vue 文件,该文件描述页面的整体布局(侧边栏,工具栏位置)
- /src/pages 存放页面,会通过 vue 路由渲染到/src/layouts/xxx.vue
- /src/store 存放 vuex
- /src/assets 存放 webpack 处理的静态资源。这里存放的图片等资源会被 wenpack 打包处理(如进行 base64 转换后嵌入 js 代码),要引用此处存放的资源,应使用
<img src="~assets/logo.png">
- /public 存放静态资源,不被 wenpack 处理,仅进行复制,故 icon 等资源一般存放在此处,调用方法如下:
<img src="logo.png"> <img src="/logo.png">
- /src-electron 存放 electron 的配置文件和 main thread 逻辑
- /dist/xxx 存放各模式下编译出来的目标文件
Quasar 框架
这里摘录一些 Quasar 框架常用的功能和组件。
实用的 CSS 样式类
字体样式控制:提供对文本的字体、字号、加粗、斜体、对齐方式的控制。如,对文本加粗,并居中对齐:
<div class="text-body1 text-weight-bold text-center">Hello Quasar</div>
颜色系统
颜色系统提供了一套主题色和标准色,通过添加 text-xxx 类,可以变更文字颜色,通过 bg-xxx 可更换背景色:
<div class="text-body1 text-weight-bold text-center text-primary bg-positive">Hello Quasar</div>
Theme builder 提供快速配置主题色号的功能。
Spacing 系统
Spacing 是指 dom 元素之间的间隔方式:
quasar spacing 系统 提供一组 CSS 类完成 spacing 工作,具体的像素数会随着响应式系统自动变化,这组类的命名规则如下:
q-[p|m][t|r|b|l|a|x|y]-[none|auto|xs|sm|md|lg|xl] T D S T - type - values: p (padding), m (margin) D - direction - values: t (top), r (right), b (bottom), l (left), a (all), x (both left & right), y (both top & bottom) S - size - values: none, auto (ONLY for specific margins: q-ml-*, q-mr-*, q-mx-*), xs (extra small), sm (small), md (medium), lg (large), xl (extra large) # 例如 <div class="q-pa-sm">...</div>
显示控制
显示控制 css 类组用于控制 dom 元素的显示情况。部分 CSS 类的效果如下:
- disabled 表示禁止选中
- hidden 表示隐藏该元素
- invisible 将元素设为不可见,但仍占据原有位置
- transparent 将组件背景色变为透明(删去背景色)
- ellipsis ellipsis-2-lines ellipsis-3-lines 将显示不下的长文本省略并在末尾添加省略号
此外,显示控制 css 类组还提供根据 window 宽度和应用所处的运行平台(手机、pad、桌面端)隐藏和显示组件的功能。
实用辅助类
这些 css 类实现了对鼠标选中的控制,滑动的控制,尺寸控制等待。
动画
Animations 提供了对 vue** **Transition 机制和 CSS 动效库 Animate.css 的封装,使用方法如下:
-
在/quasar.conf.js 中开启 Animate.css:
animations: 'all' animations: [ 'bounceInLeft', 'bounceOutRight' ]
-
使用 transition 组件包裹需要施加动画的部分
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut" > <p v-if="show">hello</p> </transition>
其中动画选项有六种(实际仅 enter-active-class 和 leave-active-class 常用):
enter-class
enter-active-class
enter-to-class (2.1.8+)
leave-class
leave-active-class
leave-to-class (2.1.8+) -
改变组件绑定的 show 属性,可看到动画效果。
布局系统
Quasar 的布局系统基于 Flexbox 开发,通过 Container (Parent)和 items (Children)两个层级管理容器中的组件,提供对组件在不同尺寸下排列的方式、位置、间距的精细化控制:
例如,
<div class="row"> <div class="col-8">two thirds</div> <div class="col-2">one sixth</div> <div class="col-auto">auto size based on content and available space</div> <div class="col">fills remaining available space</div> </div>
其中 class="row"是 Parent 层级,有如下可选类型:
最常用的是 row(横向),column(纵向)。
使用 justify 类和 items-xxx 类可控制 Parent 下子组件横向、纵向的对齐方式:
对与 Parent 中每个 Children 组件,“col-xxx” 类用于控制组件的宽(高),Parent 被等分为 12 个块,可使用“col-1”,“col-2”…指定宽高,或使用“col-auto”令 Children 适应组件内容的形状,“col”则表示指定组件宽为 Parent 剩余的全部位置。offset-xxx 可控制位置的偏置量。
除文档外,Quasar 提供 flex-playground 帮助用户理解样式的具体效果。
一个例子如下,对组件指定了背景色以更加清楚的显示其占位。
<template> <q-page class="q-pa-md column bg-green-2 justify-center items-center"> <div class="col-auto bg-grey-2"> <transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut" > <img v-if="show" alt="Quasar logo" src="~assets/quasar-logo-full.svg" /> </transition> </div> <div class="col-auto bg-grey-4"> <q-btn color="secondary" label="Show" @click="show = !show" /> </div> </q-page> </template>
q-page 是整个页面的根组件,q-page 是页面路由要求使用的。对其添加 column 使之成为 flex 布局的 parent 组件,justify-center items-center 使得内部元素垂直居中 + 水平居中。两个子组件指定 col-auto 类,以自适应组件内容,否则会有留白,如:将 col-auto 替换为 col(表示填满空间)的效果:
Layout
QLayout 是一个组件,用于管理整个窗口并使用导航栏或抽屉等元素包装页面内容,是对布局系统的一层封装,可以帮助更好地构建网站/应用程序。使用 layout builder 可快速定制一个布局方案。如:
builder 生成的代码应填写到/src/layouts/MainLayout.vue 路径。对其中的 header footer 和侧边栏属性的详细控制可在文档中找到。
组件库
Quasar 提供了海量的 vue 组件(组件是可复用的 Vue 实例,详见 vue 文档),文档中,每个组件有四类 API:
probs 是传入组件的属性,例如向 QAjaxBar 组件传入 skip-hijack 等属性,可写作:
<q-ajax-bar position="bottom" color="accent" size="10px" skip-hijack />
events 是组件能够发出的事件。组件事件用于如下场景(摘自 vue 文档):
slots 用于指定组件分发内容(文档)。注意具名插槽的使用,具名插槽机制可令组件接收多个输入插槽。在向具名插槽提供内容的时候,我们可以在一个 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
methods 是组件提供的分发,通过设置组件的 ref 属性,我们可通过 this.$refs 对组件进行索引,进而调用组件方法(vue 文档):
如:
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.xxx()
插件
某些功能需要通过插件的方式实现,如对话框、通知、全屏触发等常用功能。
utils
utils 中提供了一些实用的函数,如格式化显示日期、时间,下载触发、url 跳转、复制到剪贴板、API 触发频率限制、uid 生成、对象深拷贝等。
页面路由
页面路由是单页应用的重要环节,quasar 项目集成 vue-router 支持。在 src/layouts/MainLayout.vue 中使用 q-route-tab 编写路由接口:
<q-tabs v-model="tab" align="left"> <q-route-tab to="/" label="index" /> <q-route-tab to="/testpage" label="testpage" /> </q-tabs>
在 src/router/routes.js 中填写路由映射:
{ path: '/', component: () => import('layouts/MainLayout.vue'), children: [ { path: '', component: () => import('pages/Index.vue') }, { path: 'testpage', component: () => import('pages/TestPage.vue') }, ], },
在/src/pages 目录下放置页面:
<template> <q-page class="flex flex-center"> <h1>{{ foo }}</h1> </q-page> </template> <script> export default { name: 'PageTestPage', data() { return { foo: 'this is testpage', }; }, }; </script>
vuex
quaser 项目也提供了对 vuex 的支持(可选),Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,用于解决多组件之间的共享状态问题。在单页 app 中,vuex 典型的应用场景是同步多个页面之间的状态。在 vuex 的设计中,通过 mutation(一组函数)来变更状态(state),配合 vue 的计算属性机制,多个页面的组件之间可响应式的共享 state 的值。quaser 中,使用 vuex-模块进行组织,更易于维护。
其中每个模块文件夹包含如下文件:
使用 cli 可以代替手动复制,快速新建一个模块:
quasar new store <store_name>
下面举例说明如何使用 vuex 在两个页面之间同步状态。
命令行执行 quasar new store showcase 生成一个新的 showcase 模块,编辑 src/store/index.js 引入新模块:
编辑 src/store/showcase/state.js 定义两个欲进行同步的状态 drawerState、message:
export default function () { return { drawerState: true, message: '', }; }
编辑 src/store/showcase/mutations.js 编写修改状态的方法:
export const updateDrawerState = (state, opened) => { state.drawerState = opened; }; export const updateMessageState = (state, msg) => { state.message = msg; };
在页面组件中使用:
页面 1:index.vue
<template> <q-page class="q-pa-md column justify-center items-center q-gutter-md"> <div class="col-auto"> <transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut" > <img v-if="show" alt="Quasar logo" src="~assets/quasar-logo-full.svg" /> </transition> </div> <div class="col-auto"> <q-btn color="secondary" label="Show" @click="show = !show" /> </div> <div class="col-auto"> {{ drawerState }} <q-toggle v-model="drawerState" /> <q-btn color="secondary" label="Update" @click="updateMsg" /> </div> </q-page> </template> <script> import { date } from 'quasar'; export default { name: 'PageIndex', data() { return { show: true, }; }, methods: { updateMsg() { const { addToDate } = date; const newDate = addToDate(new Date(), { days: 7, month: 1 }); this.$store.commit('showcase/updateMessageState', newDate); }, }, computed: { drawerState: { get() { return this.$store.state.showcase.drawerState; }, set(val) { this.$store.commit('showcase/updateDrawerState', val); }, }, }, }; </script>
页面 2:testpage.vue:
<template> <q-page class="flex flex-center"> <q-toggle v-model="drawerState" /> <p>{{ msgState }}</p> </q-page> </template> <script> export default { name: 'PageTestPage', data() { return { foo: 'this is testpage', }; }, computed: { drawerState: { get() { return this.$store.state.showcase.drawerState; }, set(val) { this.$store.commit('showcase/updateDrawerState', val); }, }, msgState() { return this.$store.state.showcase.message; }, }, }; </script>
通过定义计算属性的 get,可以在 this.$store.state 被变更时自动刷新 dom 渲染。对状态的变更则一律通过 this.$store.commit 调用 src/store/showcase/mutations.js 中编写的修改状态的方法来实现。效果如下,两个页面的开关状态同步,页面 1update 变更状态可以在页面 2 体现:
electron nodejs API 调用
对于添加了 electron 模块的 quasar 项目,quasar 生成项目时已经进行了相关配置,故可在前端代码直接使用 nodejs 的模块。
API 调用
Axios 是一个基于 promise 的著名 HTTP 库,如在生成工程时选择添加 axios,则会在 src/boot/axios.js 中将 axios 库挂载到 Vue.prototype.$axios,.vue 文件中使用 this.$axios 可访问它。典型使用如下:
this.$axios .get('https://api.coindesk.com/v1/bpi/currentprice.json') .then(response => { this.info = response.data.bpi }) .catch(error => { console.log(error) this.errored = true }) .finally(() => this.loading = false)
注:例子中的 coindesk API 是一个支持跨域访问的比特币行情 API。
注意,不同于 JSONP,axios 是无法单方面解决跨域问题的,需要接口配合。如,使用 python flask 写个 API 接口,可通过 cross_origin 库添加跨域许可:
from flask import Flask, request from flask_cors import cross_origin app = Flask(__name__) @app.route('/hello') @cross_origin(resources=r'/*') def hello(): return "hello" if __name__ == '__main__': app.run(host='192.168.3.16',port=5000,debug=True)
此处服务端使用本机真实 IP 地址,若使用 127.0.0.1,则在 electron 打包的 app 中,axios 报错(xhr),从 axios 的 GitHub 讨论来看,可能是一个 bug。关于 IP、localhost、127.0.0.1 的区别可参考此文。
echart 图表
echart 是百度开发的前端图表库,已经捐助到 apache 基金会。
直接使用原生的 Echart 库,参考文章 vue 中使用 ECharts 实现折线图和饼图。
src/pages/echartDemo.vue:
<template> <q-page class="flex flex-center"> <div ref="chartPie" class="pie-wrap"></div> </q-page> </template> <script> import * as echarts from 'echarts'; export default { name: 'echartDemo', data() { return { chartPie: null, }; }, mounted() { this.$nextTick(() => { this.drawPieChart(); }); }, methods: { drawPieChart() { const mytextStyle = { color: '#333', fontSize: 18, }; const mylabel = { show: true, position: 'right', offset: [30, 40], formatter: '{b} : {c} ({d}%)', textStyle: mytextStyle, }; this.chartPie = echarts.init(this.$refs.chartPie); this.chartPie.setOption({ title: { text: 'Pie Chart', subtext: '纯属虚构', x: 'center', }, tooltip: { trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)', }, legend: { data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎'], left: 'center', top: 'bottom', orient: 'horizontal', }, series: [ { name: '访问来源', type: 'pie', radius: ['50%', '70%'], center: ['50%', '50%'], data: [ { value: 335, name: '直接访问' }, { value: 310, name: '邮件营销' }, { value: 234, name: '联盟广告' }, { value: 135, name: '视频广告' }, { value: 1548, name: '搜索引擎' }, ], animationEasing: 'cubicInOut', animationDuration: 2600, label: { emphasis: mylabel, }, }, ], }); }, }, }; </script> <style> .pie-wrap { width: 100%; height: 400px; } </style>
其中, 触发绘制的函数 drawPieChart 被挂载到 mounted(),根据 vue 生命周期钩子的描述,mounted()将在 dom 渲染完毕后执行。使用 this.$nextTick 方法可以使得 dom 刷新后 drawPieChart 再被触发。
组件化开发
对于重复的逻辑,可以抽离并编写成自定义组件,提升代码的可维护性。举例来说,构建一个折线图组件,接收 x 轴图例数组和 y 轴数据两个属性,渲染一个折线图。
工程的组件放在 src/components 目录。编辑 src/components/EchartsCategory.vue,编写组件:
<template> <div ref="chart" class="chart"></div> </template> <script> import * as echarts from 'echarts'; export default { name: 'EchartsCategory', components: {}, data() { return { myChart: null, }; }, props: { xAxisdata: { type: Array, default() { return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; }, }, yAxisdata: { type: Array, default() { return [150, 230, 224, 218, 135, 147, 260]; }, }, }, mounted() { this.$nextTick(() => { this.draw(); }); }, methods: { draw() { this.myChart = echarts.init(this.$refs.chart); this.myChart.setOption({ xAxis: { type: 'category', data: this.xAxisdata, }, yAxis: { type: 'value', }, series: [ { data: this.yAxisdata, type: 'line', }, ], }); }, }, }; </script> <style> .chart { width: 100%; height: 400px; } </style>
其中 props 定义了组件可接收的属性的规格和默认值。
在 src/pages/echartDemo.vue 中使用 EchartsCategory 这一组件:
<template> <q-page class="flex flex-center"> <EchartsCategory v-bind="EchartsCategoryData"></EchartsCategory> </q-page> </template> <script> import EchartsCategory from 'components/EchartsCategory.vue'; export default { name: 'echartDemo', components: { EchartsCategory }, data() { return { EchartsCategoryData: { xAxisdata: ['abc', 'def', 'sdsd'], yAxisdata: [12, 35, 76], }, }; }, }; </script>
使用 import 引入组件后,需在 components 中加以引用。对 EchartsCategory 组件添加 v-bind,将 props 整体传入,这和如下写法是等效的:
<EchartsCategory v-bind:xAxisdata="EchartsCategoryData.xAxisdata" v-bind:yAxisdata="EchartsCategoryData.yAxisdata"> </EchartsCategory>
文章知识点与官方知识档案匹配,可进一步学习相关知识