「Electron」 一個可圈可點的 PC 多端融合方案

天天都要寫次日的 todoList。有一天在寫的時候忽然想到,爲了讓本身清楚知道本身須要作啥、作了多少、還剩多少沒作,想寫一個電腦端程序,在技術選型的時候就選了 Electron。javascript

本篇文章的目的不是講解 API 如何使用,想知道這些能夠直接看官方文檔。本文目的旨在講明如何技術如何選擇、如何快速上手、如何調試、Electron 底層原理、工程體系方面的總結。css

1、淺談 GUI 系統

瀏覽器是如何將佈局數據計算爲像素數據的,你能實現出原理相似的渲染器嗎?
瀏覽器在各個平臺上的文字排版渲染結果是否一致,你能解釋緣由嗎?
你所負責的前端應用,其渲染性能還有多大的提高空間,你能量化地證實嗎?
你能設計實現出相似 RN 和小程序那樣的 Hybrid 方案嗎?
你能本身控制 GPU 渲染管線,實現渲染的硬件加速嗎?

GUI 起源:從 1979 年喬布斯造訪施樂 PARC 算起html

GUI 架構:過程化繪製(drawLine、drawRect)-> 面向對象抽象時代 -> 界面與樣式分離時代 -> MVC、MVVM 時代 -> 聲明式、組件式時代(Vue、React、RN、Weex、Flutter)前端

咱們能夠看到不變的是:隨着計算機科學技術的發展,爲了實現某個效果,一流程序員或者組織不斷研發各類技術框架,來提升開發效率和效果。Electron 就是這條歷史長河中誕生的 PC 端技術框架之一。vue

2、 技術選型

3天時間寫了個 PC 端應用程序。先看看結果吧java

Todo1

Todo1

Todo1

爲何要選 Electron 做爲 pc 端開發方案?node

史前時代,以 MFC 爲表明的技術棧,開發效率較低,維護成本高。 後來使用 QT 技術,特色是使用 DirectUI + 面向對象 + XML 定義 UI,適用於小型軟件、性能要求、包大小、UI 複雜度叫高的需求。 再到後來,以 QT Quick 爲表明的技術,特色是框架自己提供子控件,基於子控件組合來建立新的控件。相似於 ActionScript 的腳本化界面邏輯代碼。 新時代主要是以 ElectronCef 爲 表明。特色是界面開發以 Web 技術爲主,部分邏輯須要 Native 代碼實現。你們都熟悉的 VS Code 就是使用 Electron 開發的。適用於 UI 變化較多、體積限制不大、開發效率高的場景。git

拿 C 系列寫應用程序的體驗很差,累到奔潰。再加上有 Hybrid、React Native、iOS、Vue、React 等開發經驗,Electron 是不二選擇。程序員

3、 Quick start

執行下面命令快速體驗 Hello world,也是官方給的一個 Demo。github

git clone https://github.com/Electron/Electron-quick-start
cd Electron-quick-start
npm install && npm start

簡單介紹下 Demo 工程,工程目錄以下所示 工程目錄

在終端執行 npm start 執行的是 package.json 中的 scripts 節點下的 start 命令,也就是 Electron .. 表明執行 main.js 中的邏輯。

// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('Electron')
const path = require('path')

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // and load the index.html of the app.
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  mainWindow.webContents.openDevTools()
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') app.quit()
})

app.on('activate', function () {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

寫過 Vue、React、Native 的人看代碼很容易。好比應用程序的生命週期鉤子函數對開發者很重要,也是一個標準的作法,根據需求在鉤子函數裏面作相應的視圖建立、初始化、銷燬對象等等。好比 Electron 中的 activatewindow-all-closed 等。

app 對象在 whenReady 的時候執行 createWindow 方法。內部建立了一個 BrowserWindow 對象,指定了大小和功能設置。

  1. webPreferences:Object (可選) - 網頁功能的設置。

    1. preload: String (可選) - 在頁面運行其餘腳本以前預先加載指定的腳本。不管頁面是否集成 Node, 此腳本均可以訪問全部 Node API 腳本路徑爲文件的絕對路徑。 當 node integration 關閉時, 預加載的腳本將從全局範圍從新引入 node 的全局引用標誌。

mainWindow.loadFile('index.html') 加載了同級目錄下的 index.html 文件。也能夠加載服務器資源(部署好的網頁),好比 win.loadURL('https://github.com/FantasticLBP')

接下去看看 preload.js

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector)
    if (element) element.innerText = text
  }
  console.table(process)
  console.info(process.versions)
  for (const type of ['chrome', 'node', 'Electron']) {
    replaceText(`${type}-version`, process.versions[type])
  }
})

在頁面運行其餘腳本以前預先加載指定的腳本,不管頁面是否集成 Node, 此腳本均可以訪問全部 Node API 腳本路徑爲文件的絕對路徑。Demo 中的邏輯很簡單,就是讀取 process.versions 對象中的 node、chrome、Electron 的版本信息並展現出來。

index.html 中的內容就是主頁面顯示的內容。通常不會直接寫 html、css、js,都會根據技術背景選擇前端框架,好比 Vue、React 等,或者模版引擎 ejs 等。

4、 實現原理

Electron 分爲渲染進程和主進程。和 Native 中的概念不同的是 Electron 中主進程只有一個,渲染進程(也就是 UI 進程) 有多個。主進程在後臺運行,每次打開一個界面,會新開一個新的渲染進程。

  • 渲染進程: 用戶看到的 web 界面就是由渲染進程繪製出來的,包括 html、css、js。
  • 主進程:Electron 運行 package.json 中的 main.js 腳本的進程被稱爲主進程。在主進程中運行的腳本經過建立 web 頁面來展現用戶界面。一個 Electron 應用程序老是隻有一個主進程。

1. Chromium 架構

瀏覽器分爲單進程和多進程架構。下面先講講 Chrome 爲表明的瀏覽器過去和將來。

1.1 單進程瀏覽器

單進程瀏覽器指的是瀏覽器的全部功能模塊都是運行在同一個進程裏的,這些模塊包括網絡、插件、Javascript 運行環境、渲染引擎和頁面等。如此複雜的功能都在一個進程內運行,因此致使瀏覽器出現不穩定、不安全、不流暢等問題。

單進程瀏覽器架構示意圖

早在2007年以前,市面上的瀏覽器都是單進程架構。

  • 問題1: 不穩定

    早期瀏覽器須要藉助插件來實現相似 Web 視頻、Web 遊戲等各類「強大」的功能。但插件每每是最容易出現問題的模塊。由於運行在瀏覽器進程中,因此一個插件的意外奔潰到致使整個瀏覽器到的奔潰。

    除了插件以外,渲染引擎模塊也是不穩定的。一般一些複雜的 Javascript 代碼就有可能致使渲染引擎模塊的奔潰。和插件同樣,渲染引擎的奔潰會致使整個瀏覽器奔潰。

  • 問題2: 不流暢

    從單進程瀏覽器架構圖看出,全部頁面的渲染模塊、Javascript 執行環境、插件都是在一個線程中執行的。這意味着同一時刻只有一個模塊能夠執行。

    function execUtilCrash() {
      while (true) {
        console.log("Stay hungry, stay foolish.");
      }
    }
    execUtilCrash();

    在單進程瀏覽器架構下,該代碼在執行的時候會獨佔線程,致使其餘運行在該線程中的模塊沒機會執行,瀏覽器中的全部頁面都運行在該線程中,因此頁面都沒機會去執行任務,表現爲整個瀏覽器失去響應,也就是卡頓。

    腳本、插件 會讓單進程瀏覽器變卡頓外,頁面的內存泄露也會致使瀏覽器卡頓。一般瀏覽器內核是很是複雜的,運行一個複雜的頁面再關閉頁面,會存在內存不能徹底回收的狀況,這樣致使的問題是隨着使用時間的變長,內存泄漏問題越嚴重,內存佔用越高,可用內存愈來愈少,瀏覽器會變得愈來愈慢。

  • 問題3: 不安全

    通常瀏覽器插件都是用 C/C++ 編寫的,經過插件就能夠獲取到較多的操做系統資源,當你在頁面上運行一個插件的時候,也就意味着這個插件能「徹底」控制你的電腦。若是是惡意插件,那它就能夠作一些竊取帳號密碼等,引起安全問題

1.2 早期多進程架構瀏覽器

早期 Chrome 進程架構圖

上圖2008年 Chrome 發佈時的進程架構圖。能夠看出 Chrome 的頁面是運行在單獨的渲染進程中,同時頁面的插件也是運行在單獨的插件進行中的,進程之間經過 IPC 進行通訊。

