【大前端以前后分离01】JS前端渲染VS服务器端渲染

前言

以前看了一篇文章:@Charlie.Zheng Web系统开发构架再思考-先后端的彻底分离,文中论述了为什么要先后分离,站在前端的角度来看,是颇有必要的;可是如何说服团队使用前端渲染方案倒是一个现实问题,由于若是我是一个服务器端,我便会以为不是颇有必要,为何要先后分离,先后分离后遗留了什么问题,如何解决,都得说清楚,这样才能说服团队使用前端渲染的方案,而最近我恰好遇到了框架选型的抉择。javascript

来到新公司开始新项目了,须要作前端框架选型,由于以前内部同事采用的fis框架,而这边又是使用的php,此次也就直接采用fis基于php的解决方案:php

http://oak.baidu.com/fis-pluscss

说句实话,fis这套框架作的不错,可是若是使用php方案的话,我就须要蛋疼的在其中写smarty模板,而后彻底按照规范走,虽然fis规范比较合理,也能够接受,可是稍微深刻解后发现fis基于php的方案能够归纳为(咱们的框架用成这样,不特指fis):html

服务器端渲染html所有图给浏览器,再加载前端js处理逻辑

显然,这个不是我要的,梦想中的工做方式是作到静态html化,静态html装载js,使用json进行业务数据通讯,这就是一些朋友所谓的前端渲染了前端

JS渲染的鄙利

前端渲染会带来不少好处:java

① 彻底释放前端,运行不须要服务器;node

② 服务器端只提供接口数据服务,业务逻辑所有在前端,先后分离;react

③ 一些地方性能有所提高,好比服务器不须要解析index.html,直接返回便可;jquery

④ ......git

事实上以上的说法和优点皆没有十足的说服力,根据上述因素,咱们知道了为何咱们要采用js+json的方案,但这不表明应该采用。

好比不少朋友认为先后分离可让前端代码更加清晰,这一说法我就十分不认同,若是前端代码功力不够,绝对能够写整天书,分离是必要条件,却不是分离后前端就必定清晰,不然也不会有那么多人呼吁模块化、组件化;并且服务器端彻底能够质疑这样作的种种问题,好比:

① 前端模板解析对手机端的负担,对手机电池产生更快的消耗;

前端渲染页面内容不能被爬虫识别,SEO等于没有了;

③ 前端渲染现阶段没有完善的ABTesting方案;

④ 不能保证一个URL每次展现的内容一致,好比js分页致使路由不一致;

⑤ ......

以上的问题,一些是难点,一些是痛点,选取前端渲染方案至少得有SEO解决方案,否则一切都是空谈

因此有如此多的问题,前端凭什么说服团队使用前端渲染的方案,难道仅仅是咱们爽了,咱们以为这样好就能够了吗?

何况现状是团队中服务器端的同事资深的多,前端话语权不够,这个时候须要用数听说话,但未作调研也拿不出数据,没有数据你凭什么说服领导采用前端渲染方案?

为何要采用前端渲染

最近两年我却找到了能够说服本身采用前端渲染的缘由:

① 体验更好

Hybrid内嵌只能用静态文件

事实上咱们不能用数听说明webapp(前端渲染)的体验就必定比服务器端渲染好,因此Hybrid内嵌就变成了主要的因素,现有的Hybrid有两种方案:

① webview直连线上站点,响应速度慢,没有升级负担,离线应用不易;

② 将静态html+js+css打包进native中,直接走file模式访问,交互走json,很是简单就能够实现离线应用(某些页面的离线应用)

如今一个产品通常三套应用:PC、H5站点、APP,PC站点早就造成,H5站点通常与APP同步开发,Hybrid中的逻辑与H5的逻辑大同小异,因此

H5站点与Hybrid中的静态文件使用一套代码,这个是使用前端渲染的主要缘由,意思是H5程序结束,APP就完成80%了。

