在以前咱们有过一篇『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 来描述过程。