[譯] 設計大型 JavaScript 應用程序

這是我在 JavaScript 澳大利亞開發者大會(JSConf AU)上演講內容的文字編輯記錄。在 YouTube 上觀看整個演講視頻javascript

幻燈片文本:你好,我曾經構建過很是大型的 JavaScript 應用。前端

你好,我曾經構建過很是大型的 JavaScript 應用。我再也不那麼作了,因此我認爲如今是個好時機來回顧並分享我學到的東西。昨天我在會議聚會上喝啤酒時,有人問我:「嘿,馬爾特,到底是什麼賦予了你權利和權威,來說這個話題?」我想這個問題的答案實際上就是這個演講的主題,儘管我一般以爲談論本身有點奇怪。大概是由於,我在谷歌構建了這樣一個 JavaScript 框架。它被 Google 照片,Google 協做平臺,Google+,Google 雲端硬盤,Google Play,搜索引擎,全部這些網站使用。其中一些項目很是大,你可能已經使用了其中的一些。java

幻燈片文本:我認爲 React 很好。react

這個 Javascript 框架不是開源的。它不是開源的緣由是它與 React 同時出現,我想「世界是否真的須要另外一個 JS 框架來作選擇?」。谷歌已經擁有了一些 JS 框架,Angular 和 Polymer,而且我以爲再有一個會讓人們感到困惑,因此我只是認爲咱們應該把它留給咱們本身。但除了不是開源的,我認爲仍是有不少東西能夠從中學習,值得分享咱們一路上學到的東西。android

一張人山人海的圖片.webpack

因此,咱們來談談很是大型的應用,以及他們之間的共同點。固然可能會有不少開發者參與其中。可能有幾十人甚至更多,他們都有本身的情感和人際問題,你必需要考慮到這一點。ios

一張很是古老建築的圖片.

即便你的規模不大,也許你已經在這個領域工做了一段時間,也許你甚至不是第一個維護它的人,你可能不瞭解項目的全部結構或者內容,可能有些東西是你不太明白的,你的團隊中可能還有其餘人不瞭解應用程序的全部信息。這些都是咱們在構建很是大型的應用程序時,必須考慮的事情。git

推特: 一個沒有初級工程師的高級工程師團隊是一個工程師團隊。github

我想在這裏作的另外一件事是以咱們的職業生涯說明下背景。我想咱們不少人會認爲本身是高級工程師。或者是還差一點點,但咱們想成爲一個高級工程師。我認爲高級的意思是我幾乎能夠解決其餘人可能拋出的任何問題。我熟悉個人工具,我熟悉個人領域。而這項工做的另外一個重要部分是我讓初級工程師最終成爲高級工程師。web

幻燈片文本:初級 -> 高級 -> ?

可是會發生什麼呢?在某種程度上,咱們可能會懷疑「下一步多是什麼?」。當咱們達到這個高級階段時,咱們接下來要作什麼?對於咱們中的一些人來講,答案多是作管理,但我認爲這不該該成爲每一個人的答案,由於不是每一個人都應該成爲管理者,對嗎?咱們中有些人是很是優秀的工程師,爲何咱們不該該在咱們的餘生中也這樣作?

幻燈片文本:「我知道我會如何解決問題」

我想提出一種方法來升級到高級水平。我把本身看成高級工程師的方式是,我會說:「我知道如何解決這個問題」,而且由於我知道如何解決這個問題,因此我也能夠教別人去解決它。

幻燈片文本:「我知作別人怎麼解決這個問題」

個人理論是,下一個層次是我能夠對本身說:「我知道別人會如何解決這個問題」。

幻燈片文本:「我能夠預測 API 選擇和抽象如何影響其餘人解決問題的方式。」

讓咱們更具體一點。你說了這樣一句話:「我能夠預見我作出 API 選擇時,或者我往項目中引入抽象時,它們如何影響其餘人解決問題。」我認爲這是一個強大的概念,可讓我思考我所作的選擇對應用程序的影響。

幻燈片文本:同理心的應用。

我會稱之爲同理心的應用。你在和其餘軟件工程師一塊兒思考,你在思考你所作的事情以及你給他們的 API 是怎麼樣的,以及它們如何影響其餘工程師編寫軟件。

