koa 實現 react-view 原理

在之前我們有過一篇『React 同構實踐與思考』的專欄文章,給讀者實踐了用 React 怎麼實現同構。今天,其實講的是在實現同構過程中看到過,非常容易被忽視更小的一個點 React View - Koa 用於渲染 React 的 View 引擎。

React View

每一個 BS 架構的框架都會涉及到 View 層的展現,Koa 也不例外。我們在做 View 層的時候有兩種做法,一種是做成插件形式,對於 View 來說就是模板引擎,另一種是做成中件間的形式。

再說到 React,常常有人說它是增強版的模板引擎。這種說法即對也不對。

從表象來看的確,React 可以替換變數,有條件判斷,有循環判斷,JSX 語法讓渲染過程和 HTML 沒什麼兩樣,畢竟說到底 React 就是 JavaScript,而 React 所推崇的無狀態函數,也徹徹底底把 React 變成了像是模板的樣子。

從內在來看,React 它還是 JavaScript,它可以方便地做模塊化管理,有內部狀態,有自己的數據流。它可以做一部分 Controller,或者說,可以完全承擔 Controller 的工作。

但是在服務端,我們需要模板是為了作 HTML 的同步請求,因此說地簡單一些就只需要渲染成 HTML 的功能就可以了。當然,特殊的一點是,之所以讓 React 作模板就是可以讓服務端跑到客戶端的渲染邏輯,並解決單頁應用常常詬病的載入後白屏的問題。

言歸正傳,現在我們就帶著 React View 怎麼實現這個問題來解讀源碼。

React-View 源碼解讀

配置

配置是設計的源頭之一,一切源碼都可以從配置入手研究。

var defaultOptions = { doctype: "<!DOCTYPE html>", beautify: false, cache: process.env.NODE_ENV === "production", extname: "jsx", writeResp: true, views: path.join(__dirname, "views"), internals: false};

如果我們用過像 handlebars 或是 jade View,我們看到 React View 的配置與其它 View 的配置有幾點不同。doctype、internals 這些配置都是其它模板引擎不會有的。

模板常用的配置應該是什麼呢?

  1. viewPath,在上述配置指的是 view,就是 View 的目錄在哪裡,這是每一個模板插件或中間件都需要去配的。

  2. extname,後綴名是什麼,一般來說模板引擎都有自己獨有的後綴,當然不排除可以有喜好選擇的情況。比如對 React 而言,就可以寫成是 .jsx 或 .js 兩種不同的形式。

  3. cache,我想一般模板引擎都會帶 cache 功能,因為模板的解析是需要耗費資源的,而模板本身的改動的頻度是非常低的。每當發布的時候,我們去刷新一次模板即可。但上述配置中的 cache 並不是指這個,我們等讀源碼時再來看。

渲染

標準的渲染過程其實非常的簡單。對於 React 來說就是讀取目錄下的文件,像前端載入一樣,require 那個文件。最後利用 ReactDOMServer 中的方法來渲染。

