项目背景
刚刚参与完一个项目,背景:后台是用java,后台服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构。后台专注做业务逻辑,不想在后台做页面渲染的事情,只向前台提供数据接口。于是协商后打算将前后台完全分离,页面上的所有数据都通过ajax向后台取,页面渲染的事情完全由前台来做。另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后台完全分离的尝试。
之前开发都是同步渲染和异步渲染混搭的,有些东西可以有后台PHP帮你编译好,如通用的页面模板,后台传回的页面参数等。提前预感到这次完全分离可能会遇到少量困难,但是项目上线要紧,也不能深入搞架构,于是打算就用jQuery+handlebars,jQuery来完成页面逻辑和DOM操作,用handlebars来完成页面渲染,这个方案是如此的简单粗暴,但好处能最稳妥的保证项目按期完成。其实前后台分离并不是一件容易的工作,这么做会有诸多不完善之处,后面再谈。
浅谈前后台分离
所谓的前后台分离,究竟是分离什么呢?其实就是页面的渲染工作,之前是后台渲染好页面,交给前台来显示,分离后前台需要自己拼装html代码,而后再显示。前台来管理页面的渲染有很多好处,比方减少网络请求量,制作单页面应用等。事情听起来简单,但这么一分离又会牵扯到很多问题,比方:
以上每一个问题都够辣手,要解决好需要有设计精良又符合实际项目的方案。现在已经有很多框架可以帮我们做这些事情,Backbone, EmberJS, KnockoutJS, AngularJS, React, avalon等等,利用它们可以架构起一个富前台。但框架毕竟是框架,要利用到实际项目中,还是需要有自己的设计,框架并不能处理所有的问题。
之前也有看过淘宝团队的实践,利用nodejs做一个中间层,解决页面渲染、路由控制、SEO等事情,将前后台的分界线进行了重新定义。个人感觉这应该是一个正确的方向,有点颠覆的感觉,前台走向工程化,将变成真正的全栈式大前台。不知现在这种架构能否在淘宝全面铺开,真有点期待看看效果。
以上的框架,还有淘宝的实践,毕竟都是大牛之作,我这个小辈也只是参考学习过,未能在实际项目中使用。低头看看自己现在手头的项目,1个前台,2周时间,要完成一个完整的web项目,还是用最稳妥最低级的方式来搞吧~
基本结构
项目整体并不是一个单页应用,但有些板块需要做成局部的单页操作,像这种需要分步完成的操作,只要局部加载子页面就可。
因而,一个板块有一个主html页面,初始只有少量基本的骨架,有一个名字相同的js文件,该板块逻辑都在此js文件中,有一个名字相同的css文件,该板块的所有样式都定义在此css文件中。
需要异步加载的子页面,像上图中每个步骤的页面,我都使用jQuery的$.load()方法来加载,此方法能在页面某个容器中加载内容,并可指定回调函数,使用起来很方便。被异步加载的子页面我都用_开头,如_step1.html,用于做区分。
为了确保浏览器的前进后退按钮可用,我使用了hash来做路由标记,页面地址如:publish.html#step2。有个缺陷是hash并不会发送给服务器,所以SEO就废了。事实上使用history API也可以更优雅的处理问题,但需要考虑兼容性,还有额外工作要做,考虑时间因素,退而求其次,况且本项目也无需做SEO。或者者像淘宝的方案那样,nodejs层与浏览器层统一路由,SEO问题可以迎刃而解。但又显著不在本人的实力范围之内,汗--!
除了用$.load异步加载的子页面,剩余的局部页面就是用handlebars提供的模板渲染了,我使用了handlebars的预编译功能,不得不说很强大,一来节约了页面加载阶段所需的编译时间(编译handlebars模板),二来编译后的模板(js文件)方便复用。
接下来就是前台逻辑如何组织,由于没有用mv*框架,所以只能靠自己来写一个便于开发的结构。如上面所述,每个板块有一个主js文件,文件内容结构如下:
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定义的代码也放出来吧:
//以配置的方式代理商事件
$.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中有少量
includeHead.js中的代码如下:
function includeHead(){
var header = document.getElementById('header');
var compileHead = Handlebars.templates['head'];
var head = compileHead({});
document.write(head);
}
includeHead();
看着是有点别扭,不过为了实现功能,目前也就只能这样了。
尽管用原生的innerHTML无法加载