幻燈片文本:對簡易模式感同身受。

幸運的是,這是對簡易模式感同身受。同理心一般很難,並且這仍然很是困難。但至少與你有同感的人,他們也是軟件工程師。儘管他們可能與你大相徑庭,但他們至少同你同樣也在開發軟件。當你得到更多的經驗時,你能夠很擅長的運用這種類型的同理心。

幻燈片文本:編程模型。

考慮到這些話題,我想談談一個很是重要的術語,那就是編程模型,這個詞我會用不少次。它表明「給定一套 API,庫,框架或工具,人們如何在這種背景下編寫軟件」。我演講的真正內容是關於 API 等細微變化對編程模型的影響。

幻燈片文本:影響編程模型的示例:React,Preact,Redux,Date picker,npm。

我想舉幾個影響編程模型的例子:假設你有一個 Angular 項目,而且你說:「我將把它移植到 React 中」,這顯然會改變人們編寫軟件的方式,對吧?可是接下來你想:「啊哈,60 KB 就爲了使用一點虛擬 DOM 操做,讓咱們切換到 Preact」,這是一個 與 React API 兼容的庫,即便你作出了這個選擇,它也不會改變人們編寫軟件的方式。或許隨着項目的進展,你會以爲「單單 React 自有的狀態管理還不夠,應用會變得很複雜,我應該有一些東西來管理應用狀態,我會引入 Redux」,這將改變人們編寫軟件的方式。而後又來了個新需求「咱們須要一個日期選擇器」,你到 npm 上進行搜索,有 500 個結果,你選了一個日期組件。你挑選哪個真的很重要嗎?它絕對不會改變你編寫軟件的方式。可是,npm 以及它的龐大生態集合,絕對會改變你編寫軟件的方式。固然,這些只是可能影響人們如何編寫軟件的幾個例子。

幻燈片文本:代碼分割.

如今我想談談全部大型 JavaScript 應用在將它們交付給用戶時的一個共同點:它們最終變得很是大,以致於你不但願一開始就把整個應用一次性傳輸給用戶。爲此,咱們引入了這種稱爲代碼分割的技術。代碼分割意味着你爲應用程序定義了一組打包。因此,你會說「有些用戶只使用個人應用程序的這一部分,有些用戶使用另外一部分」,所以,當用戶實際使用應用程序時,只有使用到的部分才被下載執行。這是咱們全部人均可以作到的。像許多事情同樣,它是由閉包編譯器實現的 —— 至少在 JavaScript 世界中。但我認爲使用 webpack 進行代碼分割是最流行的方式。若是你使用的是 RollupJS,這是超棒的,他們最近也增長了對代碼分割的支持。代碼分割絕對是大家應該作的事情,可是當你將它引入到應用程序中時有一些事情須要考慮,由於它確實對編程模型有影響。

幻燈片文本:同步 -> 異步。

你有過去是同步如今成爲異步的東西。你的應用程序在沒有代碼分割時,簡單美好。整個項目只有一件大事。它啓動,而後它很穩定,你瞭解它的前世此生,你沒必要等待資源加載。有了代碼分割後,有時候你可能會說「哦,我須要那個打包文件」,因此你如今須要利用網絡來獲取所需的文件,這也使得你必須考慮網絡可能出現異常狀況,因此應用程序也變得更加複雜。

幻燈片文本:人性化。

此外,咱們須要有人介入,由於代碼拆分須要你定義如何打包,須要你考慮什麼時候加載它們,因此,那些在大家團隊的工程師們如今必須決定哪些文件打包到一塊兒,何時加載那些打包文件。每次有人介入時,都會明顯影響編程模型,由於他們必須考慮這些問題。

幻燈片文本:基於路由的代碼分割。

有一種很是成熟的方法能夠解決這個問題,它能夠將咱們從進行代碼分割的混亂中解脫出來,它被稱做基於路由的代碼分割。若是你尚未使用代碼分割,那它多是你初次進行代碼分割的方式。路由將應用程序以 URL 粒度進行分割。例如,你的產品頁面可能在 /product/ 上,而且你的分類頁面可能在其餘地方。你只需將每一個路由用的文件打包到一塊兒,而後你的應用程序將根據路由自動進行代碼分割。不管什麼時候用戶訪問路由,路由都會加載相關的打包文件,有了路由以後,你能夠忘記代碼分割的存在。再從編程模型上來看,這幾乎與將全部東西都打包到一塊兒同樣。這是一種很是好的代碼分割方法,絕對是個好的開始。

