前言
二十載光輝歲月
近年來,隨著瀏覽器效能的提升與移動網際網路浪潮的洶湧而來,Web前端開發進入了高歌猛進,日新月異的時代。這是最好的時代,我們永遠在前行,這也是最壞的時代,無數的前端開發框架、技術體系爭妍鬥豔,讓開發者們陷入困惑,乃至於無所適從。Web前端開發可以追溯於1991年蒂姆·伯納斯-李公開提及HTML描述,而後1999年W3C釋出HTML4標準,這個階段主要是BS架構,沒有所謂的前端開發概念,網頁只不過是後端工程師的順手之作,服務端渲染是主要的資料傳遞方式。接下來的幾年間隨著網際網路的發展與REST等架構標準的提出,前後端分離與富客戶端的概念日漸為人認同,我們需要在語言與基礎的API上進行擴充,這個階段出現了以jQuery為代表的一系列前端輔助工具。2009年以來,智慧手機開發普及,移動端大浪潮勢不可擋,SPA單頁應用的設計理念也大行其道,相關聯的前端模組化、元件化、響應式開發、混合式開發等等技術需求甚為迫切。這個階段催生了Angular 1、Ionic等一系列優秀的框架以及AMD、CMD、UMD與RequireJS、SeaJS等模組標準與載入工具,前端工程師也成為了專門的開發領域,擁有獨立於後端的技術體系與架構模式。而近兩年間隨著Web應用複雜度的提升、團隊人員的擴充、使用者對於頁面互動友好與效能優化的需求,我們需要更加優秀靈活的開發框架來協助我們更好的完成前端開發。這個階段湧現出了很多關注點相對集中、設計理念更為優秀的框架,譬如React、VueJS、Angular 2等元件框架允許我們以宣告式程式設計來替代以DOM操作為核心的指令式程式設計,加快了元件的開發速度,並且增強了元件的可複用性與可組合性。而遵循函數語言程式設計的Redux與借鑑了響應式程式設計理念的MobX都是非常不錯的狀態管理輔助框架,輔助開發者將業務邏輯與檢視渲染剝離,更為合理地劃分專案結構,更好地貫徹單一職責原則與提升程式碼的可維護性。在專案構建工具上,以Grunt、Gulp為代表的任務執行管理與以Webpack、Rollup、JSPM為代表的專案打包工具各領風騷,幫助開發者更好的搭建前端構建流程,自動化地進行預處理、非同步載入、Polyfill、壓縮等操作。而以NPM/Yarn為代表的依賴管理工具一直以來保證了程式碼釋出與共享的便捷,為前端社群的繁榮奠定了重要基石。
紛擾之虹
筆者在前兩天看到了Thomas Fuchs的一則Twitter,也在Reddit等社群引發了熱烈的討論:我們用了15年的時間來分割HTML、JS與CSS,然而一夕之間事務彷彿回到了原點。
分久必合,合久必分啊,無論是前端開發中各個模組的分割還是所謂的前後端分離,都不能形式化的單純按照語言或者模組來劃分,還是需要兼顧功能,合理劃分。筆者在2015-我的前端之路:資料流驅動的介面中對自己2015的前端感受總結中提到過,任何一個程式設計生態都會經歷三個階段,第一個是原始時期,由於需要在語言與基礎的API上進行擴充,這個階段會催生大量的Tools。第二個階段,隨著做的東西的複雜化,需要更多的組織,會引入大量的設計模式啊,架構模式的概念,這個階段會催生大量的Frameworks。第三個階段,隨著需求的進一步複雜與團隊的擴充,就進入了工程化的階段,各類分層MVC,MVP,MVVM之類,視覺化開發,自動化測試,團隊協同系統。這個階段會出現大量的小而美的Library。在2016的上半年中,筆者在以React的技術棧中掙扎,也試用過VueJS與Angular等其他優秀的前端框架。在這一場從直接操作DOM節點的命令式開發模式到以狀態/資料流為中心的開發模式的工具化變革中,筆者甚感疲憊。在2016的下半年中,筆者不斷反思是否有必要使用React/Redux/Webpack/VueJS/Angular,是否有必要去不斷追逐各種重新整理Benchmark 記錄的新框架?本文定名為工具化與工程化,即是代表了本文的主旨,希望能夠儘可能地脫離工具的束縛,迴歸到前端工程化的本身,迴歸到語言的本身,無論React、AngularJS、VueJS,它們更多的意義是輔助開發,為不同的專案選擇合適的工具,而不是執念於工具本身。
總結而言,目前前端工具化已經進入到了非常繁榮的時代,隨之而來很多前端開發者也甚為苦惱,疲於學習。工具的變革會非常迅速,很多優秀的工具可能都只是歷史長河中的一朵浪花,而蘊藏其中的工程化思維則會恆久長存。無論你現在使用的是React還是Vue還是Angular 2或者其他優秀的框架,都不應該妨礙我們去了解嘗試其他,筆者在學習Vue的過程中感覺反而加深了自己對於React的理解,加深了對現代Web框架設計思想的理解,也為自己在未來的工作中更自由靈活因地制宜的選擇腳手架開闊了視野。
引言的最後,我還想提及一個詞,算是今年我在前端領域看到的出鏡率最高的一個單詞:Tradeoff(妥協)。
工具化
月盈而虧,過猶不及。相信很多人都看過了2016年裡做前端是怎樣一種體驗這篇文章,2016年的前端真是讓人感覺從入門到放棄,我們學習的速度已經跟不上新框架新概念湧現的速度,用於學習上的成本遠大於實際開發專案的成本。不過筆者對於工具化的浪潮還是非常歡迎的,我們不一定要去用最新最優秀的工具,但是我們有了更多的選擇餘地,相信這一點對於大部分非處女座人士而言都是喜訊。年末還有一篇曹劉陽:2016年前端技術觀察也引發了大家的熱議,老實說筆者個人對文中觀點認同度一半對一半,不想吹也不想黑。不過筆者看到這篇文章的第一感覺當屬作者肯定是大公司出來的。文中提及的很多因為技術負債引發的技術選型的考慮、能夠擁有相對充分完備的人力去進行某個專案,這些特徵往往是中小創公司所不會具備的。
工具化的意義
工具化是有意義的。筆者在這裡非常贊同尤雨溪:Vue 2.0,漸進式前端解決方案 的思想,工具的存在是為了幫助我們應對複雜度,在技術選型的時候我們面臨的抽象問題就是應用的複雜度與所使用的工具複雜度的對比。工具的複雜度是可以理解為是我們為了處理問題內在複雜度所做的投資。為什麼叫投資?那是因為如果投的太少,就起不到規模的效應,不會有合理的回報。這就像創業公司拿風投,投多少是很重要的問題。如果要解決的問題本身是非常複雜的,那麼你用一個過於簡陋的工具應付它,就會遇到工具太弱而使得生產力受影響的問題。反之,是如果所要解決的問題並不複雜,但你卻用了很複雜的框架,那麼就相當於殺雞用牛刀,會遇到工具複雜度所帶來的副作用,不僅會失去工具本身所帶來優勢,還會增加各種問題,例如培訓成本、上手成本,以及實際開發效率等。
筆者在GUI應用程式架構的十年變遷:MVC,MVP,MVVM,Unidirectional,Clean一文中談到,所謂GUI應用程式架構,就是對於富客戶端的程式碼組織/職責劃分。縱覽這十年內的架構模式變遷,大概可以分為MV*
與Unidirectional兩大類,而Clean Architecture則是以嚴格的層次劃分獨闢蹊徑。從筆者的認知來看,從MVC到MVP的變遷完成了對於View與Model的解耦合,改進了職責分配與可測試性。而從MVP到MVVM,新增了View與ViewModel之間的資料繫結,使得View完全的無狀態化。最後,整個從MV*到Unidirectional的變遷即是採用了訊息佇列式的資料流驅動的架構,並且以Redux為代表的方案將原本MV*
中碎片化的狀態管理變為了統一的狀態管理,保證了狀態的有序性與可回溯性。 具體到前端的衍化中,在Angular 1興起的時代實際上就已經開始了從直接操作Dom節點轉向以狀態/資料流為中心的變化,jQuery 代表著傳統的以 DOM 為中心的開發模式,但現在複雜頁面開發流行的是以 React 為代表的以資料/狀態為中心的開發模式。應用複雜後,直接操作 DOM 意味著手動維護狀態,當狀態複雜後,變得不可控。React 以狀態為中心,自動幫我們渲染出 DOM,同時通過高效的 DOM Diff 演算法,也能保證效能。
工具化的不足:抽象漏洞定理
抽象漏洞定理是Joel在2002年提出的,所有不證自明的抽象都是有漏洞的。抽象洩漏是指任何試圖減少或隱藏複雜性的抽象,其實並不能完全遮蔽細節,試圖被隱藏的複雜細節總是可能會洩漏出來。抽象漏洞法則說明:任何時候一個可以提高效率的抽象工具,雖然節約了我們工作的時間,但是,節約不了我們的學習時間。我們在上一章節討論過工具化的引入實際上以承受工具複雜度為代價消弭內在複雜度,而工具化濫用的結局即是工具複雜度與內在複雜度的失衡。
談到這裡我們就會明白,不同的專案具備不同的內在複雜度,一刀切的方式評論工具的好壞與適用簡直耍流氓,而且我們不能忽略專案開發人員的素質、客戶或者產品經理的素質對於專案內在複雜度的影響。對於典型的小型活動頁,譬如某個微信H5宣傳頁,往往注重於互動動畫與載入速度,邏輯複雜度相對較低,此時Vue這樣漸進式的複雜度較低的庫就大顯身手。而對於複雜的Web應用,特別是需要考慮多端適配的Web應用,筆者會傾向於使用React這樣相對規範嚴格的庫。
React?Vue?Angular 2?
筆者最近翻譯過幾篇盤點文,發現很有趣的一點,若文中不提或沒誇Vue,則一溜的評論:垃圾文章,若文中不提或沒誇Angular 2,則一溜的評論:垃圾文章。估計若是筆者連React也沒提,估計也是一溜的評論:垃圾文章。好吧,雖然可能是筆者翻譯的確實不好,玷汙了原文,但是這種戾氣筆者反而覺得是對於技術的不尊重。React,Vue,Angular 2都是非常優秀的庫與框架,它們在不同的應用場景下各自具有其優勢,本章節即是對筆者的觀點稍加闡述。Vue最大的優勢在於其漸進式的思想與更為友好的學習曲線,Angular 2最大的優勢其相容幷包形成了完整的開箱即用的All-in-one框架,而這兩點優勢在某些情況下反而也是其劣勢,也是部分人選用React的理由。筆者覺得很多對於技術選型的爭論乃至於謾罵,不一定是工具的問題,而是工具的使用者並不能正確認識自己或者換位思考他人所處的應用場景,最終吵的驢脣不對馬嘴。
小而美的檢視層
React 與 VueJS 都是所謂小而美的檢視層Library,而不是Angular 2這樣相容幷包的Frameworks。任何一個程式設計生態都會經歷三個階段,第一個是原始時期,由於需要在語言與基礎的API上進行擴充,這個階段會催生大量的Tools。第二個階段,隨著做的東西的複雜化,需要更多的組織,會引入大量的設計模式啊,架構模式的概念,這個階段會催生大量的Frameworks。第三個階段,隨著需求的進一步複雜與團隊的擴充,就進入了工程化的階段,各類分層MVC,MVP,MVVM之類,視覺化開發,自動化測試,團隊協同系統。這個階段會出現大量的小而美的Library。
React 並沒有提供很多複雜的概念與繁瑣的API,而是以最少化為目標,專注於提供清晰簡潔而抽象的檢視層解決方案,同時對於複雜的應用場景提供了靈活的擴充套件方案,典型的譬如根據不同的應用需求引入MobX/Redux這樣的狀態管理工具。React在保證較好的擴充套件性、對於進階研究學習所需要的基礎知識完備度以及整個應用分層可測試性方面更勝一籌。不過很多人對React的意見在於其陡峭的學習曲線與較高的上手門檻,特別是JSX以及大量的ES6語法的引入使得很多的傳統的習慣了jQuery語法的前端開發者感覺學習成本可能會大於開發成本。與之相比Vue則是典型的所謂漸進式庫,即可以按需漸進地引入各種依賴,學習相關地語法知識。比較直觀的感受是我們可以在專案初期直接從CDN中下載Vue庫,使用熟悉的指令碼方式插入到HTML中,然後直接在script標籤中使用Vue來渲染資料。隨著時間的推移與專案複雜度的增加,我們可以逐步引入路由、狀態管理、HTTP請求抽象以及可以在最後引入整體打包工具。這種漸進式的特點允許我們可以根據專案的複雜度而自由搭配不同的解決方案,譬如在典型的活動頁中,使用Vue能夠兼具開發速度與高效能的優勢。不過這種自由也是有利有弊,所謂磨刀不誤砍材工,React相對較嚴格的規範對團隊內部的程式碼樣式風格的統一、程式碼質量保障等會有很好的加成。
一言蔽之,筆者個人覺得Vue會更容易被純粹的前端開發者的接受,畢竟從直接以HTML佈局與jQuery進行資料操作切換到指令式的支援雙向資料繫結的Vue代價會更小一點,特別是對現有程式碼庫的改造需求更少,重構代價更低。而React及其相對嚴格的規範可能會更容易被後端轉來的開發者接受,可能在初學的時候會被一大堆概念弄混,但是熟練之後這種嚴謹的元件類與成員變數/方法的操作會更順手一點。便如Dan Abramov所述,Facebook推出React的初衷是為了能夠在他們數以百計的跨平臺子產品持續的迭代中保證元件的一致性與可複用性。
函式式思維:抽象與直觀
近年來隨著應用業務邏輯的日益複雜與併發程式設計的大規模應用,函數語言程式設計在前後端都大放異彩。軟體開發領域有一句名言:可變的狀態是萬惡之源,函數語言程式設計即是避免使用共享狀態而避免了物件導向程式設計中的一些常見痛處。不過老實說筆者並不想一味的推崇函數語言程式設計,在下文關於Redux與MobX的討論中,筆者也會提及函數語言程式設計不可避免地會使得業務邏輯支離破碎,反而會降低整個程式碼的可維護性與開發效率。與React相比,Vue則是非常直觀的程式碼架構,每個Vue元件都包含一個script標籤,這裡我們可以顯式地宣告依賴,宣告運算元據的方法以及定義從其他元件繼承而來的屬性。而每個元件還包含了一個template標籤,等價於React中的render函式,可以直接以屬性方式繫結資料。最後,每個元件還包含了style標籤而保證了可以直接隔離元件樣式。我們可以先來看一個典型的Vue元件,非常直觀易懂,而兩相比較之下也有助於理解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 |
<script> export default { components: {}, data() { return { notes: [], }; }, created() { this.fetchNotes(); }, methods: { addNote(title, body, createdAt, flagged) { return database('notes').insert({ title, body, created_at: createdAt, flagged }); }, }; </script> <template> <div class="app"> <header-menu :addNote='addNote' > </div> </template> <style scoped> .app { width: 100%; height: 100%; postion: relative; } </style> |
當我們將視角轉回到React中,作為單向資料繫結的元件可以抽象為如下渲染函式:
1 |
View = f(Data) |
這種對使用者介面的抽象方式確實令筆者耳目一新,這樣我們對於介面的組合搭配就可以抽象為對於函式的組合,某個複雜的介面可以解構為數個不同的函式呼叫的組合變換。0.14版本時,React放棄了MixIn功能,而推薦使用高階函式模式進行元件組合。這裡很大一個考慮便是Mixin屬於物件導向程式設計,是多重繼承的一種實現,而函數語言程式設計裡面的Composition(合成)可以起到同樣的作用,並且能夠保證元件的純潔性而沒有副作用。
很多人第一次學習React的時候都會覺得JSX語法看上去非常怪異,這種背離傳統的HTML模板開發方式真的靠譜嗎?(在2.0版本中Vue也引入了JSX語法支援)。我們並不能單純地將JSX與傳統的HTML模板相提並論,JSX本質上是對於React.createElement
函式的抽象,而該函式主要的作用是將樸素的JavaScript中的物件對映為某個DOM表示。其大概思想圖示如下:
在現代瀏覽器中,對於JavaScript的計算速度遠快於對DOM進行操作,特別是在涉及到重繪與重渲染的情況下。並且以JavaScript物件代替與平臺強相關的DOM,也保證了多平臺的支援,譬如在ReactNative的協助下我們很方便地可以將一套程式碼執行於iOS、Android等多平臺。總結而言,JSX本質上還是JavaScript,因此我們在保留了JavaScript函式本身在組合、語法檢查、除錯方面優勢的同時又能得到類似於HTML這樣宣告式用法的便利與較好的可讀性。
前後端分離與全棧:技術與人
前後端分離與全棧並不是什麼新鮮的名詞,都曾引領一時風騷。五年前筆者初接觸到前後端分離的思想與全棧工程師的定義時,感覺醍醐灌頂,當時的自我定位也是希望成為一名優秀的全棧工程師,不過現在想來當時的自己冠以這個名頭更多的是為了給什麼都瞭解一點但是都談不上精通,碰到稍微深入點的問題就手足無措的自己的心理安慰罷了。Web前後端分離優勢顯著,對於整個產品的開發速度與可信賴性有著很大的作用。全棧工程師對於程式設計師自身的提升有很大意義,對於專案的初期速度有一定增速。如果劃分合理的話能夠促進整個專案的全域性開發速度與可信賴性,但是如果劃分不合理的話只會導致專案介面混亂,一團亂麻。但是這兩個概念似乎略有些衝突,我們常說的前後端分離會包含以下兩個層面:
- 將原本由服務端負責的資料渲染工作交由前端進行,並且規定前端與服務端之間只能通過標準化協議進行通訊。
- 組織架構上的分離,由早期的服務端開發人員順手去寫個介面轉變為完整的前端團隊構建工程化的前端架構。
前後端分離本質上是前端與後端適用不同的技術選型與專案架構,不過二者很多思想上也是可以融會貫通,譬如無論是響應式程式設計還是函數語言程式設計等等思想在前後端皆有體現。而全棧則無論從技術還是組織架構的劃分上似乎又回到了按照需求分割的狀態。不過呢,我們必須要面對現實,很大程度的工程師並沒有能力做到全棧,這一點不在於具體的程式碼技術,而是對於前後端各自的理解,對於系統業務邏輯的理解。如果我們分配給一個完整的業務塊,同時,那麼最終得到的是無數個碎片化相互獨立的系統。
相輔相成的客戶端渲染與服務端渲染
筆者在2015-我的前端之路提及最初的網頁是資料、模板與樣式的混合,即以經典的APS.NET、PHP與JSP為例,是由服務端的模板提供一系列的標籤完成從業務邏輯程式碼到頁面的流動。所以,前端只是用來展示資料,所謂附庸之徒。而隨著Ajax技術的流行,將WebAPP也視作CS架構,抽象來說,會認為CS是客戶端與伺服器之間的雙向通訊,而BS是客戶端與服務端之間的單向通訊。換言之,網頁端本身也變成了有狀態。從初始開啟這個網頁到最終關閉,網頁本身也有了一套自己的狀態,而擁有這種變化的狀態的基礎就是AJAX,即從單向通訊變成了雙向通訊。圖示如下:
上文描述的即是前後端分離思想的發展之路,而近兩年來隨著React的流行服務端渲染的概念重回人們的視線。需要強調的是,我們現在稱之為服務端渲染的技術並非傳統的以JSP、PHP為代表的服務端模板資料填充,更準確的服務端渲染作用的描述是對於客戶端應用的預啟動與預載入。我們千方百計將客戶端程式碼拉回到服務端執行並不是為了替換現有的API伺服器,並且在服務端執行過的程式碼同樣需要在客戶端重新執行,這裡推薦參考筆者的Webpack2-React-Redux-Boilerplate,按照三個層次地漸進描述了從純客戶端渲染到服務端渲染的遷移之路。引入服務端渲染帶來的優勢主要在於以下三個方面:
- 對瀏覽器相容性的提升,目前React、Angular、Vue等現代Web框架紛紛放棄了對於舊版本瀏覽器的支援,引入服務端渲染之後至少對於使用舊版本瀏覽器的使用者能夠提供更加友好的首屏展示,雖然後續功能依然不能使用。
- 對搜尋引擎更加友好,客戶端渲染意味著整體的渲染用指令碼完成,這一點對於爬蟲並不友好。雖然現代爬蟲往往也會通過內建自動化瀏覽器等方式支援指令碼執行,但是這樣無形會加重很多爬蟲伺服器的負載,因此Google這樣的大型搜尋引擎在進行網頁索引的時候還是依賴於文件本身。如果你希望提升在搜尋引擎上的排行,讓你的網站更方便地被搜尋到,那麼支援服務端渲染是個不錯的選擇。
- 整體載入速度與使用者體驗優化,在首屏渲染的時候,服務端渲染的效能是遠快於客戶端渲染的。不過在後續的頁面響應更新與子檢視渲染時,受限於網路頻寬與重渲染的範疇,服務端渲染是會弱於客戶端渲染。另外在服務端渲染的同時,我們也會在服務端抓取部分應用資料附加到文件中,在目前HTTP/1.1仍為主流的情況下可以減少客戶端的請求連線數與時延,讓使用者更快地接觸到所需要的應用資料。
總結而言,服務端渲染與客戶端渲染是相輔相成的,在React等框架的協助下我們也可以很方便地為開發階段的純客戶端渲染應用新增服務端渲染支援。
專案中的全棧工程師:技術全棧,需求隔離,合理分配
全棧工程師對於個人發展有很大的意義,對於實際的專案開發,特別是中小創公司中以進度為第一指揮棒的專案而言更具有非常積極的意義。但是全棧往往意味著一定的Tradeoff,步子太大,容易扯著蛋。任何技術架構和流程的調整,最好都不要去違背康威定律,即設計系統的組織,其產生的設計等同於組織之內、組織之間的溝通結構。這裡是筆者在本文第一次提及康威定律,筆者在實踐中發現,有些全棧的結果就是強行按照功能來分配任務,即最簡單的來說可能把登入註冊這一塊從資料庫設計、服務端介面到前端介面全部分配給一個人或者一個小組完成。然後這個具體的執行者,因為其總體負責從上到下的全部邏輯,在很多應該規範化的地方,特別是介面定義上就會為了求取速度而忽略了必要的規範。最終導致整個系統支離破碎成一個又一個的孤島,不同功能塊之間表述相同意義的變數命名都能發生衝突,各種奇形怪狀的id、uuid、{resource}_id令人眼花繚亂。
今年年末的時候,不少技術交流平臺上掀起了對於全棧工程師的聲討,以知乎上全棧工程師為什麼會招黑這個討論為例,大家對於全棧工程師的黑點主要在於:
- Leon-Ready:全棧工程師越來越難以存在,很多人不過濫竽充數。隨著網際網路的發展,為了應對不同的挑戰,不同的方向都需要花費大量的時間精力解決問題,崗位細分是必然的。這麼多年來每個方向的專家經驗和技能的積累都不是白來的,人的精力和時間都是有限的,越往後發展,真正意義上的全棧越沒機會出現了。
- 輪子哥:一個人追求全棧可以,那是他個人的自由。但是如果一個工作崗位追求全棧,然後還來鼓吹這種東西的話,那證明這個公司是不健康的、效率底下的。
現代經濟發展的一個重要特徵就是社會分工日益精細明確,想要成為無所不知的通才不過南柯一夢。不過在上面的聲討中我們也可以看出全棧工程師對於個人的發展是及其有意義的,它山之石,可以攻玉,融會貫通方能舉一反三。筆者在自己的小團隊中很提倡職位輪替,一般某個專案週期完成後會調換部分前後端工程師的位置,一方面是為了避免繁雜的事務性開發讓大家過於疲憊。另一方面也是希望每個人都瞭解對方的工作,這樣以後出Bug的時候就能換位思考,畢竟團隊內部矛盾,特別是各個小組之間的矛盾一直是專案管理中頭疼的問題。
工程化
斷斷續續寫到這裡有點疲累了,本部分應該會是最重要的章節,不過再不寫畢業論文估計就要被打死了T,T,筆者會在以後的文章中進行補充完善。
何謂工程化
所謂工程化,即是面向某個產品需求的技術架構與專案組織,工程化的根本目標即是以儘可能快的速度實現可信賴的產品。儘可能短的時間包括開發速度、部署速度與重構速度,而可信賴又在於產品的可測試性、可變性以及Bug的重現與定位。
- 開發速度:開發速度是最為直觀、明顯的工程化衡量指標,也是其他部門與程式設計師、程式設計師之間的核心矛盾。絕大部分優秀的工程化方案首要解決的就是開發速度,不過筆者一直也會強調一句話,磨刀不誤砍材工,我們在追尋區域性速度最快的同時不能忽略整體最優,初期單純的追求速度而帶來的技術負債會為以後階段造成不可彌補的損害。
- 部署速度:筆者在日常工作中,最長對測試或者產品經理說的一句話就是,我本地改好了,還沒有推送到線上測試環境呢。在DevOps概念深入人心,各種CI工具流行的今天,自動化編譯與部署幫我們省去了很多的麻煩。但是部署速度仍然是不可忽視的重要衡量指標,特別是以NPM為代表的難以捉摸的包管理工具與不知道什麼時候會抽個風的伺服器都會對我們的編譯部署過程造成很大的威脅,往往專案依賴數目的增多、結構劃分的混亂也會加大部署速度的不可控性。
- 重構速度:聽產品經理說我們的需求又要變了,聽技術Leader說最近又出了新的技術棧,甩現在的十萬八千里。
- 可測試性:現在很多團隊都會提倡測試驅動開發,這對於提升程式碼質量有非常重要的意義。而工程方案的選項也會對程式碼的可測試性造成很大的影響,可能沒有無法測試的程式碼,但是我們要儘量減少程式碼的測試代價,鼓勵程式設計師能夠更加積極地主動地寫測試程式碼。
- 可變性:程式設計師說:這個需求沒法改啊!
- Bug的重現與定位:沒有不出Bug的程式,特別是在初期需求不明確的情況下,Bug的出現是必然而無法避免的,優秀的工程化方案應該考慮如何能更快速地輔助程式設計師定位Bug。
無論是前後端分離,還是後端流行的MicroService或者是前端的MicroFrontend,其核心都是犧牲區域性開發速度換來更快地全域性開發速度與系統的可信賴性的提高。而區分初級程式設計師與中級程式設計師的區別可能在於前者僅會實現,僅知其然而不知其所以然,他們唯一的衡量標準就是開發速度,即功能實現速度或者程式碼量等等,不一而足。中級程式設計師則可以對自己負責範圍內的程式碼同時兼顧開發速度與程式碼質量,會在開發過程中通過不斷地Review來不斷地合併分割,從而在堅持SRP原則的基礎上達成儘可能少的程式碼量。另一方面,區分單純地Coder與TeamLeader之間的區別在於前者更注重區域性最優,這個區域性即可能指專案中的前後端中的某個具體模組,也可能指時間維度上的最近一段的開發目標。而TeamLeader則更需要運籌帷幄,統籌全域性。不僅僅要完成老闆交付的任務,還需要為產品上可能的修改迭代預留介面或者提前為可擴充套件打好基礎,磨刀不誤砍材工。總結而言,當我們探究工程化的具體實現方案時,在技術架構上,我們會關注於:
- 功能的模組化與介面的元件化
- 統一的開發規範與程式碼樣式風格,能夠在遵循SRP單一職責原則的前提下以最少的程式碼實現所需要的功能,即保證合理的關注點分離。
- 程式碼的可測試性
- 方便共享的程式碼庫與依賴管理工具
- 持續整合與部署
- 專案的線上質量保障
前端的工程化需求
當我們落地到前端時,筆者在歷年的實踐中感受到以下幾個突出的問題:
- 前後端業務邏輯銜接:在前後端分離的情況下,前後端是各成體系與團隊,那麼前後端的溝通也就成了專案開發中的主要矛盾之一。前端在開發的時候往往是根據介面來劃分模組,命名變數,而後端是習慣根據抽象的業務邏輯來劃分模組,根據資料庫定義來命名變數。最簡單而是最常見的問題譬如二者可能對於同意義的變數命名不同,並且考慮到業務需求的經常變更,後臺介面也會發生頻繁變動。此時就需要前端能夠建立專門的介面層對上遮蔽這種變化,保證介面層的穩定性。
- 多業務系統的元件複用:當我們面臨新的開發需求,或者具有多個業務系統時,我們希望能夠儘量複用已有程式碼,不僅是為了提高開發效率,還是為了能夠保證公司內部應用風格的一致性。
- 多平臺適配與程式碼複用:在移動化浪潮面前,我們的應用不僅需要考慮到PC端的支援,還需要考慮微信小程式、微信內H5、WAP、ReactNative、Weex、Cordova等等平臺內的支援。這裡我們希望能夠儘量的複用程式碼來保證開發速度與重構速度,這裡需要強調的是,筆者覺得移動端和PC端本身是不同的設計風格,筆者不贊同過多的考慮所謂的響應式開發來複用介面元件,更多的應該是著眼於邏輯程式碼的複用,雖然這樣不可避免的會影響效率。魚與熊掌,不可兼得,這一點需要因地制宜,也是不能一概而論。
宣告式的渲染或者說可變的命令式操作是任何情況下都需要的,從以DOM操作為核心到資料流驅動能夠儘量減少冗餘程式碼,提高開發效率。筆者在這裡還是想以jQuery與Angular 1的對比為例:
1 2 3 4 5 6 |
var options = $("#options"); $.each(result, function() { options.append($("<option />").val(this.id).text(this.name)); }); <div ng-repeat="item in items" ng-click="select(item)">{{item.name}} </div> |
目前React、Vue、Angular 2或其擴充套件中都提供了基於ES6的宣告式元件的支援,那麼在基本的宣告式元件之上,我們就需要構建可複用、可組合的元件系統,往往某個元件系統是由我們某個應用的大型介面切分而來的可空單元組合而成,也就是下文前端架構中的解構設計稿一節。當我們擁有大型元件系統,或者說很多的元件時,我們需要考慮元件之間的跳轉。特別是對於單頁應用,我們需要將URL對應到應用的狀態,而應用狀態又決定了當前展示的元件。這時候我們的應用日益複雜,當應用簡單的時候,可能一個很基礎的狀態和介面對映可以解決問題,但是當應用變得很大,涉及多人協作的時候,就會涉及多個元件之間的共享、多個元件需要去改動同一份狀態,以及如何使得這樣大規模應用依然能夠高效執行,這就涉及大規模狀態管理的問題,當然也涉及到可維護性,還有構建工具。現在,如果放眼前端的未來,當HTTP2普及後,可能會帶來構建工具的一次革命。但就目前而言,尤其是在中國的網路環境下,打包和工程構建依然是非常重要且不可避免的一個環節。最後,從前端的專案類別上來看,可以分為以下幾類:
- 大型Web應用:業務功能極其複雜,使用Vue,React,Angular這種MVVM的框架後,在開發過程中,元件必然越來越多,父子元件之間的通訊,子元件之間的通訊頻率都會大大增加。如何管理這些元件之間的資料流動就會成為這類WebApp的最大難點。
- Hybrid Web APP:矛盾點在於效能與使用者驗證等。
- 活動頁面
- 遊戲
MicroFrontend:微前端
微服務為構建可擴充套件、可維護的大規模服務叢集帶來的便利已是毋庸置疑,而現在隨著前端應用複雜度的日漸提升,所謂的巨石型的前端應用也是層出不窮。而與服務端應用程式一樣,大型笨重的Web應用同樣是難以維護,因此ThoughtWorks今年提出了所謂MicroFrontend微前端的概念。微前端的核心思想和微服務殊途同歸,巨型的Web應用根據頁面與功能進行切分,不同的團隊負責不同的部分,每個團隊可以根據自己的技術喜好應用相關的技術來開發相關部分,這裡BFF – backend for frontends也就派上了用場。
迴歸現實的前端開發計劃
本文的最後一個部分著眼於筆者一年中實踐規劃出的前端開發計劃,估計本文只是提綱挈領的說一下,未來會有專門的文章進行詳細介紹。緣何稱之為迴歸現實的前端開發計劃?是因為筆者感覺遇見的最大的問題在於需求的不明確、介面的不穩定與開發人員素質的參差不齊。先不論技術層面,專案開發中我們在組織層面的希望能讓每個參與的人無論水平高低都能最大限度的發揮其價值,每個人都會寫元件,都會寫實體類,但是他們不一定能寫出合適的優質的程式碼。另一方面,好的架構都是衍化而來,不同的行業領域、應用場景、介面互動的需求都會引發架構的衍化。我們需要抱著開放的心態,不斷地提取公共程式碼,保證合適的複用程度。同時也要避免過度抽象而帶來的一系列問題。筆者提倡的團隊合理搭配方式如下,這個更多的是面向於小型公司,人手不足,一個當兩個用,恨不得所有人都是全棧:
宣告式程式設計與資料流驅動:有得有失
Redux是完全的函數語言程式設計思想踐行者(如果你對於Redux還不夠理解,可以參考下筆者的深入理解Redux:10個來自專家的Redux實踐建議),其核心技術圍繞遵循Pure Function的Reducer與遵循Immutable Object的Single State Tree,提供了Extreme Predictability與Extreme Testability,相對應的需要大量的Boilerplate。而MobX則是Less Opinioned,其脫胎於Reactive Programming,其核心思想為Anything that can be derived from the application state, should be derived. Automatically,即避免任何的重複狀態。Redux使用了Uniform Single State Tree,而在後端開發中習慣了Object Oriented Programming的筆者不由自主的也想在前端引入Entity,或者說在設計思想上,譬如對於TodoList的增刪改查,筆者希望能夠包含在某個TodoList物件中,而不需要將所有的操作拆分為Creator、Reducer與Selector三個部分,我只是想簡單的展示個列表而已。筆者上大學學的第一節課就是講OOP,包括後面在C#、Java、Python、PHP等等很多後端領域的實踐中,都深受OOP思想的薰陶與灌輸。不可否認,可變的狀態是軟體工程中的萬惡之源,但是,OOP對於業務邏輯的描述與程式碼組織的可讀性、可理解性的保證相較於宣告式的,略為抽象的FP還是要好一點的。我認可函數語言程式設計的思想成為專案構建組織的不可分割的一部分,但是是否應該在任何專案的任何階段都先談程式設計思想,而後看業務需求?這無疑有點政治正確般的耍流氓了。Dan推薦的適用Redux的情況典型的有:
- 方便地能夠將應用狀態儲存到本地並且重啟動時能夠讀取恢復狀態
- 方便地能夠在服務端完成初始狀態設定,並且完成狀態的服務端渲染
- 能夠序列化記錄使用者操作,能夠設定狀態快照,從而方便進行Bug報告與開發者的錯誤重現
- 能夠將使用者的操作或者事件傳遞給其他環境而不需要修改現有程式碼
- 能夠新增重放或者撤銷功能而不需要重構程式碼
- 能夠在開發過程中實現狀態歷史的回溯,或者根據Action的歷史重現狀態
- 能夠為開發者提供全面透徹的審視和修改現有開發工具的介面,從而保證產品的開發者能夠根據他們自己的應用需求打造專門的工具
- 能夠在複用現在大部分業務邏輯的基礎上構造不同的介面
漸進的狀態管理
在不同的時間段做不同的事情,當我們在編寫純元件階段,我們需要顯式宣告所有的狀態/資料,而對於Action則可以放入Store內延後操作。以簡單的表單為例,最初的時候我們會將表單的資料輸入、驗證、提交與結果反饋等等所有的邏輯全部封裝在表單元件內。而後隨著元件複雜度的增加,我們需要針對不同功能的程式碼進行切分,此時我們就可以建立專門的Store來處理該表單的狀態與邏輯。抽象來說,我們在不同的階段所需要的狀態管理對應為:
- 原型:Local State
這個階段我們可能直接將資料獲取的函式放置到componentDidMount中,並且將UI State與Domain State都利用setState函式存放在LocalState中。這種方式的開發效率最高,畢竟程式碼量最少,不過其可擴充套件性略差,並且不利於檢視之間共享狀態。
1 2 |
// component <button onClick={() => store.users.push(user)} /> |
這裡的store僅僅指純粹的資料儲存或者模型類。
- 專案增長:External State
隨著專案逐漸複雜化,我們需要尋找專門的狀態管理工具來進行外部狀態的管理了:
1 2 3 4 5 6 7 |
// component <button onClick={() => store.addUser(user)} /> // store <a href="http://www.jobbole.com/members/Francesco246437">@action</a> addUser = (user) => { this.users.push(user); } |
這個時候你也可以直接在元件內部修改狀態,即還是使用第一個階段的程式碼風格,直接操作store物件,不過也可以通過引入Strict模式來避免這種不良好的實踐:
1 2 3 4 |
// root file import { useStrict } from 'mobx'; useStrict(true); |
- 多人協作/嚴格規範/複雜互動:Redux
隨著專案體量進一步的增加與參與者的增加,這時候使用宣告式的Actions就是最佳實踐了,也應該是Redux閃亮登場的時候了。這時候Redux本來最大的限制,只能通過Action而不能直接地改變應用狀態也就凸顯出了其意義所在(Use Explicit Actions To Change The State)。
1 2 |
// reducer (state, action) => newState |
漸進的前端架構
筆者心中的前端架構如下所示,這裡分別按照專案的流程與不同的開發時間應該開發的模組進行說明:
解構設計稿
純元件
在解構設計稿之後,我們需要總結出其中的純元件,此時所謂的StoryBook Driven Development就派上了用場,譬如筆者總結出Material UI Extension這個通用類庫。
實體類
實體類其實就是靜態型別語言,從工程上的意義而言就是可以統一資料規範,筆者在上文中提及過康威定律,設計系統的組織,其產生的設計等同於組織之內、組織之間的溝通結構。實體類,再輔以類似於TypeScript、Flow這樣的靜態型別檢測工具,不僅可以便於IDE進行語法提示,還能儘可能地避免靜態語法錯誤。同時,當業務需求發生變化,我們需要重組織部分業務邏輯,譬如修改某些關鍵變數名時,通過統一的實體類可以更方便安全地進行修改。同時,我們還需要將部分邏輯放置到實體類中進行,典型的譬如狀態碼與其描述文字之間的對映、部分靜態變數值的計算等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//零件關聯的圖紙資訊 models: [ModelEntity] = []; cover: string = ''; /** * @function 根據推匯出的零件封面地址 */ get cover() { //判斷是否存在圖紙資訊 if (this.models && this.models.length > 0 && this.models[0].image) { return this.models[0].image; } return 'https://coding.net/u/hoteam/p/Cache/git/raw/master/2016/10/3/demo.png'; } |
同時在實體基類中,我們還可以定義些常用方法:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
/** * @function 所有實體類的基類,命名為EntityBase以防與DOM Core中的Entity重名 */ export default class EntityBase { //實體類名 name: string = 'defaultName'; //預設建構函式,將資料新增到當前類中 constructor(data, self) { //判斷是否傳入了self,如果為空則預設為當前值 self = self || this; } // 過濾值為null undefined '' 的屬性 filtration() { const newObj = {}; for (let key in this) { if (this.hasOwnProperty(key) && this[key] !== null && this[key] !== void 0 && this[key] !== '') { newObj[key] = this[key]; } } return newObj; } /** * @function 僅僅將類中宣告存在的屬性複製進來 * @param data */ assignProperties(data = {}) { let properties = Object.keys(this); for (let key in data) { if (properties.indexOf(key) > -1) { this[[key]] = data[[key]]; } } } /** * @function 統一處理時間與日期物件 * @param data */ parseDateProperty(data) { if (!data) { return } //統一處理created_at、updated_at if (data.created_at) { if (data.created_at.date) { data.created_at.date = parseStringToDate(data.created_at.date); } else { data.created_at = parseStringToDate(data.created_at); } } if (data.updated_at) { if (data.updated_at.date) { data.updated_at.date = parseStringToDate(data.updated_at.date) } else { data.updated_at = parseStringToDate(data.updated_at); } } if (data.completed_at) { if (data.completed_at.date) { data.completed_at.date = parseStringToDate(data.completed_at.date); } else { data.completed_at = parseStringToDate(data.completed_at); } } if (data.expiration_at) { if (data.expiration_at.date) { data.expiration_at.date = parseStringToDate(data.expiration_at.date); } else { data.expiration_at = parseStringToDate(data.expiration_at); } } } /** * @function 將類以JSON字串形式輸出 */ toString() { return JSON.stringify(Object.keys(this)); } /** * @function 生成隨機數 * @return {string} * <a href="http://www.jobbole.com/members/kaishu6296">@private</a> */ _randomNumber() { let result = ''; for (let i = 0; i < 6; i++) { result += Math.floor(Math.random() * 10); } return result; } } |
介面
介面主要是負責進行資料獲取,同時介面層還有一個職責就是對上層遮蔽服務端介面細節,進行介面組裝合併等。筆者主要是使用總結出的Fluent Fetcher,譬如我們要定義一個最常見的登入介面:
建議開發人員介面寫好後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * 通過郵箱或手機號登入 * @param account 郵箱或手機號 * @param password 密碼 * @returns {UserEntity} */ async loginByAccount({account,password}){ let result = await this.post('/login',{ account, password }); return { user: new UserEntity(result.user), token: result.token }; } |
,直接簡單測試下:
1 2 3 4 5 |
let accountAPI = new AccountAPI(testUserToken); accountAPI.loginByAccount({account:'wyk@1001hao.com',password:'1234567'}).then((data) => { console.log(data); }); |
這裡直接使用babel-node
進行執行即可,然後由專業的測試人員寫更加複雜的Spec。
容器/高階元件
容器往往用於連線狀態管理與純元件,筆者挺喜歡IDE的LiveTemplating功能的,典型的容器模板為:
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 37 38 39 40 41 42 43 44 |
// <a href="http://www.jobbole.com/members/26707886">@flow</a> import React, { Component, PropTypes } from 'react'; import { push } from 'react-router-redux'; import { connect } from 'react-redux'; /** * 元件ContainerName,用於展示 */ @connect(null, { pushState: push, }) export default class ContainerName extends Component { static propTypes = {}; static defaultProps = {}; /** * @function 預設建構函式 * @param props */ constructor(props) { super(props); } /** * @function 元件掛載完成回撥 */ componentDidMount() { } /** * @function 預設渲染函式 */ render() { return <section className=""> </section> } } |
服務端渲染與路由
服務端渲染與路由可以參考Webpack2-React-Redux-Boilerplate。
線上質量保障:前端之難,不在前端
前端開發完成並不意味著萬事大吉,筆者在一份週報中寫道,我們目前所謂的Bug往往有如下三類:
(1)開發人員的粗心大意造成的Bug:此型別Bug不可避免,但是可控性高,並且前端目前配置專門的輔助單元測試人員,此型別Bug最多在開發初期大規模出現,隨著專案的完善會逐步減少。
(2)需求變動造成的Bug:此型別Bug不可避免,可控性一般,不過該型別Bug在正式環境下影響不大,最多影響程式設計師個人情緒。
(3)介面變動造成的Bug:此型別Bug不可避免,理論可控性較高。在上週修復的Bug中,此型別Bug所佔比重最大,建議未來後端釋出的時候也要根據版本劃分Release或者MileStone,同時在正式上線後設定一定的灰度替代期,即至少保持一段時間的雙版本相容性。
線上質量保障,往往面對的是很多不可控因素,譬如公司郵件服務欠費而導致註冊郵件無法發出等問題,筆者建立了frontend-guardian,希望在明年一年內予以完善:
- 實時反饋產品是否可用
- 如果不可用,即時通知維護人員
- 如果不可用,能夠迅速輔助定位錯誤
frontend-guardian希望能是儘量簡單的實時監控與迴歸測試工具,大公司完全可以自建體系或者基於Falcon等優秀的工具擴充套件,不過小公司特別是在創業初期希望儘可能地以較小的代價完成線上質量保障。
延伸閱讀
- 尤雨溪:Vue 2.0,漸進式前端解決方案
- 曹劉陽:2016年前端技術觀察
- 割裂的前端工程師:預測前端的2017
- 張鑫:前端技術體系大局觀
- 2017年值得關注的JavaScript框架與主題
- 2016年前端工具使用度調研報告
- 2016年裡做前端是怎樣一種體驗
- 2016前端學習路線圖
- Web前端從入門菜鳥到實踐老司機所需要的資料與指南合集
後記
2016年末如往昔一般很多優秀的總結盤點文章湧現了出來,筆者此文也是斷斷續續寫了好久,公司專案急著上線,畢業論文也是再不寫就要延期的節奏。這段時間筆者看了很多大家之作後越發覺得自己的格局與眼光頗低,這也是筆者一直在文中提及我的經驗與感觸更多的來自於中小創團隊,希望明年能夠有機會進一步開拓視野。如果哪位閱讀本文的夥伴有好的交流群推薦歡迎私信告知,三人行,必有我師,筆者也是希望能夠接觸一些真正的大神。