IMVC(同構 MVC)的前端實踐

導語

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

然而,縱觀近幾年的發展,能夠發現一點,React/Vue 和 Redux/Vuex 是分別在 MVC 中的 View 層和 Model 層作了進一步發展。若是 MVC 中的 Controller 層也推動一步,將獲得一種升級版的 MVC,咱們稱之爲 IMVC(同構 MVC)。css

IMVC 能夠實現一份代碼在服務端和瀏覽器端皆可運行,具有單頁應用和多頁應用的全部優點,而且能夠這兩種模式裏經過配置項進行自由切換。配合 Node.js、Webpack、Babel 等基礎設施,咱們能夠獲得相比以前更加完善的一種前端架構。html

目錄

  • 一、同構的概念和意義
    • 1.一、isomorphic 是什麼?
    • 1.二、isomorphic javascript
  • 二、同構的種類和層次
    • 2.一、同構的種類
    • 2.二、同構的層次
  • 三、同構的價值和做用
    • 3.一、同構的價值
    • 3.二、同構如何加快訪問體驗
    • 3.三、同構是將來的趨勢
  • 四、同構的實現策略
  • 五、IMVC 架構
    • 5.一、IMVC 的目標
    • 5.二、IMVC 的技術選型
    • 5.三、爲何不直接用 React 全家桶?
    • 5.四、用 create-app 代替 react-router
      • 5.4.一、create-app 的同構理念
      • 5.4.二、create-app 的配置理念
      • 5.4.三、create-app 的服務端渲染
      • 5.4.四、create-app 的扁平化路由理念
      • 5.4.五、create-app 的目錄結構
    • 5.五、controller 的基本模式
    • 5.六、redux 的簡化版 relite
    • 5.七、Isomorphic-MVC 的工程化設施
      • 5.7.一、如何實現代碼實時熱更新?
      • 5.7.二、如何處理 CSS 按需加載?
      • 5.7.三、如何實現代碼切割、按需加載?
      • 5.7.四、如何處理靜態資源的版本管理?
      • 5.7.五、如何管理命令行任務?
  • 六、實踐案例
  • 七、結語

一、同構的概念和意義

1.一、isomorphic 是什麼?

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

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

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

1.二、isomorphic javascript

isomorphic javascript(同構 js),是指一份 js 代碼,既然能夠跑在瀏覽器端,也能夠跑在服務端。node

IMVC
IMVC

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

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

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

惋惜的是,同構 js 其實並無獲得真正意義上的發展。由於,在 2011 年,node.js 和 ECMAScript 都不夠成熟,咱們並無很好的基礎設施,去知足同構的目標。

如今是 2017 年,狀況已經有所不一樣。ECMAScript 2015 標準定案,提供了一個標準的模塊規範,先後端通用。儘管目前 node.js 和瀏覽器都沒有實現 ES2015 模塊標準,可是咱們有 Babel 和 Webpack 等工具,能夠提早享用新的語言特性帶來的便利。

二、同構的種類和層次

2.一、同構的種類

同構 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.二、同構的層次

同構不是一個布爾值,true 或者 false;同構是一個光譜形態,能夠在很小範圍裏上實現同構,也能夠在很大範圍裏實現同構。

  • function 層次:零碎的代碼片段或者函數,支持同構。好比瀏覽器端和服務端都實現了 setTimeout 函數,好比 lodash/underscore 的工具函數都是同構的。

  • feature 層次:在這個層次裏的同構代碼,一般會承擔必定的業務職能。好比 React 和 Vue 都藉助 virtual-dom 實現了同構,它們是服務於 View 層的渲染;好比 Redux 和 Vuex 也是同構的,它們負責 Model 層的數據處理。

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

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

三、同構的價值和做用

3.一、同構的價值

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

  • SEO 友好:View 層在瀏覽器端和服務端均可以運行,意味着能夠在服務端吐出 html,支持搜索引擎的抓取。

  • 加快訪問體驗:服務端渲染能夠加快瀏覽器端的首次訪問的渲染速度,而瀏覽器端渲染,能夠加快用戶交互時的反饋速度。

  • 代碼的可維護性:同構能夠減小語言切換的成本,減少代碼的重複率,增長代碼的可維護性。

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

3.二、同構如何加快訪問體驗

純瀏覽器端渲染的問題在於,頁面須要等待 js 加載完畢以後,纔可見。

client-side renderging

IMVC
IMVC

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

服務端渲染能夠加速首次訪問的體驗,在 js 加載以前,頁面就渲染了首屏。可是,用戶只對首次加載有耐心,若是操做過程當中,頻繁刷新頁面,也會帶給用戶緩慢的感受。

SERVER-SIDE RENDERING

IMVC
IMVC

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

同構渲染則能夠獲得兩種好處,在首次加載時用服務端渲染,在交互過程當中則採起瀏覽器端渲染。

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 運行時,前端徹底掌控渲染層,而且實現渲染層的同構。既不犧牲服務端渲染的價值,也不放棄瀏覽器端渲染的便利。

這就是將來的趨勢。

四、同構的實現策略

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

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

於是,咱們只會在 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 和環境信息,選擇正確的重定向方式。

五、IMVC 架構

5.一、IMVC 的目標

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

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

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

5.二、IMVC 的技術選型

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

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

5.三、爲何不直接用 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.四、用 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.一、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.二、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.三、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.四、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.五、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.五、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.六、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.七、Isomorphic-MVC 的工程化設施

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

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

5.7.一、如何實現代碼實時熱更新?

  • 目標:一個命令啓動開發環境,修改代碼不需重啓進程
  • 作法:一個 webpack 服務於 client,另外一個 webpack 服務於 server
  • client: express + webpack-dev-middleware 在內存裏編譯
  • server: memory-fs + webpack + vm-module
  • 服務端的 webpack 編譯到內存模擬的文件系統,再用 node.js 內置的虛擬機模塊執行後獲得新的模塊

5.7.二、如何處理 CSS 按需加載?

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

5.7.三、如何實現代碼切割、按需加載?

  • 不使用 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.四、如何處理靜態資源的版本管理?

  • 以代碼的 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.五、如何管理命令行任務?

  • 使用 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 可視化查看編譯結果

六、實踐案例

七、結語

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

相關文章
相關標籤/搜索