可是這個演講的主題是設計很是大型的 JavaScript 應用程序,而且這類應用程序很快會變得巨大無比,路由自己也會隨之變大,以致於基於路由的代碼分割再也不適用。實際上我有一個關於這類應用程序的好例子。

「public speaking 101」的谷歌搜索查詢截圖。

我正在弄清楚如何成爲這場演講的公衆演講者,而且我獲得了一個很好的藍色連接列表。你徹底能夠設想這個頁面很是適合將全部文件打包到一個路由裏。

「weath」的谷歌搜索查詢截圖。

但後來我對天氣感到疑惑,由於加州有一個嚴峻的冬天,忽然間有了這個徹底不一樣的模塊。因此,這個看似簡單的路由比咱們想象的更爲複雜。

「20 usd to aud」的谷歌搜索查詢截圖。

後來我被邀請參加此次會議,我查看了 1 美圓是多少澳元,那時出現了這個複雜的貨幣轉換器。很顯然,這些專用模塊大約有 1000 多個,將它們放在同一個打包文件中是不可行的,由於打包文件的大小會有幾兆字節,用戶將會真的變得不高興。

幻燈片文本:組件級別的懶加載?

因此,咱們不能只使用基於路由的代碼分割,咱們必須想出一個不一樣的方式來作代碼分割。基於路由的代碼拆分很不錯,由於你將應用程序進行了最粗略級別的拆分,而當應用程序進一步增加時,它能起到的做用就微乎其微了。由於我喜歡直截了當,那麼作超級細粒度而不是超級粗粒度拆分怎麼樣。讓咱們想象若是咱們網站的每個組件都懶加載,會發生什麼。當你只考慮帶寬時,從效率的角度來看,這彷佛很是好。從延遲等其餘觀點來看,這多是很是糟糕的,但它確定是值得考慮。

幻燈片文本:React 組件同他們的子組件是靜態依賴關係。

但讓咱們想象一下,例如,你的應用程序使用 React。而且在 React 中,組件們同他們的子組件是靜態依賴關係。這意味着若是你懶加載你的子組件,就會改變你的編程模型,而且事情會變得不那麼美好,這讓你只好叫停這種策略。

ES6 導入示例。

假設你有一個貨幣轉換器組件,你想把它放在你的搜索頁面上,你能夠導入它,是這樣的吧?這是在 ES6 模塊中使用的普通方式。

Loadable 組件示例。

可是若是你想延遲加載它,你會獲得這樣的代碼,你把它包裝在 Loadable 組件中,你還使用一種懶加載 ES6 模塊的新方式動態導入。固然有成千上萬種方法能夠作到這一點,我不是 React 專家,但全部這些方式都會改變你編寫應用程序的方式。

幻燈片文本:靜態 -> 動態。

事情再也不那麼美好了 —— 一些靜態的東西如今變成了動態的,這是編程模型改變的另外一個警示。

幻燈片文本:誰來決定什麼時候對什麼東西進行懶加載?

你不得已忽然想知道:「誰來決定什麼時候對什麼東西進行懶加載」,由於這會影響到應用程序的等待時間。

幻燈片文本:靜態仍是動態?

人類再次出現,他們必須思考「有靜態導入,有動態導入,何時該用哪個?」。弄錯就很是糟糕了,當一個靜態導入的文件忽然變成動態導入的時候,可能會把某些東西錯誤的打包進文件。隨着時間的推移,同時你又有不少工程師在這個項目上開發,恐怕就會出錯。

幻燈片文本:分割邏輯和渲染。

接下來我會分享 Google 如何作到保證良好編程模型的前提下,又有不錯的性能的。咱們經過渲染邏輯和應用邏輯來分割組件,好比當你按下貨幣轉換器上的按鈕時發生的狀況。

幻燈片文本:僅在渲染時加載是惟一的加載邏輯。

