IMVC(同構 MVC)的前端實踐

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

導語

隨著 Backbone 等老牌框架的逐漸衰退,前端 MVC 發展緩慢,有逐漸被 MVVM/Flux 所取代的趨勢。

然而,縱觀近幾年的發展,可以發現一點,React/Vue 和 Redux/Vuex 是分別在 MVC 中的 View 層和 Model 層做了進一步發展。如果 MVC 中的 Controller 層也推進一步,將得到一種升級版的 MVC,我們稱之為 IMVC(同構 MVC)。

IMVC 可以實現一份程式碼在服務端和瀏覽器端皆可執行,具備單頁應用和多頁應用的所有優勢,並且可以這兩種模式裡通過配置項進行自由切換。配合 Node.js、Webpack、Babel 等基礎設施,我們可以得到相比之前更加完善的一種前端架構。

目錄

  • 1、同構的概念和意義
    • 1.1、isomorphic 是什麼?
    • 1.2、isomorphic javascript
  • 2、同構的種類和層次
    • 2.1、同構的種類
    • 2.2、同構的層次
  • 3、同構的價值和作用
    • 3.1、同構的價值
    • 3.2、同構如何加快訪問體驗
    • 3.3、同構是未來的趨勢
  • 4、同構的實現策略
  • 5、IMVC 架構
    • 5.1、IMVC 的目標
    • 5.2、IMVC 的技術選型
    • 5.3、為什麼不直接用 React 全家桶?
    • 5.4、用 create-app 代替 react-router
      • 5.4.1、create-app 的同構理念
      • 5.4.2、create-app 的配置理念
      • 5.4.3、create-app 的服務端渲染
      • 5.4.4、create-app 的扁平化路由理念
      • 5.4.5、create-app 的目錄結構
    • 5.5、controller 的基本模式
    • 5.6、redux 的簡化版 relite
    • 5.7、Isomorphic-MVC 的工程化設施
      • 5.7.1、如何實現程式碼實時熱更新?
      • 5.7.2、如何處理 CSS 按需載入?
      • 5.7.3、如何實現程式碼切割、按需載入?
      • 5.7.4、如何處理靜態資源的版本管理?
      • 5.7.5、如何管理命令列任務?
  • 6、實踐案例
  • 7、結語

1、同構的概念和意義

1.1、isomorphic 是什麼?