解決了不穩定問題。因爲進程之間是彼此隔離的,因此當一個頁面或者插件奔潰時,受影響的僅僅是當前的頁面或者插件進程,並不會影響到瀏覽器和其餘的頁面。也就是說解決了早期瀏覽器某個頁面或者插件奔潰致使整個瀏覽器的奔潰,從而解決了不穩定問題。

解決了不流暢問題。 一樣,Javascript 進行也是運行在渲染進程中的,因此即便當前 Javascript 阻塞了渲染進程,影響到的也只是當前的渲染頁面,並不會影響到瀏覽器和其餘頁面或者插件進程(其餘的頁面的腳本是運行在本身的渲染進程中的)。

對於內存泄漏的解決辦法更加簡單。當關閉某個頁面的時候,整個渲染進程就會被關閉,因此該進程所佔用的內存都會被系統回收,因而輕鬆解決了瀏覽器頁面的內存泄漏問題。

解決了安全問題。採用多進程架構可使用安全沙箱技術。沙箱能夠當作是操做系統給瀏覽器一個小黑盒,黑盒內部能夠執行程序,可是不能訪問操做系統資源、不能訪問硬盤數據,也不能在敏感位置讀取任何數據,例如你的文檔和桌面。Chrome 把插件進程和渲染進程使用沙箱隔離起來,這樣即便在渲染進程或者瀏覽器進程中執行了惡意代碼,惡意代碼也沒法突破沙箱限制去獲取系統權限。

沙箱隔離起來的進程必須使用 IPC 通道才能夠與瀏覽器內核進程通訊,通訊進程就會進行安全的檢查。

沙箱設計的目的是爲了讓不可信的代碼運行在必定的環境中,從而限制這些代碼訪問隔離區以外的資源。若是由於某種緣由,確實須要訪問隔離區外的資源,那麼就必須經過的指定的通道,這些通道會進行嚴格的安全檢查,來判斷請求的合法性。通道會採起默認拒絕的策略,通常採用封裝 API 的方式來實現。

1.3 目前多進程架構瀏覽器

Chrome 團隊不斷髮展,目前架構有了較新變化,最新 Chrome 架構圖以下所示

最新 Chrome 進程架構圖

最新 Chrome 瀏覽器包括:1個網絡進程、1個瀏覽器進程、1個 GPU 進程、多個渲染進程、多個插件進程。

  • 瀏覽器進程:主要負責界面顯示、用戶交互、子進程管理,同時提供存儲功能。
  • 渲染進程:核心任務是將 HTML、CSS、Javascript 轉換爲用戶能夠與之的網頁,排版引擎 Blink 和 Javascript 引擎 V8 都是運行在該進程中。默認狀況下,Chrome 爲每一個 tab 標籤頁建立一個新的渲染進程。出於安全考慮,渲染進程都是運行在沙箱機制下的。
  • GPU 進程:最先 Chrome 剛發佈的時候是沒有 GPU 進程的,而 GPU 的使用初衷是實現 css 3D 效果。隨後網頁、Chrome 的 UI 界面都選擇採用 GPU 來繪製,這使得 GPU 成爲瀏覽器廣泛需求。最後 Chrome 多進程架構中也引入了 GPU 進程。
  • 網絡進程:主要負責頁面的網絡資源請求加載。早期是做爲一個模塊運行在瀏覽器進程裏面的,最近才獨立出來做爲一個單獨的進程。
  • 插件進程:主要負責插件的運行。因插件代碼由普通開發者書寫,因此在 QA 方面可能不是那麼完善,代碼質量良莠不齊,插件容易奔潰,因此須要經過插件進程來隔離,以保證插件進程的奔潰不會對瀏覽器和頁面形成影響。

因此,你會發現打開一個頁面,查看進程發現有4個進程。凡事具備兩面性,上面說了多進程架構帶來瀏覽器穩定性、安全性、流暢性,可是也帶來一些問題:

  • 更高資源佔用:每一個進程都會包含公共基礎結構的副本(如 Javascript 運行環境),這意味着瀏覽器將會消耗更多的資源
  • 更復雜的體系結構:瀏覽器各模塊之間耦合度高、拓展性差,會致使如今的架構很難適應新需求。

Chrome 團隊一直在尋求新的彈性方案,既能夠解決資源佔用較高問題嗎,也能夠解決複雜的體系架構問題。

1.4 將來面向服務的架構

2016年 Chrome 官方團隊使用「面向服務的架構」(Services Oriented Architecture,簡稱 SOA)的思想設計了最新的 Chrome 架構。Chrome 總體架構會向現代操做系統所採用的「面向服務的架構」方向發展。