因此,如今咱們有兩件獨立的事情,而且咱們只在渲染時才加載組件的應用程序邏輯。事實證實,這是一個很是簡單的模型,由於你能夠簡單地在服務端渲染頁面,而後由實際呈現的內容,觸發下載關聯的應用程序打包文件。由於加載是經過渲染自動觸發的,這使得人得以脫離系統。

幻燈片文本:搜索結果頁面上的貨幣轉換器。

這個模型看起來不錯,但它確實有一些折中。若是你知道一般服務端渲染在 React 或 Vue.js 等框架中如何工做,這個過程被稱爲 hydration。hydration 是這樣的,你服務端渲染的一些東西,而後在客戶端再次渲染它,這意味着你必須加載代碼來渲染一些已經在頁面上的東西,這在加載代碼和執行代碼方面都是巨大的浪費。這麼作既浪費帶寬,又浪費 CPU —— 但它確實很好,由於你在客戶端忽略了服務端渲染的東西。咱們在 Google 使用的方法不是那樣的。因此,若是你設計這個很是大型的應用程序,你就會想:我是採用那種更復雜的超快速方法,仍是採用效率較低的 hydration 方式,但這樣能有個良好的編程模型?你將不得不作出這個決定。

幻燈片文本:2017 新年快樂。

個人下一個話題是我最喜歡的計算機科學問題 —— 它不是命名問題,儘管我極可能給它起了個糟糕的名字。這是「2017 年假期特別問題」。過去有人寫過一些代碼,如今再也不須要它們了,但它仍然在你的代碼庫中?...這種狀況時常發生,我認爲 CSS 的問題尤其突出。你有一個大型 CSS 文件。裏面有不少樣式選擇器。誰真的知道哪些樣式選擇器是否仍然對應着你應用中的內容?因此,你最終只能把那些代碼留在那裏。我認爲 CSS 社區處於變革的最前沿,由於他們意識到這個問題,而且他們建立了諸如 CSS-in-JS 之類的解決方案。由於你的組件能夠放到一個單獨的文件裏,2017 年假期特別問題組件,你能夠說「它再也不是 2017 問題」,你能夠刪除整個組件,而且全部相關文件一併消失。這使得刪除代碼很是容易。我認爲這是一個很是好的想法,它不只僅適用於 CSS。

幻燈片文本:不惜一切代價避免中央配置。

我想舉幾個例子,說明爲何你想不惜一切代價避免在你的應用程序中採用中央配置,由於中央配置(好比大型 CSS 文件)使得代碼難以刪除。

幻燈片文本:routes.js。

我以前在你的應用程序中談論過路由。許多應用程序都會有一個相似「routes.js」的文件,其中包含全部路由信息,這些路由將本身映射到某個根組件。這是一箇中央配置的例子,你不會但願在大型應用程序中這麼作。由於有了這種中央配置,工程師會說:「我還須要那個根組件嗎?我須要更新其餘文件,那是其餘團隊負責的文件。我不肯定是否被容許修改它。也許我該明天再作「。以後,這些文件只會愈來愈大。

幻燈片文本:webpack.config.js。

這種反模式的另外一個例子是 webpack.config.js 文件,在這裏你能夠假設你經過它構建了整個應用程序。剛開始可能沒什麼問題,但隨着時間的推移,這份配置再也不適用,你須要知道其餘團隊在應用程序中作了什麼,這樣才能對配置文件作出兼容性的調整。再一次,咱們須要一個模式來展示如何分散咱們構建過程的配置。

幻燈片文本:package.json。

這有一個很好的例子:npm 使用的 package.json。每一個軟件包都會說「我有這些依賴關係,這就是你如何運行我,如何構建個人方式」。顯然,對於全部的 npm,都不能有一個巨大的配置文件。這對於成千上萬的文件來講不起做用。這確定會讓你在 git 操做中遇到不少合併衝突。固然,npm 很是大,但我認爲咱們的許多應用程序已經變得足夠大,讓咱們不得不擔憂一樣的問題,而且必須採用相同的模式。我沒有全部的解決方案,但我認爲 CSS-in-JS 的想法將會涉及咱們應用程序的其餘方面。