isomorphic,讀作[ˌaɪsə'mɔ:fɪk],意思是:同形的,同構的。

維基百科對它的描述是:同構是在數學物件之間定義的一類對映,它能揭示出在這些物件的屬性或者操作之間存在的關係。若兩個數學結構之間存在同構對映,那麼這兩個結構叫做是同構的。一般來說,如果忽略掉同構的物件的屬性或操作的具體定義,單從結構上講,同構的物件是完全等價的。

同構,也被化用在物理、化學以及計算機等其他領域。

1.2、isomorphic javascript

isomorphic javascript(同構 js),是指一份 js 程式碼,既然可以跑在瀏覽器端,也可以跑在服務端。

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

同構 js 的發展歷史,比 progressive web app 還要早很多。2009 年, node.js 問世,給予我們前後端統一語言的想象;更進一步的,前後端公用一套程式碼,也不是不可能。

有一個網站 isomorphic.net,專門收集跟同構 js 相關的文章和專案。從裡面的文章列表來看,早在 2011 年的時候,業界已經開始探討同構 js,並認為這將是未來的趨勢。

可惜的是,同構 js 其實並沒有得到真正意義上的發展。因為,在 2011 年,node.js 和 ECMAScript 都不夠成熟,我們並沒有很好的基礎設施,去滿足同構的目標。

現在是 2017 年,情況已經有所不同。ECMAScript 2015 標準定案,提供了一個標準的模組規範,前後端通用。儘管目前 node.js 和瀏覽器都沒有實現 ES2015 模組標準,但是我們有 Babel 和 Webpack 等工具,可以提前享用新的語言特性帶來的便利。

2、同構的種類和層次

2.1、同構的種類

同構 js 有兩個種類:「內容同構」和「形式同構」。

其中,「內容同構」指服務端和瀏覽器端執行的程式碼完全等價。比如:

function add(a, b) {
    return a + b
}複製程式碼

不管在服務端還是瀏覽器端,add 函式都是一樣的。

而「形式同構」則不同,從原教旨主義的角度上看,它不是同構。因為,在瀏覽器端有一部分程式碼永遠不會執行,而在服務端另一部分程式碼永遠不會執行。比如:

function doSomething() {
  if (isServer) {
      // do something in server-side
  } else if (isClient) {
      // do something in client-side
  }
}複製程式碼

在 npm 裡,有很多 package 標榜自己是同構的,用的方式就是「形式同構」。如果不作特殊處理,「形式同構」可能會增加瀏覽器端載入的 js 程式碼的體積。比如 React,它的 140+kb 的體積,是把只在服務端執行的程式碼也包含了進去。

2.2、同構的層次

同構不是一個布林值,true 或者 false;同構是一個光譜形態,可以在很小範圍裡上實現同構,也可以在很大範圍裡實現同構。

  • function 層次:零碎的程式碼片斷或者函式,支援同構。比如瀏覽器端和服務端都實現了 setTimeout 函式,比如 lodash/underscore 的工具函式都是同構的。

  • feature 層次:在這個層次裡的同構程式碼,通常會承擔一定的業務職能。比如 React 和 Vue 都藉助 virtual-dom 實現了同構,它們是服務於 View 層的渲染;比如 Redux 和 Vuex 也是同構的,它們負責 Model 層的資料處理。

  • framework 層次:在框架層面實現同構,它可能包含了所有層次的同構,需要精心處理支援同構和不支援同構的兩個部分,如何妥善地整合在一起。

我們今天所討論的 isomorphic-mvc(簡稱 IMVC),是在 framework 層次上實現同構。

3、同構的價值和作用

3.1、同構的價值

同構 js,不僅僅有抽象上的美感,它還有很多實用價值。

  • SEO 友好:View 層在瀏覽器端和服務端都可以執行,意味著可以在服務端吐出 html,支援搜尋引擎的抓取。

  • 加快訪問體驗:服務端渲染可以加快瀏覽器端的首次訪問的渲染速度,而瀏覽器端渲染,可以加快使用者互動時的反饋速度。

  • 程式碼的可維護性:同構可以減少語言切換的成本,減小程式碼的重複率,增加程式碼的可維護性。

不使用同構方案,也可以用別的辦法實現前兩個的目標,但是別的辦法卻難以同時滿足三個目標。

3.2、同構如何加快訪問體驗

純瀏覽器端渲染的問題在於,頁面需要等待 js 載入完畢之後,才可見。

client-side renderging

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

服務端渲染可以加速首次訪問的體驗,在 js 載入之前,頁面就渲染了首屏。但是,使用者只對首次載入有耐心,如果操作過程中,頻繁重新整理頁面,也會帶給使用者緩慢的感覺。

SERVER-SIDE RENDERING

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

同構渲染則可以得到兩種好處,在首次載入時用服務端渲染,在互動過程中則採取瀏覽器端渲染。

3.3、同構是未來的趨勢

從歷史發展的角度看,同構確實是未來的一大趨勢。

在 Web 開發的早期,採用的開發模式是:fat-server, thin-client

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

前端只是薄薄的一層,負責一些表單驗證,DOM 操作和 JS 動畫。在這個階段,沒有「前端工程師」這個工種,服務端開發順便就把前端程式碼給寫了。

在 Ajax 被髮掘出來之後,Web 進入 2.0 時代,我們普遍推崇的模式是:thin-server, fat-client

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

越來越多的業務邏輯,從服務端遷移到前端。開始有「前後端分離」的做法,前端希望服務端只提供 restful 介面和資料持久化。

但是在這個階段,做得不夠徹底。前端並沒有完全掌控渲染層,起碼 html 骨架需要服務端渲染,以及前端實現不了服務端渲染。

為了解決上述問題,我們正在進入下一個階段,這個階段所採取的模式是:shared, fat-server, fat-client

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

通過 node.js 執行時,前端完全掌控渲染層,並且實現渲染層的同構。既不犧牲服務端渲染的價值,也不放棄瀏覽器端渲染的便利。

這就是未來的趨勢。

4、同構的實現策略

要實現同構,首先要正視一點,全盤同構是沒有意義的。為什麼?

服務端和瀏覽器端畢竟是兩個不同的平臺和環境,它們專注於解決不同的問題,有自身的特點,全盤同構就抹殺了它們固有的差異,也就無法發揮它們各自的優勢。

因而,我們只會在 client 和 server 有交集的部分實現同構。就是在服務端渲染 html 和在瀏覽器端複用 html 的整個過程裡,實現同構。

我們採取的主要做法有兩個:1)能夠同構的程式碼,直接複用;2)無法同構的程式碼,封裝成形式同構。

