首页 > 课堂 > FAQ问答 > 正文

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

2020-03-22 16:28:41
字体:
来源:转载
供稿:网友
  • 早几年前端还处于刀耕火种、JQuery独树一帜的时代,前后端代码的耦合度很高,一个web页面文件的代码可能是这样的:

    这意味着后端的工程师往往得负责一部分修改HTML、编写脚本的工作,而前端开发者也得了解页面上存在的服务端代码含义。

    有时候某处页面逻辑的变动,鉴于代码的混搭,可能都不确定应该请后端还是前端来改动(即使他们都能处理)。

    前端框架热潮

    有句俗话说的好——“人啊,要是擅于开口‘关我屁事’和‘关你屁事’这俩句,可以节省人生中的大部分时间”。

    随着这两年被 angular 牵头带起的各种前端MV*框架的风靡,后端可以毋须再于静态页面耗费心思,只需要专心开发数据接口供前端使用即可。得益于此,前后端终于可以安心地互相道一声“关我屁事”或“关你屁事”了。

    以 avalon 为例,前端只需要在页面加载时发送个ajax请求取得数据绑定到vm,然后做view层渲染即可:

    var vm = avalon.define({    $id: 'wrap',    list: []});fetch('data/list.php')   //向后端接口发出请求    .then(res => res.json())    .then(json => {        vm.list = json; //数据注入vm        avalon.scan();  //渲染view层    });

    静态页面的代码也由前端一手掌握,原本服务端的代码换成了 avalaon 的专用属性与插值表达式:

    <ul ms-controller='wrap'>    <li ms-repeat='list'>{el.name}</li></ul>

    前后端代码隔离的形式大大提升了项目的可维护性和开发效率,已经成为一种web开发的主流模式。它解放了后端程序员的双手,也将更多的控制权转移给前端人员(当然前端也因此需要多学习一些框架知识)。

    弊端

    前后端隔离的模式虽然给开发带来了便利,但相比水乳交融的旧模式,页面首屏的数据需要在加载的时候向服务端发去请求才能取得,多了请求等候的时间(RTT)。

    这意味着用户访问页面的时候,这段“等待后端返回数据”的时延会处于白屏状态,如果用户网速差,那么这段首屏等候时间会是很糟糕的体验。

    当然拉到数据后,还得做 view 层渲染(客户端引擎的处理还是很快的,忽略渲染的时间),这又依赖于框架本身,即框架要先被下载下来才能处理这些视图渲染操作。那么好家伙,一个 angular.min.js 就达到了 120 多KB,用着渣信号的用户得多等上一两秒来下载它。

    这么看来,单纯前后端隔离的形式存在首屏时间较长的问题,除非未来平均网速达到上G/s,不然都是不理想的体验。

    so 怎么办?相信很多朋友猜到了——用 node 来助阵。

    直出和同构

    直出说白了其实就是“服务端渲染并输出”,跟起初我们提及的前后端水乳交融的开发模式基本类似,只是后端语言我们换成了 node 。

    09年开始冒头的 node 现在成了当红炸子鸡,包含阿里、腾讯在内的各大公司都广泛地把 node 用到项目上,前后端整而为一,如果 node 的特性适用于你的项目,那么何乐而不为呢。

    我们在这边也提及了一个“同构”的概念,即前后端(这里的“后端”指的是直出端,数据接口不一定由node开发)使用同一套代码方案,方便维护。

    当前 node 在服务端有着许多主流抑或非主流的框架,包括 express、koa、thinkjs 等,能够较快上手,利用各种中间件得以进行敏捷开发。

    另外诸如 ejs、jade 这样的渲染模板能让我们轻松地把首屏内容(数据或渲染好的DOM树)注入页面中。

    这样用户访问到的便是已经带有首屏内容的页面,大大降低了等候时间,提升了体验。

    示例

    在这里我们以 koa + ejs + React 的服务端渲染为例,来看看一个简单的“直出”方案是怎样实现的。该示例也可以在我的github上下载到。

    项目的目录结构如下:

    +---data   //模拟数据接口,放了一个.json文件+---dist  //文件构建后(gulp/webpack)存放处|   +---css|   |   +---common|   |   ---page|   +---js|   |   +---component|   |   ---page|   ---views|       +---common|       ---home+---modules  //一些自行封装的通用业务模块+---routes  //路由配置---src  //未构建的文件夹    +---css     |   +---common    |   +---component    |   ---page    +---js    |   +---component //React组件    |   ---page //页面入口文件    ---views  //ejs模板        +---common        ---home

    1. node 端 jsx 解析处理

    node 端是不会自己识别 React 的jsx 语法的,故我们需要在项目文件中引入node-jsx,即使现在可以安装babel-cli 后(并添加预设)使用 babel-node 命令替代 node,但后者用起来总会出问题,故暂时还是采纳 node-jsx 方案:

    //app.jsrequire('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);

    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 html' target='_blank'>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树都先显示出来了,但这个时候是还没有取到样式的(样式打包到入口脚本了),需要等到入口脚本加载的时候才能看到正确的样式,这个过程会有一个闪动的过程,是种不舒服的体验。

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

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

    PHP编程

    郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

  • 发表评论 共有条评论
    用户名: 密码:
    验证码: 匿名发表