以前的各類模塊會被重構成爲單獨的服務(Services),每一個服務均可以運行在獨立的進程中,訪問服務必須使用定義好的接口,經過 IPC 進行通訊。從而構建一個更內聚、低耦合、易於維護和拓展的系統。

Chrome 最終把 UI、數據庫、文件、設備、網絡等模塊重構爲基礎服務。下圖是 「Chrome 面向服務的架構」的進程模型圖

Chrome 」面向服務架構「的進程模型圖

目前 Chrome 正處於老架構向新架構的過分階段,且比較漫長。

Chrome 提供靈活的彈性架構,在強大性能設備上會以多進程的方式運行基礎服務,可是在設備資源受限的狀況下,Chrome 會將不少服務整合到一個進程中,從而節省內存佔用。

Chrome彈性架構

1.5 小實驗

測試環境: MacBook Pro(macOS 10.15.3)、Chrome Version 81.0.4044.138 (Official Build) (64-bit)

操做步驟:

  1. 打開 Chrome 瀏覽器
  2. 在地址欄輸入 https://github.com/FantasticLBP
  3. 點擊 Chrome 瀏覽器右上角 ...,在下拉菜單中選擇 More Tools,在對應的展開菜單中點擊 Task Manager

實驗現象:

Chrome 進程儀表

實驗結論:

僅僅打開了 1 個頁面,爲何有 4 個進程?由於打開 1 個頁面至少須要 1 個網絡進程、1 個瀏覽器進程、1 個 GPU 進程以及 1 個渲染進程,共 4 個;若是打開的頁面有運行插件的話,還須要再加上 1 個插件進程。

從而印證了上述觀點。

1.6 特殊狀況

現象:咱們在使用 Chrome 的時候仍是會出現因爲單個頁面卡死最終崩潰致使全部頁面崩潰的狀況,why?

一般狀況下是一個頁面使用一個進程,可是,有一種狀況,叫"同一站點(same-site)",具體地講,咱們將「同一站點」定義爲根域名(例如,github.com)加上協議(例如,https:// 或者http://),還包含了該根域名下的全部子域名和不一樣的端口,好比下面這三個:

https://developer.github.com https://www.github.com https://www.github.com:8080 都是屬於同一站點,由於它們的協議都是 https,而根域名也都是 github.com。區別於瀏覽器同源策略。

Chrome 的默認策略是,每一個標籤對應一個渲染進程。可是若是從一個頁面打開了新頁面,而新頁面和當前頁面屬於同一站點時,那麼新頁面會複用父頁面的渲染進程。官方把這個默認策略叫 process-per-site-instance

直白的講,就是若是幾個頁面符合同一站點,那麼他們將被分配到一個渲染進程裏面去。

這種狀況下,一個頁面崩潰了,會致使同一站點的頁面同時崩潰,由於他們使用了同一個渲染進程。

爲何要讓他們跑在一個進程裏面呢?

由於在一個渲染進程裏面,他們就會共享JS的執行環境,也就是說A頁面能夠直接在B頁面中執行腳本。由於是同一家的站點,因此是有這個需求的

1.7 Chromium 架構

Chromium 架構

這張圖是 chromium 多進程架構圖。

多進程架構的瀏覽器解決了上述問題,至於如何解決的之後的文章會專門講解,不是本文的主要內容。

簡單描述下。

  • 主進程中的 RenderProcessHost 和 render 進程中的 RenderProcess 是用來處理進程間通訊的(IPC)。
  • Render 進程中的 RenderView 內容基於 WebKit 排版展現出來的
  • Render 進程中的 ResourceDispatcher 是用來處理資源請求的。Render 進程中若是有請求則建立一個請求 ID,轉發到 IPC,由 Browser 進程中處理後返回
  • Chromium 是多進程架構,包括一個主進程,多個渲染進程

對於 chromium 多進程架構感興趣的能夠點擊這個連接查看更多資料-Multi-process Architecture

2. Electron 架構

Electron 架構和 Chromium 架構相似,也是具備1個主進程和多個渲染進程。可是也有區別

  • 在各個進行中暴露了 Native API ,提供了 Native 能力。
  • 引入了 Node.js,因此可使用 Node 的能力

技術難點:因爲 Electron 內部整合了 Chromium 和 Node.js,主線程在某個時刻只能夠執行一個事件循環,可是2者的事件循環機制不同,Node.js 的事件循環基於 libuv,可是 Chromium 基於 message bump

因此 Electron 原理的重點就是「如何整合事件循環」。2種思路

  • Chromium 集成到 Node.js 中:用 libuv 實現 messagebump(Node-Webkit 就是這麼幹的,缺點挺多)
  • Node.js 集成到 Chromium 中(Electron 所採用的方式)

後來隨着 libuv 引入 backend_fd 的概念,至關因而 libuv 輪詢事件的文件描述符。經過輪訓 backend_fd 能夠知道 libuv 的新事件。因此 Electron 採起的作法就是將 Node.js 集成到 Chromium 中。

Node.js與Chromium通訊

上圖描述了 Node.js 如何融入到 Chromium 中。描述下原理

  • Electron 新起一個安全線程去輪訓 backend_fd
  • 當檢測到一個新的 backend_fd,也就是一個新的 Node.js 事件以後,經過 PostTask 轉發到 Chromium 的事件循環中

上述2個步驟完成了 Electron 的事件循環。

5、 如何調試

調試分爲主進程調試和渲染進程調試。

1. 渲染進程調試

看到 Demo 工程中執行 npm start 以後能夠看到主界面,Mac 端快捷鍵 comand + option + i,喚出調試界面,相似於 chrome 下的 devtools。其實就是無頭瀏覽器的那些東西。或者在代碼裏打開調試模式 mainWindow.webContents.openDevTools()

工程採用 Electron + Vue 技術,下面截圖 Vue-devtools 很方便查看 Vue 組件層級等 Vue 相關的調試

渲染進程調試

2. 主進程調試方式

主進程調試有2種方法

方法一:利用 chrome inspect 功能進行調試

  • 須要在啓動的時候給 package.json 中的 scripts 節點下的 start 命令加上調試開關
--inspect=[port]
// electrom --inspect=8001 yourApp
  • 而後打開瀏覽器,在地址欄輸入 chrome://inspect
  • 點擊 configure,在彈出的面板中填寫須要調試的端口信息
  • chrome inspect
  • 從新開啓服務 npm start,在 chrome inspect 面板的 Target 節點中選擇須要調試的頁面
  • 在面板中能夠看到主進程執行的 main.js。能夠加斷點進行調試 chrome inspect

方法二:利用 VS Code 調試 Electron 主進程。

  • 在 VS Code 的左側菜單欄,第四個功能模塊就是調試,點擊調試,彈出對話框讓你添加調試配置文件 launch.json

  • 編輯 launch.json 的文件內容。以下

    {
        "version": "0.2.0",
        "configurations": [
            {
                "type": "node",
                "request": "launch",
                "name": "Debug main process",
                "cwd": "${workspaceRoot}",
                "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/Electron",
                "windows": {
                    "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/Electron.cmd"
                },
                "args": ["."],
                "outputCapture": "std"
            }
        ]
    }
  • 在調試模點擊綠色小三角,會運行程序,能夠添加斷點信息。總體界面以下所示。能夠單步調試、能夠暫停、鼠標移上去能夠看到對象的各類信息。

    VS Code 調試功能

3. 主進程調試之 hot reload

Electron 的渲染進程中的代碼改變了,使用 Command + R 能夠刷新,可是修改主進程中的代碼則必須從新啓動 yarn run dev 。效率低下,因此爲了提高開發效率,有必要解決這個問題

Webpack 有一個 api: watch-run,能夠針對代碼文件檢測,有變化則 Restart

main Process reload

6、 軟件更新方式

更新方式 手動更新 文件覆蓋 自動更新 操做系統級別的應用商店
優勢 簡單、穩定 下載過程快 穩定、快、打擾少 統1、穩定
缺點 過程繁瑣、慢、影響使用、更新率低 慢、實現比較複雜、穩定性差、寫文件失敗 實現複雜 受應用商店侷限
適用場景 低頻更新、用戶粘性高、做爲 各類升級技術的降級方案 打補丁 高頻更新軟件、體驗要求高 操做系統應用商店上架的軟件

Electron 官方給出瞭解決方案 Squirrel,基於 Squirrel 框架完成的自動更新,這其實就是圖上「自定更新」的類別。

6、 開發 tips及其優化手段

1. 開發 tips

  1. 或許會爲網頁添加事件代碼,可是頁面看到的內容是渲染進程,因此事件相關的邏輯代碼應該寫在 html 引入的 render.js 中。

  2. render.js 中寫 Node 代碼的時候須要在 main.js 初始化 BrowserWindow 的時候,在 webPreferences 節點下添加 nodeIntegration: true 。否則會報錯:renderer.js:9 Uncaught ReferenceError: process is not defined。

  3. 從 Chrome Extenstion V2 開始,不容許執行任何 inline javascript 代碼在 html 中。不支持之內聯方式寫事件綁定代碼。好比 <button onclick="handleCPU">查看</button>

    Refused to execute inline event handler because it violates the following Content Security Policy directive:
  4. 利用 Electron 進行開發的時候,能夠當作是 NodeJS + chromium + Web 前端開發技術。NodeJS 擁有文件訪問等後端能力,chromium 提供展現功能,以及網絡能力(Electron 網絡能力不是 NodeJS 提供的,而是 chromium 的 net 模塊提供的)。web 前端開發技術方案均可以應用在 Electron 中,好比 Vue、React、Bootstrap、sass 等。

  5. 在工程化角度看,使用 yarn 比 npm 好一些,由於 yarn 會緩存已經安裝過的依賴,其餘項目只要發現存在緩存,則讀取本地的包依賴,會更加快速。

  6. 在使用 Vue、React 開發 Electron 應用時,可使用 npm 或 yarn install 包,也可使用 Electron-vue 腳手架工具。

    vue init simulatedgreg/Electron-vue my-project
    cd my-project
    npm install
    npm run dev
  7. 開發完畢後須要設置應用程序的圖標信息、版本號等,打包須要指定不一樣的平臺。

  8. 新開項目建立後會報錯.

初始化工程後會報錯 ERROR in Template execution failed: ReferenceError: process is not defined。解決辦法是使用 nvm 將 node 版本將爲 10。

繼續運行仍是報錯,以下

┏ Electron -------------------

[11000:0615/095124.922:ERROR:CONSOLE(7574)] "Extension server error: Object not found: <top>", source: chrome-devtools://devtools/bundled/inspector.js (7574)

┗ ----------------------------

解決辦法是在 main/index.dev.js 修改代碼

- require('Electron-debug')({ showDevTools: true });
+ // NB: Don't open dev tools with this, it is causing the error
+ require('Electron-debug')();

在 In main/index.js in the createWindow() function:

mainWindow.loadURL(winURL);
+  // Open dev tools initially when in development mode
+  if (process.env.NODE_ENV === "development") {
+    mainWindow.webContents.on("did-frame-finish-load", () => {
+      mainWindow.webContents.once("devtools-opened", () => {
+        mainWindow.focus();
+      });
+      mainWindow.webContents.openDevTools();
+    });
+  }
  1. Electron 多窗口與單窗口應用區別

    用途

  2. 知道 Electron 開發原理,因此大部分時間是在寫前端代碼。因此根據團隊技術沉澱、選擇對應的前端框架,好比 Vue、React、Angular。

  3. 也許開發並不難,難在視覺和 UX。不少寫過網頁的開發者或者之前端的視覺去寫 Electron App 不免會寫出網頁版的桌面應用程序,說難聽點,就是四不像 😂。因此須要轉變想法,這是在開發桌面應用程序。

  4. Electron 和 Web 開發相比,各自有側重點

    ElectronAndWeb

2. 優化手段

2.1 性能分析

由於開發過程當中,Electron 體驗就是在開發一個前端項目,因此咱們使用 Chrome 的 Performance 進行分析。

分析某段代碼的執行過程,也能夠經過下面命令生成分析文件,而後導入到 Chrome Performance 中分析:

# 輸出 cpu 和 堆分析文件
node --cpu-prof --heap-prof -e "require('request’)」「

2.2 白屏優化

  • 無論是 Native App 仍是 Web 網頁,骨架屏都是比較常見的一些技術手段。好比弱網下簡書 App 的效果

  • 懶加載。優先加載第一屏(主界面)所需的依賴,其餘的依賴延遲加載

  • 代碼分割。Webpack 工具支持代碼分割,這在前端中是很常見的一種優化手段

  • Node 模塊延遲加載或合併。Node 屬於 CMD 規範,模塊查找、文件讀取都比較耗時,某些 Node 模塊依賴模塊較多、子模塊較深這樣首屏會很慢。因此延遲加載或者選擇使用打包工具優化和合並 Node 模塊。

  • 打包優化。現代打包工具備很是多優化手段。 Webpack 支持代碼壓縮、預執行... 裁剪多餘的代碼, 減小運行時負擔。模塊合併後還能夠減少 IO 。

  • 和 Native App 中的 Hybrid 優化手段機制同樣。能夠對靜態資源進行緩存(公司基礎樣式文件、基礎組件等,設計資源更新策略)。使用 Service-Worker 進行靜態資源攔截,或者在 Native 端作請求攔截,好比 Mac 端 NSURLProtocol

  • 預加載機制。在登錄界面或者啓動動畫階段,初始化主界面,而後將主界面設置到不可見狀態。等啓動或者登錄後再將主界面設置爲可見狀態

  • 使用 Node API 儘可能避免同步操做。通常 Node API 都有同步和異步2種 API,好比 fs 文件讀取模塊

  • 對內存影響較大的功能使用 Native 的能力,好比 Database、Image、Network、Animation 等。雖然 Web 都有對應的解決方案,可是原生對於這些功能的操做權限更大、內存控制更靈活、安全性更高、性能更佳

  • 減少主進程壓力。Electron 有1個主進程和多個渲染進程,主進程是全部窗口的父進程,它負責調度各類資源。若是主進程被阻塞,將影響整個應用響應性能。

7、 技術體系搭建:全景

其實一個技術自己的難易程度並非可否在本身企業、公司、團隊內順利使用的惟一標尺,其配套的 CI/CD、APM、埋點系統、發佈更新、灰度測試等可否與現有的系統以較小成本融合纔是很大的決定要素。由於某個技術並非很是難,要是大多數開發者以爲很難,那它設計上就是失敗的。

1. 構建

構建

2. 工程解耦

工程解耦

3. 問題定位

Electron 提供的 crash 信息進行包裝。

crash 分析

import { BrowserWindow, app, dialog} from 'Electron';
 
  
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed',
   const options = {
      type: 'error',
      title: '進程崩潰了',
      message: '這個進程已經崩潰.',
      buttons: ['重載', '退出'],
    };    
   recordCrash().then(() => {
      dialog.showMessageBox(options, (index) => {
        if (index === 0) reloadWindow(mainWindow);
        else app.quit();
      });
    }).catch((e) => {
      console.log('err', e);
    });
})
 
function recordCrash() { 
    return new Promise(resolve => { 
       // 崩潰日誌請求成功.... 
      resolve();
    })
}
  
function reloadWindow(mainWin) {
  if (mainWin.isDestroyed()) {
    app.relaunch();
    app.exit(0);
  } else {
    BrowserWindow.getAllWindows().forEach((w) => {
      if (w.id !== mainWin.id) w.destroy();
    });
    mainWin.reload();
  }
}
// index.js
app.on('will-finish-launching', () => {
  if(!isDev) {
  require('./updater.js')
  }
  require('./crash-reporter').init()
 })
 
 // crash-reporter.js
 const {crashReporter} = require('Electron')

function init() {
    crashReporter.start({
        productName: 'ZanPandora',
        companyName: 'youzan',
        submitURL: 'http://127.0.0.1:33855/crash',

    })
}
module.exports = {init}

// server
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const serve = require('koa-static-server')
const router = new Router()
const compareVersions = require('compare-versions')
const multer = require('koa-multer')
const uploadCrash = multer({dest: 'crash/'})
router.post('/crash', uploadCrash.single('upload_file_minidump'), (ctx, next) => {
    console.log(ctx.req.body)
    // 存DB
})

4. 軟件更新

// package.json
"dependencies": {
    "Electron-is-dev": "^1.1.0",
    "Electron-squirrel-startup": "^1.0.0",
     // ...
  },