由于服务器端渲染须要使用动态语言,而webview只能解析html等静态文件,因此使用前端渲染就变成了必须,而这一套说辞基本能够说服多数人,自少我是信了。

拦路虎-SEO

上面说了不少前端渲染的问题,什么手机性能、手机耗电、ABTesting都不是痛点,惟一难受的是H5站点的SEO,以原来公司酒店订单来讲,有20%以上的流量来源于H5站点,浏览器是一个流量的重要来源,SEO不可丢弃。

因此前端渲染必须有解决SEO的方案,而且方法不能太烂,不然框架出来了也没人愿意用,好在此次作的项目不是webapp,SEO方案相对要简单一点,移动端展现的信息少SEO不会太难,这个进一步下降了咱们的实现难度,通过几轮摸索,我这两天想了一个简单的方案,正在验证可行性。

JS渲染应该如何作

前端渲染应该如何作?阿里的大神们事实上一直也在思考方案,而且彷佛已经有成功的产出:先后端分离的思考与实践(二)

惋惜,读过文章后,依旧没有得到对本身有用的信息,而且对应的代码也看不到,本身以前的方案:探讨webapp的SEO难题(上),连本身都以为很是戳而没有继续。

编译的过程

而最近在公司内部使用fis时候,一段代码引发了个人兴趣:

{%block name="body"%}
    {%widget name="webapp:widget/index/route/route.tpl"%}
    {%widget name="webapp:widget/index/searchCity/searchCity.tpl"%}
    {%widget name="webapp:widget/index/selectDate/selectDate.tpl"%}
{%/block%}

这段代码基于smarty模板,运行会通过一次release过程,将真正的route模板字符串与服务器data造成最终的html,这段代码引发了个人思考,却说不出来什么问题。

我偶然又看到了以前的react解决方案,彷佛也有一个编译的过程:

React.render( 
  // 这是什么不是字符串,不是数字,又不是变量的参数……WTF 
  <h1>Hello, world!</h1>, 
  document.getElementById('example') 
); 
//JSX编译转换为javascript==>
React.render( 
  React.DOM.h1(null, 'Hello, world!'), 
  document.getElementyById('example') 
); 

因此,在程序真实运行前有一个编译的过程,一个是编译才能运行,一个是运行时候须要编译,因而我在想前端渲染能够这样作吗?

页面渲染的条件

比较简单的状况下,对于前端来讲,页面html的组成须要数据与模板,而服务器也仅仅须要数据与模板,因此简单来讲:

html = data + template

先后端的模板有所不一样的是:

前端模板也许不能被服务器解析,若是模板中存在js函数,服务器模板将没法执行

可是通过咱们以前的研究,.net能够运行一个V8的环境帮助解析模板,java等也有相关的类库,因此此问题不予关注,第二个问题是:

前端数据为异步加载,服务器端为同步加载,可是:

简单状况下,服务器端与前端数据请求须要的仅仅是URL与参数

因而,一个方案彷佛变的可能。

前端渲染方案

入口页

将如咱们的index.html是这样的:

debug端:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="./libs/zepto.js"></script>
    <script type="text/javascript" src="./libs/underscore.js"></script>
    <script type="text/javascript" src="./libs/require.js"></script>
</head>
<body>
<%widget({
name: 'type',
model: 'type',
controller: 'type'
}); %>
</body>
</html>

其中name对应的为模板文件,而model对应的是数据请求所需文件,controller对应控制器,咱们这里使用grunt造成两套前端代码,分别对应服务器端前端:

注意:这里服务器实现暂时使用nodeJS,该方案设想是能够根据grunt打包支持.net/java/php等语言,可是楼主服务器战五渣,因此你懂的

服务器端:

<!DOCTYPE html>
<html>
  <head>
    <title>测试</title>
    <script type="text/javascript" src="./libs/zepto.js"></script>
    <script type="text/javascript" src="./libs/underscore.js"></script>
    <script type="text/javascript" src="./libs/require.js"></script>
  </head>
  <body>
    <%-widget({
      name: 'type',
      model: 'type',
      controller: 'type'
    }); %>
  </body>
</html>

前端:

 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6     <script type="text/javascript" src="./libs/zepto.js"></script>
 7     <script type="text/javascript" src="./libs/underscore.js"></script>
 8     <script type="text/javascript" src="./libs/require.js"></script>
 9     <script type="text/javascript">
10         require.config({
11             "paths": {
12                 "text": "./libs/require.text"
13             }
14         });
15 
16         var render = function (template, model, controller, wrapperId) {
17             require([template, model, controller],
18             function (template, model, controller) {
19                 //调用model,生成json数据
20                 model.execute(function (data) {
21                     data = JSON.parse(data);
22                     if (data.errorno != 0) return;
23                     //根据模板和data生成静态html,并造成dom结构准备插入
24                     var html = $(_.template(template)(data));
25                     var wrapper = $('#' + wrapperId);
26 
27                     //将dom结构插入,而且将多余的包裹标志层删除
28                     html.insertBefore(wrapper);
29                     wrapper.remove();
30                     //执行控制器
31                     controller.init();
32                 });
33             });
34         };
35     </script>
36 </head>
37 <body>
38 <div id="type_widget_wrapper">
39 <script type="text/javascript">
40     render('text!./template/type.html', './model/type', './controller/type', 'type_widget_wrapper');
41 </script>
42 </div>
43 </body>
44 </html>

虽然,我这里grunt的程序还没有实现,可是根据以前的经验,这是必定能实现的。

model的设计

默认入口端model为一个json对象

debug端&服务器端:

{
    "url": "http://runjs.cn/uploads/rs/279/2h5lvbt5/data.json",
    "param": {}
}

由于服务器端仅仅须要一个url一个param,因此服务器端与debug端保持一致,而前端被grunt加工为:

define(function () {
    return{
        url: './data/data.json',
        param: {},
        execute: function (success) {
            $.get(this.url, this.param, function (data) {
                success(data);
            })
        }
    };
})

显然,此数据源文件比较简单,真实状况不可能如此,咱们这里也仅仅作demo说明,后续逐步增强。

服务器端运行流程

服务器端因为是基于node的,首先须要配置app,这里将全部路由所有放到index.js中:

 1 var express = require('express');
 2 var path = require('path');
 3 var favicon = require('serve-favicon');
 4 var logger = require('morgan');
 5 var cookieParser = require('cookie-parser');
 6 var bodyParser = require('body-parser');
 7 var http = require('http');
 8 
 9 var routes = require('./routes/index');
10 
11 var app = express();
12 
13 // view engine setup
14 app.set('views', path.join(__dirname, 'views'));
15 app.set('view engine', 'ejs');
16 
17 // uncomment after placing your favicon in /public
18 //app.use(favicon(__dirname + '/public/favicon.ico'));
19 app.use(logger('dev'));
20 app.use(bodyParser.json());
21 app.use(bodyParser.urlencoded({ extended: false }));
22 app.use(cookieParser());
23 app.use(express.static(path.join(__dirname, 'public')));
24 
25 //所有路由放到index中
26 routes(app);
27 
28 // catch 404 and forward to error handler
29 app.use(function(req, res, next) {
30   var err = new Error('Not Found');
31   err.status = 404;
32   next(err);
33 });
34 
35 
36 // development error handler
37 // will print stacktrace
38 if (app.get('env') === 'development') {
39   app.use(function(err, req, res, next) {
40     res.status(err.status || 500);
41     res.render('error', {
42       message: err.message,
43       error: err
44     });
45   });
46 }
47 
48 // production error handler
49 // no stacktraces leaked to user
50 app.use(function(err, req, res, next) {
51   res.status(err.status || 500);
52   res.render('error', {
53     message: err.message,
54     error: {}
55   });
56 });
57 
58 
59 app.set('port', process.env.PORT || 3000);
60 http.createServer(app).listen(app.get('port'), function(){
61   console.log('Express server listening on port ' + app.get('port'));
62 });
63 
64 module.exports = app;
View Code

