- 原文地址:Beyond The Browser: From Web Apps To Desktop Apps
- 原文作者:本文已獲原作者 Adam Lynch 授權(quán)
- 譯文出自:掘金翻譯計(jì)劃
- 譯者: bambooom、imink
- 校對(duì)者:bambooom萍嬉、imink晴股、sunui
超越瀏覽器:從 web 應(yīng)用到桌面應(yīng)用
一開(kāi)始我是個(gè) web 開(kāi)發(fā)者,現(xiàn)在我是個(gè)全棧開(kāi)發(fā)者恶迈,但從未想過(guò)在桌面上有所作為款票。我熱愛(ài) web 技術(shù)踏幻,熱愛(ài)這個(gè)無(wú)私的社區(qū)枷颊,熱愛(ài)它對(duì)于開(kāi)源的友好,嘗試挑戰(zhàn)極限该面。我熱愛(ài)探索好看的網(wǎng)站和強(qiáng)大的應(yīng)用夭苗。當(dāng)我被指派做桌面應(yīng)用任務(wù)的時(shí)候,我非常憂慮和害怕隔缀,因?yàn)槟强雌饋?lái)很難题造,或者至少不一樣。
這并不吸引人蚕泽,對(duì)吧晌梨?你需要學(xué)一門新的語(yǔ)言,甚至三門须妻?想象一下過(guò)時(shí)的工作流仔蝌,古舊的工具,沒(méi)有任何你喜歡的有關(guān) web 的一切荒吏。你的職業(yè)發(fā)展會(huì)被怎樣影響呢敛惊?
別慌,深呼吸绰更,現(xiàn)實(shí)情況是瞧挤,作為 web 開(kāi)發(fā)者,你已經(jīng)擁有開(kāi)發(fā)現(xiàn)代桌面應(yīng)用所需的一切技能儡湾,得益于新的強(qiáng)大的 API特恬,你甚至可以在桌面應(yīng)用中發(fā)揮你最大的潛能。
本文將會(huì)介紹使用 NW.js 和 Electron 開(kāi)發(fā)桌面應(yīng)用徐钠,包括它們的優(yōu)劣癌刽,以及如何使用同一套代碼庫(kù)來(lái)開(kāi)發(fā)桌面、web 應(yīng)用,甚至更多显拜。
為什么衡奥?
首先,為什么會(huì)有人開(kāi)發(fā)桌面應(yīng)用远荠?任何現(xiàn)有的 web 應(yīng)用(不同于網(wǎng)站矮固,如果你認(rèn)為它們是不同的)都可能適合變成一個(gè)桌面應(yīng)用。你可以圍繞任何可以從與用戶系統(tǒng)集成中獲益的 web 應(yīng)用構(gòu)建桌面應(yīng)用譬淳;例如本地通知档址、開(kāi)機(jī)啟動(dòng)、與文件的交互等瘦赫。有些用戶單純更喜歡在自己的電腦中永久保存一些 app辰晕,無(wú)論是否聯(lián)網(wǎng)都可以訪問(wèn)蛤迎。
也許你有個(gè)想法确虱,但只能用作桌面應(yīng)用,有些事情只是在 web 應(yīng)用中不可能實(shí)現(xiàn)(至少還有一點(diǎn)替裆,但更多的是這一點(diǎn))校辩。你可能想要為公司內(nèi)部創(chuàng)建一個(gè)獨(dú)立的功能性應(yīng)用程序,而不需要任何人安裝除了你的 app 之外的任何內(nèi)容(因?yàn)閮?nèi)置 Node.js )辆童。也許你有個(gè)有關(guān) Mac 應(yīng)用商店的想法宜咒,也許只是你的一個(gè)個(gè)人興趣的小項(xiàng)目。
很難總結(jié)為什么你應(yīng)該考慮開(kāi)發(fā)桌面應(yīng)用把鉴,因?yàn)檎娴挠泻芏囝愋偷膽?yīng)用你可以創(chuàng)建故黑。這非常取決于你想要達(dá)到什么目的,API 是否足夠有利于開(kāi)發(fā)庭砍,離線使用將多大程度上增強(qiáng)用戶體驗(yàn)场晶。在我的團(tuán)隊(duì),這些都是毋庸置疑的怠缸,因?yàn)槲覀冊(cè)陂_(kāi)發(fā)一個(gè)聊天應(yīng)用程序诗轻。另一方面來(lái)說(shuō),一個(gè)依賴于網(wǎng)絡(luò)而沒(méi)有任何與系統(tǒng)集成的桌面應(yīng)用應(yīng)該做成一個(gè) web 應(yīng)用揭北,并且只做 web 應(yīng)用扳炬。當(dāng)用戶并不能從桌面應(yīng)用中獲得比在瀏覽器中訪問(wèn)一個(gè)網(wǎng)址更多的價(jià)值的時(shí)候,期待用戶下載你的應(yīng)用(其中自帶瀏覽器以及 Node.js)是不公平的搔体。
比起描述你個(gè)人應(yīng)該建造的桌面應(yīng)用及其原因恨樟,我更希望的是激發(fā)一個(gè)想法,或者只是激發(fā)你對(duì)這篇文章的興趣疚俱。繼續(xù)往下讀來(lái)看看用 web 技術(shù)構(gòu)造一個(gè)強(qiáng)大的桌面應(yīng)用是多么簡(jiǎn)單劝术,以及在創(chuàng)建過(guò)程中你應(yīng)該付出什么。
NW.js
桌面應(yīng)用已經(jīng)有很長(zhǎng)一段時(shí)間了,我知道你沒(méi)有很多時(shí)間夯尽,所以我們跳過(guò)一些歷史瞧壮,從 2011 年的上海開(kāi)始。來(lái)自 Intel 開(kāi)源技術(shù)中心的 Roger Wang 開(kāi)發(fā)了 node-webkit匙握,一個(gè)概念驗(yàn)證的 Node.js 模塊咆槽,這個(gè)模塊可以讓用戶創(chuàng)建一個(gè) WebKit 內(nèi)核的瀏覽器窗口并直接在 <script>
中調(diào)用 Node.js 模塊。
經(jīng)過(guò)一段時(shí)間的開(kāi)發(fā)以及將內(nèi)核從 WebKit 轉(zhuǎn)換到 Chromium(Google Chrome 基于這個(gè)開(kāi)源項(xiàng)目開(kāi)發(fā))圈纺,一個(gè)叫 Cheng Zhao 的實(shí)習(xí)生加入了這個(gè)項(xiàng)目秦忿。不久就有人意識(shí)到一個(gè)基于 Node.js 和 Chromium 運(yùn)行的應(yīng)用是一個(gè)很好的建造桌面應(yīng)用的框架。于是這個(gè)項(xiàng)目變得頗受歡迎蛾娶。
注意:node-webkit 后來(lái)更名為 NW.js灯谣,是因?yàn)轫?xiàng)目不再使用 Node.js 以及 WebKit,所以需要改一個(gè)更通用的名字蛔琅。Node.js 的替換選擇是 io.js (Node.js fork 版本)胎许,Chromium 也已經(jīng)從 WebKit 轉(zhuǎn)為它自己的版本 —— Blink。
所以罗售,如果現(xiàn)在去下載一個(gè) NW.js 應(yīng)用辜窑,實(shí)際上是下載了 Chromium、Node.js寨躁,以及真正的 app 的代碼穆碎。這不僅意味著桌面應(yīng)用也可以使用 HTML、CSS职恳、JavaScript 來(lái)寫所禀,也意味著 app 可以直接使用所有 Node.js 的 API(比如讀取或?qū)懭胗脖P),而對(duì)于終端用戶放钦,沒(méi)有比這更好的選擇了色徘。這看起來(lái)非常強(qiáng)大,但是它是怎么實(shí)現(xiàn)的呢最筒?我們先來(lái)了解一下 Chromium贺氓。
Chromium 有一個(gè)主要的后臺(tái)進(jìn)程,每個(gè)標(biāo)簽頁(yè)也會(huì)有自己的進(jìn)程床蜘。你可能注意到 Google Chrome 在 Windows 的任務(wù)管理器或者 macOS 的活動(dòng)監(jiān)視器上總是至少存在兩個(gè)進(jìn)程辙培。我并沒(méi)有嘗試在這里安排穿插主后臺(tái)進(jìn)程相關(guān)的內(nèi)容,但是它包括了 Blink 渲染引擎邢锯、V8 JavaScript 引擎(也構(gòu)建了 Node.js )以及一些從原生 API 抽象出來(lái)的平臺(tái) API扬蕊。每個(gè)獨(dú)立的標(biāo)簽頁(yè)或渲染的過(guò)程都可以使用 JavaScript 引擎、CSS 解析器等丹擎,但為了提高容錯(cuò)性尾抑,它們又和主進(jìn)程是完全隔離的歇父。渲染進(jìn)程與主進(jìn)程之間是用進(jìn)程間通信(IPC)來(lái)進(jìn)行通訊。
大致上這就是一個(gè) NW.js app 的結(jié)構(gòu)再愈,它和 Chromium 基本一致榜苫,除了每個(gè)窗口也可以訪問(wèn) Node.js。現(xiàn)在翎冲,你可以訪問(wèn) DOM垂睬,可以訪問(wèn)其他腳本、npm 安裝的模塊抗悍,或者 NW.js 提供的內(nèi)置的模塊驹饺。你的 app 默認(rèn)只有一個(gè)窗口,但從這一個(gè)窗口缴渊,可以生成其他窗口赏壹。
創(chuàng)建一個(gè)應(yīng)用很簡(jiǎn)單,只需要一個(gè) HTML 文件和一個(gè) package.json
文件衔沼,就像你平時(shí)使用 Node.js 時(shí)那樣蝌借。你可以使用 npm init --yes
新建一個(gè)默認(rèn)的。一般來(lái)說(shuō)俐巴,package.json
會(huì)指定一個(gè) JavaScript 文件作為模塊的入口(也就是使用 main
屬性)骨望,但是如果是 NW.js硬爆,你需要去編輯一下 main
指向你的 HTML 文件欣舵。
{
"name": "example-app",
"version": "1.0.0",
"description": "",
"main": "index.html",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Example app</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
只要你安裝好了 nw
(通過(guò) npm install -g nw
),你就可以在項(xiàng)目目錄下執(zhí)行 nw .
啟動(dòng) app缀磕,然后就可以看到下圖缘圈。
就是這么簡(jiǎn)單。NW.js 初始化了第一個(gè)窗口袜蚕,加載了你的 HTML 文件糟把,雖然這看起來(lái)并沒(méi)有什么,但接下來(lái)就是你來(lái)添加標(biāo)簽及樣式了牲剃,就和在 web 應(yīng)用中一樣遣疯。
你可以憑自己喜好去掉窗口欄,構(gòu)建自己的框架模板凿傅。你可以有半透明或全透明的窗口缠犀,可以有隱藏窗口或者更多。我最近嘗試使用 NW.js 做了Clippy(Office 助手)聪舒。能在 macOS 和 Windows 10 上看到它有種奇妙的滿足感辨液。
現(xiàn)在你可以寫 HTML,CSS 和 JavaScript 了箱残,你可以使用 Node.js 讀寫硬盤滔迈、執(zhí)行系統(tǒng)命令止吁、生成其他可執(zhí)行文件等等。設(shè)想一下燎悍,你甚至可以通過(guò) WebRTC 造一個(gè)多玩家的輪盤賭游戲敬惦,隨機(jī)刪除其他人的文件。
你不僅可以使用 Node.js 的 API谈山,還有所有 npm 的包仁热,現(xiàn)在已經(jīng)有超過(guò) 35 萬(wàn)個(gè)了。例如勾哩,auto-launch 是我們?cè)?Teamwork.com 做的開(kāi)源包抗蠢,用來(lái)開(kāi)機(jī)啟動(dòng) NW.js 或者 Electron 應(yīng)用。
如果你需要做一些偏底層的事思劳,Node.js 也有原生的模塊迅矛,能讓你使用 C 或者 C++ 創(chuàng)建模塊。
總之潜叛,NW.js 高效封裝了原生的 API秽褒,讓你可以簡(jiǎn)單地與桌面環(huán)境集成。比如你有一個(gè)任務(wù)欄圖標(biāo)威兜,使用系統(tǒng)默認(rèn)應(yīng)用打開(kāi)一個(gè)文件或者 URL 之類的销斟。你需要做的是使用 HTML5 notification 的 API 觸發(fā)一個(gè)通知:
new Notification('Hello', {
body: 'world'
});
Electron
你可能認(rèn)出來(lái)了,下圖是 GitHub 開(kāi)發(fā)的編輯器椒舵,Atom蚂踊。不管你是否使用 Atom,它的出現(xiàn)對(duì)于桌面應(yīng)用都是一個(gè)顛覆者笔宿。GitHub 從 2013 年開(kāi)始開(kāi)發(fā) Atom犁钟,后來(lái) Cheng Zhao 加入,fork 了 node-webkit 作為基礎(chǔ)泼橘,后來(lái)以 atom-shell 為名開(kāi)源涝动。
注意:對(duì)于 Electron 只是 node-webkit 的 fork,還是一切從頭重新做的炬灭,是很有爭(zhēng)議的醋粟。但無(wú)論哪種方式,最終都成為終端用戶的一個(gè)分支重归,因?yàn)?API 幾乎完全一致米愿。
在開(kāi)發(fā) Atom 的過(guò)程中,GitHub 改進(jìn)了一些方案提前,也解決了很多 bug吗货。2015年,atom-shell 正式更名為 Electron狈网。它的版本已經(jīng)更新到 1.0 以上(譯注:最新正式版本為v1.3.14)宙搬,并且因?yàn)?GitHub 的推行笨腥,它已經(jīng)真正發(fā)展壯大了。
和 Atom 一樣勇垛,其他用 Electron 開(kāi)發(fā)的有名項(xiàng)目包括 Slack脖母、Visual Studio Code、 Brave闲孤、HyperTerm谆级、Nylas,真的是在做著一些尖端的東西讼积。Mozilla Tofino 也是其中很有趣的一個(gè)肥照,它是 Mozilla( FireFox 的公司)的一個(gè)內(nèi)部項(xiàng)目,目標(biāo)是徹底優(yōu)化瀏覽器勤众。你沒(méi)看錯(cuò)舆绎,Mozilla 的團(tuán)隊(duì)選擇了 Electron (基于 Chromium )來(lái)做這個(gè)實(shí)驗(yàn)。
Electron 有什么不同呢们颜?
那么 Electron 和 NW.js 有什么不同吕朵?首先,Electron 沒(méi)有 NW.js 那么面向?yàn)g覽器窥突,Electron app 的入口是一個(gè)在主進(jìn)程中運(yùn)行的腳本努溃。
Electron 團(tuán)隊(duì)修補(bǔ)了 Chromium 以便嵌入多個(gè)可以同時(shí)運(yùn)行的 JavaScript 引擎,所以當(dāng) Chromium 發(fā)布新版本的時(shí)候阻问,他們不需要做任何事梧税。
注意:NW.js 與 Chromium 的綁定不太一樣,造成了 NW.js 經(jīng)常被指責(zé)不如 Electron 那樣緊跟 Chromium则拷。然而贡蓖,整個(gè) 2016 年,NW.js 每次在 Chromium 發(fā)布主要版本之后的 24 小時(shí)內(nèi)發(fā)布新版本煌茬,這很大程度也歸功于團(tuán)隊(duì)組織轉(zhuǎn)型。
回到主進(jìn)程的話題彻桃,你的應(yīng)用默認(rèn)是沒(méi)有窗口的坛善,但是你可以從主進(jìn)程開(kāi)啟任意多個(gè)窗口,每個(gè)窗口和 NW.js 一樣有自己的渲染進(jìn)程邻眷。
那么當(dāng)然眠屎,創(chuàng)建一個(gè) Electron app,你需要的只是一個(gè) JavaScript 文件(現(xiàn)在暫時(shí)只是個(gè)空文件)以及一個(gè) package.json
文件指向它肆饶。然后你只需要執(zhí)行 npm install --save-dev electron
改衩,以及 electron .
來(lái)啟動(dòng)你的 app。
{
"name": "example-app",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
// main.js 文件驯镊,現(xiàn)在是空的
沒(méi)有什么會(huì)發(fā)生葫督,因?yàn)槟愕?app 沒(méi)有默認(rèn)窗口竭鞍。接下來(lái)你可以和 NW.js 應(yīng)用一樣打開(kāi)任意多個(gè)窗口,每個(gè)都有各自的渲染進(jìn)程橄镜。
// main.js
const {app, BrowserWindow} = require('electron');
let mainWindow;
app.on('ready', () => {
mainWindow = new BrowserWindow({
width: 500,
height: 400
});
mainWindow.loadURL('file://' + __dirname + '/index.html');
});
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Example app</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
你可以在這個(gè)窗口中加載遠(yuǎn)程 URL偎快,但是一般來(lái)說(shuō)你會(huì)在本地創(chuàng)建 HTML 文件并加載它,當(dāng)當(dāng)當(dāng)當(dāng)~加載出來(lái)啦洽胶!
在 Electron 提供的內(nèi)置模塊中晒夹,像在前面例子中使用的 app
和 BrowserWindow
,大多只能要么在主進(jìn)程要么在某個(gè)渲染進(jìn)程中使用姊氓。比方說(shuō)丐怯,你只能在主進(jìn)程中管理你的所有窗口,自動(dòng)更新或者其他翔横。你可能想在主進(jìn)程中點(diǎn)擊一個(gè)按鈕觸發(fā)一些事件响逢,因此 Electron 為 IPC 提供了一些內(nèi)置方法∽厮铮基本上你可以觸發(fā)任意的事件舔亭,然后在另一端監(jiān)聽(tīng)它們。這樣蟀俊,你就可以在某一個(gè)渲染進(jìn)程中捕獲 click
事件钦铺,通過(guò) IPC 發(fā)出事件信息給主進(jìn)程,主進(jìn)程捕獲后執(zhí)行相關(guān)操作肢预。
Electron 有著不同的進(jìn)程矛洞,你需要稍微不同地組織你的 app,但這不算什么烫映。為什么人們使用 Electron 而不是 NW.js沼本?這其中有影響力的因素,它的流行造就了許多相關(guān)的工具和模塊锭沟。 Electron 的文檔更好懂抽兆,最重要的是啃洋,Electron 的 bug 更少送淆,并且有更好的 API。
Electron 的文檔非常棒被济,這值得再?gòu)?qiáng)調(diào)一下祝辣。拿 Electron API Demos app 來(lái)說(shuō)贴妻,這是個(gè) Electron app,它可以交互式的演示出你可通過(guò) Electron 的 API 做到什么蝙斜。比如新建窗口名惩,它不僅提供了 API 的描述以及示例代碼,甚至點(diǎn)擊按鈕的確可以執(zhí)行代碼并打開(kāi)新的窗口孕荠。(下圖就是 Electron API Demos app 的截圖)
如果你通過(guò) Electron 的 bug 追蹤器提交問(wèn)題娩鹉,你可以在幾天之內(nèi)得到回復(fù)攻谁。我曾經(jīng)見(jiàn)過(guò) NW.js 有經(jīng)過(guò)三年都未修復(fù)的 bug,我并不是堅(jiān)決反對(duì)他們這么做底循,開(kāi)發(fā)開(kāi)源項(xiàng)目采用的語(yǔ)言和使用這個(gè)項(xiàng)目的開(kāi)發(fā)者了解的語(yǔ)言如此的不同巢株,是非常難維護(hù)的。NW.js 和 Electron 主要是用 C++ (以及少部分 Objective C++)寫的熙涤,但是使用這兩個(gè)項(xiàng)目的人寫的是 JavaScript阁苞。我非常感激 NW.js 給我們的幫助。
Electron 彌補(bǔ)了 NW.js API 上的一些不足祠挫。比如那槽,你可以綁定全局的鍵盤快捷鍵,這樣即使你的 app 并沒(méi)有獲取焦點(diǎn)等舔,鍵盤事件也可以被捕獲骚灸。曾經(jīng)我在 NW.js 的應(yīng)用中碰到過(guò)一個(gè) API 的漏洞,就是我在 Windows 上可以綁定 Control + Shift + A
快捷鍵達(dá)到預(yù)期目的慌植,但是實(shí)際上到了 Mac 上綁定的快捷鍵是 Command + Shift + A
甚牲,這個(gè)的確是有意而為之的,但是仍然很奇怪蝶柿。沒(méi)有任何方法可以在 Mac 上綁定 Control
鍵丈钙。另外,如果想綁定 Command
鍵交汤,在 Mac 上的確沒(méi)問(wèn)題雏赦,而到了 Windows 和 Linux 上綁定的卻是 Windows
鍵。Electron 的團(tuán)隊(duì)發(fā)現(xiàn)了這些問(wèn)題(我猜是在給 Atom 添加快捷鍵的時(shí)候)芙扎,然后他們很快更新了他們自己的全局快捷鍵(globalShortcut)API星岗,以上遇到的情況就可以正常工作了。公平起見(jiàn)戒洼,NW.js 修復(fù)了前一個(gè)問(wèn)題俏橘,但一直沒(méi)有修復(fù)后一個(gè)。
還有其他一些不同的地方施逾。比如說(shuō)敷矫,之前原生的 notification 通知,在最近的 NW.js 版本中汉额,變成了 Chrome 風(fēng)格的了。這種通知不會(huì)進(jìn)入到 Mac OS X 或者 Windows 10 的通知中心里面榨汤,但是在 npm 上有方便使用的模塊解決蠕搜。如果你想做一些有趣的有關(guān)音頻或視頻的東西,建議使用 Electron收壕,因?yàn)橛行┙獯a器和 NW.js 不兼容妓灌。
Electron 還添加了一些新的 API轨蛤,更加多地與桌面端的集成,并且內(nèi)置了自動(dòng)升級(jí)虫埂,我稍后會(huì)談到祥山。
但是感覺(jué)如何呢?
感覺(jué)很好掉伏,當(dāng)然缝呕,它并不是原生的。現(xiàn)在大多數(shù)桌面應(yīng)用并不會(huì)長(zhǎng)得像資源管理器或者 Finder斧散,所以用戶并不介意或者意識(shí)到用戶界面背后是 HTML供常。你愿意的話,你可以使之更像原生應(yīng)用鸡捐,但是我并不認(rèn)為那樣會(huì)讓用戶體驗(yàn)更好栈暇。比如,你可以在用戶將鼠標(biāo)懸停在按鈕上時(shí)箍镜,不讓光標(biāo)變成手源祈,一般原生的桌面應(yīng)用都是這樣做的,但是這樣做有什么好的嗎色迂?當(dāng)然也有像 Photon Kit 這樣的類似 Bootstrap 的 CSS 框架香缺,可以做出 macOS 風(fēng)格的組件。(下圖是 Photon Kit 做出的組件 demo)
性能
性能表現(xiàn)如何呢脚草?會(huì)很慢或者延遲嗎赫悄?其實(shí)你的 app 本質(zhì)上來(lái)說(shuō)仍然是 web 應(yīng)用,所以它會(huì)和在 Google Chrome 中運(yùn)行的 web app 非常類似馏慨。你可能會(huì)創(chuàng)造出高性能的或者反應(yīng)遲緩的 app埂淮,但是沒(méi)關(guān)系,你已經(jīng)有分析并提升性能的技能了写隶。app 基于 Chromium 最好的其中一點(diǎn)就是你可以使用它的開(kāi)發(fā)者工具倔撞。你可以在 app 內(nèi)調(diào)試或者遠(yuǎn)程調(diào)試,Electron 團(tuán)隊(duì)也開(kāi)發(fā)了一款開(kāi)發(fā)者工具的插件叫 Devtron 來(lái)監(jiān)控一些 Electron 特定的信息慕趴。
不過(guò)痪蝇,你的桌面應(yīng)用可以比 web 應(yīng)用的性能更高。因?yàn)槟憧梢詣?chuàng)建一個(gè)工作窗口冕房,一個(gè)用于執(zhí)行耗能昂貴工作的隱藏窗口躏啰。因?yàn)槊總€(gè)進(jìn)程都是孤立的,所以任何在這個(gè)窗口中進(jìn)行的計(jì)算或者處理不會(huì)影響到其他可見(jiàn)窗口的渲染進(jìn)程耙册,上下滾動(dòng)等等给僵。
記住你總可以生成系統(tǒng)指令、可執(zhí)行文件,或者原生代碼帝际,如果真的需要的話(你不會(huì)真的這么做的)蔓同。
分發(fā)
NW.js 和 Electron 都支持很多平臺(tái),包括 Windows蹲诀,Mac 和 Linux斑粱。Electron 不支持 Windows XP 和 Vista,但 NW.js 支持脯爪。將 NW.js 應(yīng)用上線到 Mac App Store 有些棘手则北,你必須繞幾個(gè)彎子。而 Electron 支持直接的 Mac App Strore 兼容的版本披粟,和普通的版本一樣咒锻,只是某些模塊你無(wú)法訪問(wèn),比如自動(dòng)更新(因?yàn)槟愕?app 會(huì)通過(guò) Mac App Store 進(jìn)行更新所以可以接受)守屉。
Electron 甚至支持 ARM 版本惑艇,所以你的 app 可以在 Chromebook 或者樹(shù)莓派上運(yùn)行,最終拇泛,Google 可能會(huì)逐步淘汰 Chrome 封裝應(yīng)用 (Packaged App)滨巴,但是 NW.js 仍然支持將應(yīng)用程序移植到 NW.js 應(yīng)用,并且仍然可以訪問(wèn)相同的 Chromium API俺叭。
雖然 32 位和 64 位的版本都支持恭取,所以你完全可以使用 64 位的 Mac 和 Windows 應(yīng)用。但是熄守,為了兼容蜈垮,32 位和 64 位 Linux 應(yīng)用程序是都需要的。
假如 Electron 勝出裕照,你想發(fā)行一個(gè) Electron 應(yīng)用攒发。有一個(gè)很不錯(cuò)的 Node.js 包叫 electron-packager 可以幫你將 app 打包成一個(gè) .app
或者 .exe
文件。也有其他幾個(gè)類似的項(xiàng)目晋南,包括交互式的一步一步告訴你該怎么做惠猿。不過(guò),你應(yīng)該用 electron-builder负间,它以 electron-packager 為基礎(chǔ)偶妖,添加了其他幾個(gè)相關(guān)的模塊,生成的是 .dmg
文件和 Windows 安裝包政溃,并且為你處理好了代碼簽名的問(wèn)題趾访。這很重要,如果沒(méi)有這一步董虱,你的應(yīng)用將會(huì)被操作系統(tǒng)認(rèn)為是不可信的腹缩,你的應(yīng)用程序可能會(huì)觸發(fā)防毒軟件的運(yùn)行,Microsoft SmartScreen 可能會(huì)嘗試阻止用戶啟動(dòng)你的應(yīng)用空扎。
關(guān)于代碼簽名的令人討厭的事情是藏鹊,你必須單獨(dú)為某個(gè)平臺(tái)簽名你的應(yīng)用程序,比如在 Mac 上簽名 Mac 應(yīng)用转锈,在 Windows 簽名 Windows 應(yīng)用盘寡。因此,如果你很在乎發(fā)行桌面應(yīng)用的話撮慨,就必須為每個(gè)發(fā)行版本分別構(gòu)建適用于不同平臺(tái)的應(yīng)用(以及分別簽名)竿痰。
這可能會(huì)感到不夠自動(dòng)化很繁瑣,特別是如果你習(xí)慣于在 web 上創(chuàng)建砌溺。幸運(yùn)的是影涉,electron-builder 被創(chuàng)造出來(lái)完成這些自動(dòng)化工作。我說(shuō)的是持續(xù)集成工具例如 Jenkins规伐、CodeShip蟹倾、Travis-CI、AppVeyor(Windows 集成)等猖闪。這些工具可以讓你按一個(gè)按鈕或者每次更新代碼到 GitHub 時(shí)重新構(gòu)建你的桌面應(yīng)用鲜棠。
自動(dòng)更新
NW.js 沒(méi)有支持自動(dòng)更新,但是由于我們可以隨意使用 Node.js培慌,我們可以做任何事情豁陆。開(kāi)源模塊可以幫你實(shí)現(xiàn),比如 node-webkit-updater 可以下載并替換為更新版本的 app吵护。當(dāng)然你也可以自己造輪子盒音。
通過(guò) autoUpdater API,Electron 自帶支持自動(dòng)更新馅而。但是它不支持 Linux 系統(tǒng)祥诽,所以我們建議發(fā)布你的 app 到 Linux 包管理器。不必?fù)?dān)心用爪,這在 Linux 上很常見(jiàn)原押。autoUpdater
API 使用非常簡(jiǎn)單,給定一個(gè) URL 就可以調(diào)用 checkForUpdates
方法偎血。因?yàn)樗鞘录?qū)動(dòng)诸衔,所以你可以訂閱 update-downloaded
事件,一旦該事件觸發(fā)颇玷,就調(diào)用 restartAndInstall
方法來(lái)下載新版本 app 并且重啟笨农。你可以監(jiān)聽(tīng)一些其他的事件,將自動(dòng)更新和用戶界面很好的捆綁起來(lái)帖渠。
注意:你可以使用多個(gè)更新渠道谒亦,比如 Google Chrome 和 Google Chrome Canary。
API 背后的邏輯可就沒(méi)這么簡(jiǎn)單了。它是基于 Squirrel 更新框架份招,用來(lái)區(qū)分 Mac 和 Windows 平臺(tái)切揭,對(duì)應(yīng)的軟件分別是 Squirrel.Mac 和 Squirrel.Windows。
Mac 上的 Electron app 和更新有關(guān)的代碼非常簡(jiǎn)單锁摔,但是你還是需要一個(gè)簡(jiǎn)單的服務(wù)器廓旬。一旦你調(diào)用 autoUpdater 模塊中的 checkForUpdates
的方法,它會(huì)訪問(wèn)服務(wù)器谐腰。如果沒(méi)有更新孕豹,服務(wù)器返回 204(“No Content”);如果有更新十气,則返回 200 和一個(gè)包含 .zip
文件 URL 的 JSON励背。再回到客戶端 app,Squirrel 知道接下來(lái)該怎么做:它會(huì)下載 .zip
砸西,解壓然后觸發(fā)相應(yīng)的事件叶眉。
Windows 平臺(tái)上 app 的更新需要更多點(diǎn)功夫。你不一定需要一臺(tái)服務(wù)器籍胯。你可以把靜態(tài)文件部署在某些地方竟闪,比如亞馬遜的 AWS S3,或者甚至放在本地機(jī)器杖狼,可以方便測(cè)試炼蛤。雖然 Mac 平臺(tái)上的 Squirrel 和 Windows 平臺(tái)上的 Squirrel 有些不同,但是依然有折中的辦法來(lái)實(shí)現(xiàn)更新蝶涩,比如給每個(gè)平臺(tái)都分別部署一個(gè)服務(wù)器理朋,或者把更新文件放在 S3 或者其他地方。
Squirrel.Windows 有些很不錯(cuò)的特性是 Squirrel.Mac 所沒(méi)有的绿聘。Squirrel.Windows 在后臺(tái)實(shí)現(xiàn)更新嗽上,所以當(dāng)你調(diào)用restartAndInstall
,速度會(huì)更快熄攘,因?yàn)楸镜匾呀?jīng)提前下載好了需要的更新文件兽愤。Squirrel.Windows 也支持 delta 更新,比如 app 檢測(cè)到新版本需要更新挪圾,需要更新的部分會(huì)以補(bǔ)丁包的方式被下載和安裝浅萧,而不是重新下載整個(gè)新的 app。假如當(dāng)前的 app 要比最新版本低三個(gè)版本哲思,Squirrel.Windows 甚至可以按照遞增的方式來(lái)下載和安裝需要的更新洼畅。當(dāng)然如果當(dāng)前 app 已經(jīng)落后最新版本 15 個(gè)版本,Squirrel.Windows 就直接下載和安裝整個(gè)最新的 app棚赔。這些功能底層已經(jīng)幫你實(shí)現(xiàn)好了帝簇,API 使用起來(lái)依然很簡(jiǎn)單徘郭。你只需要檢查更新,系統(tǒng)會(huì)幫你找到最優(yōu)方案實(shí)現(xiàn)更新丧肴,并且告知用戶更新完畢残揉。
注意:雖然這些補(bǔ)丁包也必須部署在服務(wù)器上,但是 electron-builder 會(huì)幫你生成這些文件闪湾。
感謝 Electron 社區(qū)冲甘,讓我們不一定非要構(gòu)建自己的服務(wù)器。有很多開(kāi)源項(xiàng)目幫助你實(shí)現(xiàn)把更新文件部署在 S3 上途样,或者用 GitHub release,甚至還有提供后臺(tái)控制面板來(lái)管理不同的更新版本濒憋。
桌面應(yīng)用和網(wǎng)頁(yè)應(yīng)用的對(duì)決
那么桌面 app 到底和 web app 有些哪些不同何暇?讓我們來(lái)看看你可能遇到的一些意想不到的問(wèn)題或收獲,比如在 web 平臺(tái)上使用 API 的副作用以及工作流中的痛點(diǎn)還有維護(hù)困難等凛驮。
第一件事情就是瀏覽器限定(browser lock-in)裆站,你也許會(huì)因此暗自高興。假如你只做桌面 app黔夭,你很清楚用戶用的是哪個(gè)版本的 Chromium宏胯。讓我們來(lái)假設(shè)一下:你可以在 app 當(dāng)中用到 flexbox,ES6本姥,原生的 WebSocket肩袍,WebRTC 以及任何你想到的東西。你甚至可以在 app 當(dāng)中開(kāi)啟尚在測(cè)試的 Chromium 特性婚惫,或者允許使用 localStorage氛赐。你根本不用處理任何跨瀏覽器的兼容問(wèn)題∠认希基于 Node.js API 和 NPM艰管,你可以做任何事情。
注意:但你依然需要考慮用戶在使用什么樣的操作系統(tǒng)蒋川。不過(guò)相比較不同瀏覽器之間的問(wèn)題牲芋,跨操作系統(tǒng)的兼容性處理要更簡(jiǎn)單些。
處理 file://
另外一個(gè)有趣的事情是你的 app 要做到離線優(yōu)先(offline-first)捺球。在構(gòu)建 app 的時(shí)候需要牢記的是缸浦,用戶即使在沒(méi)有網(wǎng)路的情況下也能正常使用 app,載入本地文件懒构。你需要認(rèn)真考慮 app 在網(wǎng)絡(luò)條件差的情況下餐济,如何正常工作。你可能需要改變思考問(wèn)題的方式胆剧。
注意:你可以載入遠(yuǎn)程 URL絮姆,但是我不建議這么做醉冤。
我給出的建議是不要完全相信 navigator.onLine
。這個(gè)屬性會(huì)返回布爾值來(lái)反饋是否存在網(wǎng)絡(luò)連接篙悯,不過(guò)請(qǐng)注意誤報(bào)蚁阳。如果有本地連接它就返回 true 而不去驗(yàn)證連接的有效性。網(wǎng)絡(luò)連接雖然顯示成功鸽照,但是可能實(shí)際上無(wú)法正常訪問(wèn)網(wǎng)頁(yè)螺捐。比如本地機(jī)器到 Vagrant 虛擬機(jī)的連接會(huì)被誤認(rèn)為是成功的網(wǎng)絡(luò)連接。所以矮燎,請(qǐng)使用 Sindre Sorhus 的 is-online
來(lái)復(fù)核網(wǎng)絡(luò)連接狀態(tài)定血。它會(huì) ping 互聯(lián)網(wǎng)的根服務(wù)器或者一些著名網(wǎng)站的 favicon 文件。比如:
const isOnline = require('is-online');
if(navigator.onLine){
// hmm there's a connection, but is the Internet accessible?
isOnline().then(online => {
console.log(online); // true or false
});
}
else {
// we can trust navigator.onLine when it says there is no connection
console.log(false);
}
說(shuō)到本地文件诞外,有幾件事情需要注意澜沟,比如你無(wú)法使用少協(xié)議(protocol less)的 URL,我的意思是比如用 //
代替 http://
或者 https://
峡谊。理論上茫虽,如果一個(gè) web app 在請(qǐng)求 //example.com/hello.json
時(shí),瀏覽器會(huì)把地址擴(kuò)展為 http://example.com/hello.json
或者 https://example.com/hello.json
(如果當(dāng)前頁(yè)面是通過(guò) HTTPS 加載)既们。在我們的 app 當(dāng)中濒析,如果這么做,當(dāng)前頁(yè)面會(huì)使用 file://
協(xié)議啥纸。所以号杏,當(dāng)我們請(qǐng)求同樣的 URL 時(shí)候,app 會(huì)把地址擴(kuò)展為 file://example.com/hello.json
然后請(qǐng)求失敗脾拆。我們真正要擔(dān)心的是那些第三方模塊馒索;那些作者可能并沒(méi)有按照桌面 app 的思路來(lái)制作模塊。
你不會(huì)使用到 CDN名船,因?yàn)檩d入本地文件基本上是瞬間完成的绰上。而且不像瀏覽器,你沒(méi)有同時(shí)請(qǐng)求數(shù)量的限制渠驼,至少不會(huì)像 HTTP/1.1 那樣蜈块。你可以并發(fā)載入盡可能多的文件。
大量文件生成
構(gòu)建一個(gè)可靠穩(wěn)固的桌面 app 需要生產(chǎn)大量的文件迷扇。你需要為一個(gè)自動(dòng)更新的系統(tǒng)生成可執(zhí)行文件和安裝包百揭。然后對(duì)應(yīng)的每一個(gè)更新,都需要再次構(gòu)建可執(zhí)行文件和更多的安裝包(因?yàn)槿绻腥巳ツ愕木W(wǎng)站下載蜓席,他們應(yīng)當(dāng)下載到最新版本)以及針對(duì)增量更新(delta update)的更新補(bǔ)丁器一。
文件大小仍然是一個(gè)需要考慮的問(wèn)題。一個(gè)“Hello, World!”的 Electron app 壓縮包是 40 MB厨内。在構(gòu)建 web app 的時(shí)候祈秕,除了遵循一些常見(jiàn)規(guī)則外(比如寫更少的代碼渺贤、壓縮文件、使用更少的依賴等等)请毛,我可以提供的意見(jiàn)不多志鞍。“Hello World” app 本質(zhì)上就是一個(gè)包含了 HTML 文件的 app方仿;占 app 體積的絕大多數(shù)文件是來(lái)自 Chromium 和 Node.js固棚。至少在 Windows 平臺(tái)上增量更新可以有效減少下載文件的大小。但是我希望用戶不要在 2G 網(wǎng)絡(luò)上去下載文件仙蚜。
預(yù)判意外狀況
在日后你一定會(huì)遇到一些意想不到的事情此洲。有些事情要比其他更明顯而且讓人惱火。比如你制作了一個(gè)音樂(lè)播放器的 app鳍征,它支持迷你化黍翎,在其他應(yīng)用之上用小窗口展示。假如用戶點(diǎn)擊了下拉菜單艳丛,app 會(huì)展示可選項(xiàng),從 app 的底部邊界溢出趟紊。如果你使用了非原生的包(比如 select2 或者 chosen)氮双,你會(huì)因此陷入麻煩。在打開(kāi)下拉菜單的時(shí)候霎匈,它會(huì)被 app 的底部邊界切割戴差。用戶會(huì)看到很少的選項(xiàng)甚至什么也看不到,這確實(shí)讓人無(wú)語(yǔ)铛嘱。當(dāng)然這件事也會(huì)發(fā)生在瀏覽器上暖释。但是用戶不太可能會(huì)調(diào)整窗口到那么小。
你也許會(huì)知道墨吓,在 Mac 上每一個(gè)窗口都有一個(gè) header 和 body球匕。當(dāng)窗口沒(méi)有聚焦的時(shí)候,如果你把鼠標(biāo)停留在 header 里面的圖標(biāo)或者按鈕上帖烘,窗口的外觀會(huì)對(duì)應(yīng)的顯示為鼠標(biāo)停留狀態(tài)亮曹。舉個(gè)例子,macOS 上窗口的關(guān)閉按鈕在未被停留時(shí)是灰色模糊的秘症,當(dāng)鼠標(biāo)停留時(shí)照卦,按鈕變成紅色。但是如果鼠標(biāo)只是停留在 body 上乡摹,窗口外觀不會(huì)發(fā)生改變役耕。這是有意而為之的設(shè)計(jì)。讓我們?cè)倩氐轿覀兊淖烂?app聪廉,基于 Chromium 的 app 是沒(méi)有 header瞬痘,整個(gè) web app 就是窗口 body故慈。你可以不用原生的框架而創(chuàng)建自己的 HTML 按鈕來(lái)取代原生的最小化,最大化還有關(guān)閉按鈕图云。如果窗口沒(méi)有被聚焦惯悠,當(dāng)鼠標(biāo)停留的時(shí)候,窗口不會(huì)有任何變化竣况。Hover 的樣式?jīng)]有被應(yīng)用克婶,這總讓人感覺(jué)不太對(duì)。更糟糕的是丹泉,只有在點(diǎn)擊關(guān)閉按鈕的時(shí)候情萤,窗口才會(huì)被聚焦。然后你還得再次點(diǎn)擊關(guān)閉按鈕來(lái)真正關(guān)閉當(dāng)前窗口摹恨。
雪上加霜的是筋岛,Chromium 有一個(gè) bug 可以掩蓋這個(gè)問(wèn)題,讓你以為窗口會(huì)按照你期待的樣子工作晒哄。把鼠標(biāo)從窗口外移動(dòng)到窗口內(nèi)的元素睁宰,如果你移動(dòng)得足夠快,hover 樣式會(huì)被應(yīng)用寝凌。這是已經(jīng)確認(rèn)的 bug柒傻。把 hover 樣式應(yīng)用在一個(gè)模糊化的窗口 body 上“并不滿足當(dāng)前系統(tǒng)平臺(tái)的要求”,日后該 bug 會(huì)被修復(fù)较木。但愿我上面說(shuō)的話不會(huì)讓你太心碎红符。事實(shí)上,你可以創(chuàng)建一個(gè)足夠漂亮的自定義窗口控制區(qū)伐债,但現(xiàn)實(shí)是許多用戶會(huì)因此苦惱(他們會(huì)懷疑這到底是不是原生的)预侯。
所以你必須用到 Mac 原生的按鈕。沒(méi)有其他更好的辦法了峰锁。對(duì)于 NW.js app萎馅,你必須開(kāi)啟使用原生框架(你也可以通過(guò)在 package.json
里面把 window
的屬性 frame
設(shè)置為 false
來(lái)關(guān)閉使用原生框架)。
Electron app 也可以實(shí)現(xiàn)同樣效果祖今。比如設(shè)置 new BrowserWindow({width: 800, height: 600, frame: true})
來(lái)創(chuàng)建窗口校坑。Electron 官方團(tuán)隊(duì)就是這么做的,他們還加入另外一種不錯(cuò)的選項(xiàng):把 titleBarStyle
設(shè)置成 hidden
會(huì)隱藏原生標(biāo)題欄但是通過(guò)覆蓋 app 左上角來(lái)保留原生的窗口控制千诬。 這樣就解決了之前的問(wèn)題耍目,但同時(shí)可以使用在左上角使用自定義按鈕。
// main.js
const {app, BrowserWindow} = require('electron');
let mainWindow;
app.on('ready', () => {
mainWindow = new BrowserWindow({
width: 500,
height: 400,
titleBarStyle: 'hidden'
});
mainWindow.loadURL('file://' + __dirname + '/index.html');
});
下面這張圖徐绑,我禁用了標(biāo)題欄然后設(shè)置了html
的背景圖片:
詳見(jiàn) Electron 官方文檔 “Frameless Window57”
工具
你可以盡情地使用在構(gòu)建 web app 時(shí)候用到的工具邪驮。你的 app 其實(shí)就是 HTML,CSS 還有 JavaScript 不是嗎傲茄?針對(duì)桌面 app 開(kāi)源社區(qū)也有豐富的插件和模塊供你使用毅访,比如你可以用 Gulp 插件來(lái)為你的 app 簽名(如果你不打算用 electron-builder)沮榜。Electron-connect 可以用來(lái)監(jiān)控文件改動(dòng),如果主要的腳本文件有改動(dòng)喻粹,它會(huì)在打開(kāi)的窗口中應(yīng)用這些改動(dòng)或者重啟 app蟆融。畢竟這就是 Node.js,你可以做任何事情守呜。你也可以在 app 中用到 webpack 如果你想的話型酥,雖然我不知道為什么要這么做,但這也是一個(gè)選擇嘛查乒。詳情見(jiàn) awesome-electron 獲取更多資源弥喉。
版本發(fā)布流程
維護(hù)和開(kāi)發(fā)一個(gè)桌面應(yīng)用是怎么樣的體驗(yàn)?首先玛迄,發(fā)行版本流是完全不一樣的由境。觀念上就需要重新調(diào)整。在開(kāi)發(fā) web app 的時(shí)候蓖议,如果部署了之后然后遇到問(wèn)題虏杰,這些都不是事。你直接修復(fù) bug 就行了勒虾。新用戶直接訪問(wèn)頁(yè)面或者老用戶重新加載頁(yè)面就能得到最新的代碼嘹屯。開(kāi)發(fā)者一旦有新任務(wù),就直接去完成任務(wù)或者修復(fù) bug 就好了婚肆。但是開(kāi)發(fā)桌面 app 可不是這樣舌劳。一旦冒失犯錯(cuò),就無(wú)法撤回。這特別像開(kāi)發(fā)移動(dòng) app 一樣征绸。你構(gòu)建了 app,然后發(fā)布翅萤,就不可能撤回了窒盐。有些用戶可能都不會(huì)從立即更新到最新的修復(fù)版本。這些存在于舊版本的 bug 可能會(huì)讓你非程杜悖苦惱雄妥。
量子力學(xué)
考慮到要服務(wù)于不同版本的 app,你的代碼會(huì)以不同的形式和狀態(tài)而存在依溯。多個(gè)版本的客戶端(桌面 app)會(huì)以多種方式訪問(wèn)你的 API老厌。所以你得認(rèn)真考慮 API 的版本控制問(wèn)題,做好測(cè)試黎炉。當(dāng) API 有變化時(shí)枝秤,你無(wú)法獲知此次變動(dòng)會(huì)不會(huì)造成問(wèn)題。一個(gè)月前發(fā)布的版本可能會(huì)因?yàn)橐恍┐a的變動(dòng)而發(fā)生崩潰慷嗜。
亟待解決的問(wèn)題
你也許會(huì)遇到一些很奇怪的問(wèn)題淀弹,一些涉及到奇怪的賬戶管理丹壕,反病毒軟件或者更糟。我之前遇到過(guò)一個(gè)案例薇溃,用戶自己安裝某些文件導(dǎo)致系統(tǒng)環(huán)境變量被修改菌赖。這直接導(dǎo)致了我們的 app 當(dāng)中某個(gè)重要的依賴安裝失敗,因?yàn)橄到y(tǒng)命令無(wú)法找到沐序。這些案例提醒我們有些情況下必須劃清界限琉用,這對(duì)我們的 app 很重要,所以不能忽略報(bào)錯(cuò)薄啥,但我們也不能幫用戶修好電腦辕羽。對(duì)于遇到這種問(wèn)題的用戶,他們的多數(shù)桌面應(yīng)用頂多也是無(wú)法正常啟動(dòng)垄惧。最后我們決定如果再次報(bào)錯(cuò)刁愿,用戶會(huì)看到一條鏈接到文檔的報(bào)錯(cuò)信息,這個(gè)文檔用來(lái)解釋錯(cuò)誤為什么會(huì)發(fā)生到逊,同時(shí)告訴用戶如何一步步去修復(fù)錯(cuò)誤铣口。
當(dāng)然,一些基于 web 的顧慮將不再適配于桌面 app觉壶,比如一些歷史遺留的瀏覽器問(wèn)題脑题。但有一些新的問(wèn)題需要考慮,比如在 Windows 上文件路徑有 256 字節(jié)大小的限制铜靶。
舊版本的 npm 采用遞歸的文件結(jié)構(gòu)存儲(chǔ)依賴叔遂。你的依賴都各自存儲(chǔ)在項(xiàng)目中的 node_modules
目錄下的文件夾里(例如, node_modules/a
)争剿。如果依賴模塊自己本身也有依賴模塊已艰,這些子級(jí)的子級(jí)依賴會(huì)被存儲(chǔ)在父級(jí)的 node_modules
中,比如 node_modules/a/node_modules/b
蚕苇。因?yàn)?Node.js 和 npm 鼓勵(lì)使用小巧的單用途模塊哩掺,你可能會(huì)很容易遇到長(zhǎng)路徑,比如 path/to/your/project/node_modules/a/node_modules/b/node_modules/c/.../n/index.js
涩笤。
注意:版本 3 之后 npm 盡可能地扁平化依賴關(guān)系樹(shù)嚼吞。但是也存在一些其他原因?qū)е麻L(zhǎng)路徑。
我們之前遇到一個(gè)問(wèn)題蹬碧,就是在特定版本的 Windows 上因?yàn)槁窂教L(zhǎng) app 無(wú)法正常啟動(dòng)或者啟動(dòng)之后就崩潰舱禽。這是個(gè)很頭痛的問(wèn)題。使用 Electron 時(shí)锰茉,你可以把所有代碼放在 asar archive 當(dāng)中呢蔫。雖然使用這種方法也存在例外而不能保證永遠(yuǎn)都能正常使用。
我們做了一個(gè)小小的 Gulp 插件 gulp-path-length 用來(lái)告知開(kāi)發(fā)者當(dāng)前 app 當(dāng)中是否存在任何危險(xiǎn)的長(zhǎng)文件路徑。終端用戶將 app 放在哪里才能最終決定是否存在長(zhǎng)文件路徑片吊。舉個(gè)例子绽昏,假如安裝包安裝在 C:\Users\<username>\AppData\Roaming
,當(dāng) app 構(gòu)建完成(在本地通過(guò)持續(xù)集成服務(wù)完成)俏脊,gulp-path-length 會(huì)用來(lái)監(jiān)控是否當(dāng)前目錄下存在長(zhǎng)文件路徑(比如用戶機(jī)器上的用戶名過(guò)長(zhǎng)而導(dǎo)致問(wèn)題)全谤。
var gulp = require('gulp');
var pathLength = require('gulp-path-length');
gulp.task('default', function(){
gulp.src('./example/**/*', {read: false})
.pipe(pathLength({
rewrite: {
match: './example',
replacement: 'C:\\Users\\this-is-a-long-username\\AppData\\Roaming\\Teamwork Chat\\'
}
}));
});
關(guān)鍵性錯(cuò)誤真的很致命
因?yàn)樗械淖詣?dòng)更新都發(fā)生在 app 內(nèi)部,在每次檢查更新前爷贫,未捕獲的異常會(huì)導(dǎo)致 app 崩潰认然。假設(shè)你發(fā)現(xiàn)了一個(gè) bug 然后發(fā)布了新版本進(jìn)行修復(fù)。如果用戶啟動(dòng) app漫萄,自動(dòng)更新開(kāi)始下載卷员,然后 app 崩潰。如果用戶重新啟動(dòng) app腾务,自動(dòng)更新再次下載毕骡,再次崩潰...所以,你必須想盡辦法讓用戶知道他們需要重新安裝 app岩瘦。相信我未巫,這確實(shí)很糟糕。
分析和 bug 報(bào)告
你很可能想追蹤 app 的使用情況和各種錯(cuò)誤启昧。首先 Google Analytics 不起作用叙凡。你得找到一個(gè)分析工具可以支持 file://
URL。如果你正使用工具來(lái)追查錯(cuò)誤密末,假如工具支持發(fā)布版本追蹤握爷,一定要確保錯(cuò)誤和版本掛鉤。例如严里,如果你使用 Sentry 追蹤錯(cuò)誤饼拍,確保在設(shè)定客戶端的時(shí)候設(shè)定了正確的 release
屬性 ,這樣錯(cuò)誤會(huì)按照版本分類田炭。否則當(dāng)你收到錯(cuò)誤報(bào)告準(zhǔn)備修復(fù)錯(cuò)誤的時(shí)候,你會(huì)持續(xù)收到錯(cuò)誤報(bào)告和日志漓柑,這當(dāng)中會(huì)包含一些誤報(bào)教硫。而這些誤報(bào)來(lái)自用戶正在使用舊版本 app。
Electron 包含了 crashReporter
模塊辆布,該模塊在 app 完全崩潰后(例如整個(gè) app 崩潰瞬矩,而不是錯(cuò)誤拋出)自動(dòng)向開(kāi)發(fā)者發(fā)送報(bào)告。你也可以監(jiān)聽(tīng)一些事件用來(lái)指示 app 的渲染進(jìn)程無(wú)法響應(yīng)锋玲。
安全
當(dāng)接收用戶輸入或者信任第三方腳本的時(shí)候需要格外注意景用,因?yàn)閻阂夤粽邥?huì)用各種意想不到的方式來(lái)使用 Node.js。而且記住永遠(yuǎn)不要在未經(jīng)檢查直接接受用戶輸入并傳值到原生 API 或者命令。
也不要相信來(lái)自 vendors 的代碼伞插。我們最近遇到的問(wèn)題來(lái)自公司 X 的分析應(yīng)用的第三方代碼片段割粮。官方團(tuán)隊(duì)在發(fā)布的新版本當(dāng)中包含了問(wèn)題代碼,導(dǎo)致了 app 致命錯(cuò)誤媚污。當(dāng)用戶啟動(dòng) app 的時(shí)候舀瓢,代碼片段從 CDN 獲取最新的 JavaScript 代碼然后運(yùn)行,隨后拋出異常導(dǎo)致 app 無(wú)法繼續(xù)運(yùn)行耗美。任何正在運(yùn)行的 app 都不會(huì)受到影響京髓,但是一旦重新打開(kāi) app 就會(huì)產(chǎn)生問(wèn)題。我們聯(lián)系公司 X 客服商架,隨后他們發(fā)布了修復(fù)版本堰怨。如果再次重啟 app 就會(huì)正常運(yùn)行了,雖然已經(jīng)解決了問(wèn)題蛇摸,但是回頭想想還是很讓人擔(dān)心备图。如果我們不去強(qiáng)制受影響的用戶手動(dòng)下載修復(fù)版本的 app,我們自己就很難直接解決問(wèn)題皇型。
該怎么樣才能規(guī)避風(fēng)險(xiǎn)呢诬烹?也許你可以試著捕獲報(bào)錯(cuò),但是你完全不知道公司 X 在 JavaScript 里面究竟做了什么弃鸦。你最好使用更可靠穩(wěn)固的代碼绞吁。你可以加入一層抽象,不直接在 <script>
指向公司 X 的URL而使用 Google Tag Manager 或者你自己的 API 來(lái)返回包含有 <script>
標(biāo)簽的 HTML 文件或者包含所有第三方依賴的單獨(dú)的 JavaScript 文件唬格。這樣在避免重新安裝新版本的情況下家破,指定任意第三方代碼片段被加載。
但是购岗,假如 API 不再返回用來(lái)分析的代碼片段汰聋,之前被代碼片段創(chuàng)建的全局變量依然會(huì)存在你的代碼當(dāng)中,這些全局變量會(huì)嘗試調(diào)用未定義的函數(shù)喊积。所以我們并沒(méi)有完全解決問(wèn)題烹困。而且,如果用戶沒(méi)有聯(lián)網(wǎng)就打開(kāi) app乾吻,API 調(diào)用會(huì)失敗髓梅。你并不想在離線時(shí)限制你的 app。當(dāng)然你可以用上次成功請(qǐng)求的緩存文件來(lái)用作離線版本的加載绎签。但是如果當(dāng)前版本出現(xiàn)問(wèn)題怎么辦枯饿,你又回到了之前提到的問(wèn)題(如果不強(qiáng)制用戶下載新版本,app 就會(huì)崩潰)诡必。
另外一種解決方案是創(chuàng)建一個(gè)隱藏窗口加載包含了所有第三方代碼片段的本地HTML 文件奢方。這樣,任何由全局變量導(dǎo)致的問(wèn)題會(huì)在這個(gè)隱藏窗口里報(bào)錯(cuò),而主要窗口不受影響蟋字。如果你需要在主要窗口當(dāng)中調(diào)用 這些 API 或者 全局變量稿蹲,你可以通過(guò) IPC 的方式來(lái)實(shí)現(xiàn)。通過(guò) IPC 向主進(jìn)程發(fā)送一個(gè)事件愉老,然后該事件會(huì)被發(fā)送到隱藏窗口當(dāng)中场绿。如果隱藏窗口沒(méi)有任何問(wèn)題,它會(huì)監(jiān)聽(tīng)事件同時(shí)調(diào)用第三方函數(shù)嫉入。這樣就可以解決之前提到的問(wèn)題焰盗。
這會(huì)帶來(lái)安全問(wèn)題。萬(wàn)一來(lái)自公司 X 的惡意攻擊者在他們的 JavaScript 中包含有危險(xiǎn)的 Node.js 代碼咒林?我們肯定死慘了熬拒。幸運(yùn)的是,Electron 里有一個(gè)很不錯(cuò)的設(shè)置用來(lái)禁止在給定窗口中執(zhí)行 Node.js 代碼垫竞,使惡意代碼不會(huì)運(yùn)行:
// main.js
const {app, BrowserWindow} = require('electron');
let thirdPartyWindow;
app.on('ready', () => {
thirdPartyWindow = new BrowserWindow({
width: 500,
height: 400,
webPreferences: {
nodeIntegration: false
}
});
thirdPartyWindow.loadURL('file://' + __dirname + '/third-party-snippets.html');
});
自動(dòng)化測(cè)試
NW.js 本身不包含對(duì)測(cè)試的支持澎粟。但是由于你可以使用 Node.js, 技術(shù)上欢瞪,測(cè)試是可行的活烙。 例如 Chrome Remote Interface 可以用來(lái)測(cè)試 app 當(dāng)中的按鈕點(diǎn)擊。但這個(gè)還是有點(diǎn)牽強(qiáng)遣鼓,因?yàn)槟銦o(wú)法觸發(fā)原生窗口按鈕的點(diǎn)擊啸盏,也就無(wú)法測(cè)試。
Electron 官方團(tuán)隊(duì)開(kāi)發(fā)了 Spectron 用來(lái)自動(dòng)測(cè)試骑祟。它支持測(cè)試原生控制按鈕回懦,管理窗口還有模擬 Electron 事件。它甚至可以在持續(xù)集成構(gòu)建中運(yùn)行次企。
var Application = require('spectron').Application
var assert = require('assert')
describe('application launch', function () {
this.timeout(10000)
beforeEach(function () {
this.app = new Application({
path: '/Applications/MyApp.app/Contents/MacOS/MyApp'
})
return this.app.start()
})
afterEach(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop()
}
})
it('shows an initial window', function () {
return this.app.client.getWindowCount().then(function (count) {
assert.equal(count, 1)
})
})
})
考慮到你的 app 就是 HTML 文件怯晕,僅僅在靜態(tài)文件中添加指向測(cè)試工具的腳本,你可以用任何工具來(lái)測(cè)試 web app缸棵。但是你得確保 app 可以在沒(méi)有 Node.js 的 web 瀏覽器中依然可以運(yùn)行舟茶。
桌面和 Web
這不僅僅是關(guān)乎桌面 app 或者 web app。作為一個(gè) web 開(kāi)發(fā)者堵第,你可以用任何工具制作 app 確保在任何平臺(tái)和環(huán)境中運(yùn)行稚晚。但是為什么沒(méi)有一勞永逸的辦法呢?我們還需要努力型诚,但這是值得的。接下來(lái)我會(huì)提到一些相關(guān)的話題和工具鸳劳,考慮到它們太過(guò)復(fù)雜狰贯,我就點(diǎn)到為止。
首先,忘記什么“瀏覽器限定”和原生 WebSockets 等等其他的事情涵紊。ES6 也是如此.你要么寫純粹的 ES5傍妒,要么用類似 Babel 的工具來(lái)把 ES6 代碼編譯成 ES5,供 web 使用摸柄。
你的代碼里也會(huì)寫滿了許多瀏覽器不會(huì)理解的 require
(用來(lái)引入其他腳本文件或者模塊)颤练。使用支持 CommonJS 的模塊打包器,比如 Rollup驱负,webpack 或者 Browserify嗦玖。當(dāng)構(gòu)建 web app 的時(shí)候,模塊打包器會(huì)遍歷代碼跃脊,找到所有的 require
然后把他們放在一個(gè)腳本文件里宇挫。
任何用到 Node.js 或者 Electron API(比如寫盤操作或者集成桌面環(huán)境)的代碼都不應(yīng)該在 app 運(yùn)行在 web 端的時(shí)候被調(diào)用。你可以通過(guò)檢測(cè) process.version.nwjs
和 process.versions.electron
是否存在來(lái)判斷酪术。如果存在器瘪,則表明 app 當(dāng)前運(yùn)行在桌面環(huán)境。
即便如此绘雁,你仍會(huì)在 web app 上加載大量冗余代碼橡疼。假設(shè)你的代碼中 if(app.isInDesktop)
后面緊接著和桌面環(huán)境有關(guān)的 require
代碼。與其在 app 運(yùn)行的時(shí)候來(lái)檢測(cè)當(dāng)前運(yùn)行環(huán)境庐舟,同時(shí)設(shè)置對(duì)應(yīng)的 app.isInDesktop
欣除,不如把 true
和 false
當(dāng)做 flag 在構(gòu)建的時(shí)候傳值到 app。在它進(jìn)行靜態(tài)和樹(shù)狀分析(也就是消除無(wú)用代碼)時(shí)继阻,這將有助于模塊捆綁的選擇耻涛。它會(huì)知道 app.isInDesktop
是否為 true
。因此瘟檩,當(dāng)你運(yùn)行 web app 的時(shí)候抹缕,它不會(huì)到代碼里去找對(duì)應(yīng)的 if
條件,或者找到相關(guān)的 require
墨辛。
持續(xù)交付
我們對(duì)于版本發(fā)行的觀念也需要換一換了卓研,這非常有挑戰(zhàn)性。當(dāng)你在開(kāi)發(fā) web app 的時(shí)候睹簇,你希望能夠頻繁發(fā)布新的改動(dòng)奏赘。我相信在持續(xù)交付中,小的增量改動(dòng)可以快速回滾太惠。理想情況是磨淌,經(jīng)過(guò)足夠的測(cè)試,一個(gè)實(shí)習(xí)生也可以把改動(dòng)的代碼 push 到 master 分支凿渊,然后讓 web app 自動(dòng)測(cè)試和部署梁只。
我們之前談到缚柳,你不能像 web app 那樣在桌面 app 中實(shí)現(xiàn)同樣的效果。沒(méi)錯(cuò)搪锣,理論上如果你使用 Electron 的話秋忙,electron-builder 可以自動(dòng)測(cè)試,而且 spectron 也可以測(cè)試构舟。我不知道還有誰(shuí)這么做灰追,我自己不會(huì)有信心這么做。記住狗超,錯(cuò)誤的代碼不可以撤銷弹澎,你可能打破正常的更新流。而且抡谐,你也不想讓桌面 app 更新太過(guò)頻繁裁奇。更新不會(huì)悄無(wú)聲息的發(fā)生,不像 web app 那樣麦撵,這對(duì)于用戶來(lái)說(shuō)其實(shí)很不友好刽肠。而且在 macOS 上不支持增量更新,用戶必須針對(duì)每一個(gè)發(fā)行版本都要下載完整的新版本的 app免胃,不管更新是多么的小音五。
你得找到一個(gè)平衡點(diǎn)。一個(gè)妥協(xié)的做法是針對(duì) web app 要盡可能快的更新和修復(fù)問(wèn)題羔沙,對(duì)于桌面 app 每周或者每月更新一次就可以躺涝,除非你要發(fā)布新功能。你也不能指責(zé)用戶選擇安裝桌面 app扼雏。沒(méi)有什么比等待很久來(lái)發(fā)布新功能更糟糕的事情了坚嗜。你可以采用功能發(fā)布控制器(feature-flag)API 來(lái)在同一平臺(tái)同一時(shí)間發(fā)布新功能,但這又是另外一個(gè)話題了诗充。我第一次學(xué)習(xí)和了解到功能發(fā)布控制器是來(lái)自 Etsy 的工程師 VP苍蔬,Mike Brittain 的講話,持續(xù)交付:骯臟的細(xì)節(jié)(需翻墻)
總結(jié)
那么你已經(jīng)掌握了蝴蜓。只要一點(diǎn)點(diǎn)努力碟绑,你就可以在簡(jiǎn)歷中加上”桌面 app 開(kāi)發(fā)者“的標(biāo)簽了。我們從創(chuàng)建第一個(gè)現(xiàn)代桌面 app茎匠,打包格仲,分發(fā),講到售后服務(wù)還有更多诵冒。但愿我提到的一些陷阱和坑對(duì)你來(lái)說(shuō)并沒(méi)有那么可怕凯肋。你已經(jīng)知道它們的前因后果了。你需要做的就是看一遍 API 文檔汽馋。感謝那些可供我們?nèi)我馐褂玫膹?qiáng)大的 API侮东,你可以從 web 開(kāi)發(fā)者的技能樹(shù)上獲取更多有價(jià)值的東西午笛。我希望可以在 NW.js 和 Electron 社區(qū)中看到你的身影。
掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū)苗桂,文章來(lái)源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android告组、iOS煤伟、React、前端木缝、后端便锨、產(chǎn)品、設(shè)計(jì) 等領(lǐng)域我碟,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃放案。