IMVC(同構 MVC)的前端實踐

IT大咖說發表於2019-02-10

IMVC(同構 MVC)的前端實踐

內容來源: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。

同構的實現策略

同構的第一要旨是全盤同構沒有意義,服務端和客戶端作為不同的平臺,專注解決的是不同的問題,全盤同構會抹殺它們固有的差異,也就無法發揮各自的優勢。因此,只需要在有交集的部分進行同構。對於內容同構的程式碼可以直接複用,內容不同構的封裝成形式同構。

形式同構的實現思路

IMVC(同構 MVC)的前端實踐

形式同構的實現思路就是抽象,來看下獲取User Agent 字串的例子。客戶端通過navigator.userAgent 直接拿到字串,服務端則使用req.get(“user-agent”) 。要想實現同構,我們可以在服務端構造一個全域性的navigator 物件,模擬客戶端環境。也可以封裝一個 getUserAgent 函式,自行判斷從何處取UserAgent 的值。

IMVC(同構 MVC)的前端實踐

Cookies處理在我們的場景裡,存在快捷通道,因為我們只專注首次渲染的同構,其它的操作可以放在瀏覽器端二次渲染的時候再處理。

重定向最少有三種以上的實現方式:

  1. 改變前端location 位置

  2. 前端使用pushState 方法,只改變路徑並觸發函式 ,但是不進行頁面渲染

  3. 服務端採用302 重定向,通過封裝函式判斷環境以及重定向方法

IMVC的目標

現在來看下IMVC 所需要實現的目標:

  1. 用法簡單,初學者也能快速上手

  2. 只維護一套ES2015+ 的程式碼

  3. 既是單頁應用,優勢多頁應用(SPA + SSR)

  4. 可以部署到任意釋出路徑(Basename / RootPath)

  5. 一條命令啟動完備的開發環境

  6. 一條命令完成打包 / 部署過程

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的組成就瞭解了。

  1. history 是react-router 依賴的底層庫

  2. path-to-regexp 是 expressjs 依賴的底層庫

  3. 在View(React) 層和Model 層之外實現Controller 層

我們認為React 和 Redux 分別對應MVC 的 View 和 Model,它們都是同構的,我們需要的是實現 Controller 層的同構。

Create-app的同構理念

  1. 服務端和客戶端進行 URL 的輸入,Router 解析 URL 匹配對應的mvc元件

  2. 呼叫模組載入器載入元件,然後初始化 Controller

  3. 呼叫 Controller.init 方法,返回view 例項

  4. 呼叫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 的具體工程實施。

  1. node.js 執行時,npm 包管理

  2. expressjs 服務端框架

  3. babel 編譯ES2015+ 程式碼到 ES5

  4. webpack 打包和壓縮原始碼

  5. standard.js 檢查程式碼規範

  6. prettier.js + git-hook 程式碼自動美化排版

  7. 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 視覺化檢視編譯結果。

相關文章