您的位置:首页 > 其它

降低首屏时间,“直出”是个什么概念?

2015-12-27 15:08 239 查看
早几年前端还处于刀耕火种、JQuery独树一帜的时代,前后端代码的耦合度很高,一个web页面文件的代码可能是这样的:

//app.js
require('node-jsx').install({  //让node端能解析jsx
extension: '.js'
});

var fs = require('fs'),
koa = require('koa'),
compress = require('koa-compress'),
render = require('koa-ejs'),
mime = require('mime-types'),
r_home = require('./routes/home'),
limit = require('koa-better-ratelimit'),
getData = require('./modules/getData');

var app = koa();

app.use(limit({ duration: 1000*10 ,
max: 500, accessLimited : "您的请求太过频繁,请稍后重试"})
);
app.use(compress({
threshold: 50,
flush: require('zlib').Z_SYNC_FLUSH
}));

render(app, {  //ejs渲染配置
root: './dist/views',
layout: false ,
viewExt: 'ejs',
cache: false,
debug: true
});

getData(app);

//首页路由
r_home(app);

app.use(function*(next){
var p = this.path;
this.type = mime.lookup(p);
this.body = fs.createReadStream('.'+p);
});

app.listen(3300);


View Code
2. 首页路由('./routes/home')配置

var router = require('koa-router'),
getHost = require('../modules/getHost'),
apiRouter = new router();

var React = require('react/lib/ReactElement'),
ReactDOMServer = require('react-dom/server');
var List = React.createFactory(require('../dist/js/component/List'));

module.exports = function (app) {

var data = this.getDataSync('../data/names.json'),  //取首屏数据
json = JSON.parse(data);

var lis = json.map(function(item, i){
return (
<li>{item.name}</li>
)
}),
props = {color: 'red'};

apiRouter.get('/', function *() {  //首页
yield this.render('home/index', {
title: "serverRender",
syncData: {
names: json,  //将取到的首屏数据注入ejs模板
props: props
},
reactHtml:  ReactDOMServer.renderToString(List(props, lis)),
dirpath: getHost(this)
});
});

app.use(apiRouter.routes());

};


注意这里我们使用了 ReactDOMServer.renderToString 来渲染 React 组件为纯 HTML 字符串,注意 List(props, lis) ,我们还传入了 props 和 children。

其在 ejs 模板中的应用为:

<div class="wrap" id="wrap"><%-reactHtml%></div>


就这么简单地完成了服务端渲染的处理,但还有一处问题,如果组件中绑定了事件,客户端不会感知。

所以在客户端我们也需要再做一次与服务端一致的渲染操作,鉴于服务端生成的DOM会被打上 data-react-id 标志,故在客户端渲染的话,react 会通过该标志位的对比来避免冗余的render,并绑定上相应的事件。

这也是我们把所要注入组件中的数据(syncData)传入 ejs 的原因,我们将把它作为客户端的一个全局变量来使用,方便客户端挂载组件的时候用上:

ejs上注入直出数据:

<script>
syncData = JSON.parse('<%- JSON.stringify(syncData) %>');
</script>


页面入口文件(js/page/home.js)挂载组件:

import React from 'react';
import ReactDOM from 'react-dom';
var List = require('../component/List');

var lis = syncData.names.map(function(item, i){
return (
<li>{item.name}</li>
)
});
ReactDOM.render(
<List {...syncData.props}>
{lis}
</List>,
document.getElementById('wrap')
);


3. 辅助工具

为了玩鲜,在部分模块里写了 es2015 的语法,然后使用 babel 来做转换处理,在 gulp 和 webpack 中都有使用到,具体可参考它们的配置。

另外鉴于服务端对 es2015 的特性支持不完整,配合 babel-core/register 或者使用 babel-node 命令都存在兼容问题,故针对所有需要在服务端引入到的模块(比如React组件),在koa运行前先做gulp处理转为es5(这些构建模块仅在服务端会用到,客户端走webpack直接引用未转换模块即可)。

ejs文件中样式或脚本的内联处理我使用了自己开发的 gulp-embed ,有兴趣的朋友可以玩一玩。

4. issue

说实话 React 的服务端渲染处理整体开发是没问题的,就是开发体验不够好,主要原因还是各方面对 es2015 支持不到位导致的。

虽然在服务端运行前,我们在gulp中使用babel对相关模块进行转换,但像 export default XXX 这样的语法转换后还是无法被服务端支持,只能降级写为 module.exports = XXX。但这么写,在其它模块就没法 import XXX from 'X' 了(改为 require('X')代替),总之不爽快。只能期待后续 node(其实应该说V8) 再迭代一些版本能更好地支持 es2015 的特性。

另外如果 React 组件涉及列表项,常规我们会加上 key 的props特性来提升渲染效率,但即使前后端传入相同的key值,最终 React 渲染出来的 key 值是不一致的,会导致客户端挂载组件时再做一次渲染处理。

对于这点我个人建议是,如果是静态的列表,那么统一都不加 key ,如果是动态的,那么就加吧,客户端再渲染一遍感觉也没多大点事。(或者你有更好方案请留言哈~)

5. 其它

有时候服务端引入的模块里面,有些东西是仅仅需要在客户端使用到的,我们以这个示例中的组件 component/List 为例,里面的样式文件

require('css/component/List');


不应当在服务端执行的时候使用到,但鉴于同构,前后端用的一套东西,这个怎么解决呢?其实很好办,通过 window 对象来判断即可(只要没有什么中间件给你在服务端也加了window接口):

var isNode = typeof window === 'undefined';

if(!isNode){
require('css/component/List');
}


不过请注意,这里我通过 webpack 把组件的样式也打包进了客户端的页面入口文件,其实不妥当。因为通过直出,页面在响应的时候就已经把组件的DOM树都先显示出来了,但这个时候是还没有取到样式的(样式打包到入口脚本了),需要等到入口脚本加载的时候才能看到正确的样式,这个过程会有一个闪动的过程,是种不舒服的体验。

所以走直出的话,建议把首屏的样式抽离出来内联到头部去。

唠唠磕磕就说了这么多,欢迎讨论交流,共勉~

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: