项目背景 刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构。后端专注做业务逻辑,不想在后端做页面渲染
- 资源的按需加载。尤其是在单页应用中。
- 页面展现逻辑。分离让前端的逻辑陡增,需要有一个良好的前端架构,如mvc模式。
- 数据校验。因为页面数据都是从后端请求来的,必须校验要展示的数据是否合法,避免xss或其他安全问题。
- 短暂白屏。因为页面不是同步渲染的,在请求数据完毕之前,页面是白屏的,体验很不好。
- 代码的复用。众多的模板、逻辑模块需要良好组织实现可复用。
- 路由控制。无刷新的前端体验同时毁掉了浏览器的后退按钮,前端视图需要有一套路由机制。
- SEO。服务端不再返回页面,前端根据不同的逻辑呈现不同的视图(并非页面),要对搜索引擎友好需要做很多额外的工作。
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中有一些<script>标签,外链着要使用的库,通过innerHTML插入<scirpt>标签,浏览器并不会发送请求加载对应的js文件,所以就出问题了。 搜索、尝试了多种方法后,最终的方案定为:用document.write()将编译结果写到页面,这样<script>标签能够正常加载。所以每个页面使用头部的代码就变成这样:
<script src="static/js/tpl/head.js"></script> <div id="header"> <script src="static/js/includeHead.js"></script> </div>includeHead.js中的代码如下:
function includeHead(){ var header = document.getElementById('header'); var compileHead = Handlebars.templates['head']; var head = compileHead({}); document.write(head); } includeHead();看着是有点别扭,不过为了实现功能,目前也就只能这样了。 ----------补充于 2015.1.27--------------- 虽然用原生的innerHTML无法加载<script>标签中的内容,但是jQuery的$().html()方法进行了优化,可以查找到<script>标签并且执行里面的代码,所以用$().html()是可以完成上面的工作的。 这么一看,这个蹩脚的方案就可以替换了。 路由控制 如上面所述,jQuery的$.load()方法可以满足加载子页面的需求,现在需要解决的问题是,不管用户刷新页面还是前进后退,我们都得根据hash值来渲染对应的视图,其实就是路由控制。这个时候就需要监听hashchange事件了,我定义了一个loadPage方法用来加载子页面,然后绑定监听器如下:
window.onhashchange = this.loadPage;
在loadPage方法中,根据hash的值来调用$.load()方法,子页面的初始化工作,在$.load()的回调函数中指定。
这样做还有一个便捷之处,我们切换视图不必手动调loadPage方法,只需要修改页面的hash就可以了,hash发生变化被监听到,自动加载对应的子页面。例如,点击下一步进入步骤二:
'.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,如下:
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可以输出你所要查询的参数值,然后可以直接写在模板中,如:
<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。