Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
談起桌面應用開發(fā)技術, 我們會想到.Net下的WinForm, Java下的JavaFX以及Linux下的QT. 這些技術對于Web應用程序員來說一般比較陌生, 因為大多Web應用程序員的開發(fā)技能是前端的JavaScript和后端的Java,PHP等語言.
如果Web應用程序員想開發(fā)桌面應用怎么辦? 主流的桌面應用開發(fā)技術的學習曲線不低, 上手比較困難. 而Electron的出現(xiàn)給Web應用程序員帶來了福音.
Electron簡介:
Electron 是 Github 發(fā)布跨平臺桌面應用開發(fā)工具,支持 Web 技術開發(fā)桌面應用開發(fā),其本身是基于 C++ 開發(fā)的,GUI 核心來自于 Chrome,而 JavaScript 引擎使用 v8...
簡單的說, Electron平臺就是用Javascript把UI和后臺邏輯打通, 后臺主進程使用NodeJs豐富的API完成復雜耗時的邏輯, 而UI進程則借助Chrome渲染html完成交互.
我之前使用SpringBoot開發(fā)了一套市長信箱抓取Web應用. 由于沒服務器部署, 所以我現(xiàn)在想把同樣的功能移植到桌面端, 作成一個桌面應用. 對于開發(fā)平臺我有以下需求:
而Electron作為開發(fā)平臺正好能滿足我的這些需求, 通過一天的摸索, 我完成了這個桌面應用, 并最終打包出Mac平臺下的DMG安裝文件. 工程代碼: https://github.com/ybak/watcher
下面將介紹我是如何使用Electron平臺開發(fā)這個桌面應用.
回顧: 市長信箱郵件抓取Web應用
動手之前, 我先分析一下之前所做的抓取Web應用. 它的架構(gòu)如下:
應用分可為四部分:
設計: 使用Electron構(gòu)建抓取桌面應用
將要實現(xiàn)的桌面應用, 同樣也需要需要完成這四部分的工作. 我做了以下設計:
Electron主進程借助NodeJs豐富的生態(tài)系統(tǒng)完成網(wǎng)頁抓取與數(shù)據(jù)存儲與搜索的功能, UI進程則完成頁面的渲染工作.
實現(xiàn): 使用Electron構(gòu)建抓取桌面應用
1. 抓取程序的實現(xiàn):
市長信箱郵件多達上萬封, JavaScript異步的特點, 會讓人不小心就寫出上千并發(fā)請求的程序, 短時間內(nèi)大量試圖和抓取目標服務器建立連接的行為會被服務器拒絕服務, 從而造成抓取流程失敗. 所以抓取程序要做到:
我使用以下三個NodeJs組件:
代碼: crawlService.js
//使用request獲取頁面內(nèi)容 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控制請求并發(fā), 順序的抓取郵件分頁內(nèi)容 async.eachSeries(pagesCollection, function (page, crawlNextPage) { pageCrawl(page, totalPageSize, updater, crawlNextPage); }) });
2. 數(shù)據(jù)庫的實現(xiàn):
抓取后的內(nèi)容存儲方式有較多選擇:
文本文件雖然保存簡單, 但不利于查詢和搜索, 顧不采用.
搜索引擎一般需要獨立部署, 不利于桌面應用的安裝, 這里暫不采用.
獨立部署的數(shù)據(jù)庫有和搜索引擎同樣的問題, 所以像連接外部Mysql的方式這里也不采用.
綜合考慮, 我需要一種內(nèi)嵌數(shù)據(jù)庫. 幸好NodeJs的組件非常豐富, nedb是一個不錯的方案, 它可以將數(shù)據(jù)同時保存在內(nèi)存和磁盤中, 同時是文檔型內(nèi)嵌數(shù)據(jù)庫, 使用mongodb的語法進行數(shù)據(jù)操作.
代碼: dbService.js
//建立數(shù)據(jù)庫連接 const db=new Datastore({filename: getUserHome()+'/.electronapp/watcher/12345mails.db', autoload: true}); ...... //使用nedb插入數(shù)據(jù) 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});//處理查詢結(jié)果 });
3. UI的實現(xiàn):
桌面應用的工程目錄如圖:
我將UI頁面放到static文件夾下. 在Electron的進行前端UI開發(fā)和普通的Web開發(fā)方式一樣, 因為Electron的UI進程就是一個Chrome進程. Electron啟動時, 主進程會執(zhí)行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頁面開發(fā)的過程中, 有一點需要注意的是: 默認情況下頁面會出現(xiàn)jQuery, require等組件加載失敗的情況, 這是因為瀏覽器window加載了NodeJs的一些方法, 和jQuery類庫的方法沖突. 所以我們需要做些特別的處理, 在瀏覽器window中把這些NodeJs的方法刪掉:
代碼:preload.js
// 解決require沖突導致jQuery等組件不可用的問題 window.nodeRequire=require; delete window.require; delete window.exports; delete window.module; // 解決chrome調(diào)試工具devtron不可用的問題 window.__devtron={require: nodeRequire, process: process}
4. 通信的實現(xiàn):
在Web應用中, 頁面和服務的通信都是通過ajax進行, 那我們的桌面應用不是也可以采用ajax的方式通信? 這樣理論雖然上可行, 但有一個很大弊端: 我們的應用需要打開一個http的監(jiān)聽端口, 通常個人操作系統(tǒng)都禁止軟件打開http80端口, 而打開其他端口也容易和別的程序造成端口沖突, 所以我們需要一種更優(yōu)雅的方式進行通信.
Electron提供了UI進程和主進程通信的IPC API, 通過使用IPC通信, 我們就能實現(xiàn)UI頁面向NodeJs服務邏輯發(fā)起查詢和抓取請求,也能實現(xiàn)NodeJs服務主動向UI頁面通知抓取進度的更新.
使用Electron的IPC非常簡單.
首先, 我們需要在UI中使用ipcRenderer, 向自定義的channel發(fā)出消息.
代碼: app.js
const ipcRenderer=nodeRequire('electron').ipcRenderer; //提交查詢表單 $('form.searchForm').submit(function (event) { $('#waitModal').modal('show'); event.preventDefault(); ipcRenderer.send('search-keyword', $('input.keyword').val());//發(fā)起查詢請求 }); ipcRenderer.on('search-reply', function(event, data) {//監(jiān)聽查詢結(jié)果 $('#waitModal').modal('hide'); if (data.mails) { var template=Handlebars.compile($('#template').html()); $('div.list-group').html(template(data)); } });
然后, 需要在主進程執(zhí)行的NodeJs代碼中使用ipcMain, 監(jiān)聽之前自定義的渠道, 就能接受UI發(fā)出的請求了.
代碼: crawlService.js
const ipcMain=require('electron').ipcMain; ipcMain.on('search-keyword', (event, arg)=> { ....//處理查詢邏輯 }); ipcMain.on('start-crawl', (event, arg)=> { ....//處理抓取邏輯 });
桌面應用打包
解決完以上四個方面的問題后, 剩下的程序?qū)懫饋砭秃唵瘟? 程序調(diào)試完后, 使用electron-builder, 就可以編譯打包出針對不同平臺的可執(zhí)行文件了.
最近筆者終于把H5-Dooring的后臺管理系統(tǒng)初步搭建完成, 有了初步的數(shù)據(jù)采集和數(shù)據(jù)分析能力, 接下來我們就復盤一下其中涉及的幾個知識點,并一一闡述其在Dooring H5可視化編輯器中的解決方案. 筆者將分成3篇文章來復盤, 主要解決場景如下: 如何使用JavaScript實現(xiàn)前端導入和導出excel文件(H5編輯器實戰(zhàn)復盤) 前端如何基于table中的數(shù)據(jù)一鍵生成多維度數(shù)據(jù)可視化分析報表 * 如何實現(xiàn)會員管理系統(tǒng)下的權(quán)限路由和權(quán)限菜單
以上場景也是前端工程師在開發(fā)后臺管理系統(tǒng)中經(jīng)常遇到的或者即將遇到的問題, 本文是上述介紹中的第一篇文章, 你將收獲: 使用JavaScript實現(xiàn)前端導入excel文件并自動生成可編輯的Table組件 使用JavaScript實現(xiàn)前端基于Table數(shù)據(jù)一鍵導出excel文件 * XLSX和js-export-excel基本使用
本文接下來的內(nèi)容素材都是基于H5可視化編輯器(H5-Dooring)項目的截圖, 如果想實際體驗, 可以訪問H5-Dooring網(wǎng)站實際體驗. 接下來我們直接開始我們的方案實現(xiàn).
在開始實現(xiàn)之前, 我們先來看看實現(xiàn)效果.
導入excel文件并通過antd的table組件渲染table:
編輯table組件:
保存table數(shù)據(jù)后實時渲染可視化圖表:
以上就是我們實現(xiàn)導入excel文件后, 編輯table, 最后動態(tài)生成圖表的完整流程.
導入excel文件的功能我們可以用javascript原生的方式實現(xiàn)解析, 比如可以用fileReader這些原生api,但考慮到開發(fā)效率和后期的維護, 筆者這里采用antd的Upload組件和XLSX來實現(xiàn)上傳文件并解析的功能. 由于我們采用antd的table組件來渲染數(shù)據(jù), 所以我們需要手動將解析出來的數(shù)據(jù)轉(zhuǎn)換成table支持的數(shù)據(jù)格式.大致流程如下:
所以我們需要做的就是將Upload得到的文件數(shù)據(jù)傳給xlsx, 由xlsx生成解析對象, 最后我們利用javascript算法將xlsx的對象處理成ant-table支持的數(shù)據(jù)格式即可. 這里我們用到了FileReader對象, 目的是將文件轉(zhuǎn)化為BinaryString, 然后我們就可以用xlsx的binary模式來讀取excel數(shù)據(jù)了, 代碼如下:
// 解析并提取excel數(shù)據(jù)
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支持的數(shù)據(jù)格式
let sourceData=Object.values(draftObj).map((item,i)=> ({ key: i + '', name: item[0], value: item[1]}))
經(jīng)過以上處理, 我們得到的sourceData即是ant-table可用的數(shù)據(jù)結(jié)構(gòu), 至此我們就實現(xiàn)了表格導入的功能.
table表格的編輯功能實現(xiàn)其實也很簡單, 我們只需要按照antd的table組件提供的自定義行和單元格的實現(xiàn)方式即可. antd官網(wǎng)上也有實現(xiàn)可編輯表格的實現(xiàn)方案, 如下:
大家感興趣的可以研究一下. 當然自己實現(xiàn)可編輯的表格也很簡單, 而且有很多方式, 比如用column的render函數(shù)來動態(tài)切換表格的編輯狀態(tài), 或者使用彈窗編輯等都是可以的.
根據(jù)table數(shù)據(jù)動態(tài)生成圖表這塊需要有一定的約定, 我們需要符合圖表庫的數(shù)據(jù)規(guī)范, 不過我們有了table數(shù)據(jù), 處理數(shù)據(jù)規(guī)范當然是很簡單的事情了, 筆者的可視化庫采用antv的f2實現(xiàn), 所以需要做一層適配來使得f2能消費我們的數(shù)據(jù).
還有一點就是為了能使用多張圖表, 我們需要對f2的圖表進行統(tǒng)一封裝, 使其成為符合我們應用場景的可視化組件庫.
我們先看看f2的使用的數(shù)據(jù)格式:
const data=[
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 }
];
此數(shù)據(jù)格式會渲染成如下的圖表:
所以說我們總結(jié)下來其主要有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: 處理數(shù)據(jù)
const dataX=data.map(item=> ({ ...item, value: Number(item.value) }));
// Step 2: 載入數(shù)據(jù)源
chart.source(dataX);
// Step 3:創(chuàng)建圖形語法,繪制柱狀圖,由 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的也類似, 基本原理都一致.
同樣的, 我們實現(xiàn)將table數(shù)據(jù)一鍵導出為excel也是類似, 不過方案有所不同, 我們先來看看在Dooring中的實現(xiàn)效果.
以上就是用戶基于后臺采集到的數(shù)據(jù), 一鍵導出excel文件的流程, 最后一張圖是生成的excel文件在office軟件中的呈現(xiàn).
一鍵導出功能主要用在H5-Dooring的后臺管理頁面中, 為用戶提供方便的導出數(shù)據(jù)能力. 我們這里導出功能也依然能使用xlsx來實現(xiàn), 但是綜合對比了一下筆者發(fā)現(xiàn)有更簡單的方案, 接下來筆者會詳細介紹, 首先我們還是來看一下流程:
很明顯我們的導出流程比導入流程簡單很多, 我們只需要將table的數(shù)據(jù)格式反編譯成插件支持的數(shù)據(jù)即可. 這里筆者使用了js-export-excel來做文件導出, 使用它非常靈活,我們可以自定義: 自定義導出的excel文件名 自定義excel的過濾字段 * 自定義excel文件中每列的表頭名稱
由于js-export-excel支持的數(shù)據(jù)結(jié)構(gòu)是數(shù)組對象, 所以我們需要花點功夫把table的數(shù)據(jù)轉(zhuǎn)換成數(shù)組對象, 其中需要注意的是ant的table數(shù)據(jù)結(jié)構(gòu)中鍵對應的值可以是數(shù)組, 但是js-export-excel鍵對應的值是字符串, 所以我們要把數(shù)組轉(zhuǎn)換成字符串,如[a,b,c]變成'a,b,c', 所以我們需要對數(shù)據(jù)格式進行轉(zhuǎn)換, 具體實現(xiàn)如下:
const generateExcel=()=> {
let option={}; //option代表的就是excel文件
let dataTable=[]; //excel文件中的數(shù)據(jù)內(nèi)容
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中每列所獲取的數(shù)據(jù)源
}
}
let tableKeys=Object.keys(dataTable[0]);
option.fileName=tableName; //excel文件名稱
option.datas=[
{
sheetData: dataTable, //excel文件中的數(shù)據(jù)源
sheetName: tableName, //excel文件中sheet頁名稱
sheetFilter: tableKeys, //excel文件中需顯示的列數(shù)據(jù)
sheetHeader: tableKeys, //excel文件中每列的表頭名稱
}
]
let toExcel=new ExportJsonExcel(option); //生成excel文件
toExcel.saveExcel(); //下載excel文件
}
注意, 以上筆者實現(xiàn)的方案對任何table組件都使用, 可直接使用以上代碼在大多數(shù)場景下使用. 至此, 我們就實現(xiàn)了使用JavaScript實現(xiàn)前端導入和導出excel文件的功能.
所以, 今天你又博學了嗎?
以上教程筆者已經(jīng)集成到H5-Dooring中,對于一些更復雜的交互功能,通過合理的設計也是可以實現(xiàn)的,大家可以自行探索研究。
地址:H5-Dooring | 一款強大的H5編輯器
如果想學習更多H5游戲, webpack,node,gulp,css3,javascript,nodeJS,canvas數(shù)據(jù)可視化等前端知識和實戰(zhàn),歡迎在《趣談前端》一起學習討論,共同探索前端的邊界。
鄒個人站點:http://www.itzoujie.com/
不懂后端的前端不是一個大前端,不懂后端的前端會大大限制你的發(fā)展空間,所以小鄒在網(wǎng)上找了一篇不錯的文章來分享給大伙,這里說一下,小鄒的個人站點技術棧是(node+express+vue+mysql),跟這篇文章的技術棧略有不同,當然站點里面涉及的組件庫和小程序等,小鄒這里就不一一說了。好了,下面直接開始分享:
Vue + vuex + element-ui + webpack + nodeJs + koa2 + mongodb
說明:
build 文件講解
說明:
1.admin - 后臺管理界面源碼
src - 代碼區(qū)域:
2.client - web端界面源碼
跟后臺管理界面的結(jié)構(gòu)基本一樣。
3.server - 服務端源碼
說明:
開發(fā)中用的一些依賴模塊
components
這個文件夾一般放入常用的組件, 比如 Loading組件等等。
views
所有模塊頁面。
store
vuex 用來統(tǒng)一管理公用屬性, 和統(tǒng)一管理接口。
登陸
登陸是采用 jsonwebtoken方案 來實現(xiàn)整個流程的。
1. jwt.sign(payload,secretOrPrivateKey,[options,callback]) 生成TOKEN
2. jwt.verify(token,secretOrPublicKey,[options,callback]) 驗證TOKEN
3.獲取用戶的賬號密碼。
4.通過 jwt.sign 方法來生成token:
5.每次請求數(shù)據(jù)的時候通過 jwt.verify 檢測token的合法性 jwt.verify(token,secret)。
權(quán)限
通過不同的權(quán)限來動態(tài)修改路由表。
通過 vue的 鉤子函數(shù) beforeEach 來控制并展示哪些路由, 以及判斷是否需要登陸。
通過調(diào)用 getUserInfo方法傳入 token 獲取用戶信息, 后臺直接解析 token 獲取里面的信息返回給前臺。
通過調(diào)用 setRoutes方法 動態(tài)生成路由。
axios 請求封裝,統(tǒng)一對請求進行管理
面包屑 / 標簽路徑
上面介紹了幾個主要以及必備的后臺管理功能,其余的功能模塊 按照需求增加就好
前臺展示的頁面跟后臺管理界面差不多, 也是用vue+webpack搭建,基本的結(jié)構(gòu)都差不多。
權(quán)限
主要是通過 jsonwebtoken 的verify方法檢測 cookie 里面的 token 驗證它的合法性。
日志是采用 log4js 來進行管理的, log4js 算 nodeJs 常用的日志處理模塊,用起來額也比較簡單。
log4js 的日志分為九個等級,各個級別的名字和權(quán)重如下:
1.圖。
2.設置 Logger 實例的類型 logger=log4js.getLogger('cheese')。
3.通過 Appender 來控制文件的 名字、路徑、類型 。
4.配置到 log4js.configure。
5.便可通過 logger 上的打印方法 來輸出日志了 logger.info(JSON.stringify(currTime:當前時間為${Date.now()}s ))。
設計思路
當應用程序啟動時候,讀取指定目錄下的 js 文件,以文件名作為屬性名,掛載在實例 app 上,然后把文件中的接口函數(shù),擴展到文件對象上。
讀取出來的便是以下形式:
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函數(shù)
注意事項:
1. cnpm run server 啟動服務器 //沒裝cnpm的使用npm命令
2.啟動時,記得啟動mongodb數(shù)據(jù)庫,賬號密碼 可以在 server/config.js 文件下進行配置
3. db.createUser({user:"cd",pwd:"123456",roles:[{role:"readWrite",db:'test'}]})(mongodb 注冊用戶)
4. cnpm run dev:admin 啟動后臺管理界面
5.登錄后臺管理界面錄制數(shù)據(jù)
6.登錄后臺管理時需要在數(shù)據(jù)庫 創(chuàng)建 users 集合注冊一個賬號進行登錄
7. cnpm run dev:client 啟動前臺頁面
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。