這是一篇面向移動端開發(fā)者的科普性文章,從前端開發(fā)的最初流程開始,結合示范代碼,討論開發(fā)流程的演變過程,希望能覆蓋一部分前端開發(fā)技術棧,從而對前端開發(fā)的相關概念形成初步的認識。 本文會提供一些示范代碼,然而他們無法運行,也不需要完全看懂,更多的是方便讀者對相關概念和方案有更加具體形象的感受和更清晰的理解。 1 移動端與前端的區(qū)別 在開發(fā) App 的過程中,我們不會刻意思考開發(fā)流程,因為一切看上去都非常自然??梢员镜卮_定的內容就直接寫死,否則異步發(fā)起網絡請求并動態(tài)的修改,最后把源代碼編譯成可執(zhí)行的二進制文件供客戶安裝。 前端開發(fā)和移動端開發(fā)就有本質的不同了。一個網頁的最終展現(xiàn)樣式受到 HTML CSS 的影響,而 JavaScript 腳本負責用戶交互。一個頁面不會被編譯成可執(zhí)行文件,它僅僅由幾個文本文件組成,由服務端明文下發(fā)給瀏覽器并繪制到屏幕上。 下文中可能會反復提到“渲染”的概念,除非特別說明,它不表示解析 HTML 文檔(DOM)并繪制到屏幕上這個過程,因為這一步由瀏覽器內核實現(xiàn),普通情況下不需要做過多了解和干預。 網頁可以分為靜態(tài)、動態(tài)兩種,前者就是一個 HTML 文件,后者可能只是一份模板,在請求時動態(tài)的計算出數(shù)據(jù),最后拼接成 HTML 格式的字符串,這個過程就被稱為渲染。 前端與移動端開發(fā)另一個顯著差異是: 雖然可以在本地調試 HTML,但實際上這些 HTML 的內容需要部署在服務端,這樣才能在用戶發(fā)起 HTTP 請求時返回 HTML 格式的文本。 2 前端開發(fā)的混沌時代 一開始,我們沒有任何工具,只能靠蠻力。我們知道 Servlet 是由 Java 編寫的服務端程序,可以方便的處理 HTTP 請求和返回,因此可以直接把 HTML 文本當做字符串返回,也就是上文所說的渲染: (上下滑動看代碼) 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(); } } 理論上來說,我們已經可以開始所有前端開發(fā)了,但在這混沌初開的年代,想寫出一份可維護的代碼恐怕得用上“洪荒之力”。把 UI 和業(yè)務邏輯寫在一起是一種非常強的耦合,不利于項目日后的拓展。也無法要求每個人同時會寫 Java 和前端。 3 后端 MVC 上述方案的問題之一在于邏輯混亂不清,移動開發(fā)者在入門階段大多也經歷過,解決方案比較簡單:使用 MVC,把業(yè)務邏輯抽離到 Controller 中,讓 View 層專注于顯示 UI。 4 MVC 方案實現(xiàn) 在前端開發(fā)領域,也有類似的技術,比如 JSP,它經過編譯后變成 Servlet。在寫 JSP 的時候,我們更加關心頁面樣式,因此代碼看起來就像是 HTML,不過在 <% %> 代碼塊中可以調用 Java 函數(shù): (上下滑動看代碼) <HTML> <HEAD> <TITLE>JSP測試頁面---HelloWorld!</TITLE> </HEAD> <BODY> <% out.println('<h1>Hello World!</h1>'); %> </BODY> </HTML> JSP 相當于 View 層,它從 Model 中獲取數(shù)據(jù),說的再具體一點,是使用后端的語言(比如 Java)去訪問 Model 層提供的接口。 Controller 作為直接和客戶端接觸的模塊,負責解析請求,數(shù)據(jù)校驗,路由分發(fā),結果返回等等邏輯。路由分發(fā)是指根據(jù)請求路徑的不同,調用不同的 Model 和 View 提供服務。 5 MVC的缺點與改進 使用了 MVC 架構(比如大名鼎鼎的 Struts)后,似乎職責變清晰了,前端開發(fā)者負責寫 JSP,后端開發(fā)者負責寫 Controller 和 Model,然而在實際開發(fā)時還是有諸多問題。 首先業(yè)務邏輯依然沒有嚴格區(qū)分,如果沒有良好的編碼規(guī)范,JSP 中就會混入大量業(yè)務邏輯。而一個框架存在的作用應該是讓沒有接受很多培訓的新人也能寫出合格的代碼。此外,前端開發(fā)者還需要對后端邏輯有大致的了解,熟悉后端編程語言,因此也存在很多溝通、學習成本。 6 前端只寫Demo 一種解決方案是前端開發(fā)者只寫 Demo,也就是提供靜態(tài)的 HTML 效果給后端開發(fā)者,由后端開發(fā)者去完成視圖層(比如 JSP)的開發(fā)。這樣前端開發(fā)者就完全不需要了解后端知識了。 可惜這種想法很好,但是一旦付諸實現(xiàn)就會遇到不少問題。首先后端開發(fā)者依賴于前端的 Demo,只有看到 HTML 文件他們才可以開始實現(xiàn) View 層。而前端又依賴于后端開發(fā)者完成整體的開發(fā),才能通過網絡訪問來檢查最終的效果,否則他們無法獲取真實的數(shù)據(jù)。 更糟糕的是,一旦需求發(fā)生變動,上述流程還需要重新走一遍,前后端的交流依舊無法避免。概況來說就是前后端對接成本太高。 舉個例子,在開發(fā) App 的時候,你的同事給你發(fā)來一段代碼,其中是一個本地寫死的視圖,然后告訴你:“這個按鈕的文字要從數(shù)據(jù)庫,那個圖片的內容要通過網絡請求獲取,你把代碼改造一下吧。”。于是你花了半天時間改好了代碼,PM 跑來告訴你按鈕文字寫死就好,但是背景顏色要從數(shù)據(jù)庫讀取,另外,再加一個按鈕吧。WTF? 顯然這種開發(fā)流程效率極低,難以接受。 7 HTMl模板 其實一定程度上來說,JSP 可以看做 HTML 模板的雛形。HTML 大體上肩負了兩個任務: 頁面框架和內容描述。所謂的 HTML 模板是指利用一種結構化的語法,表示出 HTML 的框架,同時把具體數(shù)據(jù)抽離出來。 比如 <p>111</p> 表示一個段落,其中內容為 “111”。如果用模板來表示,可以寫作 <p>Content</p> 或者 p Content 等等??傊灰m結于具體語法,我們只要知道:
比如在 Controller 層,可以這樣調用:
模板中的代碼如下:
熟悉前端開發(fā)的讀者可能會發(fā)現(xiàn),這其實采用了 Sails.js EJS 開發(fā)。前者是基于 JavaScript 的服務端應用程序,后者是基于 HTML 語法的模板,另一種風格的模板是 Jade,不過本文的目的并不是重點介紹這些工具如何使用,就不再贅述了。 回到模板的概念上來,它相對于 JSP 的優(yōu)勢在于,利用工具強行禁止前端開發(fā)者在視圖層寫業(yè)務邏輯。前端開發(fā)者只需要關心 UI 實現(xiàn)并確定 HTML 中的變量。而后端開發(fā)者只要傳入參數(shù)即可獲取 HTML 格式的字符串。 模板開發(fā)的另一個好處是前后端可以同步開發(fā)。雙方約定一個數(shù)據(jù)格式,前端就可以模擬出假數(shù)據(jù)并用來自測,后端也可以用生成的數(shù)據(jù)與假數(shù)據(jù)對比進行測試。同時,這個約定的數(shù)據(jù)格式扮演了契約和文檔的作用,規(guī)范了雙方的數(shù)據(jù)交流形式,從而節(jié)省交流的時間成本。關于更多 Mock Server 的話題,請參考 這個連接。 8 后端MVC架構總結 使用后端 MVC 架構加上模板開發(fā)是當前比較主流的一種開發(fā)模型,但它也不是完美的。由于模板由前端開發(fā)者完成,所以要求前端開發(fā)者對后端環(huán)境(注意這里不是實現(xiàn)細節(jié))有所了解。 舉個簡單例子,大型應用的后端要分很多文件夾,這就要求前端對代碼組織結構有所了解,上傳文件時需要掌握 ssh、vim 并且依賴于服務端環(huán)境。 總的來說,采用服務端 MVC 方案時,HTML 在后端渲染,整體開發(fā)流程也全部基于后端環(huán)境。因此前端工程師不可避免的需要依賴于后端(雖然使用模板后情況已經大大改善)。 9 AJAX與前端MVC AJAX 的全稱是 Asynchronous Javascript And XML,即 “異步 JavaScript 和 XML”。它并非一個框架,而是一種編程思想。它利用 JavaScript 異步發(fā)起請求,結果以 XML 格式返回,隨后 JavaScript 可以根據(jù)返回結果局部操作 DOM。 AJAX 最大的優(yōu)點是不用重新加載全部數(shù)據(jù),而是只要獲取改動的內容即可。這在移動端編程中看上去是天經地義的,而前端開發(fā)則需要專門使用 AJAX 來實現(xiàn),默認情況下網頁的任何一處微小改動都需要重新加載整個網頁。 類比移動應用就會發(fā)現(xiàn),AJAX 適合做單頁面更新,但是不擅長頁面跳轉,就像你的 app 頁面跳轉都是新建一個 UIViewController/Activity 而不是直接把當前頁面的內容全部換掉。 得益于 AJAX 的提出,HTML 在前端渲染變成了可能。我們可以下載一個空殼 HTML 文件和一個 JavaScript 腳本,然后在 JavaScript 腳本中獲取數(shù)據(jù),為 DOM 添加節(jié)點。 這時候就出現(xiàn)了很多前端的 MVC 框架,比如 Backbone.js,AngularJS(姑且認為MVVM 是 MVC 的變種) 等一堆名詞,你可以從 這里 找到他們各自的 Demo。以我相對熟悉的 React 為例: (上下滑動看代碼) <!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 編程了。所有的邏輯都寫在 <script> 標簽塊中的 JavaScript 代碼中。 我們還創(chuàng)建了一個 LikeButton 組件,它可以擁有自己的方法,管理自己的狀態(tài)等等。這里舉 React 的例子可能略有不妥,因為它其實只是一個 View 層的實現(xiàn)方案,還需要配合 Flux 或 Redux 架構。不過也足以感受一下純 JavaScript 開發(fā)的感覺了。 這種開發(fā)模式和移動端開發(fā)非常類似,使用 JavaScript 調用 DOM 接口來繪制視圖,使用 JavaScript 來實現(xiàn)業(yè)務邏輯,處理數(shù)據(jù),發(fā)起網絡請求等。你完全可以理解為單純用 JavaScript 在瀏覽器上開發(fā)移動應用,只不過用戶下載的是 JavaScript 腳本而非傳統(tǒng)靜態(tài)語言編譯后的二進制文件。 使用了前端 MVC 框架后,對于單頁應用(Single Page Application)來說,前后端各司其職,唯一的聯(lián)系就變成了 API 調用,前端開發(fā)者不再依賴后端環(huán)境,HTML 和 JavaScript 都可以放在 CDN 服務器上。這就是我們常說的 前后端分離 的基本概念,即前端負責展現(xiàn),后端負責數(shù)據(jù)輸出。 10 前后端分離的缺點 然而在我看來,上述前后端分離方案遠遠遜色于后端 MVC 模板的開發(fā)流程,這是因為在實際開發(fā)中,純 SPA 的場景并不多見,即使移動端也不是只有一個視圖,而是有很多頁面跳轉邏輯,更何況到處都是超鏈接的網頁呢? 我們來看看在 SPA 和多頁面跳轉并存的情況下,采用前端 MVC 框架進行前后端分離存在哪些不足。 11 雙端MVC不統(tǒng)一 前端的 MVC 主要處理單個頁面內的邏輯,而后端 MVC 框架處理的是整個 Web 服務器的邏輯。借用 jsconf 大會 上 赫門 的圖片來表示:
由于前后端 MVC 框架關注的重點不同,它們的地位自然也不同。前端的 MVC 框架負責頁面展示,因此它只是后端 MVC 框架的 View 層(或許只是一部分 View)。 這就會導致如下問題:
12 SEO SEO(搜索引擎優(yōu)化)是一個移動開發(fā)者從來不考慮,但前端開發(fā)者視作生命的問題。搜索引擎的工作原理是訪問每個網頁,然后分析 HTML 中的標簽和關鍵字并做記錄。 一個純異步的網頁,HTML 幾乎是空殼子,而偏偏關鍵的數(shù)據(jù)都是動態(tài)下發(fā)的,這就影響了搜索引擎爬蟲的工作過程,他們會認為該網頁什么都沒有,即使記錄下來的也是非關鍵數(shù)據(jù)。 早些年谷歌推出了 Hash-bang 協(xié)議 來彌補 AJAX 對 SEO 造成的負面影響,它的本質是為爬蟲提供后端渲染的降級處理機制。目前谷歌的爬蟲一定程度上可以閱讀 JavaScript 代碼并爬取相關數(shù)據(jù),但 AJAX 在對爬蟲的支持上終究不如 HTML 文本直接傳輸。 13 性能不夠 從上文中 React 的示范代碼可以看出,HTML 文件非常小,很快就被打開。但是頁面的渲染邏輯直到 JavaScript 文件被下載后才能開始執(zhí)行,這就會導致一段時間的白屏。 在移動網絡上,前端渲染 HTML 的性能當然不如后端渲染,頻繁發(fā)送 HTTP 請求也會影響加載體驗,因此依賴于前端渲染的頁面,在性能方面還有很大的提高空間。 14 集中 Or 分離 很多年前,JavaScript 和 CSS 并不用單獨寫在外部文件中,而是直接內嵌在 HTML 代碼里:
為了便于管理和修改,我們把 CSS 和 JavaScript 分離出來。然而到了 React 中,好像走了回頭路,所有邏輯都放在 JavaScript 中。 我的理解是,這種做法適合組件化,我們很容易定義出一個組件并且重用。這種思想對于復雜的 SPA 來說或許適用,但對于并不復雜但有多個頁面的網頁來說,就顯得太重了。引入了 React 這樣的框架,以及 MVC 的結構,反而會顯得過度設計,增加代碼量和復雜度。 考慮到之前所說的前后端邏輯不能復用的問題,這就更容易導致性能問題。 15 Node.js 前后端分離的哲學 至此,我們已經嘗試過后端 MVC 架構,HTML 模板,前端 MVC 架構等多種方案,但結果總是難以令人滿意,是時候總結原因了。 我們在進行上述實踐的過程中,過度強調物理層上的前后端分離,但是忽略了兩者天然就存在一定的耦合。實際上,前端開發(fā)者不僅關注 View 的實現(xiàn),還應該負責一部分 Controller 中的邏輯,后端開發(fā)者則應該關心數(shù)據(jù)獲取與處理,以及一些跨終端的業(yè)務邏輯。 如果頁面渲染在后端實現(xiàn),會導致前端開發(fā)者依賴后端實現(xiàn)和開發(fā)環(huán)境,后端開發(fā)者被迫熟悉前端邏輯(此時不是調用 API 而是直接生成數(shù)據(jù)并套用模板,這就要求把獲取的數(shù)據(jù)轉換成模板需要的數(shù)據(jù))。 如果頁面渲染全部放在前端,業(yè)務邏輯就會太靠前,從而導致不能復用。這種做法似乎有些矯枉過正了。此外,上文中也介紹了不少 AJAX 的缺點,就不贅述了。 我們似乎陷入了兩難的境地,頁面渲染不管是放在前端還是后端都不合適。其實這很好理解,頁面渲染涉及數(shù)據(jù)邏輯和 UI,他們理應分別由前后端開發(fā)者分別負責,單獨交給任何一方都顯得不合適。 但如果前端工程師可以寫后端代碼,問題不就迎刃而解了么?實際上數(shù)據(jù)的處理可以分為兩個步驟:從數(shù)據(jù)庫或第三方服務獲取數(shù)據(jù),把數(shù)據(jù)轉化為 View 可用的形式。前者往往和 C /Java 服務器相關,后者則和前端模板相關,它的作用更像是 MVVM 架構中的 ViewModel。 Node.js 分層 我在上一篇文章中初步介紹了 Node.js 的定位:“一個用 JavaScript 進行開發(fā)的后端應用程序框架”。因此它恰好可以完美的解決前端不了解后端邏輯和代碼的問題。 Node.js 作為一個中間層,調用上游 Java 服務器提供的服務,獲取數(shù)據(jù)。自身負責處理業(yè)務邏輯,路由請求,cookie 讀寫,頁面渲染等。前端則負責應用 CSS 樣式和 JavaScript 交互,類似于最早期原始的模型。 借用 玉伯的 Web研發(fā)模式演變 中的圖片來說明: node.js 中間層 16 實戰(zhàn)應用 這不是一篇介紹 Node.js 的文章,我也不熟悉相關框架的應用,舉這個例子是為了演示 Node.js 是如何做前后端分離的。 我選擇了 Sails.js 框架,項目的代碼在 Github: sails-react-example,這里簡單的分析一下。 視圖都放在 views 目錄下,采用 EJS 為模板,由 Node.js 負責在服務端渲染: (上下滑動看代碼) <div class='container'> <h1><%= __('Comment') %>s for SAILS<small>js</small> REACT<small>js</small></h1> </div> <script src='/js/commentMain.js'></script> Controllers 負責頁面轉發(fā)與模板渲染,具體的服務轉交給 Services 去完成: module.exports = { app : function(req, res) { // 如果有必要,在這里調用 Services 獲取數(shù)據(jù) return res.view({}); }, }; 這個 Demo 中沒有實現(xiàn) Services,通常它用于和真正的后端進行交互,可以視情況選擇 HTTP 或 SOAP,并對返回結果做處理。此外還有 policies/responses 模塊分別對 HTTP 請求和返回內容做處理。 前端的相關代碼都封裝在模板層,能夠與 Node.js 無縫接合。 17 風險控制 雖然我們用增加 Node.js 處理層的方式解決了前后端分離中的一些痛點,但在實際應用中還是需要考慮得更加周全。 新增一層后,勢必會導致性能損耗。然而分層本就是一個在衡量得失后做出的權衡,可以通過各種優(yōu)化把性能損耗降到最低。況且,在 Node.js 這一層還可以使用 BigPipe 來處理多個異步請求。 傳統(tǒng)網頁在加載頁面時,首先獲取 HTML,然后獲取其中引用的 CSS 和 JavaScript。在服務端準備數(shù)據(jù)和網絡傳輸過程中,瀏覽器需要一直等待,而 BigPipe 將頁面分成若干小塊,雖然每個塊的加載邏輯不變,但塊與塊之間可以形成流水線作業(yè),避免瀏覽器無意義的等待。 使用 BigPipe 技術在一定場景下可以代替 Ajax 的多個異步請求。具體介紹可以參考 BigPipe學習研究。 使用 Node.js 后,對前端開發(fā)者的技術要求提高了,編碼工作量也會相應的增加。然而這都是工程化的必經之路,編碼量增加的背后其實是溝通、維護效率的提高。 18 總結 為了處理前后端復雜的邏輯,我們嘗試了使用了后端 MVC 框架來分離業(yè)務,用 HTML 模板來分離數(shù)據(jù)和 UI 樣式。使用了 Ajax 技術的網頁更適合單頁應用,雖然做到了物理層的分離,但在處理多頁面時還是有不少問題。 實際上頁面渲染本就是前后端共同關心的話題,我們更應該根據(jù)業(yè)務邏輯進行前后端分離。最終選擇了 Node.js,借助它使用 JavaScript 的特性,由前端工程師負責獲取數(shù)據(jù)后的處理和模板的使用,分擔了一部分原本邏輯上屬于前端,但技術上偏向后端的任務。這種開發(fā)模式看上去像是一種倒退,其實是螺旋式的上升與返璞歸真。 Node.js 基于事件和異步 I/O 的特性,以及優(yōu)秀的處理高并發(fā)的能力非常適合前后端分離的場景,使用 BigPipe 等技術也足以將分層帶來的損耗降到最低。選擇 Node.js 做前后端分離并不一定最佳實踐,但在目前看來有不錯的應用,同時也需要一邊探索一邊前進。
作者:bestswifter |
|