前后端分离之SPA与跨域访问

引言

最近在开发一个以医院业务为背景的Web项目,前端采用React+Ant Design,后端采用SSM(Spring-SpringMVC-MyBatis)框架。由于就我和另外一名同学开发,我负责前端,他负责后端,两者分开独立完成,部署均在各自电脑的IP:Port上,所以在调试阶段如果想要追求开发效率,就必须攻克前后端工程分离所带来的两个问题:跨域访问、状态保存。然而前后端分离的这种架构,其实不仅是在调试阶段带来了便利,在后期部署做负载均衡等方面也颇具优势。而随之诞生的SPA(Single Page Application)即单页面应用正好适合这种分离架构的需要。下面我们详细探讨下。

前后端分离

传统的前后端混合的形式,是把页面、图片、样式、js这些属于前端的静态文件,以及src目录下编译好的.class、xml这些属于后端的文件统一放在webapp下,然后启动Web服务器即可在同一IP:Port下访问页面与api服务,如下图所示。

backfront2

这种方式将静态与动态文件混合,经常是前端要进行页面跳转了,请求后端给返回所需的页面,比如返回JSP页面,而JSP实质是一个Servlet,在它返回前端之前,实质是先将获取到的数据渲染在了html页面上,最后再一并返回前端的。而事实上,JSP所做的渲染工作更适合前端来负责,后端只负责提供接口、处理逻辑、存取数据、返回所需JSON数据,即只应涉及到MVC模式中的Modal与Controller。

而前后端分离模式正好相反,它将前端的所有静态文件与后端的所有动态文件分别置于不同的IP或不同的Port(只要Port不同就会造成跨域)下,前端若要访问后端的服务,只要指定后端的IP:Port与api的URL即可访问到,职责很明确,如下图所示。

backfront1

前后端分离带来的好处是明显的:

  • 服务器压力减小到最小,所有数据到页面的渲染工作可由前端js完成,摆脱业务逻辑与呈现逻辑在Java模版引擎中的耦合与混乱
  • 前端与后端可同时开发,前后端完全去耦,不再需要等待后端返回页面,效率显著提升
  • 多终端应用中接口统一,后端由于仅负责提供api接口,所以与前端彻底划清界限,前端的变化不会对后端造成任何影响,即后端不需为前端平台的任何改变而做出改变,前端是采用浏览器?还是移动设备?后端都只是提供统一的接口服务。但这也就意味着终端如果采用浏览器,其内置的一些状态机制如cookie、session最好不要使用,因为一方面跨域会造成cookie的sessionId失效,另一方面就算不失效,后端也无法与不支持这种状态机制的移动终端相适应,势必造成实现两套服务。
  • 按需加载提高用户体验,后端不再需要解析页面,前端的路由配置、模块化引入等功能可提升交互速度

SPA

前后端分离的同时也意味着前端页面需要肩负更加艰巨的任务,比如渲染页面与路由系统。因为后端不再提供页面了,所以转而交给了前端。然而前端具有动态特性东西的也就只有js了,无论交互逻辑还是路由,都要靠他来实现。

目前很流行的SPA单页面应用颠覆了传统的多页面交互,如下图所示:

single_page_model_144

当我们需要跳转页面的时候,并不是真的去访问一个全新的html页面,而是用js动态去替换之前的页面元素(比如传统的jQuery操作dom元素、React中的JSX数据状态渲染),同时改变浏览器链接处的锚点,实现前端对URL的完全掌控(React生态圈中react-router提供了很好的支持)。SPA无需任何模板去控制输入,其展现全部依靠js,目前主流前端框架Angular、React、Vue、Backbone均提供了对SPA很好的支持,可以说他们的技术出发点就是SPA。显然,这种交互方式避免了页面刷新,提供了更好的性能与用户体验。

跨域访问

这个好说,直接在后端加个拦截器,在里面对HTTP响应报文的header设置跨域允许即可,如下代码所示:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
    response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Content-Type, Accept, TOKEN, Content-Range, Content-Disposition, Content-Description");
    response.setHeader("Access-Control-Max-Age", "3600");
    filterChain.doFilter(servletRequest, servletResponse); 
}

再记得向web.xml文件中加入拦截器相关配置:

<filter>
    <filter-name>crossOriginFilter</filter-name>
    <filter-class>com.yhch.interceptor.CrossOriginInterceptor</filter-class>
</filter>
<filter-mapping>
    <filter-name>crossOriginFilter</filter-name>
    <url-pattern>/api/*</url-pattern>
</filter-mapping>

前端在采用AJAX访问时需要带上contentType字段,否则会报异常。示例代码如下:

$.ajax({
   url : SERVER_ADDRESS + '/api/user',
   type : 'POST',
   contentType: 'application/json',
   data : JSON.stringify({userName : 'ken'}),
   dataType : 'json',
   beforeSend: (request) => request.setRequestHeader(SESSION.TOKEN, sessionStorage.getItem(SESSION.TOKEN)),
   success : (result) => {...}
});

结语

说了那么多前后端分离的好,我们也要看到他的不好之处:

  • 开发单页面应用时,前端Route与服务器端Route不匹配
  • 重要内容都在前端组装,不利于SEO(搜索引擎优化)
  • 模板语言不相通

总之整体看来,个人感觉前后端分离还是利大于弊,特别是开发效率、后端统一提供接口、无状态性这几个优点很是优雅。本章提到过,前后端分离势必会造成跨域访问,虽然本章介绍了如何在后端设置header允许跨域访问,但跨域带来的麻烦仍未完全解决,其中一个就是cookie无法共享,导致后端无法高效记录前端的状态(如登录状态、购物车等)。接下来会写一篇讲述如何去维持前后端的状态。

Leave a Comment.