谈起桌面应用开发技术, 我们会想到.Net下的WinForm, Java下的JavaFX以及Linux下的QT. 这些技术对于Web应用程序员来说一般比较陌生, 因为大多Web应用程序员的开发技能是前端的JavaScript和后端的Java,PHP等语言.
如果Web应用程序员想开发桌面应用怎么办? 主流的桌面应用开发技术的学习曲线不低, 上手比较困难. 而Electron的出现给Web应用程序员带来了福音.
Electron简介:
Electron 是 Github 发布跨平台桌面应用开发工具,支持 Web 技术开发桌面应用开发,其本身是基于 C++ 开发的,GUI 核心来自于 Chrome,而 JavaScript 引擎使用 v8...
简单的说, Electron平台就是用Javascript把UI和后台逻辑打通, 后台主进程使用NodeJs丰富的API完成复杂耗时的逻辑, 而UI进程则借助Chrome渲染html完成交互.
我之前使用SpringBoot开发了一套市长信箱抓取Web应用. 由于没服务器部署, 所以我现在想把同样的功能移植到桌面端, 作成一个桌面应用. 对于开发平台我有以下需求:
而Electron作为开发平台正好能满足我的这些需求, 通过一天的摸索, 我完成了这个桌面应用, 并最终打包出Mac平台下的DMG安装文件. 工程代码: https://github.com/ybak/watcher
下面将介绍我是如何使用Electron平台开发这个桌面应用.
回顾: 市长信箱邮件抓取Web应用
动手之前, 我先分析一下之前所做的抓取Web应用. 它的架构如下:
应用分可为四部分:
设计: 使用Electron构建抓取桌面应用
将要实现的桌面应用, 同样也需要需要完成这四部分的工作. 我做了以下设计:
Electron主进程借助NodeJs丰富的生态系统完成网页抓取与数据存储与搜索的功能, UI进程则完成页面的渲染工作.
实现: 使用Electron构建抓取桌面应用
1. 抓取程序的实现:
市长信箱邮件多达上万封, JavaScript异步的特点, 会让人不小心就写出上千并发请求的程序, 短时间内大量试图和抓取目标服务器建立连接的行为会被服务器拒绝服务, 从而造成抓取流程失败. 所以抓取程序要做到:
我使用以下三个NodeJs组件:
代码: crawlService.js
//使用request获取页面内容 request('http://12345.chengdu.gov.cn/moreMail', (err, response, body)=> { if (err) throw err; //使用cheerio解析html var $=cheerio.load(body), totalSize=$('div.pages script').html().match(/iRecCount=\d+/g)[0].match(/\d+/g)[0]; ...... //使用async控制请求并发, 顺序的抓取邮件分页内容 async.eachSeries(pagesCollection, function (page, crawlNextPage) { pageCrawl(page, totalPageSize, updater, crawlNextPage); }) });
2. 数据库的实现:
抓取后的内容存储方式有较多选择:
文本文件虽然保存简单, 但不利于查询和搜索, 顾不采用.
搜索引擎一般需要独立部署, 不利于桌面应用的安装, 这里暂不采用.
独立部署的数据库有和搜索引擎同样的问题, 所以像连接外部Mysql的方式这里也不采用.
综合考虑, 我需要一种内嵌数据库. 幸好NodeJs的组件非常丰富, nedb是一个不错的方案, 它可以将数据同时保存在内存和磁盘中, 同时是文档型内嵌数据库, 使用mongodb的语法进行数据操作.
代码: dbService.js
//建立数据库连接 const db=new Datastore({filename: getUserHome()+'/.electronapp/watcher/12345mails.db', autoload: true}); ...... //使用nedb插入数据 db.update({_id: mail._id}, mail, {upsert: true}, function (err, newDoc) {}); ...... //使用nedb进行邮件查询 let match={$regex: eval('/' + keyword + '/')}; //关键字匹配 var query=keyword ? {$or: [{title: match}, {content: match}]} : {}; db.find(query).sort({publishDate: -1}).limit(100).exec(function (err, mails) { event.sender.send('search-reply', {mails: mails});//处理查询结果 });
3. UI的实现:
桌面应用的工程目录如图:
我将UI页面放到static文件夹下. 在Electron的进行前端UI开发和普通的Web开发方式一样, 因为Electron的UI进程就是一个Chrome进程. Electron启动时, 主进程会执行index.js文件, index.js将初始化应用的窗口, 设置大小, 并在窗口加载UI入口页面index.html.
代码:index.js
function createMainWindow() { const win=new electron.BrowserWindow({ width: 1200, height: 800 });//初始应用窗口大小 win.loadURL(`file://${__dirname}/static/index.html`);//在窗口中加载页面 win.openDevTools();//打开chrome的devTools win.on('closed', onClosed); return win; }
在UI页面开发的过程中, 有一点需要注意的是: 默认情况下页面会出现jQuery, require等组件加载失败的情况, 这是因为浏览器window加载了NodeJs的一些方法, 和jQuery类库的方法冲突. 所以我们需要做些特别的处理, 在浏览器window中把这些NodeJs的方法删掉:
代码:preload.js
// 解决require冲突导致jQuery等组件不可用的问题 window.nodeRequire=require; delete window.require; delete window.exports; delete window.module; // 解决chrome调试工具devtron不可用的问题 window.__devtron={require: nodeRequire, process: process}
4. 通信的实现:
在Web应用中, 页面和服务的通信都是通过ajax进行, 那我们的桌面应用不是也可以采用ajax的方式通信? 这样理论虽然上可行, 但有一个很大弊端: 我们的应用需要打开一个http的监听端口, 通常个人操作系统都禁止软件打开http80端口, 而打开其他端口也容易和别的程序造成端口冲突, 所以我们需要一种更优雅的方式进行通信.
Electron提供了UI进程和主进程通信的IPC API, 通过使用IPC通信, 我们就能实现UI页面向NodeJs服务逻辑发起查询和抓取请求,也能实现NodeJs服务主动向UI页面通知抓取进度的更新.
使用Electron的IPC非常简单.
首先, 我们需要在UI中使用ipcRenderer, 向自定义的channel发出消息.
代码: app.js
const ipcRenderer=nodeRequire('electron').ipcRenderer; //提交查询表单 $('form.searchForm').submit(function (event) { $('#waitModal').modal('show'); event.preventDefault(); ipcRenderer.send('search-keyword', $('input.keyword').val());//发起查询请求 }); ipcRenderer.on('search-reply', function(event, data) {//监听查询结果 $('#waitModal').modal('hide'); if (data.mails) { var template=Handlebars.compile($('#template').html()); $('div.list-group').html(template(data)); } });
然后, 需要在主进程执行的NodeJs代码中使用ipcMain, 监听之前自定义的渠道, 就能接受UI发出的请求了.
代码: crawlService.js
const ipcMain=require('electron').ipcMain; ipcMain.on('search-keyword', (event, arg)=> { ....//处理查询逻辑 }); ipcMain.on('start-crawl', (event, arg)=> { ....//处理抓取逻辑 });
桌面应用打包
解决完以上四个方面的问题后, 剩下的程序写起来就简单了. 程序调试完后, 使用electron-builder, 就可以编译打包出针对不同平台的可执行文件了.
最近笔者终于把H5-Dooring的后台管理系统初步搭建完成, 有了初步的数据采集和数据分析能力, 接下来我们就复盘一下其中涉及的几个知识点,并一一阐述其在Dooring H5可视化编辑器中的解决方案. 笔者将分成3篇文章来复盘, 主要解决场景如下: 如何使用JavaScript实现前端导入和导出excel文件(H5编辑器实战复盘) 前端如何基于table中的数据一键生成多维度数据可视化分析报表 * 如何实现会员管理系统下的权限路由和权限菜单
以上场景也是前端工程师在开发后台管理系统中经常遇到的或者即将遇到的问题, 本文是上述介绍中的第一篇文章, 你将收获: 使用JavaScript实现前端导入excel文件并自动生成可编辑的Table组件 使用JavaScript实现前端基于Table数据一键导出excel文件 * XLSX和js-export-excel基本使用
本文接下来的内容素材都是基于H5可视化编辑器(H5-Dooring)项目的截图, 如果想实际体验, 可以访问H5-Dooring网站实际体验. 接下来我们直接开始我们的方案实现.
在开始实现之前, 我们先来看看实现效果.
导入excel文件并通过antd的table组件渲染table:
编辑table组件:
保存table数据后实时渲染可视化图表:
以上就是我们实现导入excel文件后, 编辑table, 最后动态生成图表的完整流程.
导入excel文件的功能我们可以用javascript原生的方式实现解析, 比如可以用fileReader这些原生api,但考虑到开发效率和后期的维护, 笔者这里采用antd的Upload组件和XLSX来实现上传文件并解析的功能. 由于我们采用antd的table组件来渲染数据, 所以我们需要手动将解析出来的数据转换成table支持的数据格式.大致流程如下:
所以我们需要做的就是将Upload得到的文件数据传给xlsx, 由xlsx生成解析对象, 最后我们利用javascript算法将xlsx的对象处理成ant-table支持的数据格式即可. 这里我们用到了FileReader对象, 目的是将文件转化为BinaryString, 然后我们就可以用xlsx的binary模式来读取excel数据了, 代码如下:
// 解析并提取excel数据
let reader=new FileReader();
reader.onload=function(e) {
let data=e.target.result;
let workbook=XLSX.read(data, {type: 'binary'});
let sheetNames=workbook.SheetNames; // 工作表名称集合
let draftObj={}
sheetNames.forEach(name=> {
// 通过工作表名称来获取指定工作表
let worksheet=workbook.Sheets[name];
for(let key in worksheet) {
// v是读取单元格的原始值
if(key[0] !=='!') {
if(draftObj[key[0]]) {
draftObj[key[0]].push(worksheet[key].v)
}else {
draftObj[key[0]]=[worksheet[key].v]
}
}
}
});
// 生成ant-table支持的数据格式
let sourceData=Object.values(draftObj).map((item,i)=> ({ key: i + '', name: item[0], value: item[1]}))
经过以上处理, 我们得到的sourceData即是ant-table可用的数据结构, 至此我们就实现了表格导入的功能.
table表格的编辑功能实现其实也很简单, 我们只需要按照antd的table组件提供的自定义行和单元格的实现方式即可. antd官网上也有实现可编辑表格的实现方案, 如下:
大家感兴趣的可以研究一下. 当然自己实现可编辑的表格也很简单, 而且有很多方式, 比如用column的render函数来动态切换表格的编辑状态, 或者使用弹窗编辑等都是可以的.
根据table数据动态生成图表这块需要有一定的约定, 我们需要符合图表库的数据规范, 不过我们有了table数据, 处理数据规范当然是很简单的事情了, 笔者的可视化库采用antv的f2实现, 所以需要做一层适配来使得f2能消费我们的数据.
还有一点就是为了能使用多张图表, 我们需要对f2的图表进行统一封装, 使其成为符合我们应用场景的可视化组件库.
我们先看看f2的使用的数据格式:
const data=[
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 }
];
此数据格式会渲染成如下的图表:
所以说我们总结下来其主要有2个纬度的指标, 包括它们的面积图, 饼图, 折线图, 格式都基本一致, 所以我们可以基于这一点封装成组件的可视化组件, 如下:
import { Chart } from '@antv/f2';
import React, { memo, useEffect, useRef } from 'react';
import ChartImg from '@/assets/chart.png';
import styles from './index.less';
import { IChartConfig } from './schema';
interface XChartProps extends IChartConfig {
isTpl: boolean;
}
const XChart=(props: XChartProps)=> {
const { isTpl, data, color, size, paddingTop, title }=props;
const chartRef=useRef(null);
useEffect(()=> {
if (!isTpl) {
const chart=new Chart({
el: chartRef.current || undefined,
pixelRatio: window.devicePixelRatio, // 指定分辨率
});
// step 2: 处理数据
const dataX=data.map(item=> ({ ...item, value: Number(item.value) }));
// Step 2: 载入数据源
chart.source(dataX);
// Step 3:创建图形语法,绘制柱状图,由 genre 和 sold 两个属性决定图形位置,genre 映射至 x 轴,sold 映射至 y 轴
chart
.interval()
.position('name*value')
.color('name');
// Step 4: 渲染图表
chart.render();
}
}, [data, isTpl]);
return (
<div className={styles.chartWrap}>
<div className={styles.chartTitle} style={{ color, fontSize: size, paddingTop }}>
{title}
</div>
{isTpl ? <img src={ChartImg} alt="dooring chart" /> : <canvas ref={chartRef}></canvas>}
</div>
);
};
export default memo(XChart);
当然其他的可视化组件也可以用相同的模式封装,这里就不一一举例了. 以上的组件封装使用react的hooks组件, vue的也类似, 基本原理都一致.
同样的, 我们实现将table数据一键导出为excel也是类似, 不过方案有所不同, 我们先来看看在Dooring中的实现效果.
以上就是用户基于后台采集到的数据, 一键导出excel文件的流程, 最后一张图是生成的excel文件在office软件中的呈现.
一键导出功能主要用在H5-Dooring的后台管理页面中, 为用户提供方便的导出数据能力. 我们这里导出功能也依然能使用xlsx来实现, 但是综合对比了一下笔者发现有更简单的方案, 接下来笔者会详细介绍, 首先我们还是来看一下流程:
很明显我们的导出流程比导入流程简单很多, 我们只需要将table的数据格式反编译成插件支持的数据即可. 这里笔者使用了js-export-excel来做文件导出, 使用它非常灵活,我们可以自定义: 自定义导出的excel文件名 自定义excel的过滤字段 * 自定义excel文件中每列的表头名称
由于js-export-excel支持的数据结构是数组对象, 所以我们需要花点功夫把table的数据转换成数组对象, 其中需要注意的是ant的table数据结构中键对应的值可以是数组, 但是js-export-excel键对应的值是字符串, 所以我们要把数组转换成字符串,如[a,b,c]变成'a,b,c', 所以我们需要对数据格式进行转换, 具体实现如下:
const generateExcel=()=> {
let option={}; //option代表的就是excel文件
let dataTable=[]; //excel文件中的数据内容
let len=list.length;
if (len) {
for(let i=0; i<len; i++) {
let row=list[i];
let obj:any={};
for(let key in row) {
if(typeof row[key]==='object') {
let arr=row[key];
obj[key]=arr.map((item:any)=> (typeof item==='object' ? item.label : item)).join(',')
}else {
obj[key]=row[key]
}
}
dataTable.push(obj); //设置excel中每列所获取的数据源
}
}
let tableKeys=Object.keys(dataTable[0]);
option.fileName=tableName; //excel文件名称
option.datas=[
{
sheetData: dataTable, //excel文件中的数据源
sheetName: tableName, //excel文件中sheet页名称
sheetFilter: tableKeys, //excel文件中需显示的列数据
sheetHeader: tableKeys, //excel文件中每列的表头名称
}
]
let toExcel=new ExportJsonExcel(option); //生成excel文件
toExcel.saveExcel(); //下载excel文件
}
注意, 以上笔者实现的方案对任何table组件都使用, 可直接使用以上代码在大多数场景下使用. 至此, 我们就实现了使用JavaScript实现前端导入和导出excel文件的功能.
所以, 今天你又博学了吗?
以上教程笔者已经集成到H5-Dooring中,对于一些更复杂的交互功能,通过合理的设计也是可以实现的,大家可以自行探索研究。
地址:H5-Dooring | 一款强大的H5编辑器
如果想学习更多H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在《趣谈前端》一起学习讨论,共同探索前端的边界。
邹个人站点:http://www.itzoujie.com/
不懂后端的前端不是一个大前端,不懂后端的前端会大大限制你的发展空间,所以小邹在网上找了一篇不错的文章来分享给大伙,这里说一下,小邹的个人站点技术栈是(node+express+vue+mysql),跟这篇文章的技术栈略有不同,当然站点里面涉及的组件库和小程序等,小邹这里就不一一说了。好了,下面直接开始分享:
Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodb
说明:
build 文件讲解
说明:
1.admin - 后台管理界面源码
src - 代码区域:
2.client - web端界面源码
跟后台管理界面的结构基本一样。
3.server - 服务端源码
说明:
开发中用的一些依赖模块
components
这个文件夹一般放入常用的组件, 比如 Loading组件等等。
views
所有模块页面。
store
vuex 用来统一管理公用属性, 和统一管理接口。
登陆
登陆是采用 jsonwebtoken方案 来实现整个流程的。
1. jwt.sign(payload,secretOrPrivateKey,[options,callback]) 生成TOKEN
2. jwt.verify(token,secretOrPublicKey,[options,callback]) 验证TOKEN
3.获取用户的账号密码。
4.通过 jwt.sign 方法来生成token:
5.每次请求数据的时候通过 jwt.verify 检测token的合法性 jwt.verify(token,secret)。
权限
通过不同的权限来动态修改路由表。
通过 vue的 钩子函数 beforeEach 来控制并展示哪些路由, 以及判断是否需要登陆。
通过调用 getUserInfo方法传入 token 获取用户信息, 后台直接解析 token 获取里面的信息返回给前台。
通过调用 setRoutes方法 动态生成路由。
axios 请求封装,统一对请求进行管理
面包屑 / 标签路径
上面介绍了几个主要以及必备的后台管理功能,其余的功能模块 按照需求增加就好
前台展示的页面跟后台管理界面差不多, 也是用vue+webpack搭建,基本的结构都差不多。
权限
主要是通过 jsonwebtoken 的verify方法检测 cookie 里面的 token 验证它的合法性。
日志是采用 log4js 来进行管理的, log4js 算 nodeJs 常用的日志处理模块,用起来额也比较简单。
log4js 的日志分为九个等级,各个级别的名字和权重如下:
1.图。
2.设置 Logger 实例的类型 logger=log4js.getLogger('cheese')。
3.通过 Appender 来控制文件的 名字、路径、类型 。
4.配置到 log4js.configure。
5.便可通过 logger 上的打印方法 来输出日志了 logger.info(JSON.stringify(currTime:当前时间为${Date.now()}s ))。
设计思路
当应用程序启动时候,读取指定目录下的 js 文件,以文件名作为属性名,挂载在实例 app 上,然后把文件中的接口函数,扩展到文件对象上。
读取出来的便是以下形式:
app.controller.admin.other.markdown_upload_img
便能读取到 markdown_upload_img 方法。
在把该形式的方法赋值过去就行:
router.post('/markdown_upload_img',app.controller.admin.other.markdown_upload_img)
通过 mongoose 链接 mongodb
封装返回的send函数
注意事项:
1. cnpm run server 启动服务器 //没装cnpm的使用npm命令
2.启动时,记得启动mongodb数据库,账号密码 可以在 server/config.js 文件下进行配置
3. db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]})(mongodb 注册用户)
4. cnpm run dev:admin 启动后台管理界面
5.登录后台管理界面录制数据
6.登录后台管理时需要在数据库 创建 users 集合注册一个账号进行登录
7. cnpm run dev:client 启动前台页面
*请认真填写需求信息,我们会在24小时内与您取得联系。