在以前咱們有過一篇『React 同構實踐與思考』的專欄文章,給讀者實踐了用 React 怎麼實現同構。今天,其實講的是在實現同構過程當中看到過,可能很是容易被忽視更小的一個點 —— React View。html
每個 BS 架構的框架都會涉及到 View 層的展示,Koa 也不例外。咱們在作 View 層的時候有兩種作法,一種是作成插件形式,對於 View 來講就是模板引擎,另外一種是作成中件間的形式。前端
再說到 React,經常有人說它是加強版的模板引擎。這種說法即對也不對。node
從表象來看的確,React 能夠替換變量,有條件判斷,有循環判斷,JSX 語法讓渲染過程和 HTML 沒什麼兩樣,畢竟說到底 React 就是 JavaScript,而 React 所推崇的無狀態函數,也不折不扣把 React 變成了像是模板的樣子。react
從內在來看,React 它仍是 JavaScript,它能夠方便地作模塊化管理,有內部狀態,有本身的數據流。它能夠作一部分 Controller,或者說,能夠徹底承擔 Controller 的工做。git
可是在服務端,咱們須要模板是爲了做 HTML 的同步請求,所以說地簡單一些就只須要渲染成 HTML 的功能就能夠了。固然,特殊的一點是,之因此讓 React 做模板就是可讓服務端跑到客戶端的渲染邏輯,並解決單頁應用經常詬病的加載後白屏的問題。github
言歸正傳,如今咱們就帶着 React View 怎麼實現這個問題來解讀源碼。緩存
配置是設計的源頭之一,一切源碼均可以從配置入手研究。babel
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 這些配置都是其它模板引擎不會有的。架構
模板經常使用的配置應該是什麼呢?app
viewPath,在上述配置指的是 view,就是 View 的目錄在哪裏,這是每個模板插件或中間件都須要去配的。
extname,後綴名是什麼,通常來講模板引擎都有本身獨有的後綴,固然不排除能夠有喜愛選擇的狀況。好比對 React 而言,就能夠寫成是 .jsx
或 .js
兩種不一樣的形式。
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,默認時是關閉的。任何須要編譯的模板引擎通常都會有相似的配置。在 Reat 中,由於 render
後的代碼是一連串的字符串,返回到前臺的時候都是沒法閱讀的代碼。在有必要時,咱們能夠開啓這個配置。
綁定到上下文
最後一步,儘管有一個開關控制,但咱們看到最後是把內容綁定到 this.body
下的。 這裏省略了整個實現過程是在 app.context.render
方法下,便是重寫了 app.context
下的 render
方法,用於渲染 React。若是說 app.context.render
方法是 function*
,那麼咱們的 react-view,就會變爲中間件。
咱們從一開始就看到了配置中就有 cache 配置,這個 cache 是否是咱們所想呢?咱們來看下源代碼:
// match function for cache clean var 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。
我想不少開發者在寫 React 組件的時候用的是 ES6 Class 來寫的,並且會用到不少 ES6/ES7 的方法,不巧的是 Node 還不支持有些高級特性。所以就引到了一個話題,服務端怎麼引用 babel?
在業務有 babel-node 這類解決方案,但這畢竟是一個實驗性的 Node,咱們不會拿生產環境去冒險。
在 koa/react-view 中間件內,有一段說明,它建議開發者在使用的時候加入 babel-register 做實時編譯。關於這個問題,固然也能夠寫在中間件內,在加載模板前引入。隨着 Node 對 ES6 方法支持的完善,也許有一天也用不到了。
其實,實現 View 很是簡單,咱們也從一些維度看到了設計一個 xx-view 的通常方法。在具體實現的時候,咱們能夠用一些更好的方法去作,好比用類來抽象 View,用 Promise 來描述過程。