// index.js
app.on('will-finish-launching', () => {
  if(!isDev) {
    require('./updater.js')
  }
  require('./crash-reporter').init()
})

// updater.js
const {autoUpdater, app, dialog} = require('Electron')
if(process.platform == 'darwin') {
    autoUpdater.setFeedURL('http://127.0.0.1:33855/darwin?version=' + app.getVersion())
} else {
    autoUpdater.setFeedURL('http://127.0.0.1:33855/win32?version=' + app.getVersion())
}

autoUpdater.checkForUpdates() // 定時輪訓、服務端推送
autoUpdater.on('update-available', () => {
    console.log('update-available')
})

autoUpdater.on('update-downloaded', (e, notes, version) => {
    // 提醒用戶更新
    app.whenReady().then(() => {
        let clickId = dialog.showMessageBoxSync({
            type: 'info',
            title: '升級提示',
            message: '已爲你升級到最新版,是否當即體驗',
            buttons: ['立刻升級', '手動重啓'],
            cancelId: 1,
        })
        if(clickId === 0) {
            autoUpdater.quitAndInstall()
            app.quit()
        }
    })
})

autoUpdater.on('error', (err) => {
    console.log('error', err)
})

// server
// index.js
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const serve = require('koa-static-server')
const router = new Router()
const compareVersions = require('compare-versions')
const multer = require('koa-multer')
const uploadCrash = multer({dest: 'crash/'})
router.post('/crash', uploadCrash.single('upload_file_minidump'), (ctx, next) => {
    console.log(ctx.req.body)
    // 存DB
})
function getNewVersion(version) {
    if(!version) return null
    let maxVersion = {
        name: '1.0.1',
        pub_date: '2020-02-01T12:26:53+1:00',
        notes: '新增功能AAA',
        url: `http://127.0.0.1:33855/public/ZanPandora-1.0.1-mac.zip`
    }
    if(compareVersions.compare(maxVersion.name , version, '>')) {
        return maxVersion
    }
    return null
}
router.get('/win32/RELEASES', (ctx, next) => {
    let newVersion = getNewVersion(ctx.query.version)
    if(newVersion) {
        ctx.body='BBC6F98A5CD32C675AAB6737A5F67176248B900C ZanPandora-1.0.1-full.nupkg 62177782'
    } else {
        ctx.status = 204
    }
})
router.get('/win32/*.nupkg', (ctx, next) => {
    // redirect s3 靜態文件服務
    ctx.redirect(`/public/${ctx.params[0]}.nupkg`)
})
router.get('/darwin', (ctx, next) => {
   // 處理Mac更新, ?version=1.0.0&uid=123
   let {version} = ctx.query
    let newVersion = getNewVersion(version)
    if(newVersion) {
        ctx.body = newVersion
    } else {
        ctx.status = 204
    }
})
app.use(serve({rootDir: 'public', rootPath: '/public'}))
app.use(router.routes())
    .use(router.allowedMethods())

