前后端分離的歷史
軟件開發(fā)早期是沒(méi)有前后端分離的概念的,為什么?因?yàn)楫?dāng)時(shí)壓根兒就沒(méi)有前端工程師砸王,專門的前端工程師大約是在2005年。在此之前前端是不受重視的峦阁。這其實(shí)和軟件的發(fā)展有關(guān)谦铃,說(shuō)到這里又不得不提到j(luò)s的發(fā)展歷史。JavaScript誕生于1995年榔昔。起初它的主要目的是處理以前由服務(wù)器端負(fù)責(zé)的一些表單驗(yàn)證驹闰。后來(lái)大家對(duì)頁(yè)面的要求越來(lái)越高,js又給web多了一些動(dòng)態(tài)功能撒会。大家對(duì)前端的需求就是展示靜態(tài)內(nèi)容或者簡(jiǎn)單的動(dòng)態(tài)內(nèi)容(比如CGI返回?cái)?shù)據(jù)拼接輸出“動(dòng)態(tài)”內(nèi)容)
前后端分離
1998年ajax技術(shù)的出現(xiàn)嘹朗,允許客戶端腳本發(fā)送HTTP請(qǐng)求(XMLHTTP),并且局部刷新頁(yè)面诵肛,這種突破性的創(chuàng)新使得web高速發(fā)展屹培,推動(dòng)了web的發(fā)展。隨著HTML5怔檩,CSS3惫谤,ES6(簡(jiǎn)稱356)的出現(xiàn),web正以前所未有的速度前進(jìn)珠洗,web工程師從無(wú)到有,再到現(xiàn)在web工程師被賦予了很多花環(huán)若专,機(jī)遇和挑戰(zhàn)许蓖。也因此前后端不得不逐漸的分離。
前后端合并
正所謂合久必分调衰,分久必合膊爪。 前幾年被熱炒的全棧工程師,以及前后端同構(gòu)技術(shù)都反映了前端端在分離之后又逐漸合并的跡象嚎莉。為什么會(huì)這樣呢米酬?前后端分離雖然減少了后端開發(fā)的工作(最初前端都是后端寫的,比如.net 的 aspx java的jsp等)趋箩。但前后端分離地不干凈導(dǎo)致一些溝通問(wèn)題反而不如一個(gè)人來(lái)做赃额。為了解決這個(gè)問(wèn)題,主要又兩種方式:
- 全棧工程師叫确。 將所有工作交給一個(gè)人跳芳,或者有著前后端知識(shí)的一群人,這樣溝通起來(lái)成本比較低竹勉。
- 稍后講飞盆。
那還又必要分離嗎?
上面講述了前后端合并,那么還有必要分離嗎吓歇? 非常又必要D跛!城看!
剛才說(shuō)為了解決前后端溝通問(wèn)題主要又兩種方式女气,下面說(shuō)下第二種。
- 建立中間層析命。 前后端問(wèn)題產(chǎn)生的原因主要是兩個(gè)主卫,一個(gè)是知識(shí)背景,技術(shù)棧不同鹃愤,難以互相理解簇搅。二是 前后端是一個(gè)依賴的關(guān)系,前端需要依賴后端的數(shù)據(jù)接口软吐,因此存在工作上的先后關(guān)系瘩将。 建立中間層可以有效減少上述的問(wèn)題。 目前淘寶凹耙,挖財(cái)姿现,51,有贊肖抱,二維火都在嘗試這種方式备典。這也是我將會(huì)在后面重點(diǎn)描述的方式。
最近出現(xiàn)的docker容器技術(shù)等有效地減少了后端的工作量意述,讓后端更加專注業(yè)務(wù)邏輯的編寫提佣。我覺得node的出現(xiàn)也大概如此吧,它的出現(xiàn)不是用來(lái)替代apache成為下一個(gè)web容器荤崇,我覺得他更是擴(kuò)展了前端的領(lǐng)域拌屏,使得可以將一部分后端無(wú)法做(比如ssr)或者不好做(不愿意做)的東西(比如接口聚合),移交給前端术荤。
我不喜歡被冠以前端工程師倚喂,后端工程師的帽子。 我喜歡工程師這個(gè)稱呼瓣戚,我覺得我是以工程化的方式解決問(wèn)題的角色端圈,而不應(yīng)該限定于某一部分職責(zé)。這個(gè)我主張前后端分離不矛盾子库,每個(gè)角色都自己的分工枫笛,都有自己精細(xì)化的一面。我們不試圖取代后端刚照,我們只是專注做好自己罷了刑巧。
前后端耦合
我有幸在我的職業(yè)生涯中經(jīng)歷了前后端耦合喧兄,前后端開發(fā)分離,部署分離的“分離階段”啊楚, 以及大廠(alibaba)在這些方面的解決方案吠冤,所以非常榮幸能夠?qū)⒆约旱慕?jīng)驗(yàn)和心得與大家分享。
最初在百世做前端的時(shí)候恭理,我經(jīng)歷了前后端極度耦合的方式前后端雖然由不同的人來(lái)書寫拯辙,那恰好也是我的領(lǐng)導(dǎo)首次嘗試前后端分離模式,在此之前前后端通常都是一名后端人員來(lái)寫的颜价。這種模式雖然將前后端工作進(jìn)行了切分涯保,但是其耦合的成分還是非常大。
首先我們來(lái)看下我們的具體做法周伦。
- 前后端約定接口夕春。
- 前端根據(jù)后端接口進(jìn)行開發(fā)。
- 后端進(jìn)行開發(fā)专挪。(2與3基本并行)
- 前端打包代碼及志,并將代碼發(fā)送給后端。后端放到war包下寨腔。
tomcat/conf/web.xml配置如下:
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
將war文件直接復(fù)制到tomcat/webapps下
我們后端使用的是java速侈,具體做法:
- 如果是Intellij Idea,在導(dǎo)入前端項(xiàng)目之后,右鍵項(xiàng)目 add framework support --> web application迫卢,這時(shí)將會(huì)把前端項(xiàng)目轉(zhuǎn)換為一個(gè)javaweb項(xiàng)目倚搬,然后將靜態(tài)資源放在生成的web目錄下即可。
- 如果是eclipse乾蛤,可以新建一個(gè)javaweb項(xiàng)目然后將靜態(tài)資源放入web或者webcontent目錄下每界,或者直接先導(dǎo)入前端項(xiàng)目,然后通過(guò) project facts 將項(xiàng)目轉(zhuǎn)換為dynamic web項(xiàng)目并勾選 js等相關(guān)配置幻捏。
然后,運(yùn)行項(xiàng)目時(shí)把后端的war包和前端的war包一同添加到 deployment中運(yùn)行即可
這一階段我們實(shí)現(xiàn)了初步的前后端分離命咐。 但是上述做法有兩個(gè)問(wèn)題篡九。
由于上述的做法存在嚴(yán)重的問(wèn)題在于前端對(duì)于發(fā)布控制力明顯不足,比如版本控制不好做醋奠。 另外由于發(fā)布通常需要兩個(gè)編譯環(huán)境榛臼,即jdk編譯后端代碼,node環(huán)境編譯前端代碼窜司。前端通常需要安裝后端環(huán)境如jdk沛善,后端也需要安裝前端環(huán)境如node。不管是學(xué)習(xí)成本還是溝通成本都是一個(gè)問(wèn)題塞祈。
前端通常需要等待后端給出測(cè)試數(shù)據(jù)后才能開工金刁。所以我上面說(shuō)“基本并行”
由于存在上面的兩個(gè)問(wèn)題,我們進(jìn)一步探索出了下面的前后端”分離“模式。
前后端“分離”
這一階段我們通過(guò)nginx將前后端部署分離尤蛮,通過(guò)mock服務(wù)器將前后端時(shí)間上的耦合給減少了媳友。
下面是我們的具體做法:
- 前后端約定接口
- 前端開發(fā)
- 后端開發(fā)(2和3完全并行)
- 前端打包,并將代碼通過(guò)ftp上傳到文件服務(wù)器产捞。
- 配置nginx將靜態(tài)請(qǐng)求代理到上面發(fā)布文件的文件服務(wù)器醇锚,后臺(tái)接口代理到apache web server。
nginx 配置 通常是這樣的:
server {
listen 80;
server_name example.com;
charset utf-8;
#access_log logs/host.access.log main;
location / {
proxy_pass http://tomcat_pool;
proxy_redirect off;
proxy_set_header HOST $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|woff|woff2|ttf|eot|map)$ {
root D:\Workspaces\front-end;
index index.html;
}
大廠的方案
這里主要介紹下阿里巴巴-釘釘?shù)那昂蠖朔蛛x解決方案坯临。阿里這樣的大廠通常有著自己內(nèi)部的成熟系統(tǒng)支撐業(yè)務(wù)焊唬,比如阿里的持續(xù)集成系統(tǒng)aone,配置服務(wù)diamond看靠,分布式服務(wù)系統(tǒng)hsf等等赶促。 小公司通常沒(méi)有經(jīng)濟(jì)實(shí)力去搭建自己的內(nèi)部系統(tǒng),我有幸接觸到了這些業(yè)界較為頂級(jí)的系統(tǒng)衷笋,對(duì)它們的運(yùn)作方式有了一定的了解芳杏。這里簡(jiǎn)單介紹下阿里巴巴-釘釘?shù)那昂蠖朔蛛x解決方案。
釘釘?shù)木唧w做法如下:
- 前后端約定接口
- 約定接口上dip(一個(gè)mock服務(wù)器)
- 前端開發(fā)
- 后端開發(fā)(2和3完全并行)
- 前端打包辟宗,push觸發(fā)測(cè)試環(huán)境和預(yù)發(fā)環(huán)境的云構(gòu)建爵赵,
打tag觸發(fā)線上環(huán)境云構(gòu)建自動(dòng)發(fā)布靜態(tài)資源到cdn。 - 需要發(fā)布只需要負(fù)責(zé)人修改diamond 配置即可泊脐,版本控制變得簡(jiǎn)單空幻。
可以發(fā)現(xiàn)這種方式其實(shí)就是比上面多了兩個(gè)點(diǎn),一是云構(gòu)建自動(dòng)化容客,二是diamond配置秕铛。 云構(gòu)建其實(shí)屬于自動(dòng)化,我將在流程自動(dòng)化里面詳細(xì)描述缩挑,云構(gòu)建本身和
前后端分離沒(méi)有關(guān)系但两。 其實(shí)兩者的區(qū)別在于diamond配置。那么為什么配置diamond是前后端分離的工作呢供置?作為一個(gè)配置服務(wù)谨湘,diamond將專業(yè)性比較強(qiáng)的東西抽離出來(lái),將包括但不限于版本管理的內(nèi)容可以交給沒(méi)有任何前端背景的人來(lái)做芥丧。
前后端分離的理想方式
上面的這種方法是非常高效的一種方式紧阔,但仍然不是我認(rèn)為的理想的前后端分離方式。
我認(rèn)為理想的前后端分離方式是后端提供純粹的接口续担,只需要提供數(shù)據(jù)-系統(tǒng)的數(shù)據(jù)或者根據(jù)根據(jù)二方庫(kù)獲取數(shù)據(jù)返回前端擅耽,剩下的邏輯前端做。
這樣由于后端提供元數(shù)據(jù)物遇,前端只需要組合乖仇,前后端在邏輯和時(shí)間上沒(méi)有了耦合憾儒。先來(lái)一張圖來(lái)描述下:
[圖片上傳失敗...(image-171853-1512354045077)]
如圖,后端只是提供原子數(shù)據(jù)这敬,保證數(shù)據(jù)穩(wěn)定輸出就可以了航夺,事實(shí)上保證系統(tǒng)穩(wěn)定很多已經(jīng)是運(yùn)維再做的事了。前端需要根據(jù)需要進(jìn)行接口整合崔涂,服務(wù)端渲染阳掐,mock數(shù)據(jù)等工作。
那么整個(gè)流程具體是怎么工作的呢冷蚂? 可以下面這張圖:
[圖片上傳失敗...(image-3c747a-1512354045077)]
可以看出請(qǐng)求首先留到ngxin(反向代理)缭保,nginx判斷是否是靜態(tài)請(qǐng)求(html),如果是則轉(zhuǎn)發(fā)到node服務(wù)器蝙茶,node服務(wù)器會(huì)判斷是否需要進(jìn)行ssr艺骂,如果需要?jiǎng)t調(diào)用后臺(tái)接口拼裝html,將html和應(yīng)用狀態(tài)一起返回給前端隆夯。 否過(guò)不需要ssr钳恕,則直接返回靜態(tài)資源,并設(shè)置緩存信息蹄衷。
如果不是靜態(tài)資源忧额,判斷頭部信息(比如有一個(gè)自定義字段reselect: 'node' | ''),是否需要請(qǐng)求合并愧口,如果需要?jiǎng)t請(qǐng)求到node端睦番,如果不需要直接轉(zhuǎn)發(fā)給后端服務(wù)器。
ngxin配置大概是這樣:
map $reselect/node $reselect {
default "";
"node" "reselect/node";
}
server {
listen 80;
server_name demo.io;
charset utf-8;
autoindex off;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Ssl on;
if ($reselect="reselect/node"){
proxy_pass http://node-demo.io;
break;
}
proxy_pass http://java-demo.io;
}
location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|woff|woff2|ttf|eot|map)$ {
root D:\Workspaces\front-end;
index index.html;
}
}
中間層可以做的事情非常多耍属,除了我剛才說(shuō)的服務(wù)端渲染托嚣,接口組合,mock數(shù)據(jù)厚骗,還可以做很多別的事示启。我在這里不會(huì)講述ssr,接口組合领舰,mock如何具體做夫嗓,因?yàn)檫@不是本章的重點(diǎn),而且也有很多最佳實(shí)踐提揍,我要說(shuō)的是如何將這些“最佳實(shí)踐“ 組合起來(lái)啤月,如何在我們工作中將其應(yīng)用起來(lái)煮仇,并且具有良好的擴(kuò)展性劳跃。
那么中間層拿到請(qǐng)求具體流程上面已經(jīng)講過(guò)了,現(xiàn)在我們以實(shí)戰(zhàn)的角度浙垫,講下代碼該怎么組織刨仑。
但是為了方便大家理解郑诺,我簡(jiǎn)單介紹下。 首先是服務(wù)端渲染杉武,我以react+redux做服務(wù)端渲染講解辙诞,為了簡(jiǎn)單起見,沒(méi)有引入react-router轻抱,大家直接看代碼理解:
服務(wù)端:
<body>
<div id="container"><%- html %></div>
<script>
window.__INITIAL_STATE__ = <%- JSON.stringify(initState) %>
</script>
</body>
const store = buildStore(rootReducer, {});
Promise.all([
store.dispatch(fetchUserInfo()),
store.dispatch(fetchPosts())
])
.then(()=>{
const html = renderToString(
<Provider store={store}>
<RouterContext {...renderProps}/>
</Provider>
)
const initState = store.getState();
res.render('index', html ,initState);
})
客戶端:
const initState = window.__INITIAL_STATE__;
const store = storeApp(initState);
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
{routesApp}
</Router>
</Provider>,
document.getElementById('container'));
上面這是對(duì)服務(wù)端渲染的一個(gè)極簡(jiǎn)實(shí)現(xiàn)飞涂,那么接口聚合呢?mock呢祈搜? 加入其他功能呢较店? 是不是對(duì)我們express 本身侵入性太大。
在這里我借鑒了微服務(wù)的理念容燕,同時(shí)利用第二章節(jié)要講模塊化的思想梁呈,組織了中間層。
那么究竟我是怎么設(shè)計(jì)的呢蘸秘,請(qǐng)繼續(xù)往下看官卡。
[圖片上傳失敗...(image-120d20-1512354045077)]
我們的關(guān)注點(diǎn)就是服務(wù)集群,如果需要增加集群就直接修改配置即可醋虏。
下面基于docker + docker-compose + node + nginx 做一個(gè)中間層系統(tǒng)寻咒。
docker-compose.yml
version: '2'
services:
nginx:
build: ./nginx
container_name: ms_nginx
links:
- posts
- users
ports:
- "80:80"
api:
build: ./api
container_name: ms_posts
environment:
- loglevel=none
volumes:
- "./posts:/src/app"
working_dir: "/src/app"
ports:
- "8080:8080"
- "5858:5858"
# command: npm run start
command: npm run start:dev
sever-render:
build: ./sever-render
container_name: ms_sever-render
volumes:
- "./users:/src/app"
working_dir: "/src/app"
command: npm start
nginx的配置
worker_processes 4;
events { worker_connections 1024; }
http {
server {
listen 80;
charset utf-8;
location / {
proxy_pass http://server-render:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location ~ ^/api {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://users:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
這樣我們實(shí)現(xiàn)了不同的系統(tǒng)分流,并且彼此之間接觸耦合灰粮,通過(guò)nginx 去 分發(fā)仔涩。這樣還存在一個(gè)問(wèn)題就是樣板代碼比較多,
有沒(méi)有一種方法粘舟,讓我們只關(guān)注業(yè)務(wù)本身熔脂,而不需要樣板代碼呢?大家可以關(guān)注下 faas, 這里不在贅述。
當(dāng)我們的業(yè)務(wù)逐漸復(fù)雜,系統(tǒng)逐漸增多痹栖,域名逐漸增多温峭,會(huì)發(fā)現(xiàn)很多東西都在nginx中,這時(shí)候需要將配置分開趟济,每一個(gè)子業(yè)務(wù)一個(gè)配置文件。
- 在nginx安裝目前下 新建 vhost 文件夾
- 在文件夾下創(chuàng)建 *.conf 的文件 ,如 a.example.com.conf 秽荞,命名規(guī)則大多以域名的方法來(lái)命名文件。
- 輯 conf 文件抚官,把我們平常放在 nginx.conf 里的 server{...} 段復(fù)制過(guò)來(lái)直接粘貼到 conf 里扬跋。
在 nginx.conf 的 http{...} 段中加入 include E:/nginx-1.8.1/vhosts/.conf;
注:這里 include 需要用到全路徑,且文件后綴是用 conf**
這里介紹一個(gè)淘寶開源軟件tengine凌节,他是nginx的超集钦听。有很多強(qiáng)大的功能洒试,我之前的公司百世就是用的tengine。
總結(jié)
本章介紹了四種前后端分離的方式和階段朴上,這里需要強(qiáng)調(diào)的是并不是越往后的方式越好垒棋,問(wèn)題的關(guān)鍵點(diǎn)還是選擇合適的
方式,根據(jù)當(dāng)前所處階段選擇合適的分離方式痪宰,提高單體和整體作戰(zhàn)效率才是明智之舉叼架。
以上內(nèi)容摘自github下自己在寫的一本書,大家可以關(guān)注一下衣撬,持續(xù)更新碉碉,地址https://github.com/azl397985856/automate-everything。