內容來源:2017 年 3 月 11 日,攜程研發高級經理古映傑在「攜程技術沙龍 | 新一代前端技術實踐」進行《IMVC(同構 MVC)的前端實踐》演講分享。IT 大咖說(微信id:itdakashuo)做爲獨家視頻合做方,經主辦方和講者審閱受權發佈。
css
閱讀字數:2738 | 7分鐘閱讀html
嘉賓演講視頻及PPT回顧:suo.im/4VPTN5前端
隨着 Backbone 等老牌框架的逐漸衰退,前端 MVC 發展緩慢,有逐漸被 MVVM/Flux 所取代的趨勢。然而,縱觀近幾年的發展,能夠發現一點,React / Vue 和 Redux / Vuex 是分別在 MVC 中的 View 層和 Model 層作了進一步發展。若是 MVC 中的 Controller 層也推動一步,將獲得一種升級版的 MVC,咱們稱之爲 IMVC(同構 MVC)。node
IMVC 能夠實現一份代碼在服務端和瀏覽器端皆可運行,具有單頁應用和多頁應用的全部優點,而且可在這兩種模式裏經過配置項進行自由切換。配合 Node.js、Webpack、Babel 等基礎設施,咱們能夠獲得相比以前更加完善的一種前端架構。react
IMVC的「I」指的是ISOMORPHIC ,也就是同構,最初它是數學上的概念,描述兩個對象之間的某種一致性。在前端領域中ISOMORPHIC JAVASCRIPT 則是指一段前端代碼在客戶端和服務端均可運行,它在2012年就已經被提出,算是歷史悠久的概念了。webpack
同構分爲內容同構和形式同構,內容同構指一樣的代碼在客戶端和服務端作等價的事情。形式同構經過判斷所處環境來執行某段代碼,也就是說在客戶端或者服務端始終有一部分代碼沒有執行。git
同構並非一種非是即彼的判斷,它更像是光譜,既能夠是小範圍的也能夠是大範圍。小範圍的同構,例如原生的js 在瀏覽器和Node 中代碼並無差別,只是DOM API 和 Node API 不一樣而已,這就是函數層面的同構,即代碼片斷相同。還有一種特性層的同構,指的是業務中不一樣職能特性的同構,好比Vue 2.0在客戶端和服務端都能運行,這就是Vue 這個特性層的同構。另外就是框架層同構,框架基本上包含了須要的全部的層次,而框架層的同構就是實現平衡,判斷某個部分是否須要同構,並將同構與非同構部分融洽結合起來。web
首先是SEO-friendly 的實現。其次第一次打開網頁時沒必要等待JS 加載完成才能看到內容,頁面的交互也可以獲得即時響應,這就是速度上的優點。同構的運用使得服務端和客戶端都使用同一套代碼,有效的下降了維護成本。ajax
早期客戶端 JS 的做用就只是DOM 操做以及表單驗證之類的事情,由服務端去實現業務邏輯、路由跳轉、頁面渲染等方面的事務。現階段前端變的愈加龐大,原先服務端須要處理的事情一部分被交由前端完成。能夠發現早期是服務端臃腫,客戶端輕便,現階段則相反。express
將來經過同構能夠實現部分功能共享,好比頁面的跳轉、渲染、業務邏輯。讓NodeJS去接管渲染層,後端部分向後再退一層,只負責數據持久化以及提供Restful API。
同構的第一要旨是全盤同構沒有意義,服務端和客戶端做爲不一樣的平臺,專一解決的是不一樣的問題,全盤同構會抹殺它們固有的差別,也就沒法發揮各自的優點。所以,只須要在有交集的部分進行同構。對於內容同構的代碼能夠直接複用,內容不一樣構的封裝成形式同構。
形式同構的實現思路就是抽象,來看下獲取User Agent 字符串的例子。客戶端經過navigator.userAgent 直接拿到字符串,服務端則使用req.get(「user-agent」) 。要想實現同構,咱們能夠在服務端構造一個全局的navigator 對象,模擬客戶端環境。也能夠封裝一個 getUserAgent 函數,自行判斷從何處取UserAgent 的值。
Cookies處理在咱們的場景裏,存在快捷通道,由於咱們只專一首次渲染的同構,其它的操做能夠放在瀏覽器端二次渲染的時候再處理。
重定向最少有三種以上的實現方式:
改變前端location 位置
前端使用pushState 方法,只改變路徑並觸發函數 ,可是不進行頁面渲染
服務端採用302 重定向,經過封裝函數判斷環境以及重定向方法
如今來看下IMVC 所須要實現的目標:
用法簡單,初學者也能快速上手
只維護一套ES2015+ 的代碼
既是單頁應用,優點多頁應用(SPA + SSR)
能夠部署到任意發佈路徑(Basename / RootPath)
一條命令啓動完備的開發環境
一條命令完成打包 / 部署過程
IMVC 只是一個架構上的理念,理論上並不要求使用特定的技術棧,只須要實現指望的目標就好了。可是,要達成目標仍是要作出一些選擇,下面是咱們如今的選擇,固然將來可能升級或者作出改變。
一、Router: create-app = history + path-to-regexp
二、View: React = renderToDOM || renderToString
三、Model: relite = redux-like library
四、Ajax: isomorphic-fetch
能夠看到咱們的技術選型中使用了不少的React相關的技術,可是卻並無直接使用React 全家桶。
目前的React 全家桶實際上是野生的,Facebook 官方並不會使用,只是認知度比較高而已。React-Router的理念也難以知足要求,查看view-source 會發現它沒有實現同構。另外Redux 適用於大型應用,而咱們的主要場景是中小型。
不管是Redux 仍是 React-Router 升級都很是頻繁,致使學習成本太高,須要封裝一層更簡潔的API。
面對社區變幻無窮的框架,正確的作法應該是業務開發使用一層專屬的封裝,底層運行時使用社區流行的方案。用create-app 替代 React-Router並不表明須要全盤重寫,而是引用須要的部分,拋棄本來的理念。來看下Create-app的組成就瞭解了。
history 是react-router 依賴的底層庫
path-to-regexp 是 expressjs 依賴的底層庫
在View(React) 層和Model 層以外實現Controller 層
咱們認爲React 和 Redux 分別對應MVC 的 View 和 Model,它們都是同構的,咱們須要的是實現 Controller 層的同構。
服務端和客戶端進行 URL 的輸入,Router 解析 URL 匹配對應的mvc組件
調用模塊加載器加載組件,而後初始化 Controller
調用 Controller.init 方法,返回view 實例
調用view-engine 將 view 的實例根據環境渲染成 html 或 native-ui 等。
因爲客戶端模塊是異步加載而服務端是同步加載,要想在他們之間作到平衡就須要實現一個Create-app的配置。
服務端和瀏覽器端分別有本身的入口文件:client-entry.js 和 server.entry.js。咱們只需提供不一樣的配置便可。
在服務端,加載 controller 模塊的方式是 commonjsLoader;在瀏覽器端,加載 controller 模塊的方式則爲 webpackLoader。
在服務端和瀏覽器端,view-engine 也被配置爲不一樣的 ReactDOM 和 ReactDOMServer。每一個 controller 實例,都有 context 參數,它也是來自配置。經過這種方式,咱們能夠在運行時注入不一樣的平臺特性。這樣既分割了代碼,又實現了形式同構。
咱們認爲正確的服務端渲染應該只有惟一的路由表和請求,僅根據輸入的URL 和環境信息返回所有的渲染內容。
├── 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。
上面談論的是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 內置的虛擬機模塊執行後獲得新的模塊。
問題根源:瀏覽器只在 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 的數據渲染頁面。
一、使用 npm-scripts 在 package.json 裏完成 git、webpack、test、prettier等任務的串並聯邏輯
二、npmstart 啓動完整的開發環境
三、npmrun start:client 啓動不帶服務端渲染的開發環境
四、npmrun build 啓動自動化編譯,構建與壓縮部署的任務
五、npmrun build:show-prod 用 webpack-bundle-analyzer 可視化查看編譯結果。