app.listen(33855)

8、 Electron 應用場景

業界拿 Electron 作了不少東西,好比你們都在用的 VSCode、Atom、一些工具軟件的實現、不少大廠的面試軟件、一些不是很是注重渲染效率的產品、一些集團內部的開發工具等。Electron 大有可爲,有不少想象空間去作一些事情。舉幾個例子

1. 字節跳動

  • Electron作了一個工具,能直接查看線上包的函數耗時,無任何侵入

  • 調試工具的合集

  • 研發需求管理

  • 代碼合併工具(MR)。區別於 gitlab 的特性,沒有多倉合代碼的能力。

    好比同時在主工程系修改了7個 pod 、1個主工程的代碼,須要分批次提交,不具有同時將8個倉庫的代碼原子性合入

    pod 的 changeLog、版本號等須要設計自動發版的流程

  • 把性能調試和效率工具都整合到一塊兒了。

2. 阿里

  • 沙盒的查看與操做,好比在 PC 端查看移動沙盒內文件內容、數據庫文件的查看與 SQL 執行等
  • lint 功能、檢測無用方法、在線日誌解密查看、數據庫文件的解密查看、jspatch 的 mock、
  • 針對網絡請求和響應的操做,好比自定義請求、延遲、mock response、
  • Mock、查看
  • 性能測試:cpu、load、fps、啓動耗時(機房有高速攝像機解幀,模擬點擊 icon、啓動頁啓動、App 首頁出現、首頁圖片出現、App 能夠滾動交互了。不須要 hook 去監控耗時)
  • pre-main、main、首頁打點
  • 數據板:埋點體系的數據,看坑位的點擊效果
  • 應用數據:沙盒、Cookie、數據
  • 開發工具:CPU、內存、OOM
  • 視覺:移動端元素的參考線
  • 網絡、開關數據、卡頓、內存泄漏

一些原則,一言以蔽之:該工具只作數據的查看、Mock 等工做,不作線上數據的干擾和生產。

主要實現方式:經過 deviceID、ip 地址等與設備產生鏈接,將一切能夠標準化的流程都抽象、自動化、好比性能調試和效率工具都整合到一塊兒。

相關文章
相關標籤/搜索