前言

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

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

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

说句实话,fis这套框架做的不错,但是如果使用php方案的话,我就需要蛋疼的在其中写smarty模板,然后完全按照规范走,虽然fis规范比较合理,也可以接受,但是稍微深入解后发现fis基于php的方案可以概括为(我们的框架用成这样,不特指fis):

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

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

JS渲染的鄙利

前端渲染会带来很多好处:

① 完全释放前端,运行不需要服务器;

② 服务器端只提供接口数据服务,业务逻辑全部在前端,前后分离;

③ 一些地方性能有所提升,比如服务器不需要解析index.html,直接返回即可;

④ ......

事实上以上的说法和优势皆没有十足的说服力,根据上述因素,我们知道了为什么我们要采用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>

前端:

 <!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>
<script type="text/javascript">
require.config({
"paths": {
"text": "./libs/require.text"
}
}); var render = function (template, model, controller, wrapperId) {
require([template, model, controller],
function (template, model, controller) {
//调用model,生成json数据
model.execute(function (data) {
data = JSON.parse(data);
if (data.errorno != 0) return;
//根据模板和data生成静态html,并形成dom结构准备插入
var html = $(_.template(template)(data));
var wrapper = $('#' + wrapperId); //将dom结构插入,并且将多余的包裹标志层删除
html.insertBefore(wrapper);
wrapper.remove();
//执行控制器
controller.init();
});
});
};
</script>
</head>
<body>
<div id="type_widget_wrapper">
<script type="text/javascript">
render('text!./template/type.html', './model/type', './controller/type', 'type_widget_wrapper');
</script>
</div>
</body>
</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中:

 var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var http = require('http'); var routes = require('./routes/index'); var app = express(); // view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); // uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); //全部路由放到index中
routes(app); // catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
}); // development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
} // production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
}); app.set('port', process.env.PORT || 3000);
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
}); module.exports = app;

index的代码:

 var express = require('express');
var path = require('path');
var ejs = require('ejs');
var fs= require('fs');
var srequest = require('request-sync'); var project_path = path.resolve();
var routerCfg = require(project_path + '/routerCfg.json'); //定义页面读取方法,需要同步读取
var widget = function(opts) {
var model = require(project_path + '/model/' + opts.model + '.json') ;
//var controller =project_path + '/controller/' + opts.controller + '.js';
var tmpt = fs.readFileSync(project_path + '/template/' + opts.name + '.html', 'utf-8'); //设置代理,直接使用ip不能读取数据,但是设置代理的化,代理不生效,只能直接读取线上了......
var res = srequest({ uri: model.url, qs: model.param}); var html = ejs.render(tmpt, JSON.parse(res.body.toString('utf-8'))); //插入控制器,这个路径可能需要调整
html += '<script type="text/javascript">require(["controller/' + opts.controller + '"], function(controller){controller.init();});</script>'; return html;
}; var initRounter = function(opts, app) {
//根据路由配置生成路由
for(var k in opts) {
app.get('/' + k, function (req, res) {
res.render(k, { widget: widget});
});
}
}; module.exports = function(app) {
//加载所有路由配置
initRounter(routerCfg, app);
};

简单加载流程:

核心点:对于服务器端来说,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>

客户端流程

客户端由于需要异步性,所以生成的结构是这样的:

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

核心代码为:

 var render = function (template, model, controller, wrapperId) {
require([template, model, controller],
function (template, model, controller) {
//调用model,生成json数据
model.execute(function (data) {
data = JSON.parse(data);
if (data.errorno != 0) return;
//根据模板和data生成静态html,并形成dom结构准备插入
var html = $(_.template(template)(data));
var wrapper = $('#' + wrapperId); //将dom结构插入,并且将多余的包裹标志层删除
html.insertBefore(wrapper);
wrapper.remove();
//执行控制器
controller.init();
});
});
};

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

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

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

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

运行结果:

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

 <!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>
<script type="text/javascript">
require.config({
"paths": {
"text": "./libs/require.text"
}
}); var render = function (template, model, controller, wrapperId) {
require([template, model, controller],
function (template, model, controller) {
//调用model,生成json数据
model.execute(function (data) {
data = JSON.parse(data);
if (data.errorno != 0) return;
//根据模板和data生成静态html,并形成dom结构准备插入
var html = $(_.template(template)(data));
var wrapper = $('#' + wrapperId); //将dom结构插入,并且将多余的包裹标志层删除
html.insertBefore(wrapper);
wrapper.remove();
//执行控制器
controller.init();
});
});
};
</script>
</head>
<body>
<div id="type_widget_wrapper">
<script type="text/javascript">
render('text!./template/type.html', './model/type', './controller/type', 'type_widget_wrapper');
</script>
</div> </body>
</html>

整体目录

PS:目录有一定缺少,因为程序尚未完全完成,而最近工作忙起来了......

问题&后续

因为这个方案是自己想的,肯定认为是有一定可行性的,但是有几个问题必须得解决。

debug烦

如所示,开始阶段我们一般都只开发debug层,但是要调试却每次需要grunt工具release一下才能运行client中的程序,显然不好,需要解决。

模板嵌套

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

后续

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

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

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

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

④ ......

github

https://github.com/yexiaochai/sword

微博求粉