舉幾個例子。

獲取 User-Agent 字串。

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

我們可以在服務端用 req.get('user-agent') 模擬出 navigator 全域性物件,也可以提供一個 getUserAgent 的方法或函式。

獲取 Cookies。

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

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

Cookies 的主要用途發生在 ajax 請求的時候,在瀏覽器端 ajax 請求可以設定為自動帶上 Cookies,所以只需要在服務端默默地在每個 ajax 請求頭裡補上 Cookies 即可。

Redirects 重定向處理

IMVC
IMVC

圖片來源:www.slideshare.net/spikebrehm/…

重定向的場景比較複雜,起碼有三種情況:

  • 服務端 302 重定向: res.redirect(xxx)
  • 瀏覽器端 location 重定向:location.href = xxxlocation.replace(xxx)
  • 瀏覽器端 pushState 重定向:history.push(xxx)history.replace(xxx)

我們需要封裝一個 redirect 函式,根據輸入的 url 和環境資訊,選擇正確的重定向方式。

5、IMVC 架構

5.1、IMVC 的目標

IMVC 的目標是框架層面的同構,我們要求它必須實現以下功能

  • 用法簡單,初學者也能快速上手
  • 只維護一套 ES2015+ 的程式碼
  • 既是單頁應用,又是多頁應用(SPA + SSR)
  • 可以部署到任意釋出路徑 (Basename/RootPath)
  • 一條命令啟動完備的開發環境
  • 一條命令完成打包/部署過程

有些功能屬於執行時的,有些功能則只服務於開發環境。JavaScript 雖然是一門解釋型語言,但前端行業發展到現階段,它的開發模式已經變得非常豐富,既可以用最樸素的方式,一個記事本加上一個瀏覽器,也可以用一個 IDE 加上一系列開發、測試和部署流程的支援。

5.2、IMVC 的技術選型

  • Router: create-app = history + path-to-regexp
  • View: React = renderToDOM || renderToString
  • Model: relite = redux-like library
  • Ajax: isomorphic-fetch

理論上,IMVC 是一種架構思路,它並不限定我們使用哪些技術棧。不過,要使 IMVC 落地,總得做出選擇。上面就是我們當前選擇的技術棧,將來它們可能升級或者替換為其它技術。

5.3、為什麼不直接用 React 全家桶?

大家可能注意到,我們使用了許多 React 相關的技術,但卻不是所謂的 React 全家桶,原因如下:

  • 目前的 React 全家桶其實是野生的,Facebook 並不用
  • React-Router 的理念難以滿足要求
  • Redux 適用於大型應用,而我們的主要場景是中小型
  • 升級頻繁導致學習成本過高,需封裝一層更簡潔的 API

目前的全家桶,只是社群裡的一些熱門庫的組合罷了。Facebook 真正用的全家桶是 react|flux|relay|graphql,甚至他們並不用 React 做服務端渲染,用的是 PHP。

我們認為 React-Router 的理念在同構上是錯誤的。它忽視了一個重大事實:服務端是 Router 路由驅動的,把 Router 和作為 View 的 React 捆綁起來,View 已經例項化了,Router 怎麼再載入 Controller 或者非同步請求資料呢?

從函數語言程式設計的角度看,React 推崇純元件,需要隔離副作用,而 Router 則是副作用來源,將兩者混合在一起,是一種汙染。另外,Router 並不是 UI,卻被寫成 JSX 元件的形式,這也是有待商榷的。

所以,即便是當前最新版的 React-Router-v4,實現同構渲染時,做法也複雜而臃腫,服務端和瀏覽器端各有一個路由表和發 ajax 請求的邏輯。點選這裡檢視程式碼

至於 Redux,其作者也已在公開場合表示:「你可能不需要 Redux」。在引入 redux 時,我們得先反思一下引入的必要性。

