koa 实现 react-view 原理
tnsq7778
8年前
<p>在之前我们有过一篇『React 同构实践与思考』的专栏文章,给读者实践了用 React 怎么实现同构。今天,其实讲的是在实现同构过程中看到过,非常容易被忽视更小的一个点 React View - Koa 用于渲染 React 的 View 引擎。</p> <h2>React View</h2> <p>每一个 BS 架构的框架都会涉及到 View 层的展现,Koa 也不例外。我们在做 View 层的时候有两种做法,一种是做成插件形式,对于 View 来说就是模板引擎,另一种是做成中件间的形式。</p> <p>再说到 React,常常有人说它是增强版的模板引擎。这种说法即对也不对。</p> <p>从表象来看的确,React 可以替换变量,有条件判断,有循环判断,JSX 语法让渲染过程和 HTML 没什么两样,毕竟说到底 React 就是 JavaScript,而 React 所推崇的无状态函数,也彻彻底底把 React 变成了像是模板的样子。</p> <p>从内在来看,React 它还是 JavaScript,它可以方便地做模块化管理,有内部状态,有自己的数据流。它可以做一部分 Controller,或者说,可以完全承担 Controller 的工作。</p> <p>但是在服务端,我们需要模板是为了作 HTML 的同步请求,因此说地简单一些就只需要渲染成 HTML 的功能就可以了。当然,特殊的一点是,之所以让 React 作模板就是可以让服务端跑到客户端的渲染逻辑,并解决单页应用常常诟病的加载后白屏的问题。</p> <p>言归正传,现在我们就带着 React View 怎么实现这个问题来解读源码。</p> <h2>React-View 源码解读</h2> <h3>配置</h3> <p>配置是设计的源头之一,一切源码都可以从配置入手研究。</p> <pre> <code class="language-javascript">var defaultOptions = { doctype: '<!DOCTYPE html>', beautify: false, cache: process.env.NODE_ENV === 'production', extname: 'jsx', writeResp: true, views: path.join(__dirname, 'views'), internals: false }; </code></pre> <p>如果我们用过像 handlebars 或是 jade View,我们看到 React View 的配置与其它 View 的配置有几点不同。doctype、internals 这些配置都是其它模板引擎不会有的。</p> <p>模板常用的配置应该是什么呢?</p> <ol> <li> <p>viewPath,在上述配置指的是 view,就是 View 的目录在哪里,这是每一个模板插件或中间件都需要去配的。</p> </li> <li> <p>extname,后缀名是什么,一般来说模板引擎都有自己独有的后缀,当然不排除可以有喜好选择的情况。比如对 React 而言,就可以写成是 .jsx 或 .js 两种不同的形式。</p> </li> <li> <p>cache,我想一般模板引擎都会带 cache 功能,因为模板的解析是需要耗费资源的,而模板本身的改动的频度是非常低的。每当发布的时候,我们去刷新一次模板即可。但上述配置中的 cache 并不是指这个,我们等读源码时再来看。</p> </li> </ol> <h3>渲染</h3> <p>标准的渲染过程其实非常的简单。对于 React 来说就是读取目录下的文件,像前端加载一样,require 那个文件。最后利用 ReactDOMServer 中的方法来渲染。</p> <pre> <code class="language-javascript">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 </code></pre> <p>这里我们截取最关键的片段,正如我们预估的渲染过程一样。但我们看到,从流程上看有四个细节:</p> <p><strong>设置 doctype 的目的</strong></p> <p>在一般模板中我们很少看到将 doctype 放在配置中配置,但因为 React 的特殊性,让我们不得不这么做。原因很简单,React render 方法返回时一定需要一个包裹的元素,比如 div,ul,甚至 html,因此,我们需要手动去加 doctype。</p> <p><strong>渲染 React 组件</strong></p> <p>renderToString 和 renderToStaticMarkup 都是 'react-dom/server' 下的方法,与 render 不同,render 方法需要指定具体渲染到 DOM 上的节点,但那两个方法都只返回一段 HTML 字符串。这一点让 React 成为模板语言而存在。它们两个方法的区别在于:</p> <ul> <li> <p>renderToString 方法渲染的时候带有 data-reactid 属性,意味着可以做 server render,React 在前端会认识服务端渲染的内容,不会重新渲染 DOM 节点,开始执行 componentDidMount 继续执行后续生命周期。</p> </li> <li> <p>renderToStaticMarkup 方法渲染时没有 data-reactid,把 React 当做是纯模板来使用,这个时候只渲染 body 外的框架是比较合适的。</p> </li> </ul> <p>在 render 方法里,我们看到 React.createElement 方法。是因为在服务端 render 方法没有 babel 编译,因此写的其实是 <component {...locals} /> 编译后的代码。</p> <p><strong>美化 HTML</strong></p> <p>options.beautify 配置了我们是否要美化 HTML,默认时是关闭的。任何需要编译的模板引擎一般都会有类似的配置。在 React 中,因为 render 后的代码是一连串的字符串,返回到前台的时候都是无法阅读的代码。在有必要时,我们可以开启这个配置。</p> <p><strong>绑定到上下文</strong></p> <p>最后一步,尽管有一个开关控制,但我们看到最后是把内容绑定到 this.body 下的。 这里省略了整个实现过程是在 app.context.render 方法下,即是重写了 app.context 下的 render 方法,用于渲染 React。如果说 app.context.render 方法是 function*,那么我们的 react-view,就会变为中间件。</p> <h3>Cache</h3> <p>我们从一开始就看到了配置中就有 cache 配置,这个 cache 是不是我们所想呢?我们来看下源代码:</p> <pre> <code class="language-javascript">// match function for cache clean var match = createMatchFunction(options.views); ... if (!options.cache) { cleanCache(match); } </code></pre> <p>这里的 cache 指的是模板缓存么。事实上不完全是,我们来看一下 cleanCache 方法就明白了:</p> <pre> <code class="language-javascript">function cleanCache(match) { Object.keys(require.cache).forEach(function(module) { if (match(require.cache[module].filename)) { delete require.cache[module]; } }); } </code></pre> <p>因为我们读取 React 文件用的是 require 方法,而在 Node 中 require 方法是有缓存的,Node 在每个第一次 Load Module 时就会将该 Module 缓存,存入全局的 _cache 中,在一般情况下我们当然需要这么做。但在模板加载这个情景下就不同了。</p> <p>在这里的确我们全局缓存了 React 模板文件,但这个文件是编译前的文件。而我们需要缓存的是编译后的文件,也就是说 markup 是我们需要缓存的值。</p> <p>在这里我们想想怎么去实现,方便起见,我们可以新增一个 <a href="/misc/goto?guid=4959673749250970189" rel="nofollow,noindex"> lru-cache </a> ,用它的好处是 lru 封装了很多关于 cache 时效与容量的开关。</p> <pre> <code class="language-javascript">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); } } </code></pre> <p>当然,我们现在这种情形下都需要清除 require 的 cache。</p> <h3>Babel</h3> <p>我想很多开发者在写 React 组件的时候用的是 ES6 Class 来写的,而且会用到很多 ES6/ES7 的方法,不巧的是 Node 还不支持有些高级特性。因此就引到了一个话题,服务端怎么引用 babel?</p> <p>在业务有 babel-node 这类解决方案,但这毕竟是一个实验性的 Node,我们不会拿生产环境去冒险。</p> <p>在 koa/react-view 中间件内,有一段说明,它建议开发者在使用的时候加入 babel-register 作实时编译。关于这个问题,当然也可以写在中间件内,在加载模板前引入。随着 Node 对 ES6 方法支持的完善,也许有一天也用不到了。</p> <h2>总结</h2> <p>其实,实现 View 非常简单,我们也从一些维度看到了设计一个 xx-view 的一般方法。在具体实现的时候,我们可以用一些更好的方法去做,比如用类来抽象 View,用 Promise 来描述过程。这些留给读者自己去优化。</p> <p> </p> <p>来自: <a href="/misc/goto?guid=4959673749330585175" rel="nofollow">https://zhuanlan.zhihu.com/p/21110875</a></p> <p> </p>