刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差很少了,如今要经过web的方式对外提供服务,也就是B/S架构。后端专一作业务逻辑,不想在后端作页面渲染的事情,只向前端提供数据接口。因而协商后打算将先后端彻底分离,页面上的全部数据都经过ajax向后端取,页面渲染的事情彻底由前端来作。另外还有一个紧急的状况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!因而在这样的背景下,决定开始一次先后端彻底分离的尝试。css
以前开发都是同步渲染和异步渲染混搭的,有些东西能够有后端PHP帮你编译好,如通用的页面模板,后端传回的页面参数等。提早预感到此次彻底分离可能会遇到一些困难,可是项目上线要紧,也不能深刻搞架构,因而打算就用jQuery+handlebars,jQuery来完成页面逻辑和DOM操做,用handlebars来完成页面渲染,这个方案是如此的简单粗暴,但好处能最稳妥的保证项目定期完成。其实先后端分离并非一件容易的工做,这么作会有诸多不完善之处,后面再谈。html
所谓的先后端分离,究竟是分离什么呢?其实就是页面的渲染工做,以前是后端渲染好页面,交给前端来显示,分离后前端须要本身拼装html代码,而后再显示。前端来管理页面的渲染有不少好处,好比减小网络请求量,制做单页面应用等。事情听起来简单,但这么一分离又会牵扯到不少问题,好比:前端
以上每个问题都够棘手,要处理好须要有设计精良又符合实际项目的方案。如今已经有不少框架能够帮咱们作这些事情,Backbone, EmberJS, KnockoutJS, AngularJS, React, avalon等等,利用它们能够架构起一个富前端。但框架毕竟是框架,要利用到实际项目中,仍是须要有本身的设计,框架并不能解决全部的问题。java
以前也有看过淘宝团队的实践,利用nodejs作一个中间层,处理页面渲染、路由控制、SEO等事情,将先后端的分界线进行了从新定义。我的感受这应该是一个正确的方向,有点颠覆的感受,前端走向工程化,将变成真正的全栈式大前端。不知如今这种架构是否在淘宝全面铺开,真有点期待看看效果。node
以上的框架,还有淘宝的实践,毕竟都是大牛之做,我这个小辈也只是参考学习过,未能在实际项目中使用。低头看看本身如今手头的项目,1个前端,2周时间,要完成一个完整的web项目,仍是用最稳妥最低级的方式来搞吧~git
项目总体并非一个单页应用,但有些模块须要作成局部的单页操做,像这种须要分步完成的操做,只需局部加载子页面便可。github
所以,一个模块有一个主html页面,初始只有一些基本的骨架,有一个名字相同的js文件,该模块逻辑都在此js文件中,有一个名字相同的css文件,该模块的全部样式都定义在此css文件中。web
须要异步加载的子页面,像上图中每一个步骤的页面,我都使用jQuery的$.load()方法来加载,此方法能在页面某个容器中加载内容,并可指定回调函数,使用起来很方便。被异步加载的子页面我都用_开头,如_step1.html,用于作区分。ajax
为了确保浏览器的前进后退按钮可用,我使用了hash来作路由标记,页面地址如:publish.html#step2。有个缺陷是hash并不会发送给服务器,因此SEO就废了。事实上使用history API也能够更优雅的解决问题,但须要考虑兼容性,还有额外工做要作,考虑时间因素,退而求其次,何况本项目也无需作SEO。或者像淘宝的方案那样,nodejs层与浏览器层统一路由,SEO问题能够迎刃而解。但又明显不在本人的实力范围以内,汗–!json
除了用$.load异步加载的子页面,剩余的局部页面就是用handlebars提供的模板渲染了,我使用了handlebars的预编译功能,不得不说很强大,一来节约了页面加载阶段所需的编译时间(编译handlebars模板),二来编译后的模板(js文件)方便复用。
接下来就是前端逻辑如何组织,由于没有用mv*框架,因此只能靠本身来写一个便于开发的结构。如上面所述,每一个模块有一个主js文件,文件内容结构以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
var publish = {
//该模块初始化入口
init : function(){
this.renderData(param);
this.initListeners();
},
//内部所用的函数
renderData : function(param){
//渲染数据。。
},
//统一绑定监听器
initListeners : function(){
$(document.body).delegates({
'.btn' : function(){
//点击事件
},
'.btn2' : function(){
//点击事件2
},
'.checkbox' : {
'change' : function(){
//change事件
}
}
});
}
}
|
每一个模块给一个命名空间,全部的方法都挂在上面,js文件中只作函数的定义,不当即执行任何东西,而后在html文件中调用入口方法:publish.init()。业务逻辑都封装到函数中,如上面的renderData,而后供其余地方调用。页面的事件监听器统一都注册在body元素上,用事件代理来完成,为了不写太多的on、click之类代码,为jQuery扩展了一个delegates方法,用来以配置的方式统一绑定监听器,用法如上所示。把delegates定义的代码也放出来吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//以配置的方式代理事件
$.fn.delegates = function(configs) {
el = $(this[0]);
for (var name in configs) {
var value = configs[name];
if (typeof value == 'function') {
var obj = {};
obj.click = value;
value = obj;
};
for (var type in value) {
el.delegate(name, type, value[type]);
}
}
return this;
}
|
基本的结构就是这样,没有什么新技术,只是把现有的东西作了一下组合。但工做到此还远远没有结束,在实际应用中还会有一些东西须要处理,下面来详细说说:
这是一个比较棘手的问题,通常通用的头部和底部会放一些公共的代码,如页面外层结构html代码,站点使用的库如jQuery、handlebars,站点通用js和css文件。在传统的开发中,一般是写一个单独的文件如head.html,在其余页面中用后端代码如include语句引入,由此来进行复用。
如今先后端分离后,没法依靠后端来给你渲染,因此得在前端作了。既然用了handlebars,很容易想到把公用部分写成一个模板,而后预编译出来,生成一个header.js文件,而后在其余页面引用。然而在实际操做中发现了一个问题,handlebars是静态模板,编译后生成的字符串经过innerHTML的方式插入到页面,在通常的模板中这样是没问题的。如今有个问题是header中有一些<script>标签,外链着要使用的库,经过innerHTML插入<scirpt>标签,浏览器并不会发送请求加载对应的js文件,因此就出问题了。
搜索、尝试了多种方法后,最终的方案定为:用document.write()将编译结果写到页面,这样<script>标签可以正常加载。因此每一个页面使用头部的代码就变成这样:
1
2
3
4
|
<script src="static/js/tpl/head.js"></script>
<div id="header">
<script src="static/js/includeHead.js"></script>
</div>
|
includeHead.js中的代码以下:
1
2
3
4
5
6
7
|
function includeHead(){
var header = document.getElementById('header');
var compileHead = Handlebars.templates['head'];
var head = compileHead({});
document.write(head);
}
includeHead();
|
看着是有点别扭,不过为了实现功能,目前也就只能这样了。
如上面所述,jQuery的$.load()方法能够知足加载子页面的需求,如今须要解决的问题是,无论用户刷新页面仍是前进后退,咱们都得根据hash值来渲染对应的视图,其实就是路由控制。这个时候就须要监听hashchange事件了,我定义了一个loadPage方法用来加载子页面,而后绑定监听器以下:
1
|
window.onhashchange = this.loadPage;
|
在loadPage方法中,根据hash的值来调用$.load()方法,子页面的初始化工做,在$.load()的回调函数中指定。
这样作还有一个便捷之处,咱们切换视图没必要手动调loadPage方法,只须要修改页面的hash就能够了,hash发生变化被监听到,自动加载对应的子页面。例如,点击下一步进入步骤二:
1
2
3
|
'.next' : function(){
location.href = '#step2';
}
|
如此便实现了一个简单的路由控制,因为不是整站单页面,也没有多级路由,这样彻底能够知足需求。至于SEO,就只能呵呵了,正好项目也不须要作SEO,不然此方法得做罢。
另外想说的一点就是页面的缓存,异步加载来的内容能够存在localStorage中,也能够放在页面上进行显隐控制,这样用户在频繁切换视图的时候无需再次请求,回到上一步的时候以前填好的表单数据也不会消失,体验会很是好。
有时候咱们须要给访问的页面传参数,好比访问一个设备的详细信息页,要把设备id给传过去,detail.html?id=1,这样detail页面能够根据id去请求对应的数据。传统由后端渲染的页面量子管通环,url中的参数会发送到服务端,服务端接收后能够再渲染到页面上供js使用。咱们如今不行了,请求页面压根不跟后端打交道,但这个参数是必不可少的,因此须要前端有一套传递参数的机制。
其实很是简单,经过location.href能够拿到当前的url地址,而后进行字符串匹配,把参数提取出来就能够了。看上去挺土鳖的,但工做起来良好,另外也有考虑过用cookie来传递,感受有点麻烦。
因为这些参数一般是写在<a>标签上的,而<a>标签又是根据动态数据渲染出来的(由于是动态参数),咱们不可能在页面渲染完后,用js修改全部<a>标签的href值,给它追加一个参数。怎么办呢?这时候handlebars就派上用场了,咱们可使用handlebars万能的helper,在渲染页面的时候直接查询url中的参数,而后输出在编译好的代码中。我在handlebars中注册了一个helper,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Handlebars.registerHelper('param', function(key, options){
var url = location.href.replace(/^[^?=]*\?/ig, '').split('#')[0];
var json = {};
url.replace(/(^|&)([^&=]+)=([^&]*)/g, function (a, b, key , value){
try {
key = decodeURIComponent(key);
} catch(e) {}
try {
value = decodeURIComponent(value);
} catch(e) {}
if (!(key in json)) {
json[key] = /\[\]$/.test(key) ? [value] : value;
}
else if (json[key] instanceof Array) {
json[key].push(value);
}
else {
json[key] = [json[key], value];
}
});
return key ? json[key] : json;
});
|
这个名为param的helper能够输出你所要查询的参数值,而后能够直接写在模板中,如:
1
|
<a href="detail.html?id={{param id}}">设备详细信息</a>
|
这样就方便多了!可是这么作有没有问题呢?实际上是有些不完美的,若是你考虑“性能”二字的话。一个url中参数的值是固定的,而你每次使用这个helper都会计算一遍,白白作了多余的事情。若是handlebars能够在模板中定义常量就行了,惋惜我找遍文档没发现有这个功能。只能为了方便牺牲性能了,也正印证了我标题中所说的“简单粗暴”,呵呵。
因为数据是由后端传来的,有不少不肯定性,数据可能不合法,或者结构有错,或者直接是空的。所以前端有必要对数据作一个合法性的校验。借助handlebars,能够很方便的进行数据校验。没错,就是利用helper。handlebars内置的helper如if、each都支持else语句,出错信息能够在else中输出。若是须要个性化的校验,咱们能够本身定义helper来完成,关于如何自定义helper,我以前研究了下,写过一篇文章:http://www.cnblogs.com/lvdabao/p/handlebars_helper.html。总之自定义helper很强大,能够完成你所需的任何逻辑。
数据的格式化,如日期、数字等,也能够经过helper来完成。
另一方面,前端还应对数据进行html转义,避免xss,因为handlebars已经给作了html转义,因此咱们能够直接忽略此项了。
本文是我刚刚参加完一个项目后所写,记录一下整个过程遇到的问题及处理方式,其余的一些细碎点如表单异步提交什么的,不是本文重点,不写了。这是我第一次实践先后端彻底分离的项目,整个前端全由我来设计、开发。2周时间,凭着这套方案,项目定期开发完成,并且还提早完成了,预留出一天多的时间测试了一遍。
虽然开发任务是完成了,可是回头看一下整个方案,并非很优雅也没有什么技术含量,文章开头提到的几个问题都没有解决。因此命题为简单粗暴的方案,都是为了赶工期啊。
最后,若是给我再来一次的机会,而且时间充足,我必定要尝试用mv*方案来搞一下,或angular,或avalon。