幻燈片文本:依賴關係樹。

更抽象地說,我會描述這個想法,即咱們負責如何抽象地設計咱們的應用程序,如何組織它,做爲承擔塑造咱們的應用程序的依賴樹的責任。當我說「依賴」時,個人意思是很是抽象的。它多是模塊依賴關係,多是數據依賴關係,服務依賴關係,還有不少不一樣的類型。

幻燈片文本:由路由和 3 個根組件構成的依賴關係樹示例。

顯然,咱們都有超複雜的應用程序,但我會用一個很是簡單的例子。它只有 4 個組成部分。它有一個路由,知道如何從應用程序的一個路由到下一個路由,它有幾個根組件:A、B 和 C。

幻燈片文本:中心導入問題。

正如我以前提到的那樣,這具備中心導入問題。

幻燈片文本:由路由和3個根組件構成的依賴關係樹示例。路由導入根組件。

由於路由如今必須導入全部的根組件,若是你想刪除其中的一個,你不得不進入路由文件,刪除引用,刪除路由,並最終你有了 2017 假期特別問題。

幻燈片文本:導入 -> 加強。

咱們在谷歌已經爲此提出了一個解決方案,我想向大家介紹一下,我想咱們歷來沒有談過這件事。咱們提出了一個新概念。它被稱爲加強。這是你用來代替導入的東西。

幻燈片文本:導入 -> 加強。

實際上,這與導入是相反的。這是一個逆向依賴。若是你加強一個模塊,你會讓這個模塊對你有依賴性。

幻燈片文本:由路由和3個根組件構成的依賴關係樹示例。根組件加強了路由。

看看依賴關係圖,它發生了什麼,仍然是相同的組件,但箭頭指向相反的方向。所以,不是路由導入根組件,根組件宣佈本身加強了路由的功能。這意味着我能夠經過刪除文件來刪除根組件。由於它再也不加強路由,因此這是刪除組件的惟一操做。

幻燈片文本:誰來決定什麼時候使用加強?

這真的很棒,若是它不是再次涉及人性化。他們如今必須考慮「我是該導入它,仍是使用加強?我在哪一種狀況下使用哪種方式?」。

圖片:危險。危險化學品。

這是這個問題的特別糟糕的狀況,由於加強模塊的能力,可以使系統中的全部其餘東西都依賴於你是很是強大的,若是出錯的話,就會很是危險。很容易想象這可能會致使很是糟糕的狀況。因此,在谷歌咱們認爲這是一個好主意,但咱們也認爲它是非法的,沒有人可使用它 —— 有一個例外:生成的代碼。它實際上很是適合於生成的代碼,它解決了生成代碼的一些固有問題。有了生成的代碼,你有時必須導入你甚至看不到的文件,必須猜想他們的名字。可是,若是生成的文件剛好不可見,並加強了它所需的任何內容,那麼你就沒有這些問題。你根本不須要知道這些文件。他們只是神奇地加強了中央註冊表。

幻燈片文本:單文件組件指向其加強路由的組件。

咱們來看一個具體的例子。咱們這裏有個單文件組件。咱們在其上運行代碼生成器,並從中提取這個小的路由定義文件。那個路由文件只是說「嘿,路由,我在這裏,請導入我」。顯然,你能夠將這種模式用於各類其餘事情。也許你正在使用 GraphQL,你的路由應該知道你的數據依賴關係,那麼你可使用相同的模式。

幻燈片文本:基本打包文件。

不幸的是,這不只僅是咱們所須要知道的。第二個我最喜歡的計算機科學問題,我稱之爲「基礎垃圾打包文件」。在應用程序的打包邏輯中的基本打包文件老是會被加載,而與用戶與應用程序的交互方式無關。因此,這一點尤爲重要,由於若是它很大,那麼全部進一步深刻的東西都會很大。若是它很小,那麼依賴文件也有可能變小。一個小故事:在某個時候,我加入了 Google Plus JavaScript 基礎架構團隊,而且我發現他們的基礎打包文件包含 800 KB 的 JavaScript。因此,我對你的警告是:若是你想比 Google Plus 更成功,就不要讓你的 JS 基礎打包文件超過 800 KB,但不幸的是你的文件體積很難維持在理想狀態。

