当前位置: 首页 > 科技观察

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

时间:2023-03-12 18:04:32 科技观察

项目背景刚刚参与了一个项目,背景:后台用的是java,后台服务已经开发的差不多了,现在开始服务了通过web对外提供,即B/S-frame。后端专注于业务逻辑,不想在后端做页面渲染,只为前端提供数据接口。所以协商后,打算前后端完全分离,页面上所有数据通过ajax从后端获取,页面渲染完全由前端完成。此外,还有一个紧急情况。该项目需要紧急启动。整个网站的开发时间只有两周,两周!所以在这样的背景下,我决定开始前后端完全分离的尝试。之前的开发是同步渲染和异步渲染的混合。有些东西后台PHP可以给你编译好,比如通用的页面模板,后台返回的页面参数等等。我提前预感到这个完全分离可能会遇到一些困难,但是项目上线了,并且无法深入架构,所以打算使用jQuery+handlebars,jQuery完成页面逻辑和DOM操作,使用handlebars完成页面渲染。计划如此简单粗暴,但效益最能保证项目按期完工。其实,前后端分离并不是一件容易的事,这样做会有很多不完善的地方,后面再说。浅谈前后端分离所谓前后端分离,究竟是什么分离呢?其实就是页面的渲染。之前,后端渲染页面,交给前端展示。分离之后,前端需要自己组装html代码,然后显示出来。前端管理页面渲染有很多好处,比如减少网络请求量,制作单页应用等。事情听起来很简单,但是这样的分离会牵扯到很多问题,比如:资源的按需加载。特别是在单页应用程序中。页面呈现逻辑。分离使得前端逻辑急剧增加,需要好的前端架构,比如MVC模型。数据验证。因为页面数据都是从后台请求的,所以需要验证显示的数据是否合法,避免xss或者其他安全问题。简短的白色屏幕。因为页面没有同步渲染,请求的数据还没完成页面就一片空白,体验很差。代码重用。众多的模板和逻辑模块需要很好地组织起来以实现可重用性。路由控制。无刷新的前端体验也破坏了浏览器的后退按钮,前端视图需要路由机制。搜索引擎优化。服务器不再返回页面,前端根据不同的逻辑呈现不同的视图(不是页面)。对搜索引擎友好需要做很多额外的工作。上述每个问题都非常棘手,需要处理适合实际项目的精心设计的解决方案。已经有很多框架可以帮我们做这些事情,Backbone、EmberJS、KnockoutJS、AngularJS、React、avalon等等,用它们来构建丰富的前端。但是框架毕竟是框架,要在实际项目中使用,还是需要有自己的设计,框架并不能解决所有问题。之前也看过淘宝团队的做法,用nodejs作为中间层处理页面渲染、路由控制、SEO等事情,重新定义了前后端的分界线。我个人觉得这应该是正确的方向。感觉有点颠覆。前端正在走向工程化,将成为真正的全栈大前端。不知道现在淘宝有没有全面铺开这种结构,很期待看到效果。以上这些框架,还有淘宝的做法,毕竟都是神作。本人大三,只是学习参考,没能在实际项目中使用。低头看我现在的项目,一个前端,两个星期,完成一个完整的web项目,还是用最安全最先进的方式来吧~项目整体的基本结构不是单页的application,但是有些模块需要做成部分单页操作。对于需要逐步完成的操作,只需要部分加载子页面即可。因此,一个模块有一个html主页面,最初只有一些基本的骨架,一个同名的js文件,模块逻辑都在这个js文件里,还有一个同名的css文件,定义了模块的所有样式在这个CSS文件中。对于需要异步加载的子页面,比如上图中每一步的页面,我都是使用jQuery的$.load()方法来加载的。该方法可以加载页面某个容器中的内容,可以指定回调函数。使用起来很容易。对于异步加载的子页面,如_step1.html,我以_开头,以区分它们。为了保证浏览器的前进后退按钮可用,我使用hash做路由标签,页面地址如:publish.html#step2。一个缺陷是哈希不会发送到服务器,因此SEO将无用。其实使用historyAPI也可以更优雅的解决问题,但是需要考虑兼容性,还有额外的工作要做,时间因素也考虑在内,退而求其次,本项目没有需要做SEO。或者像淘宝的方案,nodejs层和浏览器层在路由上统一,SEO问题就可以轻松解决。不过显然不在自己的实力范围之内,汗——!除了用$.load异步加载的子页面外,其余部分页面使用handlebars提供的模板渲染。我用的是handlebars的预编译功能,很强大。首先,它节省了页面加载阶段所需的编译时间(编译handlebars模板),其次,编译后的模板(js文件)易于复用。接下来就是如何组织前端逻辑了,因为没有用到mv*框架,所以只能自己写一个容易开发的结构。上面说过,每个模块都有一个主js文件,文件内容结构如下:varpublish={//模块初始化入口init:function(){this.renderData(param);this.initListeners();},//内部使用的函数renderData:function(param){//渲染数据。.},//统一绑定监听器initListeners:function(){$(document.body).delegates({'.btn':function(){//点击事件},'.btn2':function(){//点击event2},'.checkbox':{'change':function(){//changeevent}}});}}给每个模块一个命名空间,所有的方法都挂在上面,在js文件中定义即可函数,先不执行任何东西,然后调用html文件中的入口方法:publish.init()。业务逻辑封装成函数,比如上面的renderData,然后从其他地方调用。页面的事件监听器都注册在body元素上,通过事件代理来完成。为了避免on、click等代码写太多,jQuery扩展了一个delegates方法,用于统一方式绑定和监听设备,如上图。让我们也发布委托定义的代码://Delegateeventsinaconfiguredway$.fn.delegates=function(configs){el=$(this[0]);for(varnameinconfigs){varvalue=configs[name];if(typeofvalue=='function'){varobj={};obj.click=value;value=obj;};for(vartypeinvalue){el.delegate(name,type,value[type]);}}returnthis;}基本结构是这样的,没有新的技术,只是现有东西的组合。但工作远未结束。在实际应用中还有一些事情需要处理。下面详细说说:公共头底部的引用是一个棘手的问题。一般一般的header和bottom都会放一些公共的代码,比如页面外部结构的html代码,站点使用的jQuery,handlebars等库,还有站点通用的js和css文件。传统开发中,通常是单独写一个head.html等文件,在其他页面使用include语句等后端代码导入,实现复用。现在前后端分离了,不能靠后端给你渲染,只能在前端做。既然用了handlebars,很容易想到把公共部分写成模板,然后预编译,生成header.js文件,然后在其他页面引用。但是在实际操作中发现了一个问题。handlebars是静态模板,编译后生成的字符串通过innerHTML插入到页面中。这在一般模板中是没有问题的。现在有个问题就是header里面有一些标签就可以正常加载了。于是使用每个页面头部的代码就变成了这样:includeHead.js">

