關(guān)于electron + vue開發(fā)IM應(yīng)用的一些分享

前言

關(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)聽似嗤。on創(chuàng)建監(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)化扰她;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兽掰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子徒役,更是在濱河造成了極大的恐慌孽尽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忧勿,死亡現(xiàn)場離奇詭異杉女,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鸳吸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門熏挎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人层释,你說我怎么就攤上這事婆瓜】旒” “怎么了贡羔?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長个初。 經(jīng)常有香客問我乖寒,道長,這世上最難降的妖魔是什么院溺? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任楣嘁,我火速辦了婚禮,結(jié)果婚禮上珍逸,老公的妹妹穿的比我還像新娘逐虚。我一直安慰自己,他們只是感情好谆膳,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布叭爱。 她就那樣靜靜地躺著,像睡著了一般漱病。 火紅的嫁衣襯著肌膚如雪买雾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天杨帽,我揣著相機(jī)與錄音漓穿,去河邊找鬼。 笑死注盈,一個(gè)胖子當(dāng)著我的面吹牛晃危,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播老客,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼僚饭,長吁一口氣:“原來是場噩夢啊……” “哼纠俭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起浪慌,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤冤荆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后权纤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钓简,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年汹想,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了外邓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡古掏,死狀恐怖损话,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情槽唾,我是刑警寧澤丧枪,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站庞萍,受9級(jí)特大地震影響拧烦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钝计,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一恋博、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧私恬,春花似錦债沮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至永高,卻和暖如春隧土,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背命爬。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國打工曹傀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饲宛。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓皆愉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子幕庐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容