前言
關(guān)于electron
實(shí)現(xiàn)以前端技術(shù)棧像棘,開發(fā)桌面端應(yīng)用的框架,且可以跨平臺(tái)支持,兼容Mac沛硅、Windows眼刃、Linux
electron的一些特點(diǎn)
1.主進(jìn)程和渲染進(jìn)程
electron應(yīng)用核心分為主進(jìn)程和渲染進(jìn)程兩個(gè)部分,其中應(yīng)用本身(app)摇肌、窗口(BrowserWindow)等涉及操作系統(tǒng)底層的均為主進(jìn)程內(nèi)容擂红;而渲染頁面,事件觸發(fā)等前端相關(guān)的围小,均為子進(jìn)程昵骤。
electron與web端的主要區(qū)別即主進(jìn)程的操作,且又可通過渲染進(jìn)程向主進(jìn)程傳遞消息肯适,觸發(fā)主進(jìn)程的事件变秦,從而實(shí)現(xiàn)web代碼對(duì)底層的操控。
主進(jìn)程和渲染進(jìn)程的通信方式:
- 渲染進(jìn)程監(jiān)聽事件框舔,主進(jìn)程發(fā)送對(duì)應(yīng)消息觸發(fā)回調(diào)
this.$electron.ipcRenderer.on('app-quit', (e, data) => {
// 回調(diào)函數(shù)
})
mainWindow.webContents.send('app-quit')
- 主進(jìn)程監(jiān)聽事件蹦玫,渲染進(jìn)程發(fā)送對(duì)應(yīng)消息觸發(fā)回調(diào)
ipcMain.on('closeAutoStart', () => {
// 回調(diào)函數(shù)
})
ipcRenderer.send('closeAutoStart')
2.窗口
electron應(yīng)用初始化的時(shí)候都需要?jiǎng)?chuàng)建一個(gè)主窗口
mainWindow = new BrowserWindow({
height: 800,
width: 1280,
minHeight: 800,
minWidth: 1280,
useContentSize: true,
frame: false,
fullscreenable: false,
icon: path.resolve(__static, 'tray1.ico'),
webPreferences: {
webSecurity: false,
nodeIntegration: true,
enableRemoteModule: true,
},
show: false
})
mainWindow.loadURL(winURL);
其中winURL即為項(xiàng)目啟動(dòng)地址
如何創(chuàng)建一個(gè)子窗口,以圖片預(yù)覽為例
let _previewWindow = new BrowserWindow({
minWidth: windowWidth,
minHeight: windowHeight,
width: windowWidth,
height: windowHeight,
x: screenWidth / 2 - windowWidth / 2, //位移居中
y: screenHeight / 2 - windowHeight / 2, //位移居中
useContentSize: true,
movable: true,
icon: path.resolve(__static, 'tray1.ico'),
frame: false, //是否顯示默認(rèn)工具欄
webPreferences: {
nodeIntegration: true,
sandbox: true,
devTools: false,
enableRemoteModule: true,
preload: path.resolve(__static, 'preload.js')
},
skipTaskbar: false, //任務(wù)欄圖標(biāo)
show: true,
// window_id
})
_previewWindow.loadFile(path.resolve(__static, 'preview/index.html'))
此處采用了新起一個(gè)項(xiàng)目刘绣,并單獨(dú)打包樱溉,直接加載打包后的首頁。此方法的好處是不用重復(fù)加載一次原項(xiàng)目的冗余資源纬凤,極大提升窗口加載速度福贞,并減少內(nèi)存消耗。
另外使用了preload參數(shù)停士,preload.js為所有窗口共用挖帘,所有可以在其中定義事件,并且在新的子項(xiàng)目中調(diào)用恋技,同時(shí)觸發(fā)小智內(nèi)部的事件
此方案之后應(yīng)該為需要新起窗口時(shí)的統(tǒng)一處理方案肠套。
preload.js
window.previewImageLoaded = function () {
ipcRenderer.send("picture-preview-loaded");
}
ipcRenderer.on("changeImgData", (event, data) => {
window.previewChangeImgData ? window.previewChangeImgData(data) : ''
});
通過修改全局變量的方式實(shí)現(xiàn)父向子數(shù)據(jù)傳遞,通過調(diào)用事件發(fā)送消息的方式實(shí)現(xiàn)子向父的事件傳遞猖任。
小智的核心技術(shù)方案
1你稚、websocket連接
- 心跳與續(xù)期
發(fā)送心跳時(shí)會(huì)判斷與本地token有效期是否超過24小時(shí),如果超過朱躺,向服務(wù)器發(fā)送參數(shù)刁赖,同時(shí)重置本地token有效期。這樣可以保證token在線時(shí)每天續(xù)期长搀,不會(huì)過期宇弛。
function heartbeat() {
console.log('socket', 'ping')
hearbeat_timer = setInterval(() => {
// 發(fā)心跳的時(shí)候超過一天更新用戶token有效期
let tempTime = Number(localStorage.getItem('XZUserTokenDate'))
if (tempTime && new Date().getTime() - tempTime > 24 * 60 * 60 * 1000) {
var req = new proto.pb.C2SHeartbeat()
req.Token = String(eStore.get('XZUserToken'))
sendSocketMsg(
proto.pb.MSG.Heartbeat,
proto.pb.C2SHeartbeat.encode(req).finish(),
null
)
localStorage.setItem('XZUserTokenDate', String(new Date().getTime()))
} else {
sendSocketMsg(proto.pb.MSG.Heartbeat, 0, null)
}
}, 5000)
}
- 重連機(jī)制
連接異常時(shí),直接彈出服務(wù)器異常彈窗源请,然后每5秒自動(dòng)重連枪芒,重連20次后不再自動(dòng)重連彻况,轉(zhuǎn)為需手動(dòng)重連。
可以保證后臺(tái)下的無感知重連舅踪。
reConnect() {
console.log("重新連接" + this.connectTime);
Log.logInfo("重新連接" + this.connectTime + "_" + new Date().getTime());
if (this.autoReconnect) {
if (this.connectTime < this.connectTimes) {
this.connectTime++;
this.inConnect = 5;
this.websocketTimeout = window.setInterval(() => {
this.inConnect--;
if (this.inConnect === 0) {
clearInterval(this.websocketTimeout);
setReconnectStatus(true);
initSocket(() => {});
}
}, 1000);
} else {
this.autoReconnect = false;
clearInterval(this.websocketTimeout);
}
}
},
2纽甘、請(qǐng)求接口
因?yàn)閣ebsocket為異步消息,一開始是通過發(fā)送消息時(shí)記錄數(shù)據(jù)抽碌,收到消息時(shí)調(diào)用store修改值悍赢,發(fā)送端監(jiān)聽store里面的變量來進(jìn)行回調(diào)處理。此方案會(huì)極大增加邏輯復(fù)雜度货徙,且不好維護(hù)左权。所以后面封裝了異步轉(zhuǎn)同步的方法。核心代碼如下
export const ReqMap = new Map<number, { resolve: Function, reject: Function }>();
export function createRequest<F extends (askId: number, ...args: any[]) => any, CB extends (buffer: Uint8Array | Reader, askId?: number) => any>
(req: F, cb: CB): (...args: FormData<F>) => Promise<ReturnType<CB>> {
const askId = getAskId();
return (...args) => new Promise<Uint8Array | Reader>((resolve, reject) => {
req(askId, ...args);
ReqMap.set(askId, { resolve, reject });
}).then((buffer) => {
return cb(buffer, askId);
});
}
export function responseHandler(msg: Uint8Array | Reader, askId: number) {
const req = ReqMap.get(askId);
if (req) {
req.resolve(msg);
ReqMap.delete(askId);
}
}
主要邏輯是構(gòu)建一個(gè)Map對(duì)象痴颊,發(fā)送消息時(shí)赏迟,將Promise的回調(diào)及對(duì)應(yīng)askId存于Map內(nèi)。收到消息時(shí)調(diào)用對(duì)應(yīng)askId的promise.resolve方法蠢棱,從而執(zhí)行回調(diào)瀑梗。其中askId默認(rèn)生成
例子:
export function getSessionMembersCount(askId: number, sessionId: number) {
try {
var res = new client.pb.C2SAskSessionMemberCount()
res.SessionId = sessionId
sendSocketMsg(
client.pb.MSG.AskSessionMemberCount,
client.pb.C2SAskSessionMemberCount.encode(res).finish(),
askId
)
} catch (e) {
console.error('操作失敗' + e)
}
}
export function sessionMembersCountRes(
buffer: Uint8Array | Reader,
askId: number
) {
var res = client.pb.S2CAskSessionMemberCount.decode(buffer)
if (res.Success.Code == client.pb.ErrorCode.Ok) {
return res.Count
} else {
Message.error(returnErrorMsg(res.Success.Code))
return null
}
}
createRequest(
getSessionMembersCount,
sessionMembersCountRes
)(this.gid).then((res) => {
if (res) {
this.channelMemberCount = res
this.topWidthChange()
}
})
首先傳入發(fā)送消息和消息回調(diào)的處理方法,第二個(gè)可以傳入消息回調(diào)需要的參數(shù)裳扯。會(huì)生成一個(gè)Promise對(duì)象抛丽,并且將其resolve方法存入,在消息回調(diào)時(shí)調(diào)用此resolve方法饰豺。從而實(shí)現(xiàn)一個(gè)閉環(huán)亿鲜,即發(fā)送消息 => 收到消息 => 觸發(fā)resolve,完成Promise冤吨,并且通過askId一一對(duì)應(yīng)蒿柳。從而省去用store的值才能監(jiān)聽發(fā)送消息和收到消息之間的對(duì)應(yīng)關(guān)系。
3漩蟆、關(guān)于數(shù)據(jù)庫
目前使用的是場景主要是存儲(chǔ)消息
初始化數(shù)據(jù)庫(使用的typeorm建立better-sqlite3數(shù)據(jù)庫連接垒探,其中better-sqlite3需要vscode2015/2017環(huán)境)
TODO:嘗試用原生語句是否能加快速度
// 查詢
var res = await getRepository(Msg, dbName)
.createQueryBuilder('msg')
.where('sessionId=:sessionId', { sessionId: sessionId })
.andWhere('msg.seq > :min', { min: minId - 10 })
.andWhere('msg.seq < :max', { max: minId + 11 })
.orderBy('seq', 'DESC')
.getMany()
// 添加
await getConnection(dbName)
.createQueryBuilder()
.insert()
.into(Msg)
.values([_msg])
.execute()
4、小智的存儲(chǔ)數(shù)據(jù)方式
首先包括消息的存儲(chǔ)方式:數(shù)據(jù)庫
其次關(guān)于頻道session等怠李,均存于內(nèi)存
用戶token\已下載文件列表(需要持久化的)圾叼,存于electron-store
服務(wù)器列表本地目錄,存于用戶config.json下捺癞,
其他不需要持久化的用戶信息夷蚊、服務(wù)器地址ID,存于localstorage下面
TODO:存儲(chǔ)方式略亂髓介,應(yīng)細(xì)分為兩種惕鼓,
- 需要持久化存儲(chǔ)的,如消息唐础、已下載文件列表箱歧、用戶token及是否自動(dòng)登錄(為了兼容意外關(guān)閉)矾飞,根據(jù)查詢要求和數(shù)據(jù)量,可采用數(shù)據(jù)庫和eStore兩種方式
- 單次登錄內(nèi)使用呀邢,不需要持久化洒沦,如session、頻道驼鹅、團(tuán)隊(duì)等微谓,可存于內(nèi)存森篷、$store
5输钩、小智的數(shù)據(jù)通信方式
- 主進(jìn)程和渲染進(jìn)程通信
- 通過store監(jiān)聽實(shí)現(xiàn)全局通信(目前主要使用的方式)
即收到推送消息后,進(jìn)行數(shù)據(jù)處理仲智,并將操作內(nèi)存里的值买乃,或者將值直接賦予store.state。頁面上钓辆,監(jiān)聽store.getters剪验,監(jiān)聽到變化后即可做對(duì)應(yīng)操作 - 簡單的父子組件通信 :event,:data前联,$emit
- 全局事件總線eventBus功戚,可進(jìn)行全局的事件監(jiān)聽,目前主要用于快捷鍵監(jiān)聽似嗤。
emit調(diào)用監(jiān)聽事件。小tips:使用eventBus一定要注意重復(fù)使用的頁面里烁落,destroy頁面時(shí)一定得$off移除事件乘粒,不然會(huì)出現(xiàn)事件未能解綁導(dǎo)致的內(nèi)存泄漏。
6伤塌、關(guān)于內(nèi)存泄漏
小智目前已出現(xiàn)多次內(nèi)存泄漏灯萍,而且目前依然有一些沒有發(fā)現(xiàn)。
常見造成內(nèi)存泄漏的情況:
- 未解綁的事件(絕大多數(shù)情況)每聪,包括切換頁面時(shí)旦棉,未銷毀的eventBus、document.on等事件監(jiān)聽
- 未銷毀的定時(shí)器药薯,一些setInterval他爸,快速切換時(shí),并沒有執(zhí)行完成并銷毀果善,如果不手動(dòng)銷毀也會(huì)導(dǎo)致內(nèi)存泄漏
- 重復(fù)的new 對(duì)象诊笤,目前主要出現(xiàn)在一起統(tǒng)一處理方法上,如處理msg\session巾陕,會(huì)導(dǎo)致數(shù)據(jù)層面的內(nèi)存泄漏讨跟,因影響比較小所以暫未處理
- keep-alive主動(dòng)緩存纪他,目前少數(shù)頁面有使用,緩存后無法徹底銷毀(已嘗試各種方法均無效)晾匠,但是可以實(shí)現(xiàn)0延遲加載頁面茶袒,慎用。
如何檢測:
主要利用chrome memory快照凉馆,查詢detached 相關(guān)的dom薪寓,即未被銷毀的dom元素,按層級(jí)慢慢找澜共,然后慢慢定位具體操作向叉,然后找關(guān)聯(lián)的事件綁定是否有未解綁的。有一些第三方組件嗦董,比如quill也會(huì)有一些自帶綁定事件導(dǎo)致內(nèi)存泄漏母谎,目前已處理了其回車之前的內(nèi)存泄漏,之后還可以考慮采用單例的方式處理京革。
7奇唤、小智能打開外部鏈接
目前是使用iframe內(nèi)嵌的方式,根據(jù)應(yīng)用名稱來創(chuàng)建iframe匹摇,并放于最頂層咬扇,通過絕對(duì)定位的方式處理位置。同時(shí)廊勃,記錄所有創(chuàng)建的iframe懈贺,通過修改其ClassName來控制顯示隱藏,為避免網(wǎng)頁緩存供搀,URL每次重新打開新增時(shí)間戳隅居。
同時(shí)為了實(shí)現(xiàn)切換時(shí)保留緩存,iframe不會(huì)自動(dòng)銷毀葛虐,只是隱藏胎源,除非手動(dòng)關(guān)閉。
TODO:electron內(nèi)置組件BrowserView嘗試
let tempindex = this.tabDatas.findIndex((tab) => {
return tab.name === app.Name
})
if (tempindex == -1) {
this.tabDatas.push({
name: app.Name,
url: jumpUrl,
})
let iframe = document.createElement('iframe')
iframe.className = 'custom_iframe'
iframe.src = jumpUrl + `&tempTIme=${new Date().getTime()}`
iframe.setAttribute('frameborder', 0)
document.body.appendChild(iframe)
this.iframeArray.push({
name: app.Name,
iframe: iframe,
})
}
8屿脐、關(guān)于小智桌面端未來的優(yōu)化方向
- 存儲(chǔ)相關(guān)
team從數(shù)據(jù)庫存儲(chǔ)改為內(nèi)存存儲(chǔ)涕蚤,測試原生語句查庫的使用,存庫和查庫方式及效率優(yōu)化的诵。 - 內(nèi)存相關(guān)
處理數(shù)據(jù)內(nèi)存泄漏万栅;不在屏幕內(nèi)的消息設(shè)法減少其dom顯示,僅保留占位西疤;可復(fù)用的組件比如輸入框烦粒,采用單例 - 性能相關(guān)
主進(jìn)程資源按需分步加載;優(yōu)化處理內(nèi)存數(shù)據(jù)方式;查庫寫庫優(yōu)化扰她;