技術乾貨 | Thinking in FE 更現代的 Web 開發

前端,是一個經常會被小覷的技術領域,在大多不明所以的人眼裡,前端不過是排排版、布布局,甚至是一些前端的新手也會這樣認為(這裡的前端並不特指 Web 前端,移動端也可歸結為前端)。那麼前端真的就如此無趣且一成不變么?

之所以本系列取名為 Thinking in FE,是因為 Thinking 讓人沉靜、不浮躁,就該用這種心態來面對前端。作為本系列的第一篇,我覺得是很有必要把 Web 前端拿出來說說,這幾年 Web 前端變革得太快,如果你還是以為吃透了 float 就吃透了整個布局,搞定了 css + div 就能縱橫 Web FE 的話,本篇就是為你而準備的。

開發模式的變革

前幾年,當我還奮戰在 Web 前端開發的第一線時,那時候的項目開發模式是簡單但容易出問題的。當時後端主要使用的技術是微軟的 WebAPI,前端的 IDE 自然就被 Visual Studio 包攬了(當然那時 WebStorm、PhpStorm 也都在不同項目中承擔著 IDE 的角色)。IDE 倒不是什麼問題,當時主要的問題在於前端第三方庫依賴的管理,基本上都是手動引用,長時間後會連一些庫的具體版本都忘記了。這在多人協作開發時很容易出問題,也不利於項目的持續和快速發展,久而久之一個項目會變得陳舊、死氣沉沉。

現在的開發模式,悄然變得更輕卻又更重了。更輕的是 IDE,我們開始傾向於使用像 Sublime、Atom、VSCode 這種輕量級的文本編輯器,再配合一些日常需要的小插件;而更重的是項目的依賴管理和構建方式,依賴管理已經被 NodeJS 的包管理工具npm包攬了,基本上我們需要什麼樣的庫,只要簡單的npm install一下就可以了,而構建工具很多,比如 Webpack、Gulp、Grunt、Yeoman 等,但也慢慢的有被 Webpack 一統江湖的趨勢。

有了依賴管理、構建工具,並且可以通過npm配合其他工具來執行單元測試,我們便可以很容易的將項目進行持續集成。這才是更加現代化的開發模式,而整個 Web 前端的生態也趨向完整了。

百花齊放的開發語言

作為一個站在時代前沿的 Web 前端開發者,可能會是所有開發工種中接觸開發語言最多的一個,至少你需要掌握三門語言:html、css、javascript,這是最終的宿主,也就是瀏覽器所原生支持的三種語言,分別用於:結構、樣式、交互。但如果你真的只會這三種語言,那你肯定算不上一個合格的 Web 前端開發者,隨著廣大先驅者的智慧凝結,這三種基礎的語言衍化出了很多獨立的語言,而這些衍化的產物已經越來越被現代化的 Web 前端所廣泛使用。

從 html 所衍化出來的是各種模板語言,比如 backbone、angular 所提供的。模板的作用是將結構高度抽象,從而避免很多不必要的重複工作,並且使得前端頁面更加動態化。html 本身是靜態的描述語言,有了模板的支持,我們可以像下面這樣來讓其動態化:

從 css 所衍化出來的,便是和樣式相關的語言了,與 html 語言一樣,css 也是一種靜態的描述語言,本身不支持變數和條件分支。作為對 css 的擴充,市面上出現了像 less、sass 這樣一些語言,它們使得樣式的描述更加結構化,並且可以通過變數很方面的來修改和維護,這對需要提供樣式定製化的第三方組件而言還是非常有用的。下面是 sass 的變數和嵌套示例:

最後從 javascript 中衍化出來的,便是很多對 javascript 特性進行補充的語言了。javascript 本身是基於原型的語言,自身也有一些設計上的缺陷,最常見的便是變數的作用域問題,也就是所謂的變數提升問題。不過在 ES6 出來後,javascript 得到了質的提升,而在這之前,出現了 javascript 的替代語言,以 typescript 和 coffeescript 最為常用,並且現在還被廣泛使用著。無論是 typescript 還是 coffeescript,它們都是對 javascript 的補充,而 coffeescript 更像是一門新的語言。它們使得 javascript 更加的面向對象,並引入了更多函數式語言的特性,讓書寫更加優雅、舒適,下面一段 coffeescript(摘自Coffee-Script中文網),大家感受下:

自 ES6 出來並受到很多工具的支持後,已經更加推薦直接使用 ES6 來編寫項目了,ES6 彌補了 javascript 之前一直缺乏的原生模塊化支持(這裡說的是原生,排除 CommonJS、AMD、CMD 規範的第三方實現),對面向對象也有了更好的支持,並且明確了變數、作用域,也引入了很多函數式編程的概念。最重要的是在2013年 ES6 標準就已經確定了,對於新的提案 TC39 只會往 ES7 納入,所以在項目中使用不會面臨像使用 Swift 一樣不斷變更的窘境。

上面說過了,瀏覽器原生只支持最基本的那三種語言,那麼如果想使用這些衍化出來的語言或者是現在還不被很好支持的ES6、ES7,我們需要相應的轉換工具。而這些轉換操作都可以非常簡單的使用 webpack 對應的 loader 來完成。不得不說 webpack 已經成為了 Web 前端構建的一站式工具,通過組合不同的 loader,我們可以完成轉換->合併->壓縮->打包等一系列中間過程。

React 的顛覆

如果要論這幾年來,對 Web 前端思想產生顛覆性的框架,那應該是非 React 莫屬了。Facebook 在2013年開源了這個框架,由此引發了一系列的變革。React 的核心思想是組件化,化整為零,分而治之。而 React 出現的原因,也正是因為 Facebook 對當時市面上所有的前端框架都不滿意,既然不滿意,他們就立馬自己做了一個。

在 React 出來之前,市面上使用較多的都是一些MV*系列的框架,比較有代表性的應該算是谷歌的 Angular 了。但這類框架的學習曲線還是比較高的,最重要的是,對於一般人而言它們所表述的意圖不夠直觀。從視圖到模型,雖然力求低耦合,但還是不得不進行約定、依賴,因為最終視圖和模型需要綁定,那無論如何解耦都不可能做到乾淨利落,約定只會徒增維護的複雜度。

對此,React 提出了組件的概念(當然這個概念在其它領域早就有過),一個組件就是一個高內聚的封裝。對外部而言組件的輸入是屬性(props),輸出是最終的視圖,屬性是恆定的,也就是說外部輸入之後,就不會被改變了。而讓組件改變的是狀態(state),對於 React 而言,狀態是由組件內部進行維護的,這種思想讓組件變得更加內聚、可控。下面是一個非常簡單的 React 組件:

上面這個組件擁有一個hidden的狀態,而render方法中的內容也是讓人一目了然(JSX語法讓組件更加內聚)。通過界面交互或其它一些手段我們可以改變hidden的值,而這會實時體現到render方法中。React 自己維護了一套虛擬 DOM,一般情況下我們不必刻意考慮渲染性能問題,但如果你想自己控制是否重繪的話,React 的組件也給你提供了這樣的控制能力。

React 的組件除了內聚之外,還可以進行組合,一個組件可以嵌入其它多個組件。這使得我們在進行實際開發之前,需要對即將完成的內容進行組件劃分,在通用和簡單兩方面來作權衡。也就是說,React 的思想已經顛覆了我們思考問題的方式,而它給我們帶來的收穫是組件的不斷積累,以及開發速度和可維護性的提高。

單項數據流 Flux

在絕大數MV*系列架構的框架中,視圖 DOM 和視圖模型之間是進行雙向綁定的,這種強綁定的情形在很多複雜的場景下會帶來讓人無法維護的問題。當這樣的情況越來越普遍時,Facebook 提出了單向數據流的概念,並把這種思想稱之為Flux,且推出了官方實現flux。不得不說 Facebook 是個了不起的公司,也不得不說 Web 前端一直是這些新思維的探路者。不過flux很快就被另一個開源項目慢慢取代了,也就是社區中非常火爆的redux,但思想還是一致的。

這裡有必要解釋一下單向的概念,整個 Flux 的數據流如下:

  1. 用戶觸發 View 的某個操作,View 向 Dispatcher 發出一個 Action
  2. Dispatcher 收到 Action 後,對 Store 進行更新
  3. Store 更新後,發出事件通知 View
  4. View 收到事件後,進行頁面更新

