內容來源:2017 年 3 月 11 日,攜程研發高階經理古映傑在“攜程技術沙龍 | 新一代前端技術實踐”進行《IMVC(同構 MVC)的前端實踐》演講分享。IT 大咖說(微信id:itdakashuo)作為獨家視訊合作方,經主辦方和講者審閱授權釋出。
閱讀字數:2738 | 7分鐘閱讀
嘉賓演講視訊及PPT回顧:suo.im/4VPTN5
摘要
隨著 Backbone 等老牌框架的逐漸衰退,前端 MVC 發展緩慢,有逐漸被 MVVM/Flux 所取代的趨勢。然而,縱觀近幾年的發展,可以發現一點,React / Vue 和 Redux / Vuex 是分別在 MVC 中的 View 層和 Model 層做了進一步發展。如果 MVC 中的 Controller 層也推進一步,將得到一種升級版的 MVC,我們稱之為 IMVC(同構 MVC)。
IMVC 可以實現一份程式碼在服務端和瀏覽器端皆可執行,具備單頁應用和多頁應用的所有優勢,並且可在這兩種模式裡通過配置項進行自由切換。配合 Node.js、Webpack、Babel 等基礎設施,我們可以得到相比之前更加完善的一種前端架構。
IMVC(同構MVC)
IMVC的“I”指的是ISOMORPHIC ,也就是同構,最初它是數學上的概念,描述兩個物件之間的某種一致性。在前端領域中ISOMORPHIC JAVASCRIPT 則是指一段前端程式碼在客戶端和服務端都可執行,它在2012年就已經被提出,算是歷史悠久的概念了。
同構的種類
同構分為內容同構和形式同構,內容同構指同樣的程式碼在客戶端和服務端做等價的事情。形式同構通過判斷所處環境來執行某段程式碼,也就是說在客戶端或者服務端始終有一部分程式碼沒有執行。
同構的層次
同構並不是一種非是即彼的判斷,它更像是光譜,既可以是小範圍的也可以是大範圍。小範圍的同構,例如原生的js 在瀏覽器和Node 中程式碼並沒有差異,只是DOM API 和 Node API 不同而已,這就是函式層面的同構,即程式碼片段相同。還有一種特性層的同構,指的是業務中不同職能特性的同構,比如Vue 2.0在客戶端和服務端都能執行,這就是Vue 這個特性層的同構。另外就是框架層同構,框架基本上包含了需要的所有的層次,而框架層的同構就是實現平衡,判斷某個部分是否需要同構,並將同構與非同構部分融洽結合起來。
同構的價值
首先是SEO-friendly 的實現。其次第一次開啟網頁時不必等待JS 載入完成才能看到內容,頁面的互動也能夠得到即時響應,這就是速度上的優勢。同構的運用使得服務端和客戶端都使用同一套程式碼,有效的降低了維護成本。
同構是未來的趨勢
早期客戶端 JS 的作用就只是DOM 操作以及表單驗證之類的事情,由服務端去實現業務邏輯、路由跳轉、頁面渲染等方面的事務。現階段前端變的越發龐大,原先服務端需要處理的事情一部分被交由前端完成。可以發現早期是服務端臃腫,客戶端輕便,現階段則相反。
未來通過同構可以實現部分功能共享,比如頁面的跳轉、渲染、業務邏輯。讓NodeJS去接管渲染層,後端部分向後再退一層,只負責資料持久化以及提供Restful API。
同構的實現策略
同構的第一要旨是全盤同構沒有意義,服務端和客戶端作為不同的平臺,專注解決的是不同的問題,全盤同構會抹殺它們固有的差異,也就無法發揮各自的優勢。因此,只需要在有交集的部分進行同構。對於內容同構的程式碼可以直接複用,內容不同構的封裝成形式同構。
形式同構的實現思路
形式同構的實現思路就是抽象,來看下獲取User Agent 字串的例子。客戶端通過navigator.userAgent 直接拿到字串,服務端則使用req.get(“user-agent”) 。要想實現同構,我們可以在服務端構造一個全域性的navigator 物件,模擬客戶端環境。也可以封裝一個 getUserAgent 函式,自行判斷從何處取UserAgent 的值。
Cookies處理在我們的場景裡,存在快捷通道,因為我們只專注首次渲染的同構,其它的操作可以放在瀏覽器端二次渲染的時候再處理。
重定向最少有三種以上的實現方式:
-
改變前端location 位置
-
前端使用pushState 方法,只改變路徑並觸發函式 ,但是不進行頁面渲染
-
服務端採用302 重定向,通過封裝函式判斷環境以及重定向方法
IMVC的目標
現在來看下IMVC 所需要實現的目標:
-
用法簡單,初學者也能快速上手
-
只維護一套ES2015+ 的程式碼
-
既是單頁應用,優勢多頁應用(SPA + SSR)
-
可以部署到任意釋出路徑(Basename / RootPath)
-
一條命令啟動完備的開發環境
-
一條命令完成打包 / 部署過程
IMVC的技術選型
IMVC 只是一個架構上的理念,理論上並不要求使用特定的技術棧,只需要實現期望的目標就行了。但是,要達成目標還是要做出一些選擇,下面是我們現在的選擇,當然未來可能升級或者做出改變。
1、Router: create-app = history + path-to-regexp
2、View: React = renderToDOM || renderToString
3、Model: relite = redux-like library
4、Ajax: isomorphic-fetch
為什麼不直接使用 REACT 全家桶
可以看到我們的技術選型中使用了很多的React相關的技術,但是卻並沒有直接使用React 全家桶。
目前的React 全家桶其實是野生的,Facebook 官方並不會使用,只是認知度比較高而已。React-Router的理念也難以滿足要求,檢視view-source 會發現它沒有實現同構。另外Redux 適用於大型應用,而我們的主要場景是中小型。
無論是Redux 還是 React-Router 升級都非常頻繁,導致學習成本過高,需要封裝一層更簡潔的API。
用create-app 替代 React-Router
面對社群千變萬化的框架,正確的做法應該是業務開發使用一層專屬的封裝,底層執行時使用社群流行的方案。用create-app 替代 React-Router並不代表需要全盤重寫,而是引用需要的部分,拋棄原本的理念。來看下Create-app的組成就瞭解了。
-
history 是react-router 依賴的底層庫
-
path-to-regexp 是 expressjs 依賴的底層庫
-
在View(React) 層和Model 層之外實現Controller 層
我們認為React 和 Redux 分別對應MVC 的 View 和 Model,它們都是同構的,我們需要的是實現 Controller 層的同構。
Create-app的同構理念
-
服務端和客戶端進行 URL 的輸入,Router 解析 URL 匹配對應的mvc元件
-
呼叫模組載入器載入元件,然後初始化 Controller
-
呼叫 Controller.init 方法,返回view 例項
-
呼叫view-engine 將 view 的例項根據環境渲染成 html 或 native-ui 等。
Create-app的配置理念
由於客戶端模組是非同步載入而服務端是同步載入,要想在他們之間做到平衡就需要實現一個Create-app的配置。
服務端和瀏覽器端分別有自己的入口檔案:client-entry.js 和 server.entry.js。我們只需提供不同的配置即可。
在服務端,載入 controller 模組的方式是 commonjsLoader;在瀏覽器端,載入 controller 模組的方式則為 webpackLoader。
在服務端和瀏覽器端,view-engine 也被配置為不同的 ReactDOM 和 ReactDOMServer。每個 controller 例項,都有 context 引數,它也是來自配置。通過這種方式,我們可以在執行時注入不同的平臺特性。這樣既分割了程式碼,又實現了形式同構。
Create-app 的服務端渲染
我們認為正確的服務端渲染應該只有唯一的路由表和請求,僅根據輸入的URL 和環境資訊返回全部的渲染內容。
Create-app 的目錄結構
├── src // 原始碼目錄
│ ├── app-demo // demo目錄
│ ├── app-abcd // 專案abcd 平臺目錄
│ │ ├── components // 專案共享元件
│ │ ├── shared // 專案共享方法
│ │ └── BaseController// 繼承基類 Controller 的專案層 Controller
│ │ ├── home // 具體頁面
│ │ │ ├── controller.js// 控制器
│ │ │ ├── model.js // 模型
│ │ │ └── view.js // 檢視
│ │ ├── * // 其他頁面
│ │ └── routes.js // abc 專案扁平化路由
│ ├── app-* // 其他專案
│ ├── components // 全域性共享元件
│ ├── shared // 全域性共享檔案
│ │ └── BaseController // 基類Controller
│ ├── index.js // 全域性js 入口
│ └── routes.js // 全域性扁平化路由
├── static // 原始碼 build 的目標靜態資料夾
上面展示的是 Create-app 的目錄結構,它和Redux 的傳統目錄結構不同。每個頁面都是單獨的資料夾,包含Controller、model、view。整個專案頁面使用routers 路由表串起來。create-app採取了「整站 SPA」的模式,全域性只有一個入口檔案index.js。
ISOMORPHIC-MVC的工程化實施
上面談論的是IMVC 在執行時的功能和特點,下面看下IMVC 的具體工程實施。
-
node.js 執行時,npm 包管理
-
expressjs 服務端框架
-
babel 編譯ES2015+ 程式碼到 ES5
-
webpack 打包和壓縮原始碼
-
standard.js 檢查程式碼規範
-
prettier.js + git-hook 程式碼自動美化排版
-
mocha 單元測試
如何實現程式碼實時熱更新
使用webpack 的 node.js API 管理 webpack 程式,客戶端採用express + webpack-dev-middleware 在記憶體裡編譯,服務端採用memory-fs + webpack + vm-module。服務端的webpack 編譯到記憶體模擬的檔案系統,再用 node.js 內建的虛擬機器模組執行後得到新的模組。
如何處理 css 按需載入
問題根源:瀏覽器只在 dom-ready 之前會等待 css 資源載入後再渲染頁面
問題描述:當單頁跳轉到另一個 url,css 資源還沒載入完,頁面顯示成混亂佈局
處理辦法:將 css 視為預載入的 ajax 資料,以 style 標籤的形式按需引入
優化策略:用 context 快取預載入資料,避免重複載入
如何實現程式碼切割、按需載入
不使用webpack-only 的語法require.Ensure。在瀏覽器裡require 被編譯為載入函式,非同步載入。在node.js 裡require 是同步載入。
如何處理靜態資源的版本管理
以程式碼的 hash 為檔名,增量釋出。用webpack.stats.plugin.js 生成靜態資源表。Express 使用stats.json 的資料渲染頁面。
如何管理命令列任務
1、使用 npm-scripts 在 package.json 裡完成 git、webpack、test、prettier等任務的串並聯邏輯
2、npmstart 啟動完整的開發環境
3、npmrun start:client 啟動不帶服務端渲染的開發環境
4、npmrun build 啟動自動化編譯,構建與壓縮部署的任務
5、npmrun build:show-prod 用 webpack-bundle-analyzer 視覺化檢視編譯結果。