毫無疑問,Redux 的模式是優秀的,結構清晰,易維護。然而同時它也是繁瑣的,實現一個功能,你可能得跨資料夾地運算元個檔案,才能完成。這些代價所帶來的顯著好處,要在 app 複雜到一定程度時,才能真正體會。其它模式裡,app 複雜到一定程度後,就難以維護了;而 Redux 的可維護性還依然堅挺,這就是其價值所在。(值得一提的是,基於 redux 再封裝一層簡化的 API,我認為這很可能是錯誤的做法。Redux 的原始碼很簡潔,意圖也很明確,要簡化固然也是可以的,但它為什麼自己不去做?它是不是刻意這樣設計呢?你的封裝是否損害了它的設計目的呢?)

在使用 Redux 之前要考慮的是,我們 web-app 屬於大型應用的範疇嗎?

前端領域日新月異,框架和庫的頻繁升級讓開發者應接不暇。我們需要根據自身的需求,進行二次封裝,得到一組更簡潔的 API,將部分複雜度隱藏起來,以降低學習成本。

5.4、用 create-app 代替 react-router

create-app 是我們為了同構而實現的一個 library,它由下面三部分組成:

  • history: react-router 依賴的底層庫
  • path-to-regexp: expressjs 依賴的底層庫
  • Controller:在 View(React) 層和 Model 層之外實現 Controller 層

create-app 複用 React-Router 的依賴 history.js,用以在瀏覽器端管理 history 狀態;複用 expressjspath-to-regexp,用以從 path pattern 中解析引數。

我們認為,ReactRedux 分別對應 MVCViewModel,它們都是同構的,我們需要的是實現 Controller 層的同構。

5.4.1、create-app 的同構理念

IMVC
IMVC

create-app 實現同構的方式是:

  • 輸入 url,router 根據 url 的格式,匹配出對應的 controller 模組
  • 呼叫 module-loader 載入 controller 模組,拿到 Controller 類
  • View 和 Model 從屬於 Controller 類的屬性
  • new Controller(location, context) 得到 controller 例項
  • 呼叫 controller.init 方法,該方法必須返回 view 的例項
  • 呼叫 view-engine 將 view 的例項根據環境渲染成 html 或者 dom 或者 native-ui 等

上述過程在服務端和瀏覽器端都保持一致。

5.4.2、create-app 的配置理念

服務端和瀏覽器端載入模組的方式不同,服務端是同步載入,而瀏覽器端則是非同步載入;它們的 view-engine 也是不同的。如何處理這些不一致?

答案是配置。

const app = createApp({
    type: 'createHistory',
    container: '#root',
    context: {
        isClient: true|false,
        isServer: false|true,
        ...injectFeatures
    },
    loader: webpackLoader|commonjsLoader,
    routes: routes,
    viewEngine: ReactDOM|ReactDOMServer,
})
app.start() || app.render(url, context)複製程式碼

服務端和瀏覽器端分別有自己的入口檔案:client-entry.js 和 server.entry.js。我們只需提供不同的配置即可。

在服務端,載入 controller 模組的方式是 commonjsLoader;在瀏覽器端,載入 controller 模組的方式則為 webpackLoader。

在服務端和瀏覽器端,view-engine 也被配置為不同的 ReactDOM 和 ReactDOMServer。

每個 controller 例項,都有 context 引數,它也是來自配置。通過這種方式,我們可以在執行時注入不同的平臺特性。這樣既分割了程式碼,又實現了形式同構。

5.4.3、create-app 的服務端渲染

我們認為,簡潔的,才是正確的。create-app 實現服務端渲染的程式碼如下:

const app = createApp(serverSettings)
router.get('*', async (req, res, next) => {
  try {
    const { content } = await app.render(req.url, serverContext)
    res.render('layout', { content })
  } catch(error) {
    next(error)
  }
})複製程式碼

沒有多餘的資訊,也沒有多餘的程式碼,輸入一個 url 和 context,返回具有真實資料 html 字串。

5.4.4、create-app 的扁平化路由理念

React-Router 支援並鼓勵巢狀路由,其價值存疑。它增加了程式碼的閱讀成本,以及各個路由模組之間的關係與 UI(React 元件)的巢狀耦合在一起,並不靈活。

使用扁平化路由,可以使程式碼解耦,容易閱讀,並且更為靈活。因為,UI 之間的複用,可以通過 React 元件的直接巢狀來實現。

基於路由巢狀關係來複用 UI,容易遇上一個尷尬場景:恰好只有一個頁面不需要共享頭部,而頭部卻不在它的控制範疇內。

