寫在最開始
這是一篇面向移動端開發者的科普性文章,從前端開發的最初流程開始,結合示範程式碼,討論開發流程的演變過程,希望能覆蓋一部分前端開發技術棧,從而對前端開發的相關概念形成初步的認識。
本文會提供一些示範程式碼,然而他們無法執行,也不需要完全看懂,更多的是方便讀者對相關概念和方案有更加具體形象的感受和更清晰的理解。
在寫作過程中,我閱讀學習了淘寶在前後端分離和前端開發技術演變方面的部落格,受益匪淺,相關文章都羅列在文末的參考資料中。同時由於自身能力有限,對很多概念的理解比較淺顯,甚至有誤,歡迎交流指正。
移動端與前端的區別
在開發 App 的過程中,我們不會刻意思考開發流程,因為一切看上去都非常自然。可以本地確定的內容就直接寫死,否則非同步發起網路請求並動態的修改,最後把原始碼編譯成可執行的二進位制檔案供客戶安裝。
前端開發和移動端開發就有本質的不同了。一個網頁的最終展現樣式受到 HTML + CSS 的影響,而 JavaScript 指令碼負責使用者互動。一個頁面不會被編譯成可執行檔案,它僅僅由幾個文字檔案組成,由服務端明文下發給瀏覽器並繪製到螢幕上。
下文中可能會反覆提到“渲染”的概念,除非特別說明,它不表示解析 HTML 文件(DOM)並繪製到螢幕上這個過程,因為這一步由瀏覽器核心實現,普通情況下不需要做過多瞭解和干預。
網頁可以分為靜態、動態兩種,前者就是一個 HTML 檔案,後者可能只是一份模板,在請求時動態的計算出資料,最後拼接成 HTML 格式的字串,這個過程就被稱為渲染。
前端與移動端開發另一個顯著差異是: 雖然可以在本地除錯 HTML,但實際上這些 HTML 的內容需要部署在服務端,這樣才能在使用者發起 HTTP 請求時返回 HTML 格式的文字。
前端開發的混沌時代
一開始,我們沒有任何工具,只能靠蠻力。我們知道 Servlet 是由 Java 編寫的服務端程式,可以方便的處理 HTTP 請求和返回,因此可以直接把 HTML 文字當做字串返回,也就是上文所說的渲染:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class HelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.println("<html><head><title>Hello World Sample</title></head>"); out.println("<body><h1>Hello World Title<h1><h2>" +new Date().toLocaleString() + "</h2></body></html>"); out.flush(); } } |
理論上來說,我們已經可以開始所有前端開發了,但在這混沌初開的年代,想寫出一份可維護的程式碼恐怕得用上“洪荒之力”。把 UI 和業務邏輯寫在一起是一種非常強的耦合,不利於專案日後的擴充。也無法要求每個人同時會寫 Java 和前端。
後端 MVC
上述方案的問題之一在於邏輯混亂不清,移動開發者在入門階段大多也經歷過,解決方案比較簡單:使用 MVC,把業務邏輯抽離到 Controller 中,讓 View 層專注於顯示 UI。
MVC 方案實現
在前端開發領域,也有類似的技術,比如 JSP,它經過編譯後變成 Servlet。在寫 JSP 的時候,我們更加關心頁面樣式,因此程式碼看起來就像是 HTML,不過在 程式碼塊中可以呼叫 Java 函式:
1 2 3 4 5 6 7 8 9 10 |
<HTML> <HEAD> <TITLE>JSP測試頁面---HelloWorld!</TITLE> </HEAD> <BODY> <% out.println("<h1>Hello World!</h1>"); %> </BODY> </HTML> |
JSP 相當於 View 層,它從 Model 中獲取資料,說的再具體一點,是使用後端的語言(比如 Java)去訪問 Model 層提供的介面。
Controller 作為直接和客戶端接觸的模組,負責解析請求,資料校驗,路由分發,結果返回等等邏輯。路由分發是指根據請求路徑的不同,呼叫不同的 Model 和 View 提供服務。
MVC 的缺點與改進
使用了 MVC 架構(比如大名鼎鼎的 Struts)後,似乎職責變清晰了,前端開發者負責寫 JSP,後端開發者負責寫 Controller 和 Model,然而在實際開發時還是有諸多問題。
首先業務邏輯依然沒有嚴格區分,如果沒有良好的編碼規範,JSP 中就會混入大量業務邏輯。而一個框架存在的作用應該是讓沒有接受很多培訓的新人也能寫出合格的程式碼。此外,前端開發者還需要對後端邏輯有大致的瞭解,熟悉後端程式語言,因此也存在很多溝通、學習成本。
前端只寫 Demo
一種解決方案是前端開發者只寫 Demo,也就是提供靜態的 HTML 效果給後端開發者,由後端開發者去完成檢視層(比如 JSP)的開發。這樣前端開發者就完全不需要了解後端知識了。
可惜這種想法很好,但是一旦付諸實現就會遇到不少問題。首先後端開發者依賴於前端的 Demo,只有看到 HTML 檔案他們才可以開始實現 View 層。而前端又依賴於後端開發者完成整體的開發,才能通過網路訪問來檢查最終的效果,否則他們無法獲取真實的資料。
更糟糕的是,一旦需求發生變動,上述流程還需要重新走一遍,前後端的交流依舊無法避免。概況來說就是前後端對接成本太高。
舉個例子,在開發 App 的時候,你的同事給你發來一段程式碼,其中是一個本地寫死的檢視,然後告訴你:“這個按鈕的文字要從資料庫,那個圖片的內容要通過網路請求獲取,你把程式碼改造一下吧。”。於是你花了半天時間改好了程式碼,PM 跑來告訴你按鈕文字寫死就好,但是背景顏色要從資料庫讀取,另外,再加一個按鈕吧。WTF?
顯然這種開發流程效率極低,難以接受。
HTML 模板
其實一定程度上來說,JSP 可以看做 HTML 模板的雛形。HTML 大體上肩負了兩個任務: 頁面框架和內容描述。所謂的 HTML 模板是指利用一種結構化的語法,表示出 HTML 的框架,同時把具體資料抽離出來。
比如 <p>111</p>
表示一個段落,其中內容為 “111”。如果用模板來表示,可以寫作 <p>Content</p>
或者 p Content
等等。總之,不要糾結於具體語法,我們只要知道:
資料 + 模板 = HTML 原始碼
比如在 Controller 層,可以這樣呼叫:
1 2 |
// 這裡沒有指定模板的名稱,是因為使用了依賴倒置的思想,在配置檔案中繫結了 Controller 對應的 View return res.view({title: "111"}); |
模板中的程式碼如下:
1 |
<h1><%= Content%></h1> |
熟悉前端開發的讀者可能會發現,這其實採用了 Sails.js + EJS 開發。前者是基於 JavaScript 的服務端應用程式,後者是基於 HTML 語法的模板,另一種風格的模板是 Jade,不過本文的目的並不是重點介紹這些工具如何使用,就不再贅述了。
回到模板的概念上來,它相對於 JSP 的優勢在於,利用工具強行禁止前端開發者在檢視層寫業務邏輯。前端開發者只需要關心 UI 實現並確定 HTML 中的變數。而後端開發者只要傳入引數即可獲取 HTML 格式的字串。
模板開發的另一個好處是前後端可以同步開發。雙方約定一個資料格式,前端就可以模擬出假資料並用來自測,後端也可以用生成的資料與假資料對比進行測試。同時,這個約定的資料格式扮演了契約和文件的作用,規範了雙方的資料交流形式,從而節省交流的時間成本。關於更多 Mock Server 的話題,請參考 這個連線。
後端 MVC 架構總結
使用後端 MVC 架構加上模板開發是當前比較主流的一種開發模型,但它也不是完美的。由於模板由前端開發者完成,所以要求前端開發者對後端環境(注意這裡不是實現細節)有所瞭解。
舉個簡單例子,大型應用的後端要分很多資料夾,這就要求前端對程式碼組織結構有所瞭解,上傳檔案時需要掌握 ssh、vim 並且依賴於服務端環境。
總的來說,採用服務端 MVC 方案時,HTML 在後端渲染,整體開發流程也全部基於後端環境。因此前端工程師不可避免的需要依賴於後端(雖然使用模板後情況已經大大改善)。
AJAX 與前端 MVC
AJAX 的全稱是 Asynchronous Javascript And XML,即 “非同步 JavaScript 和 XML”。它並非一個框架,而是一種程式設計思想。它利用 JavaScript 非同步發起請求,結果以 XML 格式返回,隨後 JavaScript 可以根據返回結果區域性操作 DOM。
AJAX 最大的優點是不用重新載入全部資料,而是隻要獲取改動的內容即可。這在移動端程式設計中看上去是天經地義的,而前端開發則需要專門使用 AJAX 來實現,預設情況下網頁的任何一處微小改動都需要重新載入整個網頁。
類比移動應用就會發現,AJAX 適合做單頁面更新,但是不擅長頁面跳轉,就像你的 app 頁面跳轉都是新建一個 UIViewController/Activity 而不是直接把當前頁面的內容全部換掉。
得益於 AJAX 的提出,HTML 在前端渲染變成了可能。我們可以下載一個空殼 HTML 檔案和一個 JavaScript 指令碼,然後在 JavaScript 指令碼中獲取資料,為 DOM 新增節點。
這時候就出現了很多前端的 MVC 框架,比如 Backbone.js,AngularJS(姑且認為MVVM 是 MVC 的變種) 等一堆名詞,你可以從 這裡 找到他們各自的 Demo。以我相對熟悉的 React 為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<!DOCTYPE html> <html> <head> <script src="../build/react.js"></script> <script src="../build/react-dom.js"></script> <script src="../build/browser.min.js"></script> </head> <body> <div id="example"></div> <script type="text/babel"> var LikeButton = React.createClass({ getInitialState: function() { return {liked: false}; }, handleClick: function(event) { this.setState({liked: !this.state.liked}); }, render: function() { var text = this.state.liked ? 'like' : 'haven\'t liked'; return ( <p onClick={this.handleClick}> You {text} this. Click to toggle. </p> ); } }); ReactDOM.render( <LikeButton />, document.getElementById('example') ); </script> </body> </html> |
這段程式碼不用完全讀懂,你只要意識到,引入 React.js 這個框架後,我們就可以脫離 HTML 程式設計了。所有的邏輯都寫在 標籤塊中的 JavaScript 程式碼中。
我們還建立了一個 LikeButton
元件,它可以擁有自己的方法,管理自己的狀態等等。這裡舉 React 的例子可能略有不妥,因為它其實只是一個 View 層的實現方案,還需要配合 Flux 或 Redux 架構。不過也足以感受一下純 JavaScript 開發的感覺了。
這種開發模式和移動端開發非常類似,使用 JavaScript 呼叫 DOM 介面來繪製檢視,使用 JavaScript 來實現業務邏輯,處理資料,發起網路請求等。你完全可以理解為單純用 JavaScript 在瀏覽器上開發移動應用,只不過使用者下載的是 JavaScript 指令碼而非傳統靜態語言編譯後的二進位制檔案。
使用了前端 MVC 框架後,對於單頁應用(Single Page Application)來說,前後端各司其職,唯一的聯絡就變成了 API 呼叫,前端開發者不再依賴後端環境,HTML 和 JavaScript 都可以放在 CDN 伺服器上。這就是我們常說的 前後端分離 的基本概念,即前端負責展現,後端負責資料輸出。
前後端分離的缺點
然而在我看來,上述前後端分離方案遠遠遜色於後端 MVC + 模板的開發流程,這是因為在實際開發中,純 SPA 的場景並不多見,即使移動端也不是隻有一個檢視,而是有很多頁面跳轉邏輯,更何況到處都是超連結的網頁呢?
我們來看看在 SPA 和多頁面跳轉並存的情況下,採用前端 MVC 框架進行前後端分離存在哪些不足。
雙端 MVC 不統一
前端的 MVC 主要處理單個頁面內的邏輯,而後端 MVC 框架處理的是整個 Web 伺服器的邏輯。借用 jsconf 大會 上 赫門 的圖片來表示:
前後端 MVC 架構示意圖
由於前後端 MVC 框架關注的重點不同,它們的地位自然也不同。前端的 MVC 框架負責頁面展示,因此它只是後端 MVC 框架的 View 層(或許只是一部分 View)。
這就會導致如下問題:
- 前端的 Model 和後端的 Model 結構上高度類似,比如前端用到一個
name
屬性,後端也得在 JavaBean 中定義一個name
。有時候為了避免前端對資料做太多邏輯處理從而導致效能下降,後端可能已經做了一些預處理。這就好比我們寫 App 時,拿到的 feed 流可能是篩選、排序過的,而我們在移動端還要在 ViewModel 中做一些轉化才能給 View 使用。因此,前後端邏輯上的耦合還是無法完全避免。 - 前端的 Controller 負責頁面排程,比如控制元件的狀態管理和樣式改變等。而後端的 Controller 負責呼叫服務,使用者鑑權等。兩者完全不等價。
- 前端也有路由模組,它主要負責頁面內控制元件之間的跳轉,比如 React-Router,而後端路由則是把不同的網路請求分發給指定的 Controller。兩者邏輯也無法統一。
SEO
SEO(搜尋引擎優化)是一個移動開發者從來不考慮,但前端開發者視作生命的問題。搜尋引擎的工作原理是訪問每個網頁,然後分析 HTML 中的標籤和關鍵字並做記錄。
一個純非同步的網頁,HTML 幾乎是空殼子,而偏偏關鍵的資料都是動態下發的,這就影響了搜尋引擎爬蟲的工作過程,他們會認為該網頁什麼都沒有,即使記錄下來的也是非關鍵資料。
早些年穀歌推出了 Hash-bang 協議 來彌補 AJAX 對 SEO 造成的負面影響,它的本質是為爬蟲提供後端渲染的降級處理機制。目前谷歌的爬蟲一定程度上可以閱讀 JavaScript 程式碼並爬取相關資料,但 AJAX 在對爬蟲的支援上終究不如 HTML 文字直接傳輸。
效能不夠
從上文中 React 的示範程式碼可以看出,HTML 檔案非常小,很快就被開啟。但是頁面的渲染邏輯直到 JavaScript 檔案被下載後才能開始執行,這就會導致一段時間的白屏。
在行動網路上,前端渲染 HTML 的效能當然不如後端渲染,頻繁傳送 HTTP 請求也會影響載入體驗,因此依賴於前端渲染的頁面,在效能方面還有很大的提高空間。
集中 Or 分離?
很多年前,JavaScript 和 CSS 並不用單獨寫在外部檔案中,而是直接內嵌在 HTML 程式碼裡:
1 2 3 4 |
<p style="background-color:green" onclick="javascript:myFunction()"> This is a paragraph. </p> |
為了便於管理和修改,我們把 CSS 和 JavaScript 分離出來。然而到了 React 中,好像走了回頭路,所有邏輯都放在 JavaScript 中。
我的理解是,這種做法適合元件化,我們很容易定義出一個元件並且重用。這種思想對於複雜的 SPA 來說或許適用,但對於並不複雜但有多個頁面的網頁來說,就顯得太重了。引入了 React 這樣的框架,以及 MVC 的結構,反而會顯得過度設計,增加程式碼量和複雜度。
考慮到之前所說的前後端邏輯不能複用的問題,這就更容易導致效能問題。
Node.js
前後端分離的哲學
至此,我們已經嘗試過後端 MVC 架構,HTML 模板,前端 MVC 架構等多種方案,但結果總是難以令人滿意,是時候總結原因了。
我們在進行上述實踐的過程中,過度強調物理層上的前後端分離,但是忽略了兩者天然就存在一定的耦合。實際上,前端開發者不僅關注 View 的實現,還應該負責一部分 Controller 中的邏輯,後端開發者則應該關心資料獲取與處理,以及一些跨終端的業務邏輯。
如果頁面渲染在後端實現,會導致前端開發者依賴後端實現和開發環境,後端開發者被迫熟悉前端邏輯(此時不是呼叫 API 而是直接生成資料並套用模板,這就要求把獲取的資料轉換成模板需要的資料)。
如果頁面渲染全部放在前端,業務邏輯就會太靠前,從而導致不能複用。這種做法似乎有些矯枉過正了。此外,上文中也介紹了不少 AJAX 的缺點,就不贅述了。
我們似乎陷入了兩難的境地,頁面渲染不管是放在前端還是後端都不合適。其實這很好理解,頁面渲染涉及資料邏輯和 UI,他們理應分別由前後端開發者分別負責,單獨交給任何一方都顯得不合適。
但如果前端工程師可以寫後端程式碼,問題不就迎刃而解了麼?實際上資料的處理可以分為兩個步驟:從資料庫或第三方服務獲取資料,把資料轉化為 View 可用的形式。前者往往和 C++/Java 伺服器相關,後者則和前端模板相關,它的作用更像是 MVVM 架構中的 ViewModel。
Node.js 分層
我在上一篇文章中初步介紹了 Node.js 的定位:“一個用 JavaScript 進行開發的後端應用程式框架”。因此它恰好可以完美的解決前端不瞭解後端邏輯和程式碼的問題。
Node.js 作為一箇中間層,呼叫上游 Java 伺服器提供的服務,獲取資料。自身負責處理業務邏輯,路由請求,cookie 讀寫,頁面渲染等。前端則負責應用 CSS 樣式和 JavaScript 互動,類似於最早期原始的模型。
借用 玉伯的 Web研發模式演變 中的圖片來說明:
這裡 還有一個解釋的非常詳細的表格以供參考。
實戰應用
這不是一篇介紹 Node.js 的部落格,我也不熟悉相關框架的應用,舉這個例子是為了演示 Node.js 是如何做前後端分離的。
我選擇了 Sails.js 框架,專案的程式碼在 Github: sails-react-example,這裡簡單的分析一下。
檢視都放在 views
目錄下,採用 EJS 為模板,由 Node.js 負責在服務端渲染:
1 2 3 4 |
<div class="container"> <h1><%= __('Comment') %>s for SAILS<small>js</small> + REACT<small>js</small></h1> </div> <script src="/js/commentMain.js"></script> |
Controllers 負責頁面轉發與模板渲染,具體的服務轉交給 Services 去完成:
1 2 3 4 5 6 |
module.exports = { app : function(req, res) { // 如果有必要,在這裡呼叫 Services 獲取資料 return res.view({}); }, }; |
這個 Demo 中沒有實現 Services,通常它用於和真正的後端進行互動,可以視情況選擇 HTTP 或 SOAP,並對返回結果做處理。此外還有 policies/responses 模組分別對 HTTP 請求和返回內容做處理。
前端的相關程式碼都封裝在模板層,能夠與 Node.js 無縫接合。
風險控制
雖然我們用增加 Node.js 處理層的方式解決了前後端分離中的一些痛點,但在實際應用中還是需要考慮得更加周全。
新增一層後,勢必會導致效能損耗。然而分層本就是一個在衡量得失後做出的權衡,可以通過各種優化把效能損耗降到最低。況且,在 Node.js 這一層還可以使用 BigPipe 來處理多個非同步請求。
傳統網頁在載入頁面時,首先獲取 HTML,然後獲取其中引用的 CSS 和 JavaScript。在服務端準備資料和網路傳輸過程中,瀏覽器需要一直等待,而 BigPipe 將頁面分成若干小塊,雖然每個塊的載入邏輯不變,但塊與塊之間可以形成流水線作業,避免瀏覽器無意義的等待。
使用 BigPipe 技術在一定場景下可以代替 Ajax 的多個非同步請求。具體介紹可以參考 BigPipe學習研究。
使用 Node.js 後,對前端開發者的技術要求提高了,編碼工作量也會相應的增加。然而這都是工程化的必經之路,編碼量增加的背後其實是溝通、維護效率的提高。
總結
為了處理前後端複雜的邏輯,我們嘗試了使用了後端 MVC 框架來分離業務,用 HTML 模板來分離資料和 UI 樣式。使用了 Ajax 技術的網頁更適合單頁應用,雖然做到了物理層的分離,但在處理多頁面時還是有不少問題。
實際上頁面渲染本就是前後端共同關心的話題,我們更應該根據業務邏輯進行前後端分離。最終選擇了 Node.js,藉助它使用 JavaScript 的特性,由前端工程師負責獲取資料後的處理和模板的使用,分擔了一部分原本邏輯上屬於前端,但技術上偏向後端的任務。這種開發模式看上去像是一種倒退,其實是螺旋式的上升與返璞歸真。
Node.js 基於事件和非同步 I/O 的特性,以及優秀的處理高併發的能力非常適合前後端分離的場景,使用 BigPipe 等技術也足以將分層帶來的損耗降到最低。選擇 Node.js 做前後端分離並不一定最佳實踐,但在目前看來有不錯的應用,同時也需要一邊探索一邊前進。
參考資料
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式