利用grunt-contrib-connect和grunt-connect-proxy搭建先後端分離的開發環境

先後端分離這個詞一點都不新鮮,徹底的先後端分離在崗位協做方面,前端不寫任何後臺,後臺不寫任何頁面,雙方經過接口傳遞數據完成軟件的各個功能實現。此種狀況下,先後端的項目都獨立開發和獨立部署,在開發期間有2個問題不可避免:第一是前端調用後臺接口時的跨域問題(由於先後端分開部署);第二是前端脫離後臺服務後沒法獨立運行。本文總結最近一個項目的工做經驗,介紹利用grunt-contrib-connect和grunt-connect-proxy搭建先後端分離的開發環境的實踐過程,但願能對你有所幫助。php

注:css

(1)本文的相關內容需對前端構建工具grunt有所瞭解:http://www.gruntjs.net/getting-started,這個工具能夠完成前端全部的工程化工做,包括代碼和圖片壓縮,文件合併,靜態資源替換,js混淆,less和sass編譯成css等等,推薦沒有用過相似工具的前端開發人員去了解。html

(2)grunt-contrib-connect和grunt-connect-proxy是grunt提供的兩個插件,前者能夠啓動一個基於nodejs的靜態服務器,這樣前端就能脫離後端經過web服務的方式來訪問本身開發的東西;後者能夠把前端項目裏面某些特殊的請求代理到其它服務器,哪些請求可以經過代理轉發到別的服務器,這個規則都是可配置的,這樣就能把一些跟後臺交互的請求經過代理的方式,在開發期間,轉發到後端的服務來處理,從而避免跨域問題。前端

代碼下載java

1. 效果演示

在前面提供的代碼中,裏面有兩個文件夾:node

image

分別表明先後端獨立運行的兩個項目,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等傳統後端項目是同樣的。jquery

爲了看到請求被代理轉發的效果,請先在server文件夾下啓動服務,命令是:grunt staticServer:web

image

只要看到跟截圖運行相似的結果,就表示server端的服務啓動成功。從截圖中還能看到server端的服務的訪問地址是:http://localhost:9002/ajax

而後在client文件夾下啓動配置了代理的服務,命令是:grunt proxyServer:npm

image

只要看到跟截圖運行相似的結果,就表示client端的服務啓動成功。從截圖中能看到client端服務的訪問地址是:http://localhost:9001/,同時還能夠看到服務代理的配置:

image

這段運行結果說明,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打開開發者工具,若是在控制檯看到以下相似的消息,就說明首頁裏的請求正確地經過代理請求到了服務端的數據:

image

在client的首頁裏面,我發起了一個ajax請求,請求地址爲http://localhost:9001/provider/query/json/language/list,在client文件夾下根本不存在provider文件夾,因此若是沒有代理的話,這個請求確定會報404的錯誤;它之因此可以正確的加載,徹底是由於經過代理,請求到了server文件夾下相應的文件:

image

若是不經過代理,在localhost:9001/的服務裏,請求localhost:9002/的數據是確定會有跨域問題的,而代理能夠完美的解決這個問題。

前面這一小部分演示了demo裏面如何經過代理來解決跨域問題,下面一部分演示如何在脫離後端服務的狀況下如何正常運行前端項目,首先請關閉以前打開的client服務和server端服務以及瀏覽器打開的client頁面,而後打開client/Gruntfile.js文件,找到如下部分:

image

把provider改爲api,把false改爲true;

接着在client文件夾,運行非代理的靜態服務,這個服務不會配置代理,啓動命令是:grunt staticServer:

image

打開瀏覽器的開發者工具,在控制檯應該能夠看到以下消息:

image

這個過程是:原來經過代理請求地址是: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文件:

image

也就是說我會把跟服務端全部接口的返回的數據都按相同的路徑,在本地以json文件的形式存在api文件夾下,在沒有代理的時候,只要請求這些json文件,就能保證我全部的操做都能正確請求到數據,前端的項目也就能脫離代理運行起來了,固然這個模式下的數據都是靜態的了。

接下來我會介紹如何前面這些內容的實現細節,只介紹client裏面的要點,server裏面的內容很簡單,只要搞清楚了client,server一看就懂:)

2. Grunt配置

在瞭解配置以前,先要熟悉項目的文件夾結構:

image

僅僅是爲了完成demo,因此項目的文件夾結構和Grunt配置都作了最大程度的簡化,目的就是爲了方便理解,本文提供的不是一個解決方案,而是一個思路,在你有須要的時候能夠參考改進應用到本身的項目當中,在前端工程化這一塊,要用到的插件比demo裏面要用到的多的多,你得按需配置。就demo而言,最核心的插件固然是grunt-contrib-connect和grunt-connect-proxy,可是要完成demo,也離不開一些其它的插件:

load-grunt-tasks:我用它一次性加載package.json裏面的全部插件:

image

grunt-contrib-copy:我用它來複制src裏面的內容,粘貼到dist目錄下:

image

只要運行grunt copy任務,就會看到項目結構了多了一個dist文件夾:

image

grunt-contrib-watch: 我用它監聽文件的改變,並自動執行定義的grunt任務,同時還能夠經過livereload自動刷新瀏覽器頁面:

image

grunt-replace:我用它來替換文件中某些特殊字符串,這樣就可以在不手動更改源代碼的狀況下改變代碼。非代理模式之因此能請求到本地的靜態json數據,並非由於我手動改變了請求地址,而是改變了請求地址處理函數中的處理規則,這個規則的改變實際上就是經過grunt-replace來作的:

image

替換的規則經過getReplaceOptions這個函數來配置:

image

注意註釋部分的說明,所謂的本地模式,其實就是運行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這個變量在配置文件的前面有定義:

image

意思就是把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模塊,在配置文件的前面已經引入過:

image

這個是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文件:

image

其中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的任務嗎,它的替換規則是

image

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請求的標識:

image

第二是,grunt-contrib-connect所啓動的靜態服務,只能發送get請求,不能發送post請求,因此若是在代碼中有寫$.post的調用就沒法脫離後端服務運行起來,會報405 Method not Allowed的錯誤,而這個封裝能夠把Ajax.post這樣的請求,在本地模式的時候所有替換成get方式來處理:

image

這其實仍是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. 小結

本文總結本身這段時間作一個先後端分離的項目的一些環境準備方面的經驗,文中提到的方法幫助咱們解決了跨域和前端獨立運行的兩大問題,如今項目開發的狀況很是順利,因此從我自身的實踐來講,本文的內容是比較有參考價值的,但願可以幫助到有須要的人,謝謝閱讀:)

代碼下載

相關文章
相關標籤/搜索