includeHead.js中的代码如下:({});document.write(head);}includeHead();看起来有点别扭,但是为了实现功能,只能这样了。虽然标签并执行里面的代码,所以使用$().html()可以完成以上工作。这样,这个蹩脚的解决方案就可以被替换掉了。  路由控件上面提到jQuery的$.load()方法可以满足加载子页面的需求。现在需要解决的问题是,无论用户刷新页面还是前进后退,我们都要根据hash值渲染对应的页面。视图实际上是一个路由控件。这时候就需要监听hashchange事件了。我定义了一个加载子页面的loadPage方法,然后绑定监听如下:window.onhashchange=this.loadPage;在loadPage方法中,根据hash()方法的值调用$.load,子页面的初始化工作在$.load()的回调函数中指定。这样做也有一个方便之处。我们不需要手动调整loadPage方法来切换视图。我们只需要修改页面的哈希即可。监控哈希的变化并自动加载相应的子页面。比如点击Next进入第二步:'.next':function(){location.href='#step2';}这样实现了一个简单的路由控制,因为它不是整个站点的一个页面,还有没有多级路由,完全可以满足需求。至于SEO,只能呵呵了,正好项目不需要做SEO,不然这个方法只好放弃了。还有一个要说的就是页面的缓存。异步加载的内容可以存放在localStorage中,也可以放在页面上进行可见隐藏控制,这样用户在频繁切换视图时不需要再次请求,直接回到上一步。填好的表单数据不会消失,体验会很好。页面间传递参数有时候我们需要给访问的页面传递参数,比如访问一个设备的详细信息页面,需要传递设备id,detail.html?id=1,这样详情页就可以根据id数据请求对应的。传统的后端渲染的页面,会把url中的参数发给服务器,服务器收到后渲染到页面上供js使用。我们现在做不到,请求页面根本不和后端打交道,但是这个参数是必不可少的,所以前端需要有传递参数的机制。其实很简单。可以通过location.href获取当前的url地址,然后进行字符串匹配提取参数。它看起来很简单,但效果很好。另外,我考虑过用cookies来传递,感觉有点麻烦。由于这些参数通常写在标签上,而标签是根据动态数据渲染的(因为是动态参数),我们不可能在页面之后用js修改所有的被渲染。标签的href值,给它加一个参数。怎么做?这时候,车把就派上用场了。我们可以使用handlebars***的helper在渲染页面的时候直接查询url中的参数,然后在编译代码中输出。我在把手中注册了一个助手,如下所示:Handlebars.registerHelper('param',function(key,options){varurl=location.href.replace(/^[^?=]*\?/ig,'').split('#')[0];varjson={};url.replace(/(^|&)([^&=]+)=([^&]*)/g,function(a,b,key,value){try{key=decodeURIComponent(key);}catch(e){}try{value=decodeURIComponent(value);}catch(e){}if(!(keyinjson)){json[key]=/\[\]$/.test(key)?[value]:value;}elseif(json[key]instanceofArray){json[key].push(value);}else{json[key]=[json[key],value];}});returnkey?json[key]:json;});这个叫param的helper可以输出你要查询的参数值,然后你可以直接写在模板里,比如:设备详情这样就方便多了!但是这样做有什么问题吗?其实有点不公平,如果考虑到“性能”这个词的话。一个url中一个参数的值是固定的,每次用这个helper都会重新计算,白白做了多余的事情。如果handlebars可以在模板中定义常量就好了,可惜我查了文档,没有找到这个功能。为了方便我只能牺牲性能,这也印证了我标题中所说的“简单粗暴”,哈哈。数据校验和处理因为数据是从后台传输过来的,所以有很多不确定性。数据可能是非法的,也可能是结构错误,也可能是空的。因此,前端需要对数据的合法性进行校验。借助车把,可以轻松进行数据验证。没错,使用helper。handlebars内置的helpers,如if和each,支持else语句,错误信息可以在else中输出。如果我们需要个性化验证,可以定义helper来完成。之前研究过如何自定义helper,写过一篇文章:http://www.cnblogs.com/lvdabao/p/handlebars_helper。HTML。总之,自定义助手非常强大,可以完成你需要的任何逻辑。日期、数字等数据的格式化也可以通过助手来完成。另一方面,前端也要对数据进行html转义,避免xss。由于handlebars已经做了html转义,我们可以直接忽略这一项。总结这篇文章是我刚刚参与一个项目后写的。我会记录下整个过程中遇到的问题以及如何处理。其他的异步提交表单等细节不是本文的重点,这里就不写了。这是我第一次实践前后端完全分离的项目。整个前端都是我设计开发的。2周时间,有了这套方案,项目开发如期进行,而且提前完成,还预留了一天多的时间进行测试。虽然开发任务完成了,但是回过头来看整个方案,不是很优雅,也没有什么技术含量。文章开头提到的几个问题都没有解决。所以最简单粗暴的计划就是赶工期。***,如果再有机会,我有足够的时间,我一定会尝试使用mv*方案,或者angular,或者avalon。