index的代码:

 1 var express = require('express');
 2 var path = require('path');
 3 var ejs = require('ejs');
 4 var fs= require('fs');
 5 var srequest = require('request-sync');
 6 
 7 var project_path = path.resolve();
 8 var routerCfg = require(project_path + '/routerCfg.json');
 9 
10 //定义页面读取方法,须要同步读取
11 var widget = function(opts) {
12   var model = require(project_path + '/model/' + opts.model + '.json') ;
13   //var controller =project_path + '/controller/' + opts.controller + '.js';
14   var tmpt = fs.readFileSync(project_path + '/template/' + opts.name + '.html', 'utf-8');
15 
16   //设置代理,直接使用ip不能读取数据,可是设置代理的化,代理不生效,只能直接读取线上了......
17   var res = srequest({ uri: model.url, qs: model.param});
18 
19   var html = ejs.render(tmpt, JSON.parse(res.body.toString('utf-8')));
20 
21   //插入控制器,这个路径可能须要调整
22   html += '<script type="text/javascript">require(["controller/' + opts.controller + '"], function(controller){controller.init();});</script>';
23 
24   return html;
25 };
26 
27 var initRounter = function(opts, app) {
28   //根据路由配置生成路由
29   for(var k in opts) {
30     app.get('/' + k, function (req, res) {
31       res.render(k, { widget: widget});
32     });
33   }
34 };
35 
36 module.exports = function(app) {
37   //加载全部路由配置
38   initRounter(routerCfg, app);
39 };

简单加载流程:

核心点:对于服务器端来讲,widget为一个javascript方法,会根据参数返回一个字符串(由于须要同步返回因此模板读取,数据访问皆为同步进行)

① 访问/index路径

② 根据widget参数获取model数据(json)

③ 获取model url,而且根据param发送请求获取数据(这里的状况比较简单,先不要苛责)

④ 根据参数获取模板

⑤ 根据esj模板(相似于undersocre模板),解析生成html

⑥ 将控制器代码一require的方式添加到html,最后返回html

启动node服务,运行之获得了最终结果:

运行结果:

查看源代码,能够看到有完整的html结构:

<!DOCTYPE html>
<html>
  <head>
    <title>测试</title>
    <script type="text/javascript" src="./libs/zepto.js"></script>
    <script type="text/javascript" src="./libs/underscore.js"></script>
    <script type="text/javascript" src="./libs/require.js"></script>
  </head>
  <body>

    <ul id="type_id">
    
    <li class="type js_type">
        <h2>电脑</h2>
        <ul class="product_list">
            
                <li class="product">
                    戴尔
                </li>
            
                <li class="product">
                    苹果
                </li>
            
                <li class="product">
                    联想
                </li>
            
                <li class="product">
                    华硕
                </li>
            
        </ul>
    </li>
    
    <li class="type js_type">
        <h2>书籍</h2>
        <ul class="product_list">
            
                <li class="product">
                    三国演义
                </li>
            
                <li class="product">
                    西游记
                </li>
            
                <li class="product">
                    红楼梦
                </li>
            
                <li class="product">
                    水浒传
                </li>
            
        </ul>
    </li>
    
    <li class="type js_type">
        <h2>游戏</h2>
        <ul class="product_list">
            
                <li class="product">
                    仙剑1
                </li>
            
                <li class="product">
                    仙剑2
                </li>
            
                <li class="product">
                    仙剑3
                </li>
            
                <li class="product">
                    仙剑4
                </li>
            
        </ul>
    </li>
    