這裡整個數據流都是單向流動的(概念抽象中沒有雙向箭頭),所有狀態都維護在 Store 之中,這讓我們對狀態變更進行追蹤變得非常簡單。在redux的實現中,從 Dispatcher 到 Store 之間,我們還可以安裝很多自定義的中間件,來進行一些切面處理,比如日誌、授權、統計等。

Facebook 的 React 僅僅是提供了組件化的構建方案,而對於組件所構成的模塊並沒有提供更多架構上的支持,這點基於 Flux 思想的redux剛好可以對其進行補充。在解釋如何讓它們銜接之前,我們有必要先看點其它內容。

非同步任務編織

所有的項目開發中,為了追求更好的用戶體驗,我們不可避免的要面對非同步問題。同步操作下,我們對流程管理和安排非常簡單清晰,相比之下,非同步就沒有那麼容易去維護了。

而在 Web 前端,很長一段時間裡,ajax 幾乎就成了非同步的代名詞,因為在實際開發中,80%以上的非同步都來自於非同步網路請求。時至今日,我覺得需要重新定義下非同步在 Web 前端中的定位了,特別是在使用了redux之後。從最初的action派發,到最終的狀態變更,以及狀態變更後引發的視圖渲染,這一系列的步驟,我們都應該將其視為非同步(參考上文 Flux 的圖片)。

眾所周知,javascript 處於一個單線程的運行環境中,但非同步的引入使得我們也需要面臨一些多線程下才存在的問題。並且我們重新定義了並發的概念,在 javascript 中,並髮指的是一個非同步任務尚未完成,同時又產生了其它非同步任務。比如,我們同時發出了兩個 ajax 請求,那麼我們就必須要面對這兩個請求返回時間、順序不確定性的結果。

在 ES6 的語言標準中,引入了Promise概念,可以方便我們對非同步任務進行鏈式編排,並且可以統一進行錯誤處理,下面是一個簡單的例子:

雖然這種鏈式調用從某種程度上讓代碼更加清晰,但在對非同步返回數據需要進行條件分支判斷,或者一些更加複雜的邏輯操作時,Promise也就顯得有些力不從心了。在 ES6 中還引入了另外一個概念,叫Generator,與之對應的關鍵字是yield,Generator的特性是函數內部維護了上一次執行到的位置,而在外部調用next()控制它進一步執行(關於這方面更多的知識,請參考相關ES6手冊)。其實這點無疑是走了微軟 C# 的老路,並且在 ES7 中引入的async和await也是與 C# 同出一轍,在 C# 推出yield關鍵字後,社區也有達人以此實現了一套非同步任務編織的框架,那麼 Web 前端自然也不例外了。

這裡不得不說一下redux的一個中間件redux-saga,它是完全基於Generator特性實現的一套非同步任務編織框架,並且非常強大。一個saga對應一個Generator函數,並且saga分為兩種:

  1. watcher saga: 負責監控 redux 的 action,並且對任務進行具體編排
  2. worker saga: 處理由watcher saga編排的具體任務

如果想要了解更多關於redux-saga的內容,還是建議去翻閱下官方文檔,這裡給出一個簡單的示例,一睹它的威力(摘自saga文檔):

上面的示例中有兩個saga,其中loginFlow為 watcher 而authorize為 worker。在loginFlow中,當我們收到LOGIN_REQUEST的action時,取出其中的user和password狀態,非阻塞的去調用authorize,並且開始監控LOGOUT和LOGIN_ERROR兩個action,當收到的action為LOGOUT,此時前一個LOGIN_REQUEST可能並未執行完成,所以我們需要取消它,在這一切完成後,我們調用clearItem來清空本地存儲的token,再次回歸到監控LOGIN_REQUEST。而authorize這個saga中的流程也相對簡單明了,這裡就不作更多的闡述了。

通過saga的實現,可以與Promise進行對比,不難發現它更加的同步化,所有的代碼完全看不出非同步的影子,所以在進行複雜的非同步任務編排和分支控制時,會非常的簡潔明了。

項目的最佳實踐

上面說到了項目構建工具、各種新興的開發語言、React、Redux 以及 Redux-saga,那麼在一個實際的項目中,我們如何將它們融合起來,進行更加現代化的 Web 開發呢?其實很簡單,我們可以通過npm來進行項目包依賴管理,並且通過webpack來將它們全部串聯起來。