// routes
export default [{
    path: '/demo',
    controller: require('./home/controller')
}, {
    path: '/demo/list',
    controller: require('./list/controller')
}, {
    path: '/demo/detail',
    controller: require('./detail/controller')
}]複製程式碼

如你所見,我們的 path 對應的並不是 component,而是 controller。通過新增 controller 層,我們可以實現在 view 層的 component 例項化之前,就藉助 controller 獲取首屏資料。

next.js 也是一個同構框架,它本質上是簡化版的 IMVC,只不過它的 C 層非常薄,以至於直接掛在 View 元件的靜態方法裡。它的路由配置目前是基於 View 的檔名,其 Controller 層是 View.getInitialProps 靜態方法,只服務於獲取初始化 props。

這一層太薄了,它其實可以更為豐富,比如提供 fetch 方法,內建環境判斷,支援 jsonp,支援 mock 資料,支援超時處理等特性,比如自動繫結 store 到 view,比如提供更為豐富的生命週期 pageWillLeave(頁面將跳轉到其他路徑) 和 windowWillUnload (視窗即將關閉)等。

總而言之,副作用不可能被消滅,只能被隔離,如今 View 和 Model 都是 pure-function 和 immutabel-data 的無副作用模式,總得有角色承擔處理副作用的職能。新的抽象層 Controller 應運而生。

5.4.5、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 非常不同。它不是按照抽象的職能 actionCreator|actionType|reducers|middleware|container 來安排的,它是基於 page 頁面來劃分的,每個頁面都有三個組成部分:controller,model 和 view。

用 routes 路由表,將 page 串起來。

create-app 採取了「整站 SPA」 的模式,全域性只有一個入口檔案,index.js。src 目錄下的檔案都所有專案共享的框架層程式碼,各個專案自身的業務程式碼則在 app-xxx 的資料夾下。

這種設計的目的是為了降低遷移成本,靈活切分和合並各個專案。

  • 當某個專案處於萌芽階段,它可以依附在另一個專案的 git 倉庫裡,使用它現成的基礎設施進行快速開發。
  • 當兩個專案足夠複雜,值得分割為兩個專案時,它們可以分割為兩個專案,各自將對方的資料夾整個刪除即可。
  • 當兩個專案要合併,將它們放到同一 git 倉庫的不同 app-xxx 裡即可。
  • 我們使用本地路由表 routes.js 和 nginx 配置協調 url 的訪問規則

每個 page 的 controller.js,model.js 和 view.js 以及它們的私有依賴,將會被單獨打包到一個檔案,只有匹配 url 成功時,才會按需載入。保證多專案並存不會帶來 js 體積的膨脹。

5.5、controller 的基本模式

我們新增了 controller 這個抽象層,它將承擔連線 Model,View,History,LocalStorage,Server 等物件的職能。

Controller 被設計為 OOP 程式設計正規化的一個 class,主要目的就是為了讓它承受副作用,以便 View 和 Model 層保持函式式的純粹。

Controller 的基本模式如下:

class MyController extends BaseController {
  requireLogin = true // 是否依賴登陸態,BaseController 裡自動處理
  View = View // 檢視
  initialState = { count: 0 } // model 初始狀態initialState
  actions = actions // model 狀態變化的函式集合 actions
  handleIncre = () => { // 事件處理器,自動收集起來,傳遞給 View 元件
    let { history, store, fetch, location, context } = this // 功能分層
    let { INCREMENT } = store.actions
    INCREMENT() // 呼叫 action,更新 state, view 隨之自動更新
  }
  async shouldComponentCreate() {} // 在這裡鑑權,return false
  async componentWillCreate() {} // 在這裡 fetch 首屏資料
  componentDidMount() {} // 在這裡 fetch 非首屏資料
  pageWillLeave() {} // 在這裡執行路由跳轉離開前的邏輯
  windowWillUnload() {} // 在這裡執行頁面關閉前的邏輯
}複製程式碼

我們將所有職能物件放到了 controller 的屬性中,開發者只需提供相應的配置和定義,在豐富的生命週期裡按需呼叫相關方法即可。

它的結構和模式跟 vue 和微信小程式有點相似。

5.6、redux 的簡化版 relite

儘管作為中小型應用的架構,我們不使用 Redux,但是對於 Redux 中的優秀理念,還是可以吸收進來。