</ul><script type="text/javascript">require(["controller/type"], function(controller){controller.init();});</script>

  </body>
</html>
View Code

客户端流程

客户端因为须要异步性,因此生成的结构是这样的:

1 <div id="type_widget_wrapper">
2 <script type="text/javascript">
3     render('text!./template/type.html', './model/type', './controller/type', 'type_widget_wrapper');
4 </script>
5 </div>

核心代码为:

 1 var render = function (template, model, controller, wrapperId) {
 2     require([template, model, controller],
 3     function (template, model, controller) {
 4         //调用model,生成json数据
 5         model.execute(function (data) {
 6             data = JSON.parse(data);
 7             if (data.errorno != 0) return;
 8             //根据模板和data生成静态html,并造成dom结构准备插入
 9             var html = $(_.template(template)(data));
10             var wrapper = $('#' + wrapperId);
11 
12             //将dom结构插入,而且将多余的包裹标志层删除
13             html.insertBefore(wrapper);
14             wrapper.remove();
15             //执行控制器
16             controller.init();
17         });
18     });
19 };

① 页面加载,开始解析页面中的render方法

② render方法根据参数获取model模块与template模块

③ 执行model.execute异步请求数据,并与template造成html

④ 将html造成jquery对象,插入包装节点前,而后删除节点

运行结果:

查看源代码,能够看到,这些代码与seo毫无关系:

 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6     <script type="text/javascript" src="./libs/zepto.js"></script>
 7     <script type="text/javascript" src="./libs/underscore.js"></script>
 8     <script type="text/javascript" src="./libs/require.js"></script>
 9     <script type="text/javascript">
10         require.config({
11             "paths": {
12                 "text": "./libs/require.text"
13             }
14         });
15 
16         var render = function (template, model, controller, wrapperId) {
17             require([template, model, controller],
18             function (template, model, controller) {
19                 //调用model,生成json数据
20                 model.execute(function (data) {
21                     data = JSON.parse(data);
22                     if (data.errorno != 0) return;
23                     //根据模板和data生成静态html,并造成dom结构准备插入
24                     var html = $(_.template(template)(data));
25                     var wrapper = $('#' + wrapperId);
26 
27                     //将dom结构插入,而且将多余的包裹标志层删除
28                     html.insertBefore(wrapper);
29                     wrapper.remove();
30                     //执行控制器
31                     controller.init();
32                 });
33             });
34         };
35     </script>
36 </head>
37 <body>
38 <div id="type_widget_wrapper">
39 <script type="text/javascript">
40     render('text!./template/type.html', './model/type', './controller/type', 'type_widget_wrapper');
41 </script>
42 </div>
43 
44 
45 
46 </body>
47 </html>
View Code

总体目录

PS:目录有必定缺乏,由于程序还没有彻底完成,而最近工做忙起来了......

问题&后续

由于这个方案是本身想的,确定认为是有必定可行性的,可是有几个问题必须得解决。

debug烦

如所示,开始阶段咱们通常都只开发debug层,可是要调试却每次须要grunt工具release一下才能运行client中的程序,显然很差,须要解决。

模板嵌套

模板嵌套问题事实上是最难的,想象一下,咱们在一个模板中又有一个widget,在子模板中又有一个widget,这个就变成了一个噩梦,这里的嵌套最怕的是,父模块与子模块中有数据依赖,或者子模块为一个循环,循环却依赖父模块单个值,这个很是难解决。

后续

这个想法最近才出现,刚刚实现一定会有这样那样的问题,并且本身的知识体系也达不到架构水平,若是您发现文中任何问题,或者有更好的方案,请您留言,后续这块的研究暂时规划为:

① 完善grunt程序,造成.net方案

② 解决debug时候须要编译问题

③ 解决模板嵌套、模块数据依赖问题

④ ......

github

https://github.com/yexiaochai/sword

微博求粉

相关文章
相关标签/搜索