前後端分離這個詞一點都不新鮮,完全的前後端分離在崗位協作方面,前端不寫任何後臺,後臺不寫任何頁面,雙方通過介面傳遞資料完成軟體的各個功能實現。此種情況下,前後端的專案都獨立開發和獨立部署,在開發期間有2個問題不可避免:第一是前端呼叫後臺介面時的跨域問題(因為前後端分開部署);第二是前端脫離後臺服務後無法獨立執行。本文總結最近一個專案的工作經驗,介紹利用grunt-contrib-connect和grunt-connect-proxy搭建前後端分離的開發環境的實踐過程,希望能對你有所幫助。
注:
(1)本文的相關內容需對前端構建工具grunt有所瞭解:http://www.gruntjs.net/getting-started,這個工具可以完成前端所有的工程化工作,包括程式碼和圖片壓縮,檔案合併,靜態資源替換,js混淆,less和sass編譯成css等等,推薦沒有用過類似工具的前端開發人員去了解。
(2)grunt-contrib-connect和grunt-connect-proxy是grunt提供的兩個外掛,前者可以啟動一個基於nodejs的靜態伺服器,這樣前端就能脫離後端通過web服務的方式來訪問自己開發的東西;後者可以把前端專案裡面某些特殊的請求代理到其它伺服器,哪些請求能夠通過代理轉發到別的伺服器,這個規則都是可配置的,這樣就能把一些跟後臺互動的請求通過代理的方式,在開發期間,轉發到後端的服務來處理,從而避免跨域問題。
1. 效果演示
在前面提供的程式碼中,裡面有兩個資料夾:
分別代表前後端獨立執行的兩個專案,client表示前端,server表示服務端。在實際執行client和server裡面的服務之前,請確保已經安裝好了grunt-cli,如果沒有安裝,請按照grunt的文件先安裝好grunt-cli這個npm的包。如果你已經安裝好了grunt-cli,那麼進入到client或者server資料夾下,就能直接使用grunt的命令來啟動服務了,不需要再執行npm install 來安裝依賴了,因為client和server資料夾下已經包含進了下載好的依賴。在實際的前後端專案中,server端可以是任何架構型別的專案,java web ,php, asp.net等等都可以,demo裡面為了簡單模擬一個後臺服務,於是就利用grunt啟動一個靜態服務來充當server端,不過它實際上的作用跟java web等傳統後端專案是一樣的。
為了看到請求被代理轉發的效果,請先在server資料夾下啟動服務,命令是:grunt staticServer:
只要看到跟截圖執行類似的結果,就表示server端的服務啟動成功。從截圖中還能看到server端的服務的訪問地址是:http://localhost:9002/。
然後在client資料夾下啟動配置了代理的服務,命令是:grunt proxyServer:
只要看到跟截圖執行類似的結果,就表示client端的服務啟動成功。從截圖中能看到client端服務的訪問地址是:http://localhost:9001/,同時還可以看到服務代理的配置:
這段執行結果說明,client端裡面以/provider開頭的請求都會被代理轉發,並且會被代理到http://localhost:9002/provider 來處理。舉例來說,假如在client端裡面發起一個請求,這個請求的URL是:http://localhost:9001/provider/query/json/language/list,那麼最終處理這個請求的服務地址實際上是:http://localhost:9002/provider/query/json/language/list。
client端啟動之後,應該會自動開啟瀏覽器,訪問http://localhost:9001/,顯示的是client端的首頁。開啟首頁之後,按F12開啟開發者工具,如果在控制檯看到如下類似的訊息,就說明首頁裡的請求正確地通過代理請求到了服務端的資料:
在client的首頁裡面,我發起了一個ajax請求,請求地址為http://localhost:9001/provider/query/json/language/list,在client資料夾下根本不存在provider資料夾,所以如果沒有代理的話,這個請求肯定會報404的錯誤;它之所以能夠正確的載入,完全是因為通過代理,請求到了server資料夾下相應的檔案:
如果不通過代理,在localhost:9001/的服務裡,請求localhost:9002/的資料是肯定會有跨域問題的,而代理可以完美的解決這個問題。
前面這一小部分演示了demo裡面如何通過代理來解決跨域問題,下面一部分演示如何在脫離後端服務的情況下如何正常執行前端專案,首先請關閉之前開啟的client服務和server端服務以及瀏覽器開啟的client頁面,然後開啟client/Gruntfile.js檔案,找到以下部分:
把provider改成api,把false改成true;
接著在client資料夾,執行非代理的靜態服務,這個服務不會配置代理,啟動命令是:grunt staticServer:
開啟瀏覽器的開發者工具,在控制檯應該可以看到如下訊息:
這個過程是:原來通過代理請求地址是:http://localhost:9001/provider/query/json/language/list,在沒有代理的時候,我會把http://localhost:9001/provider/query/json/language/list這個請求改成請求http://localhost:9001/api/query/json/language/list.json ,而在我client資料夾下存在這個json檔案:
也就是說我會把跟服務端所有介面的返回的資料都按相同的路徑,在本地以json檔案的形式存在api資料夾下,在沒有代理的時候,只要請求這些json檔案,就能保證我所有的操作都能正確請求到資料,前端的專案也就能脫離代理執行起來了,當然這個模式下的資料都是靜態的了。
接下來我會介紹如何前面這些內容的實現細節,只介紹client裡面的要點,server裡面的內容很簡單,只要搞清楚了client,server一看就懂:)
2. Grunt配置
在瞭解配置之前,先要熟悉專案的資料夾結構:
僅僅是為了完成demo,所以專案的資料夾結構和Grunt配置都做了最大程度的簡化,目的就是為了方便理解,本文提供的不是一個解決方案,而是一個思路,在你有需要的時候可以參考改進應用到自己的專案當中,在前端工程化這一塊,要用到的外掛比demo裡面要用到的多的多,你得按需配置。就demo而言,最核心的外掛當然是grunt-contrib-connect和grunt-connect-proxy,但是要完成demo,也離不開一些其它的外掛:
load-grunt-tasks:我用它一次性載入package.json裡面的所有外掛:
grunt-contrib-copy:我用它來複制src裡面的內容,貼上到dist目錄下:
只要執行grunt copy任務,就會看到專案結構了多了一個dist資料夾:
grunt-contrib-watch: 我用它監聽檔案的改變,並自動執行定義的grunt任務,同時還可以通過livereload自動重新整理瀏覽器頁面:
grunt-replace:我用它來替換檔案中某些特殊字串,這樣就能夠在不手動更改原始碼的情況下改變程式碼。非代理模式之所以能請求到本地的靜態json資料,並不是因為我手動改變了請求地址,而是改變了請求地址處理函式中的處理規則,這個規則的改變實際上就是通過grunt-replace來做的:
替換的規則通過getReplaceOptions這個函式來配置:
注意註釋部分的說明,所謂的本地模式,其實就是執行grunt staticServer的時候,代理模式就是執行grunt proxyServer的時候,這段註釋要求在執行grunt staticServer之前必須先把API_NAME改成api,把DEVELOP_MODE改成true,只有這樣那些需要代理的請求才會請求本地的json檔案,在執行grunt proxyServer之前必須先把API_NAME改成provider,把DEVELOP_MODE改成false,只有這樣才能正確地將需要代理的請求進行轉發。
3. 重點:grunt-contrib-connect和grunt-connect-proxy的配置
在grunt任務配置中,通常每個外掛都會配置成一個任務,但是grunt-connect-proxy不是這樣,它是與grunt-contrib-connect一起配置的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
connect: { options: { port: '9001', hostname: 'localhost', protocol: 'http', open: true, base: { path: './', options: { index: 'html/index.html' } }, livereload: true }, proxies: [ { context: '/' + API_NAME, host: 'localhost', port: '9002', https: false, changeOrigin: true, rewrite: proxyRewrite } ], default: {}, proxy: { options: { middleware: function (connect, options) { if (!Array.isArray(options.base)) { options.base = [options.base]; } // Setup the proxy var middlewares = [require('grunt-connect-proxy/lib/utils').proxyRequest]; // Serve static files. options.base.forEach(function (base) { middlewares.push(serveStatic(base.path, base.options)); }); // Make directory browse-able. /*var directory = options.directory || options.base[options.base.length - 1]; middlewares.push(connect.directory(directory)); */ return middlewares; } } } } |
在以上配置中:
options節是通用的配置,用來配置要啟動的靜態伺服器資訊,port表示埠,hostname表示主機地址,protocol表示協議比如http,https,open表示靜態服務啟動之後是否以預設瀏覽器開啟首頁base.options.index指定的頁面,base.path用來配置站點的根目錄,demo中把根目錄配置成了當前的專案資料夾(./);
以上配置都在配置grunt-contrib-connect任務裡面,但是上面配置中的proxies節其實是grunt-connect-proxy需要的,用來配置代理資訊:context配置需要被代理的請求字首,通常配置成/開頭的一段字串,比如/provider,這樣相對站點根目錄的並以provider開頭的請求都會被代理到;host,port,https用來配置要代理到的服務地址,埠以及所使用的協議;changeOrigin配置成true即可;rewrite用來配置代理規則,proxyRewrite這個變數在配置檔案的前面有定義:
意思就是把client端裡provider開頭的部分,替換成代理服務的/provider/目錄來處理,注意/provider/這個字串最後的斜槓不能省略!比如client裡有一個請求http://localhost:9001/provider/query/json/language/list,就會被代理到http://localhost:9002/provider/query/json/language/list來處理;
default是一個connect任務的目標,用它啟動靜態服務;
proxy也是一個connect任務的目標,用它啟動代理服務,由於在demo裡,watch任務和connect任務都啟用了livereload,所以要在proxy任務里加上一個middleware中介軟體的配置,才能保證正確啟動代理,這段程式碼是官網的提供的,直接使用即可。裡面有一個serveStatic模組,在配置檔案的前面已經引入過:
這個是grunt啟動靜態服務必須的,照著用就行了。
最後看下靜態服務和代理服務的相關任務定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
grunt.registerTask('staticServer', '啟動靜態服務......', function () { grunt.task.run([ 'copy', 'replace', 'connect:default', 'watch' ]); }); grunt.registerTask('proxyServer', '啟動代理服務......', function () { grunt.task.run([ 'copy', 'replace', 'configureProxies:proxy', 'connect:proxy', 'watch' ]); }); |
在配置代理服務的時候,’configureProxies:proxy’一定要加,而且要加在connect:proxy之前,否則代理配置還沒有註冊成功,靜態服務就啟動完畢了,configureProxies這個任務並不是在配置檔案中配置的,而是grunt-connect-proxy外掛裡面定義的,只要grunt-connect-proxy被載入進來,這個任務就能用。
4. 如何傳送請求
這部分看看如何傳送請求,開啟首頁,會看到底部引用了4個js檔案:
其中util.js封裝了處理請求地址的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var DEVELOP_MODE = '@@DEVELOP_MODE'; var Util = (function(){ var BASE_URL = location.protocol + '//' + location.hostname + (location.port == '' ? '' : (':' + location.port)) + '/' + '@@CONTEXT_PATH'; return { api: function (requestPath) { var pathParts = requestPath.split('?'); pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : ''); return BASE_URL + '@@API_NAME/' + pathParts.join('?'); } } })(); |
這是原始碼,還記得那個replace的任務嗎,它的替換規則是
replace任務會把檔案中以@@開頭,按照patterns裡面的配置,將匹配到的字串替換成對應的串。在本地模式下,API_NAME是api,DEVELOP_MODE是true,CONTEXT_PATH始終是空,經過replace任務處理之後,util.js的程式碼會變成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var DEVELOP_MODE = 'true'; var Util = (function(){ var BASE_URL = location.protocol + '//' + location.hostname + (location.port == '' ? '' : (':' + location.port)) + '/' + ''; return { api: function (requestPath) { var pathParts = requestPath.split('?'); pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : ''); return BASE_URL + 'api/' + pathParts.join('?'); } } })(); |
在代理模式下,API_NAME是provider,DEVELOP_MODE是false,util.js經過replace之後就會變成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var DEVELOP_MODE = 'false'; var Util = (function(){ var BASE_URL = location.protocol + '//' + location.hostname + (location.port == '' ? '' : (':' + location.port)) + '/' + ''; return { api: function (requestPath) { var pathParts = requestPath.split('?'); pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : ''); return BASE_URL + 'provider/' + pathParts.join('?'); } } })(); |
這樣同一個請求地址,比如query/json/language/list,經過Util.api處理之後:
Util.api(‘query/json/language/list’)
在本地模式下就會返回:http://localhost:9001/api/query/json/language/list.json
在代理模式下返回:http://localhost:9001/provider/query/json/language/list
ajax.js對jquery的ajax進行了一下包裝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
var Ajax = (function(){ function create(_url, _method, _data, _async, _dataType) { //新增隨機數 if (_url.indexOf('?') > -1) { _url = _url + '&rnd=' + Math.random(); } else { _url = _url + '?rnd=' + Math.random(); } //為請求新增ajax標識,方便後臺區分ajax和非ajax請求 _url += '&_ajax=true'; return $.ajax({ url: _url, dataType: _dataType, async: _async, method: (DEVELOP_MODE == 'true' ? 'get' : _method), data: _data }); } var ajax = {}, methods = [ { name: 'html', method: 'get', async: true, dataType: 'html' }, { name: 'get', method: 'get', async: true, dataType: 'json' }, { name: 'post', method: 'post', async: true, dataType: 'json' }, { name: 'syncGet', method: 'get', async: false, dataType: 'json' }, { name: 'syncPost', method: 'post', async: false, dataType: 'json' } ]; for(var i = 0, l = methods.length; i ) { ajax[methods[i].name] = (function(i){ return function(){ var _url = arguments[0], _data = arguments[1], _dataType = arguments[2] || methods[i].dataType; return create(_url, methods[i].method, _data, methods[i].async, _dataType); } })(i); } //window.Ajax = ajax; return ajax; })(); |
提供了Ajax.get,Ajax.post,Ajax.syncGet,Ajax.syncPost以及Ajax.html這五個方法,之所以要封裝成這樣原因有2個:
第一是,統一加上隨機數和ajax請求的標識:
第二是,grunt-contrib-connect所啟動的靜態服務,只能傳送get請求,不能傳送post請求,所以如果在程式碼中有寫$.post的呼叫就無法脫離後端服務執行起來,會報405 Method not Allowed的錯誤,而這個封裝可以把Ajax.post這樣的請求,在本地模式的時候全部替換成get方式來處理:
這其實還是replace任務的功勞!
index.js就是首頁發請求的js了,可以看看:
1 2 3 4 5 |
Ajax.get(Util.api('query/json/language/list')).done(function(response){ console.log(response.data); }).fail(function(){ console.log('請求失敗'); }); |
結合util.js和ajax.js,相信你很快就能明白這個過程了。
5. 線上如何部署前後端的服務
答案還是代理。開發期間,前端通過grunt-connect-proxy把某個名稱空間下的請求全部代理到了後端服務來處理,線上部署的時候後端把專案部署到tomcat這種web伺服器裡,前端把專案部署到Nginx伺服器,然後請運維人員按照開發期間的代理規則,在Nginx伺服器上加反向代理的配置,把瀏覽器請求前端的那些需要後端支援的請求,全部代理到tomcat伺服器下的後端服務來處理。也就是說線上部署跟開發期間的互動原理是一樣的,只不過代理的提供者變成Nginx而已。
6. 小結
本文總結自己這段時間做一個前後端分離的專案的一些環境準備方面的經驗,文中提到的方法幫助我們解決了跨域和前端獨立執行的兩大問題,現在專案開發的情況非常順利,所以從我自身的實踐來說,本文的內容是比較有參考價值的,希望能夠幫助到有需要的人,謝謝閱讀:)