所以,我們實現了一個簡化版的 redux,叫做 relite。

  • actionType, actionCreator, reducer 合併
  • 自動 bindActionCreators,內建非同步 action 的支援
let EXEC_BY = (state, input) => {
    let value = parseFloat(input, 10)
    return isNaN(value) ? state : {
        ...state,
        count: state.count + value
    }
}
let EXEC_ASYNC = async (state, input) => {
    await delay(1000)
    return EXEC_BY(state, input)
}
let store = createStore(
  { EXEC_BY, EXEC_ASYNC },
  { count: 0 }
)複製程式碼

我們希望得到的是 redux 的兩個核心:1)pure-function,2)immutable-data。

所以 action 函式被設計為純函式,它的函式名就是 redux 的 action-type,它的函式體就是 redux 的 reducer,它的第一個引數是當前的 state,它的第二個引數是 redux 的 actionCreator 攜帶的資料。並且,relite 內建了 redux-promiseredux-thunk 的功能,開發者可以使用 async/await 語法,實現非同步 action。

relite 也要求 state 儘可能是 immutable,並且可以通過額外的 recorder 外掛,實現 time-travel 的功能。可以檢視這個 demo 體驗實際效果。

5.7、Isomorphic-MVC 的工程化設施

上面講述了 IMVC 在執行時裡的一些功能和特點,下面簡單地描述一下 IMVC 的工程化設施。我們採用了:

  • node.js 執行時,npm 包管理
  • expressjs 服務端框架
  • babel 編譯 ES2015+ 程式碼到 ES5
  • webpack 打包和壓縮原始碼
  • standard.js 檢查程式碼規範
  • prettier.js + git-hook 程式碼自動美化排版
  • mocha 單元測試

5.7.1、如何實現程式碼實時熱更新?

  • 目標:一個命令啟動開發環境,修改程式碼不需重啟程式
  • 做法:一個 webpack 服務於 client,另一個 webpack 服務於 server
  • client: express + webpack-dev-middleware 在記憶體裡編譯
  • server: memory-fs + webpack + vm-module
  • 服務端的 webpack 編譯到記憶體模擬的檔案系統,再用 node.js 內建的虛擬機器模組執行後得到新的模組

5.7.2、如何處理 CSS 按需載入?

  • 問題根源:瀏覽器只在 dom-ready 之前會等待 css 資源載入後再渲染頁面
  • 問題描述:當單頁跳轉到另一個 url,css 資源還沒載入完,頁面顯示成混亂佈局
  • 處理辦法:將 css 視為預載入的 ajax 資料,以 style 標籤的形式按需引入
  • 優化策略:用 context 快取預載入資料,避免重複載入

5.7.3、如何實現程式碼切割、按需載入?

  • 不使用 webpack-only 的語法 require.ensure
  • 在瀏覽器裡 require 被編譯為載入函式,非同步載入
  • 在 node.js 裡 require 是同步載入
// webpack.config.js
{
      test: /controller\.jsx?$/,
      loader: 'bundle-loader',
      query: {
        lazy: true,
        name: '[1]-[folder]',
        regExp: /[\/\\]app-([^\/\\]+)[\/\\]/.source
      },
      exclude: /node_modules/
}複製程式碼

5.7.4、如何處理靜態資源的版本管理?

  • 以程式碼的 hash 為檔名,增量釋出
  • 用 webpack.stats.plugin.js 生成靜態資源表
  • express 使用 stats.json 的資料渲染頁面
// webpack.config.js
output = {
    path: outputPath,
    filename: '[name]-[hash:6].js',
    chunkFilename: '[name]-[chunkhash:6].js'
}複製程式碼

5.7.5、如何管理命令列任務?

  • 使用 npm-scripts 在 package.json 裡完成 git、webpack、test、prettier 等任務的串並聯邏輯
  • npm start 啟動完整的開發環境
  • npm run start:client 啟動不帶服務端渲染的開發環境
  • npm run build 啟動自動化編譯,構建與壓縮部署的任務
  • npm run build:show-prod 用 webpack-bundle-analyzer 視覺化檢視編譯結果

6、實踐案例

7、結語

IMVC 經過實踐和摸索,已被證明是一種有效的模式,它以較高的完成度實現了真正意義上的同構。不再侷限於紙面上的理念描述,而是一個可以落地的方案,並且實際地提升了開發體驗和效率。後續我們將繼續往這個方向探索。

相關文章