前后端分離這個詞一點都不新鮮,完全的前后端分離在崗位協作方面,前端不寫任何后台,后台不寫任何頁面,雙方通過接口傳遞數據完成軟件的各個功能實現。此種情況下,前后端的項目都獨立開發和獨立部署,在開發期間有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一起配置的:
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啟動靜態服務必須的,照着用就行了。
最后看下靜態服務和代理服務的相關任務定義:
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封裝了處理請求地址的功能:
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的代碼會變成:
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之后就會變成:
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進行了一下包裝:
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 < l; 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了,可以看看:
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. 小結
本文總結自己這段時間做一個前后端分離的項目的一些環境准備方面的經驗,文中提到的方法幫助我們解決了跨域和前端獨立運行的兩大問題,現在項目開發的情況非常順利,所以從我自身的實踐來說,本文的內容是比較有參考價值的,希望能夠幫助到有需要的人,謝謝閱讀:)