對於webpack,我們需要安裝一系列的loader並且在webpack.config.js中進行配置,大概會用到下面這些loader:

  • babel-loader:用於轉換 ES6、ES7、JSX 語法
  • file-loader:用於簡單的文件拷貝
  • css-loader:用戶 CSS 的壓縮,打包
  • less-loader:用戶 LESS 的轉換
  • url-loader:用戶圖片資源的轉換、打包
  • html-loader:用戶 HTML 文件的鏈接替換、打包
  • html-minify-loader:用於 HTML 文件的壓縮

具體的配置需要根據項目本身而定,詳細的細節可以參考webpack的官方文檔。當我們把webpack這些loader配置完畢後,可以按照一些我們需要的框架和中間件了,大體應該如下:

  • react:react 組件化的核心庫
  • react-dom:react 提供的一些 DOM 操作輔助方法庫,用於在 DOM 上渲染 react 組件
  • react-router:使用 react 開發單頁應用時,這個庫是必須的,提供了基於 react 組件化思路的路由解決方案
  • redux:redux 庫,上文中已經有所解釋,這裡就不多說了
  • redux-actions:方便在 redux 中 action 和對應的state進行管理
  • redux-saga:redux 的中間件,用於非同步任務編織
  • react-router-redux:redux 的中間件,用於同步 redux 中的路由狀態,可以通過 redux 的 ation 來控制路由
  • reselect:在較大型項目中使用,用於react和redux連接時connect中map參數的管理,可以有效減少狀態變更,減少組件渲染的次數

有了上面的這些組件,我們基本上可以愉快的進行項目開發了,那麼在整個項目的流程中,它們是如何進行協作的呢?可以參考下圖:

可以看到,貫穿其中最多的便是action和state,而redux顯然是整個項目連接起來的樞紐,saga則是用於封裝了所有的非同步網路請求(封裝成更加業務化的任務)。這裡舉一條比較常見的流程:用戶從界面上點擊了某個按鈕,然後發送了網路請求,以及請求響應後對界面的更新:

  1. 首先從 react 組件的 View 上派發了一個 action 到 Redux
  2. Redux 的中間件 Saga 會監控這個 action 並作出網路請求
  3. 響應回來後,Saga 會通過 put 將響應的 action 派發到 Redux
  4. Redux 接收到這個 action 對相應狀態作出變更
  5. react 連接到(connect)這個狀態的相應組件會收到狀態變更,重新渲染 View

似乎看起來比較複雜,但在了解透徹相應組件的職責後,其實並沒那麼複雜。為了讓開發過程中調試更加快捷,我們還可以安裝一些開發中需要用到的工具模塊。這裡推薦使用 dora,配合dora-plugin-proxy我們可以在開發過程中模擬後端響應數據,再配合dora-plugin-webpack-hmr可以實現開發過程中,模塊的熱載入,讓我們無需不斷刷新瀏覽器就能看到最新的界面效果,當然選擇還有很多,dora 只是我覺得比較好用的一款。

Thinking

本篇帶著大家走馬觀花的將現代化的 Web 前端看了個大概,這其中更多的是在闡述思想和理念,隨著時代的發展,Web 前端並不是大多數人們眼中的「做做網頁而已」,它的理念走在了時代的前言,它的生態也比很多其它方向豐富、健全。

不小看、自以為是,要永遠抱著敬畏的態度,這是成長的基礎,也是我們作為技術人員該有的素質。

參考

  • Webpack: webpack.github.io
  • dora-js: github.com/dora-js
  • React: facebook.github.io/reac
  • Redux 中文文檔: cn.redux.js.org/
  • React-Router: github.com/reactjs/reac
  • reselect: github.com/reactjs/rese
  • Redux-Sage: yelouafi.github.io/redu
  • Flux: reactjs.cn/react/docs/f
  • ECMAScript 6 中文文檔: es6.ruanyifeng.com

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

推薦閱讀:

react配合redux的生命周期(shouldComponentUpdate)的問題?
使用 Redux-Arena 組合 React 組件
感覺redux寫起來很麻煩,目前有那些其他的狀態管理方案?
redux middleware 詳解
集成 React 和 Datatables - 並沒有宣傳的那麼難

TAG:React | Redux | 前端开发 |