var render = internals ? ReactDOMServer.renderToString : ReactDOMServer.renderToStaticMarkup;...var markup = options.doctype || "";try { var component = require(filepath); // Transpiled ES6 may export components as { default: Component } component = component.default || component; markup += render(React.createElement(component, locals));} catch (err) { err.code = "REACT"; throw err;}if (options.beautify) { // NOTE: This will screw up some things where whitespace is important, and be // subtly different than prod. markup = beautifyHTML(markup);}var writeResp = locals.writeResp === false ? false : (locals.writeResp || options.writeResp); if (writeResp) { this.type = "html"; this.body = markup;}return markup

這裡我們截取最關鍵的片段,正如我們預估的渲染過程一樣。但我們看到,從流程上看有四個細節:

設置 doctype 的目的

在一般模板中我們很少看到將 doctype 放在配置中配置,但因為 React 的特殊性,讓我們不得不這麼做。原因很簡單,React render 方法返回時一定需要一個包裹的元素,比如 div,ul,甚至 html,因此,我們需要手動去加 doctype。

渲染 React 組件

renderToString 和 renderToStaticMarkup 都是 "react-dom/server" 下的方法,與 render 不同,render 方法需要指定具體渲染到 DOM 上的節點,但那兩個方法都只返回一段 HTML 字元串。這一點讓 React 成為模板語言而存在。它們兩個方法的區別在於:

  • renderToString 方法渲染的時候帶有 data-reactid 屬性,意味著可以做 server render,React 在前端會認識服務端渲染的內容,不會重新渲染 DOM 節點,開始執行 componentDidMount 繼續執行後續生命周期。

  • renderToStaticMarkup 方法渲染時沒有 data-reactid,把 React 當做是純模板來使用,這個時候只渲染 body 外的框架是比較合適的。

在 render 方法里,我們看到 React.createElement 方法。是因為在服務端 render 方法沒有 babel 編譯,因此寫的其實是 <component {...locals} /> 編譯後的代碼。

美化 HTML

options.beautify 配置了我們是否要美化 HTML,默認時是關閉的。任何需要編譯的模板引擎一般都會有類似的配置。在 React 中,因為 render 後的代碼是一連串的字元串,返回到前台的時候都是無法閱讀的代碼。在有必要時,我們可以開啟這個配置。

綁定到上下文

最後一步,儘管有一個開關控制,但我們看到最後是把內容綁定到 this.body 下的。 這裡省略了整個實現過程是在 app.context.render 方法下,即是重寫了 app.context 下的 render 方法,用於渲染 React。如果說 app.context.render 方法是 function*,那麼我們的 react-view,就會變為中間件。

Cache

我們從一開始就看到了配置中就有 cache 配置,這個 cache 是不是我們所想呢?我們來看下源代碼:

// match function for cache cleanvar match = createMatchFunction(options.views);...if (!options.cache) { cleanCache(match);}

這裡的 cache 指的是模板緩存么。事實上不完全是,我們來看一下 cleanCache 方法就明白了:

function cleanCache(match) { Object.keys(require.cache).forEach(function(module) { if (match(require.cache[module].filename)) { delete require.cache[module]; } });}

因為我們讀取 React 文件用的是 require 方法,而在 Node 中 require 方法是有緩存的,Node 在每個第一次 Load Module 時就會將該 Module 緩存,存入全局的 _cache 中,在一般情況下我們當然需要這麼做。但在模板載入這個情景下就不同了。

在這裡的確我們全局緩存了 React 模板文件,但這個文件是編譯前的文件。而我們需要緩存的是編譯後的文件,也就是說 markup 是我們需要緩存的值。

在這裡我們想想怎麼去實現,方便起見,我們可以新增一個 lru-cache,用它的好處是 lru 封裝了很多關於 cache 時效與容量的開關。

var LRU = require("lru-cache");var cache = LRU(this.options.cacheOptions);...if (options.cache && cache.get(filepath)) { markup = cache.get(filepath);} else { var markup = options.doctype || ""; try { var component = require(filepath); } else { // Transpiled ES6 may export components as { default: Component } component = component.default || component; markup += render(React.createElement(component, locals)); } } catch (err) { err.code = "REACT"; throw err; } // beautify ... if (options.cache) { cache.set(filepath, markup); }}

當然,我們現在這種情形下都需要清除 require 的 cache。

Babel

我想很多開發者在寫 React 組件的時候用的是 ES6 Class 來寫的,而且會用到很多 ES6/ES7 的方法,不巧的是 Node 還不支持有些高級特性。因此就引到了一個話題,服務端怎麼引用 babel?

在業務有 babel-node 這類解決方案,但這畢竟是一個實驗性的 Node,我們不會拿生產環境去冒險。

在 koa/react-view 中間件內,有一段說明,它建議開發者在使用的時候加入 babel-register 作實時編譯。關於這個問題,當然也可以寫在中間件內,在載入模板前引入。隨著 Node 對 ES6 方法支持的完善,也許有一天也用不到了。

總結

其實,實現 View 非常簡單,我們也從一些維度看到了設計一個 xx-view 的一般方法。在具體實現的時候,我們可以用一些更好的方法去做,比如用類來抽象 View,用 Promise 來描述過程。這些留給讀者自己去優化。


推薦閱讀:

react 行,我等你

TAG:koa | JavaScript | React |