引言
最近在開發一個以醫院業務為背景的Web項目,前端采用React+Ant Design,后端采用SSM(Spring-SpringMVC-MyBatis)框架。由于就我和另外一名同學開發,我負責前端,他負責后端,兩者分開獨立完成,部署均在各自電腦的IP:Port上,所以在調試階段如果想要追求開發效率,就必須攻克前后端工程分離所帶來的兩個問題:跨域訪問、狀態保存。然而前后端分離的這種架構,其實不僅是在調試階段帶來了便利,在后期部署做負載均衡等方面也頗具優勢。而隨之誕生的SPA(Single Page Application)即單頁面應用正好適合這種分離架構的需要。下面我們詳細探討下。
前后端分離
傳統的前后端混合的形式,是把頁面、圖片、樣式、js這些屬于前端的靜態文件,以及src目錄下編譯好的.class、xml這些屬于后端的文件統一放在webapp下,然后啟動Web服務器即可在同一IP:Port下訪問頁面與api服務,如下圖所示。
這種方式將靜態與動態文件混合,經常是前端要進行頁面跳轉了,請求后端給返回所需的頁面,比如返回JSP頁面,而JSP實質是一個Servlet,在它返回前端之前,實質是先將獲取到的數據渲染在了html頁面上,最后再一并返回前端的。而事實上,JSP所做的渲染工作更適合前端來負責,后端只負責提供接口、處理邏輯、存取數據、返回所需JSON數據,即只應涉及到MVC模式中的Modal與Controller。
而前后端分離模式正好相反,它將前端的所有靜態文件與后端的所有動態文件分別置于不同的IP或不同的Port(只要Port不同就會造成跨域)下,前端若要訪問后端的服務,只要指定后端的IP:Port與api的URL即可訪問到,職責很明確,如下圖所示。
前后端分離帶來的好處是明顯的:
- 服務器壓力減小到最小,所有數據到頁面的渲染工作可由前端js完成,擺脫業務邏輯與呈現邏輯在Java模版引擎中的耦合與混亂
- 前端與后端可同時開發,前后端完全去耦,不再需要等待后端返回頁面,效率顯著提升
- 多終端應用中接口統一,后端由于僅負責提供api接口,所以與前端徹底劃清界限,前端的變化不會對后端造成任何影響,即后端不需為前端平臺的任何改變而做出改變,前端是采用瀏覽器?還是移動設備?后端都只是提供統一的接口服務。但這也就意味著終端如果采用瀏覽器,其內置的一些狀態機制如cookie、session最好不要使用,因為一方面跨域會造成cookie的sessionId失效,另一方面就算不失效,后端也無法與不支持這種狀態機制的移動終端相適應,勢必造成實現兩套服務。
- 按需加載提高用戶體驗,后端不再需要解析頁面,前端的路由配置、模塊化引入等功能可提升交互速度
SPA
前后端分離的同時也意味著前端頁面需要肩負更加艱巨的任務,比如渲染頁面與路由系統。因為后端不再提供頁面了,所以轉而交給了前端。然而前端具有動態特性東西的也就只有js了,無論交互邏輯還是路由,都要靠他來實現。
目前很流行的SPA單頁面應用顛覆了傳統的多頁面交互,如下圖所示:
當我們需要跳轉頁面的時候,并不是真的去訪問一個全新的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無法共享,導致后端無法高效記錄前端的狀態(如登錄狀態、購物車等)。接下來會寫一篇講述如何去維持前后端的狀態。