幻燈片文本:指向 3 個不一樣依賴關係的基礎打包文件。

這有一個例子。你的基礎打包文件須要依賴於路由,由於當你從 A 到 B 時,你須要知道 B 的路由,因此它老是在周圍。可是你真正不想要的是將任何形式的 UI 代碼打包進基礎打包文件,是由於取決於用戶如何進入你的應用程序,可能會有不一樣的用戶界面。因此,例如日期選擇器絕對不該該放在你的基礎打包文件中,結帳流程也不該該。但咱們如何防止這種狀況?不幸的是導入很是脆弱。你可能在無心中導入那個很酷的工具包,由於它有一個函數來生成隨機數。如今有人說「我須要一種自動駕駛汽車的實用工具」,而且忽然將自動駕駛汽車的機器學習算法導入到你的基礎打包文件中。相似這樣的事情很容易發生,由於導入是傳遞性的,因此問題每每會隨着時間的推移而累積起來。

幻燈片文本:禁止依賴測試。

咱們找到的解決方案是禁止依賴測試。禁止依賴測試是一種斷言,例如你的基礎打包文件不依賴於任何 UI。

幻燈片文本:斷言基本打包文件不依賴於 React.Component。

咱們來看一個具體的例子。在 React 中,每一個組件都須要繼承自 React.Component。所以,若是你的目標是基本打包文件中沒有 UI,只需添加一個測試來肯定 React.Component 不是你基本打包文件的傳遞依賴。

禁止的依賴關係被刪除。

再看一下前面的例子,當有人想添加日期選擇器時,只會出現測試失敗。而這些測試失敗一般很容易就能很好地解決,由於一般這我的並非真的想要添加依賴關係 —— 它只是經過一些傳遞路徑進入。比較這一點,當這種依賴關係已經存在了 2 年,由於你沒有測試。在這些狀況下,一般很難經過重構代碼來擺脫依賴關係。

幻燈片文本:最天然的路徑。

理想狀況下,你會發現最天然的路徑。

幻燈片文本:最直接的方式必須是正確的。

你想要達到這樣一個狀態,不管你的團隊中的工程師作什麼,最直接的方式也是正確的方式 —— 這樣他們就不會離開這條道路,因此他們天然而然地作了正確的事情。

幻燈片文本:不然添加一個確保正確的測試。

這可能不老是可行的。在那種狀況下,只需添加一個測試。但這不是不少人認爲有權作的事情。可是,爲確保你的基礎架構保持不變,請爲你的測試程序添加測試的受權。測試不只僅是爲了測試你的數學函數是否正確。它們也用於基礎架構和應用程序的主要設計特性。

幻燈片文本:避免在應用領域以外進行人爲判斷。

儘量避免在應用領域以外進行人爲判斷。在開發應用程序時,咱們必須瞭解業務,可是並不是團隊中的每位工程師都能理解代碼拆分的原理。並且他們不須要那樣作。在不是每一個人都能理解它們的時候,試着將這些東西以一種友好的方式引入到你的應用程序中,並保持其複雜性。

幻燈片文本:能夠輕鬆刪除代碼。

真的,讓刪除代碼簡單點。個人演講題爲「構建很是大型的 JavaScript 應用程序」。我能夠給出的最佳建議:不要讓你的應用程序變得很是大。最好的辦法是在還來得及的時候開始刪除東西。

幻燈片文本:沒有抽象比錯誤的抽象更好。

我想再談一點,那就是人們有時會說,沒有抽象比錯誤的抽象要好。這實際上意味着錯誤的抽象代價很是高,因此要當心。我認爲這有時會被誤解。這並不意味着你不該該有抽象。這只是意味着你必須很是當心。

咱們必須善於找到正確的抽象

幻燈片文本:同理心和經驗 -> 正確的抽象。

正如我在演講開始時所說的:實現目標的方式是使用同理心,並與團隊中的工程師一塊兒思考他們將如何使用你的 API​​ 以及他們將如何使用抽象。你如何隨着時間的推移充實這種同理心會成爲經驗。綜上所述,同理心和經驗使你可以爲你的應用程序選擇正確的抽象


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索