【大前端之前后分离01】JS前端渲染VS服务器端渲染的更多相关文章

  1. Mikit前端框架,轻量级CSS&JS前端框架

    Mikit CSS Framework Mikit介绍 Mikit是前端开发人员和前端设计师所喜爱的Web框架.Mikit的创建和设计旨在为前端社区提供最灵活而强大的CSS框架. 与许多其他网络框架不 ...

  2. 前端迷思与React.js

    前端迷思与React.js 前端技术这几年蓬勃发展, 这是当时某几个项目需要做前端技术选型时, 相关资料整理, 部分评论引用自社区. 开始吧: 目前, Web 开发技术框架选型为两种的占 80% .这 ...

  3. 一统江湖的大前端(2)—— Mock.js + Node.js 如何与后端潇洒分手

    <一统江湖的大前端>系列是自己的前端学习笔记,旨在介绍javascript在非网页开发领域的应用案例和发现各类好玩的js库,不定期更新.如果你对前端的理解还是写写页面绑绑事件,那你真的是有 ...

  4. 一统江湖的大前端(4)shell.js——穿上马甲我照样认识你

    <一统江湖的大前端>系列是自己的前端学习笔记,旨在介绍javascript在非网页开发领域的应用案例和发现各类好玩的js库,不定期更新.如果你对前端的理解还是写写页面绑绑事件,那你真的是有 ...

  5. 一统江湖的大前端(6)commander.js + inquirer.js——懒,才是第一生产力

    <一统江湖的大前端>系列是自己的前端学习笔记,旨在介绍javascript在非网页开发领域的应用案例和发现各类好玩的js库,不定期更新.如果你对前端的理解还是写写页面绑绑事件,那你真的是有 ...

  6. 前端开发之走进Vue.js

    Vue.js作为目前最热门最具前景的前端框架之一,其提供了一种帮助我们快速构建并开发前端项目的新的思维模式.本文旨在帮助大家认识Vue.js,了解Vue.js的开发流程,并进一步理解如何通过Vue.j ...

  7. 006_饿了么大前端总监sofish帮你理清前端工程师及大前端团队的成长问题!

    作者|Sofish编辑|小智 & 尾尾本文是前端之巅向 sofish 的约稿<什么样的人可以称为架构师?>.采访< 饿了么大前端团队究竟是如何落地和管理的?>以及 so ...

  8. BAT 前端开发面经 —— 吐血总结 前端相关片段整理——持续更新 前端基础精简总结 Web Storage You don't know js

    BAT 前端开发面经 —— 吐血总结   目录 1. Tencent 2. 阿里 3. 百度 更好阅读,请移步这里 聊之前 最近暑期实习招聘已经开始,个人目前参加了阿里的内推及腾讯和百度的实习生招聘, ...

  9. 系统架构:Web应用架构的新趋势---前端和后端分离的一点想法

    最近研究servlet,看书时候书里讲到了c/s架构到b/s架构的演变,讲servlet的书都很老了,现在的b/s架构已经不是几年前的b/s架构,其实b/s架构就是web应用开发,对于这样的架构我们现 ...

随机推荐

  1. .Net 初步学习笔记之一——.Net 平台与.Net FrameWork框架的关系

    .Net 包含两部分 .Net平台 和.Net FrameWork 框架 1..Net FrameWork框架包含于.Net平台. .Net FrameWork提供环境和支撑保证.Net平台运行. 2 ...

  2. EntityFramework 6 + Mysql 生成POCOs

    问题 使用EDMX文件 EF Power Tools参数不正确的解决方法 对于"异常来自 HRESULT:0x80070057 (E_INVALIDARG)",有方法说" ...

  3. 项目游戏开发日记 No.0x00000

    14软二杨近星(2014551622) ---恢复内容开始--- 2016-03-17 从开始迈进软件工程专业, 已经快两年了, 记得当初选择软件的理由是, 我要学去做东西, 我享受开发过程. 两年来 ...

  4. tg2015 信息传递 (洛谷p2661)

    题目描述 有n个同学(编号为1到n)正在玩一个信息传递的游戏.在游戏里每人都有一个固定的信息传递对象,其中,编号为i的同学的信息传递对象是编号为Ti同学. 游戏开始时,每人都只知道自己的生日.之后每一 ...

  5. 一键部署mono 免费空间支持ASP.NET MVC 再也不担心伙食费换空间了

    一直以来 部署mono 都是很头疼的事情 因为是我在是不熟悉非win环境,今天偶然发现这个项目,挺好的,分享下 https://github.com/wshearn/openshift-communi ...

  6. Hadoop3 在eclipse中访问hadoop并运行WordCount实例

    前言:       毕业两年了,之前的工作一直没有接触过大数据的东西,对hadoop等比较陌生,所以最近开始学习了.对于我这样第一次学的人,过程还是充满了很多疑惑和不解的,不过我采取的策略是还是先让环 ...

  7. 数据库设计中的Soft Delete模式

    最近几天有点忙,所以我们今天来一篇短的,简单地介绍一下数据库设计中的一种模式——Soft Delete. 可以说,该模式毁誉参半,甚至有非常多的人认为该模式是一个Anti-Pattern.因此在本篇文 ...

  8. Linux 桌面美化那点事儿

    各个 Linux 桌面发行版刚拿到手的时候,或多或少都有点儿不满意,对它们进行一些改造是必须的.网上不乏各种 Linux 桌面美化的教程和经验贴,对我们这些 Linux 爱好者来说都是很好的参考资料. ...

  9. Hadoop相关日常操作

    1.Hive相关 脚本导数据,并设置运行队列 bin/beeline -u 'url' --outputformat=tsv -e "set mapreduce.job.queuename= ...

  10. EMC与地之重新认识地

    记得在Mark的培训中,他手上拿了一个无线鼠标,然后问了一个很有意思的问题:“这个无线鼠标的地在哪里?同样,我们的手机没有和任何大地有接 触,那么这个地又在哪里呢?”这个问题确实很有意思,也确实让人很 ...