談起桌面應用開發技術, 我們會想到.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小時內與您取得聯系。