一个简单粗暴的前后端分离方案

项目背景

刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构。后端专注做业务逻辑,不想在后端做页面渲染的事情,只向前端提供数据接口。于是协商后打算将前后端完全分离,页面上的所有数据都通过ajax向后端取,页面渲染的事情完全由前端来做。另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后端完全分离的尝试。

之前开发都是同步渲染和异步渲染混搭的,有些东西可以有后端PHP帮你编译好,如通用的页面模板,后端传回的页面参数等。提前预感到这次完全分离可能会遇到一些困难,但是项目上线要紧,也不能深入搞架构,于是打算就用jQuery+handlebars,jQuery来完成页面逻辑和DOM操作,用handlebars来完成页面渲染,这个方案是如此的简单粗暴,但好处能最稳妥的保证项目按期完成。其实前后端分离并不是一件容易的工作,这么做会有诸多不完善之处,后面再谈。

浅谈前后端分离

所谓的前后端分离,到底是分离什么呢?其实就是页面的渲染工作,之前是后端渲染好页面,交给前端来显示,分离后前端需要自己拼装html代码,然后再显示。前端来管理页面的渲染有很多好处,比如减少网络请求量,制作单页面应用等。事情听起来简单,但这么一分离又会牵扯到很多问题,比如:

  • 资源的按需加载。尤其是在单页应用中。
  • 页面展现逻辑。分离让前端的逻辑陡增,需要有一个良好的前端架构,如mvc模式。
  • 数据校验。因为页面数据都是从后端请求来的,必须校验要展示的数据是否合法,避免xss或其他安全问题。
  • 短暂白屏。因为页面不是同步渲染的,在请求数据完毕之前,页面是白屏的,体验很不好。
  • 代码的复用。众多的模板、逻辑模块需要良好组织实现可复用。
  • 路由控制。无刷新的前端体验同时毁掉了浏览器的后退按钮,前端视图需要有一套路由机制。
  • SEO。服务端不再返回页面,前端根据不同的逻辑呈现不同的视图(并非页面),要对搜索引擎友好需要做很多额外的工作。

以上每一个问题都够棘手,要处理好需要有设计精良又符合实际项目的方案。现在已经有很多框架可以帮我们做这些事情,Backbone, EmberJS, KnockoutJS, AngularJS, React, avalon等等,利用它们可以架构起一个富前端。但框架毕竟是框架,要利用到实际项目中,还是需要有自己的设计,框架并不能解决所有的问题。

之前也有看过淘宝团队的实践,利用nodejs做一个中间层,处理页面渲染、路由控制、SEO等事情,将前后端的分界线进行了重新定义。个人感觉这应该是一个正确的方向,有点颠覆的感觉,前端走向工程化,将变成真正的全栈式大前端。不知现在这种架构是否在淘宝全面铺开,真有点期待看看效果。

以上的框架,还有淘宝的实践,毕竟都是大牛之作,我这个小辈也只是参考学习过,未能在实际项目中使用。低头看看自己现在手头的项目,1个前端,2周时间,要完成一个完整的web项目,还是用最稳妥最低级的方式来搞吧~

基本结构

项目整体并不是一个单页应用,但有些模块需要做成局部的单页操作,像这种需要分步完成的操作,只需局部加载子页面即可。

  

因此,一个模块有一个主html页面,初始只有一些基本的骨架,有一个名字相同的js文件,该模块逻辑都在此js文件中,有一个名字相同的css文件,该模块的所有样式都定义在此js文件中。

需要异步加载的子页面,像上图中每个步骤的页面,我都使用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中有一些<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();

  看着是有点别扭,不过为了实现功能,目前也就只能这样了。

路由控制

如上面所述,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。

时间: 2024-10-05 23:58:27

一个简单粗暴的前后端分离方案的相关文章

一个简单粗暴的前后端分离方案(转)

项目背景 刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构.后端专注做业务逻辑,不想在后端做页面渲染的事情,只向前端提供数据接口.于是协商后打算将前后端完全分离,页面上的所有数据都通过ajax向后端取,页面渲染的事情完全由前端来做.另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后端完全分离的尝试. 之前开发都是同步渲染和异步渲染混搭的,有些东西可以有后

简单粗暴的前后端分离方案

刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构.后端专注做业务逻辑,不想在后端做页面渲染的事情,只向前端提供数据接口.于是协商后打算将前后端完全分离,页面上的所有数据都通过ajax向后端取,页面渲染的事情完全由前端来做.另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后端完全分离的尝试. 之前开发都是同步渲染和异步渲染混搭的,有些东西可以有后端PHP帮

一个前后端分离方案[转载]

项目背景 背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构.后端专注做业务逻辑,不想在后端做页面渲染的事情,只向前端提供数据接口.于是协商后打算将前后端完全分离,页面上的所有数据都通过ajax向后端取,页面渲染的事情完全由前端来做.另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后端完全分离的尝试. 之前开发都是同步渲染和异步渲染混搭的,有些东西可以有后端PHP帮你编译好,

前后端分离 方案

1.前后端分离开发模式的 mock 平台预研   :   https://www.jianshu.com/p/4648463be0f5 2.前后分离接口规范   :   https://www.jianshu.com/p/c81008b68350 原文地址:https://www.cnblogs.com/wfblog/p/9612992.html

利用gulp解决前后端分离的header/footer引入问题

在我们进行前后端完全分离的时候,有一个问题一直是挺头疼的,那就是公共header和footer的引入.在传统利用后端渲染的情况下,我们可以把header.footer写成两个单独的模板,然后用后端语言的include即可在其他页面中引入.我之前在<一个简单粗暴的前后端分离方案>这篇文章中说过一种方法,就是用handlebars把header.footer模板预编译为js文件,然后在页面的头部用document.write写到页面中.这种方式的弊端也比较明显,那就是依赖一个模板引擎.在使用mvv

关于大型网站技术演进的思考(十七)--网站静态化处理—满足静态化的前后端分离(9)

前后端分离的主题虽然讲完了,但是前后端分离的内容并没有结束,本篇将继续前后端分离的问题,只不过这次前后端分离的讲述将会围绕着本系列的主题网站静态化进行.在讲本篇主题之前,我需要纠正一下前后端分离主题讲述中会让朋友们产生误导的地方,这种误导就是对时下流行的一些前后端分离方案(没有使用nodejs的前后端分离方案)的评价问题,其实本人任然觉得不管什么样的前后端分离方案只要成功被实施,并且产生了良好的效果,那么它就是一个成功的前后端分离方案,前面我以一种批判的角度讲述这些前后端分离方案,并不是想在否定

关于大型网站技术演进的思考(十四)--网站静态化处理—前后端分离—上(6)

前文讲到了CSI技术,这就说明网站静态化技术的讲述已经推进到了浏览器端了即真正到了web前端的范畴了,而时下web前端技术的前沿之一就是前后端分离技术了,那么在这里网站静态化技术和前后端分离技术产生了交集,所以今天我将讨论下前后端分离技术,前后端分离技术讨论完后,下一篇文章我将会以网站静态化技术的角度回过头来重新审视下前后端分离技术,希望通过这种审视来加深我们对两套技术的理解. 前后端分离技术我个人认为是web前端被专业化以后的必由之路,而nodejs的出现是前后端分离技术的一个强兴的催化剂,原

移动端开发者眼中的前端开发流程变迁与前后端分离

写在最开始 移动端与前端的区别 前端开发的混沌时代 后端 MVC MVC 方案实现 MVC 的缺点与改进 前端只写 Demo HTML 模板 后端 MVC 架构总结 AJAX 与前端 MVC 前后端分离的缺点 双端 MVC 不统一 SEO 性能不够 集中 Or 分离 Nodejs 前后端分离的哲学 Nodejs 分层 实战应用 风险控制 总结 参考资料 写在最开始 这是一篇面向移动端开发者的科普性文章,从前端开发的最初流程开始,结合示范代码,讨论开发流程的演变过程,希望能覆盖一部分前端开发技术栈

前后端分离和模块化

前后端分离和模块化-58到家微信首页重构之路 微信钱包内的58到家全新首页已经上线,感兴趣的同学们可以在微信中打开“我的->钱包->58到家”查看. 58到家全新首页提出重构主要是为了解决以下问题: 每个城市开通的服务项目不同,有些内容是写死在tpl中,维护非常头疼: 开通新服务或者某些UI调整(比如更换服务项的图片造成更改雪碧图)时必须走代码上线流程: 原有的前端切图.后端写逻辑的开发模式造成开发周期拉长和上线流程繁琐: 原有配置后台操作复杂,且可配置细节不完善: 首页加载速度太慢,用户体验