認(rèn)識(shí)前后端分離
在傳統(tǒng)的web應(yīng)用開發(fā)中埋合,大多數(shù)的程序員會(huì)將瀏覽器作為前后端的分界線。將瀏覽器中為用戶進(jìn)行頁面展示的部分稱之為前端逾冬,而將運(yùn)行在服務(wù)器娇妓,為前端提供業(yè)務(wù)邏輯和數(shù)據(jù)準(zhǔn)備的所有代碼統(tǒng)稱為后端。
由于前后端分離這個(gè)概念相對(duì)來說剛出現(xiàn)不久辫塌,很多人都是只聞其聲漏策,不見其形,所以可能會(huì)對(duì)它產(chǎn)生一些誤解臼氨,誤以為前后端分離只是一種web應(yīng)用開發(fā)模式掺喻,只要在web應(yīng)用的開發(fā)期進(jìn)行了前后端開發(fā)工作的分工就是前后端分離。
其實(shí)前后端分離并不只是開發(fā)模式储矩,而是web應(yīng)用的一種架構(gòu)模式
感耙。在開發(fā)階段,前后端工程師約定好數(shù)據(jù)交互接口持隧,實(shí)現(xiàn)并行開發(fā)和測(cè)試即硼;在運(yùn)行階段前后端分離模式需要對(duì)web應(yīng)用進(jìn)行分離部署,前后端之前使用HTTP或者其他協(xié)議進(jìn)行交互請(qǐng)求屡拨。然而作為一種架構(gòu)模式只酥,我們?cè)趯?shí)施的過程中主要對(duì)以下四個(gè)方面來進(jìn)行比較和重新認(rèn)識(shí)。
前后端分離大概可以從四個(gè)方面來理解:
- 交互形式
- 代碼組織方式
- 開發(fā)模式
- 數(shù)據(jù)接口規(guī)范流程
一呀狼、交互形式
在前后端分離架構(gòu)中裂允,后端只需要負(fù)責(zé)按照約定的數(shù)據(jù)格式向前端提供可調(diào)用的API服務(wù)即可。前后端之間通過HTTP請(qǐng)求進(jìn)行交互哥艇,前端獲取到數(shù)據(jù)后绝编,進(jìn)行頁面的組裝和渲染,最終返回給瀏覽器貌踏。
二十饥、代碼組織方式
在傳統(tǒng)架構(gòu)模式中,前后端代碼存放于同一個(gè)代碼庫(kù)中祖乳,甚至是同一工程目錄下逗堵。頁面中還夾雜著后端代碼。前后端工程師進(jìn)行開發(fā)時(shí)凡资,都必須把整個(gè)項(xiàng)目導(dǎo)入到開發(fā)工具中砸捏。
而前后端分離模式在代碼組織形式上有以下兩種:
-
半分離
前后端共用一個(gè)代碼庫(kù),但是代碼分別存放在兩個(gè)工程中隙赁。后端不關(guān)心或很少 關(guān)心前端元素的輸出情況垦藏,前端不能獨(dú)立進(jìn)行開發(fā)和測(cè)試,項(xiàng)目中缺乏前后端 交互的測(cè)試用例伞访。 -
分離
前后端代碼庫(kù)分離掂骏,前端代碼中有可以進(jìn)行Mock測(cè)試(通過構(gòu)造虛擬測(cè)試對(duì) 象以簡(jiǎn)化測(cè)試環(huán)境的方法)的偽后端,能支持前端的獨(dú)立開發(fā)和測(cè)試厚掷。而后端 代碼中除了功能實(shí)現(xiàn)外弟灼,還有著詳細(xì)的測(cè)試用例,以保證API的可用性冒黑,降低 集成風(fēng)險(xiǎn)田绑。
三、開發(fā)模式
我們之前的架構(gòu)屬于傳統(tǒng)的MVC架構(gòu)抡爹,整體沒有進(jìn)行前后端分離掩驱,在項(xiàng)目的開發(fā)階段,前端工程師負(fù)責(zé)編寫HTML冬竟,完成前端的頁面設(shè)計(jì)并套頁面欧穴,然后再使用模板技術(shù)將寫好的前端代碼轉(zhuǎn)換為Smarty腳本,同時(shí)內(nèi)嵌一些后端提供的模板變量和一些邏輯操作泵殴。應(yīng)用運(yùn)行期涮帘,將全部代碼進(jìn)行打包,和后端代碼部署到同一服務(wù)器上笑诅,同時(shí)會(huì)進(jìn)行簡(jiǎn)單的動(dòng)靜態(tài)分離部署调缨。
此時(shí),應(yīng)用的開發(fā)流程如下圖所示吆你。
而在實(shí)現(xiàn)前后端分離架構(gòu)之后同蜻,前端工程師只需要編寫HTML、js早处、CSS等前端資源湾蔓,然后通 過HTTP請(qǐng)求調(diào)用后端提供的服務(wù)即可。除了開發(fā)期的分離砌梆,在運(yùn)行期前后端資源也 會(huì)進(jìn)行分離部署默责。
前后端分離之后,開發(fā)流程將如下圖所示咸包。
通過上面的兩幅流程圖桃序,不難發(fā)現(xiàn),在開發(fā)模式上烂瘫,前后段分離不僅僅只是工程師的分工開發(fā)媒熊,更重要的意義在于實(shí)現(xiàn)了前后端的并行開發(fā)奇适,簡(jiǎn)化了開發(fā)流程
。
四芦鳍、數(shù)據(jù)接口規(guī)范流程
在開發(fā)期間前后端共同商定好數(shù)據(jù)接口的交互形式和數(shù)據(jù)格式嚷往。然后實(shí)現(xiàn)前后端的并行開發(fā),其中前端工程師再開發(fā)完成之后可以獨(dú)自進(jìn)行mock測(cè)試柠衅,而后端也可以使用接口測(cè)試平臺(tái)進(jìn)行接口自測(cè)皮仁,然后前后端一起進(jìn)行功能聯(lián)調(diào)并校驗(yàn)格式,最終進(jìn)行自動(dòng)化測(cè)試菲宴。
分離的四個(gè)好處
前后端分離模式和傳統(tǒng)的web應(yīng)用架構(gòu)相比有很大的不同贷祈,到底分還是不分,這還真是個(gè)問題喝峦。
從目前應(yīng)用軟件開發(fā)的發(fā)展趨勢(shì)來看势誊,主要有兩方面需要注意:
- 越來越注重用戶體驗(yàn),隨著互聯(lián)網(wǎng)的發(fā)展谣蠢,開始多終端化键科。
- 大型應(yīng)用架構(gòu)模式正在向云化、微服務(wù)化發(fā)展漩怎。
我們主要通過前后端分離架構(gòu)勋颖,為我們帶來以下四個(gè)方面的提升:
-
為優(yōu)質(zhì)產(chǎn)品打造精益團(tuán)隊(duì)
通過將開發(fā)團(tuán)隊(duì)前后端分離化,讓前后端工程師只需要專注于前端或后端的開發(fā)工作勋锤,是的前后端工程師實(shí)現(xiàn)自治饭玲,培養(yǎng)其獨(dú)特的技術(shù)特性,然后構(gòu)建出一個(gè)全棧式的精益開發(fā)團(tuán)隊(duì)叁执。 -
提升開發(fā)效率
前后端分離以后茄厘,可以實(shí)現(xiàn)前后端代碼的解耦,只要前后端溝通約定好應(yīng)用所需接口以及接口參數(shù)谈宛,便可以開始并行開發(fā)次哈,無需等待對(duì)方的開發(fā)工作結(jié)束。與此同時(shí)吆录,即使需求發(fā)生變更窑滞,只要接口與數(shù)據(jù)格式不變,后端開發(fā)人員就不需要修改代碼恢筝,只要前端進(jìn)行變動(dòng)即可哀卫。如此一來整個(gè)應(yīng)用的開發(fā)效率必然會(huì)有質(zhì)的提升。 -
完美應(yīng)對(duì)復(fù)雜多變的前端需求
如果開發(fā)團(tuán)隊(duì)能完成前后端分離的轉(zhuǎn)型撬槽,打造優(yōu)秀的前后端團(tuán)隊(duì)此改,開發(fā)獨(dú)立化,讓開發(fā)人員做到專注專精侄柔,開發(fā)能力必然會(huì)有所提升共啃,能夠完美應(yīng)對(duì)各種復(fù)雜多變的前端需求占调。 -
增強(qiáng)代碼可維護(hù)性
前后端分離后,應(yīng)用的代碼不再是前后端混合移剪,只有在運(yùn)行期才會(huì)有調(diào)用依賴關(guān)系究珊。
應(yīng)用代碼將會(huì)變得整潔清晰,不論是代碼閱讀還是代碼維護(hù)都會(huì)比以前輕松挂滓。
以上轉(zhuǎn)自前后端分離實(shí)踐(一)
利用JSON串來實(shí)現(xiàn)前后端分離
在自己最近做的項(xiàng)目中苦银,使用的是利用SSM框架中的controller層來傳出JSON串啸胧,再通過jQuery中的.getJSON()
來進(jìn)行解析赶站,再將數(shù)據(jù)傳到前端頁面。
controller層
部分代碼省略纺念,放出關(guān)鍵代碼:
ShopManagementController
@Controller
@RequestMapping("/shopadmin")
public class ShopManagementController {
@Autowired
private ShopService shopService;
@Autowired
private ShopCategoryService shopCategoryService;
@Autowired
private AreaService areaService;
@RequestMapping(value = "/getshopmanagementinfo",method = RequestMethod.GET)
@ResponseBody
private Map<String,Object> getShopManagementInfo(HttpServletRequest request) {
Map<String,Object> modelMap = new HashMap<String, Object>();
long shopId = HttpServletRequestUtil.getLong(request,"shopId");
if(shopId <= 0){
Object currentShopObj = request.getSession().getAttribute("shopId");
if(currentShopObj == null){
modelMap.put("redirect",true);
modelMap.put("url","/shopadmin/shoplist");
}else {
Shop currentShop = (Shop) currentShopObj;
modelMap.put("redirect",false);
modelMap.put("shopId",currentShop.getShopId());
}
}else{
Shop currentShop = new Shop();
currentShop.setShopId(shopId);
request.getSession().setAttribute("currentShop",currentShop);
modelMap.put("redirect",false);
}
return modelMap;
}
@RequestMapping(value = "/getshoplist",method = RequestMethod.GET)
@ResponseBody
private Map<String,Object> getShopList(HttpServletRequest request){
Map<String,Object> modelMap = new HashMap<String, Object>();
PersonInfo user = new PersonInfo();
user.setUserId(1L);
user.setName("test");
request.getSession().setAttribute("user",user);
user = (PersonInfo) request.getSession().getAttribute("user");
try{
Shop shopCondition;
shopCondition = new Shop();
shopCondition.setOwner(user);
ShopExecution se = shopService.getShopList(shopCondition,0,100);
modelMap.put("shopList",se.getShopList());
modelMap.put("user",user);
modelMap.put("success",true);
}catch (Exception e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
}
return modelMap;
}
@RequestMapping(value = "/getshopbyid",method = RequestMethod.GET)
@ResponseBody
private Map<String,Object> getShopById(HttpServletRequest request){
Map<String,Object> modelMap = new HashMap<String,Object>();
Long shopId = HttpServletRequestUtil.getLong(request,"shopId");
if(shopId > -1){
try {
Shop shop = shopService.getByShopId(shopId);
List<Area> areaList = areaService.getAreaList();
modelMap.put("shop",shop);
modelMap.put("areaList",areaList);
modelMap.put("success",true);
}catch (Exception e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
}
}else {
modelMap.put("success",false);
modelMap.put("errMsg","empty shopId");
}
return modelMap;
}
@RequestMapping(value = "/getshopinitinfo",method = RequestMethod.GET)
@ResponseBody
public Map<String,Object> getShopInitInfo(){
Map<String,Object> modelMap = new HashMap<String, Object>();
List<ShopCategory> shopCategoryList = new ArrayList<ShopCategory>();
List<Area> areaList = new ArrayList<Area>();
try {
shopCategoryList = shopCategoryService.getShopCategoryList(new ShopCategory());
areaList = areaService.getAreaList();
modelMap.put("shopCategoryList",shopCategoryList);
modelMap.put("areaList",areaList);
modelMap.put("success",true);
}catch (Exception e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
}
return modelMap;
}
@RequestMapping(value = "/modifyshop",method = {RequestMethod.POST})
@ResponseBody
public Map<String,Object> modifyShop(HttpServletRequest request) throws IOException {
Map<String,Object> modelMap = new HashMap<String, Object>();
//判斷驗(yàn)證碼是否正確
if(!CodeUtil.checkVerifyCode(request)){
modelMap.put("success",false);
modelMap.put("errMsg","輸入了錯(cuò)誤的驗(yàn)證碼");
return modelMap;
}
//1.接收并轉(zhuǎn)化相應(yīng)的參數(shù)贝椿,包括店鋪信息以及圖片信息
String shopStr = HttpServletRequestUtil.getString(request,"shopStr");
ObjectMapper mapper = new ObjectMapper();
Shop shop = null;
try {
shop = mapper.readValue(shopStr,Shop.class);
}catch (Exception e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
return modelMap;
}
CommonsMultipartFile shopImg = null;
CommonsMultipartResolver commonsMultipartResolver =new CommonsMultipartResolver(
request.getSession().getServletContext());
// 檢測(cè)文件是否有上傳文件流
if (commonsMultipartResolver.isMultipart(request)){
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
shopImg = (CommonsMultipartFile) multipartHttpServletRequest.getFile("shopImg");
}
//2.修改店鋪信息
if(shop != null && shop.getShopId() != null){
PersonInfo owner =(PersonInfo) request.getSession().getAttribute("user");
// session
owner.setUserId(1L);
shop.setOwner(owner);
ShopExecution se;
try {
if (shopImg == null){
se = shopService.modifyShop(shop, new ImageHolder(null, null));
}else {
se = shopService.modifyShop(shop,
new ImageHolder(shopImg.getInputStream(), shopImg.getOriginalFilename()));
}
if(se.getState()==ShopStateEnum.SUCCESS.getState()){
modelMap.put("success",true);
}else {
modelMap.put("success",false);
modelMap.put("errMsg",se.getStateInfo());
}
}catch (ShopOperationException e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
} catch (IOException e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
}
return modelMap;
}else {
modelMap.put("success",false);
modelMap.put("errMsg","請(qǐng)輸入店鋪Id");
return modelMap;
}
}
}
拿getShopById
做分析
//返回/getshopbyid的url,方法為get
@RequestMapping(value = "/getshopbyid",method = RequestMethod.GET)
//返回JSON串
@ResponseBody
private Map<String,Object> getShopById(HttpServletRequest request){
//初始化要返回的JSON串
Map<String,Object> modelMap = new HashMap<String,Object>();
//從Http請(qǐng)求中獲得shopId值
Long shopId = HttpServletRequestUtil.getLong(request,"shopId");
if(shopId > -1){
try {
/**
* 下面代碼做service層查詢陷谱,獲得商店Id為shopId的商店信息
*/
Shop shop = shopService.getByShopId(shopId);
List<Area> areaList = areaService.getAreaList();
modelMap.put("shop",shop);
modelMap.put("areaList",areaList);
modelMap.put("success",true);
}catch (Exception e){
modelMap.put("success",false);
modelMap.put("errMsg",e.getMessage());
}
}else {
modelMap.put("success",false);
modelMap.put("errMsg","empty shopId");
}
return modelMap;
}
再來看看前端頁面是如何處理的烙博。
(function() {
var shopId = getQueryString('shopId');
var isEdit = shopId?true:false;
var initUrl = '/shopadmin/getshopinitinfo';
var registerShopUrl = '/shopadmin/registershop';
//訪問/shopadmin/getshopbyid這個(gè)url
var shopInfoUrl = "/shopadmin/getshopbyid?shopId=" + shopId;
//從/shopadmin/getshopbyid?shopId這個(gè)url中獲得controller層返回的JSON串,通過$.getJSON函數(shù)進(jìn)行解析烟逊。
//獲取相關(guān)的鍵值對(duì)渣窜,然后插入前端HTML頁面。
function getShopInfoUrl(shopId) {
$.getJSON(shopInfoUrl,function (data) {
if(data.success){
var shop = data.shop;
$('#shop-name').val(shop.shopName);
$('#shop-addr').val(shop.shopAddr);
$('#shop-phone').val(shop.phone);
$('#shop-desc').val(shop.shopDesc);
var shopCategory = '<option data-id="'
+ shop.shopCategory.shopCategoryId +'"selected>'
+ shop.shopCategory.shopCategoryName +'</option>';
var tempAreaHtml = '';
data.areaList.map(function (item,index) {
tempAreaHtml += '<option data-id="' + item.areaId+'"selected>'
+ item.areaName+'</option>';
});
$('#shop-category').html(shopCategory);
$('#shop-category').attr('disable','disable');
$('#shop-area').html(tempAreaHtml);
$("#shop-area option[data-id='"+shop.area.areaId+"']").attr("selected","selected");
}
});
}
訪問url宪躯,即可獲得商店信息
流程圖: