前端工程化之動態數據代理

引言

在前端開發過程當中,開發者一般都會遇到前端數據不能正常獲取的問題,這就須要開發者之間’想辦法‘搞到這些數據;開發過程當中咱們可能遇到的場景:javascript

  • 後端接口數據開發中暫時不可用,須要前端在本身本地mock接口數據進行開發html

  • 重構一個已有的前端功能,在測試環境開發功能,這時可能須要使用測試環境提供的數據來進行開發前端

  • 解決線上問題,須要本地開啓服務訪問線上數據java

  • 訪問某個服務資源時,用另外一個服務器上的資源提供服務node

  • 本地服務訪問某個具體環境的數據時須要帶上某些具體認證信息,如cookie信息等webpack

  • .....git

相似這樣的場景可能還有其餘的狀況,其實他們歸結到一個問題就是:http代理。咱們可使用http代理來解決前端開發過程當中數據獲取的問題,下面就來說講各個工具中http代理的動態實現,其實原理都是同樣的。github

http代理

http代理的具體原理就不在本文中講述了,具體能夠參考這篇文章HTTP 代理原理及實現(一)web

http代理能夠分爲 普通代理隧道代理。首先說明一下,咱們這裏只講述http普通代理。npm

何爲普通代理?

http客戶端向代理服務器發送http報文,代理服務器作一箇中間的處理,好比處理一下請求或者連接,而後向服務器發送請求,並將收到的響應轉發給客戶端。

其實,普通的http代理更多扮演’中間人‘的角色,對於客戶端來講,它是服務端;對於真正要連接服務端來講它是客戶端,它負責在客戶端和服務器兩端來回傳送http報文。能夠借用上文中的一幅圖來講明:

普通代理其實又能夠分爲兩種狀況:

正向代理

正向代理通俗的說就是客戶端要訪問真正的服務器A,代理在中間進行請求響應的轉發,對服務器A來講,代理隱藏了客戶端的具體信息,客戶端對服務器A來講是透明的,不過代理能夠設置X-Forwarded-IP來告訴服務器A真正的客戶端IP

反向代理

與正向代理相反的是反向代理代理真正的服務器。 例如客戶端訪問服務器A時,實際上訪問的是代理服務器,代理服務器收到請求後而後再向真正提供服務的服務器發送請求,並將響應轉發給客戶端,這樣對客戶端來講隱藏了真正提供服務的服務器的IP和端口;

通常使用反向代理時,須要修改DNS讓域名解析到代理服務器IP。最多見的反向代理就是Nginx服務器,經過它的proxy_pass來將請求轉發到真正的提供服務的服務器。

就前端在本地開發過程當中涉及的代理通常都是正向代理,反向代理用的比較少;具體的作法是:

代理服務器經過nodejs經過`http.request(options, callback)`建立一個新的request請求來與服務器通訊,從而實現代理服務器向服務器發送請求,而後服務器返回的響應經過代理服務器response來轉發服務器的響應。

下面就以幾種前端經常使用的工具爲例中來描述動態數據代理的實現。

fis動態代理的實現

fis不管是fis2仍是fis3都是支持設置動態代理,工具設計之初都有考慮支持數據mock代理的功能的,具體能夠參考Mock假數據模擬都有詳細的介紹。

不知用過fis的同窗注意到沒有,在fis本地的服務器工程目錄(mac下默認是/Users/當前用戶/.fis-tmp/www)下有一個server.js文件,其就是用來支持動態代理前端數據用的。

經過server.js代碼,能夠看出fis支持mock前端數據須要提供一個server.conf文件(其目錄默認是在當前項目根目錄的config目錄下),經過三種指令rewrite、redirect和proxy來完成前端不一樣要求的數據mock代理;其實這三種指令是fis提供的相似語法糖的概念。

  • rewrite:因爲某些緣由,如驗證問題或者cookie問題須要重寫原有基礎上的請求響應
  • redirect:重定向到一個新的頁面網址
  • proxy:用其餘服務器上的api地址響應當前api接口

下面就描述一下fis的動態數據代理,這須要rewrite指令;

一、首先須要在server.conf文件中定義rewrite規則。

rewrite ^\/api /mock/mock.js

上面rewrite規則表面,當前本地服務的全部以/api開頭的接口pathname都會通過根目錄的mock目錄下的mock.js進行重寫。

二、重寫原有基礎的請求響應。

這一步能夠完成不少重要的做用,例如一個場景就是本地開啓的服務想訪問測試環境或者線上環境同pathname的api接口,這些環境的各類api接口服務須要經過cookie攜帶的登陸信息認證纔可使用,這時因爲跨域沒法攜帶本地cookie到指定的環境致使mock數據不能成功;

固然還有其餘不少場景如跨域、或者帶有某些邏輯的返回指定響應的狀況登登;解決這些問題通常經常使用的作法是:

http.request新建立一個http. ClientRequest實例,用新建立的請求響應實例來完成真正意義上的與接口服務器進行數據請求與響應通訊;由本地的請求響應實例來與本地客戶端通訊,接受客戶端的請求並將代理獲取的數據響應給本地客戶端。

利用http.request實現前端數據mock代理,主要利用其提供的相關事件完成,好比dataenderror事件等,下面mock.js中代碼展現了重寫本地服務的請求與響應使其帶上cookie認證信息,可以mock測試環境的api接口數據。

var http = require('http');
module.exports = function(req, res, next) {
        res.charset = 'utf8';
        res.setHeader('Content-Type', "application/json;charset=utf8");

        var buf = '';
        req.on('data', function(chunk){ buf += chunk; });
        req.on('end', function(){
                //proxy
                var beta = 'betaa.qunar.com';
                var options = {
                        hostname: beta,
                        port: 80,
                        path: req.originalUrl,
                        method: req.method,
                        headers: Object.assign({},  req.headers, {
                                'host':beta,
                                'Origin':beta,
                                'referer':beta,
                                'cookie': 'xxxx' // your login cookie info here
                        })
                };
                //在本地請求內容接受完畢後,新建一個http.request來負責與真正提供api服務數據的服務器通訊
                var _req = http.request(options, function(_res){
                        var data = "";
                        _res.setEncoding('utf8');
                        _res.on('data', function(chunk){//代理響應接受到服務器數據返回
                                data += chunk ;
                        })
                        .on('end', function(){//提供數據服務的數據接受完畢
                                res.end(data); // 由本地的響應實例來響應代理服務器接受到的數據內容
                        })
                }).on('error', function(error){
                        res.end(); //本地響應實例返回空內容
                });
                _req.write(buf); //由http.request生成的請求實例來完成請求真正的提供數據服務的服務器
                _req.end();
        })
}

dora動態代理的實現

咱們的後臺系統使用dva + antd來搭建,使用過 dva的同窗應該知道,官方推薦使用dora來搭建本地開發環境,包括本地開發服務器、webpack編譯、hmr以及數據代理proxy等等。

dora使用代理時,須要在項目根目錄下默認提供一個proxy.config.js文件,在該文件中配置前端數據代理的一些靜態和動態的數據代理,如:

'/api/user': require('./mock/user.json'),
'POST /api/login/info: {username: 'test', ret: true}
'/api/*': function(req, res){...}

具體瞭解請到dora-plugin-proxy查看,裏面由對配置規則的詳解。

dora中使用的proxy代理插件,其內部是使用阿里開源的一個代理服務器新輪子anyproxy,其提供了3類的接口能夠參考anyproxy規則接口查看。在dora-plugin-proxy內部實現中覆蓋了一些接口用於代理本地響應。

具體細節能夠看dora-plugin-proxy的源碼,下面就看一下dora代理的動態代理實現以下,仍是借上面代理的功能:

var http = require('http');
module.exports = {
  '/api/*': function(req, res){
                res.charset = 'utf8';

                var buf = req.body; //dora-plugin-proxy對req、res進行了封裝
    
                var beta = 'betaa.qunar.com';
                var options = {
                        hostname: beta,
                        port: 80,
                        path: req.originalUrl,
                        method: req.method,
                        headers: Object.assign({},  req.headers, {
                                'host':beta,
                                'Origin':beta,
                                'referer':beta,
                                'cookie': 'xxxx' // your login cookie info here
                        })
                };

                //新建一個http.request來負責與真正提供api服務數據的服務器通訊
                var _req = http.request(options, function(_res){
                        var data = "";
                        _res.setEncoding('utf8');
                        _res.on('data', function(chunk){//代理響應接受到服務器數據返回
                                data += chunk ;
                        })
                        .on('end', function(){//提供數據服務的數據接受完畢
                                res.end(data); // 由本地的響應實例來響應代理服務器接受到的數據內容
                        })
                }).on('error', function(error){
                        res.end(); //本地響應實例返回空內容
                });
                _req.write(buf); //由http.request生成的請求實例來完成請求真正的提供數據服務的服務器
                _req.end();
          }
}

細心的同窗可能從上面代碼中看出了其代理實現與fis動態代理的區別:獲取本地服務器的請求內容的方式不太同樣,直接使用req.body來獲取請求內容而不是利用事件實現。why ?

這是由於anyproxy的內部實現中,對http請求響應進行了封裝,具體說對request實例添加了**params**、**query**和**body**屬性,重寫了response使其只有5個方法的對象:
  • set(object|key, value) : 用於設置response響應頭
  • type(json|html|text|png|...) :用於專門設置響應頭中Content-Type屬性的值
  • status(200|404|304):用於設置響應的最後返回http狀態碼
  • json(jsonData): 用於將數據以json格式返回
  • jsonp(jsonData[, callbackQueryName]):用於將返回的json數據以jsonp格式返回
  • end(string|object):用於響應客戶內容並結束

這樣,dora中動態代理就能夠直接經過訪問request中的body屬性就能夠輕鬆獲取請求的內容了。

webpack-dev-server動態代理的實現

webpack-dev-server是與webpack配套的搭建本地輕量級服務器的,內部使用webpack-dev-middlemare來提供webpack的bundle,以此提供能夠訪問webpack打包生成的靜態資源的web服務。詳細的webpack-dev-server介紹能夠參考webpack dev server.cn,也能夠參考其官網。 本節就講講webpack-dev-server的前端數據代理實現。

webpack-dev-server在設計的時候就充分考慮了數據代理的實現,內部使用http-proxy-middleware來實現數據代理;http-proxy-middleware提供了不少配置項,經過提供的簡單配置就能完成幾乎大多數狀況下的數據代理。

webpack-dev-server中代理的使用方式有兩種,這跟webpack-dev-server使用是同樣的:

命令行CLI形式

此形式是在命令行中執行webpack-dev-server命令,能夠添加各類配置項,如

webpack-dev-server --inline --hot --config webpack.config.dev.js

固然它還有其餘一些配置項,具體能夠到官網上查看;固然也能夠在webpack的配置文件webpack.config.js中配置devServer配置項,用於表示webpack-dev-server的配置,其優先級比命令行低,也就是說命令行CLI和webpack.config.js中同時配置,命令行CLI形式會覆蓋它。 webpack中的devServer配置以下:

...
module: {...},
plugin: [...],
devServer: {
    hot: true,
    inline: true,
    config: 'webpack.config.dev.js',
    proxy: {
        target: 'http://beta.qunar.com',
        secure: false,
        changeOrigin: true
        ...
    }
    ...
}

這樣能夠在項目根目錄下package.json配置以下, 而後在命令行執行npm start命令就能夠啓動webpack-dev-server服務了,配置的代理也可使用了。

"scripts": {
    "start": "webpack-dev-server --inline --hot --config webpack.config.js"
  }

node API的形式

這種形式就是使用webpack-dev-server當成npm包同樣,使用其提供的node api形式來建立一個web服務,具體能夠參考官網的一個例子:

var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var webpackCfg = require('./webpack.config.js');

var compiler = webpack(webpackCfg);

var server = new WebpackDevServer(compiler, {
  // webpack-dev-server options
  contentBase: "/path/to/directory",
  hot: true,
  historyApiFallback: false,
  compress: true,
  proxy: {
    "**": "http://localhost:8080"
  },
  clientLogLevel: "info",
  // webpack-dev-middleware options
  quiet: false,
  noInfo: false,
  lazy: true,
  filename: "bundle.js",
  watchOptions: {
    aggregateTimeout: 300,
    poll: 1000
  },
  // It's a required option.
  publicPath: "/assets/",
  headers: { "X-Custom-Header": "yes" },
  stats: { colors: true }
});
server.listen(8080, "localhost", function() {});

可將上面代碼置於一個js文件中如devServer.js,那麼在package.json中像下面配置一下,而後經過npm start就能夠其中服務了。

"scripts": {
    "start": "node devServer.js"
  }

那麼話說回來了,相似上面fis與dora中爲當前請求添加有關登陸信息cookie從而使用測試環境的數據,在webpack-dev-server中如何實現呢?

既然webpack-dev-server對數據代理有充分的支持,因此相似上面的功能在webpack-dev-server中很容易實現,經過簡單的配置便可:

devServer: {
    ...
    proxy: {//代理相關的配置
      '/api/**': {
        target: 'http://beta.qunar.com',
        changeOrigin: true,
        secure: false,
        headers: {
          "Cookie": '...' // your login cookie info here
        }
      }
    }
}

webpack-dev-server能夠很輕鬆的經過配置能完成相關數據代理,那麼問題來了,有些場景可能須要一些額外的處理邏輯,須要配置動態代理,在其中處理相關業務邏輯;

那麼webpack-dev-server能像fis和dora那樣配置動態的代理麼?

剛開始,查看http-proxy-middleware相關配置項,沒有發現有專門知足的配置項。無心間看到了bypass這個配置項,其配置的function它能夠訪問請求的request和response對象;可是bypass這個屬性的意義是配置一些請求跳過代理,貌似與咱們要求不太符合。

最後看了webpack-dev-server內部bypass實現的源碼:

options.proxy.forEach(function(proxyConfig) {
    var bypass = typeof proxyConfig.bypass === 'function';
    var context = proxyConfig.context || proxyConfig.path;
    var proxyMiddleware;
    // It is possible to use the `bypass` method without a `target`.
    // However, the proxy middleware has no use in this case, and will fail to instantiate.
    if(proxyConfig.target) {
        proxyMiddleware = httpProxyMiddleware(context, proxyConfig);
    }

    app.use(function(req, res, next) {
        var bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;

        if(bypassUrl) {
            req.url = bypassUrl;
            next();
        } else if(proxyMiddleware) {
            return proxyMiddleware(req, res, next);
        }
    });
});

從其源碼實現中,咱們能夠得出一個結論:

webpack-dev-server的proxy代理配置項中若沒有配置target屬性,而且bypass對應的屬性值不返回值或者返回false,那麼就不會走http-proxy-middleware代理中間件,也就是說沒有走webpack-dev-server真正的代理。

鑑於上面這一結論,由於bypass配置的函數是會執行一遍的,那麼咱們能夠在bypass配置項的內容中用http.request來生成新的http request對象來完成動態的數據代理,從而能夠實現一些場景邏輯。例如相似fis功能代碼邏輯以下:

devServer: {
 ...
 proxy: {
    "/api/**": {
        secure: false,
        changeOrigin: true,
        bypass: function(req, res) {
            res.charset = 'utf8';
            var buf = '';
            req.on("data", function(thunk){
              buf += thunk;
            })
            .on("end", function(){
                var http = require('http');
                var testHost = 'beta.qunar.com';
                var options = {
                    hostname: testHost,
                  port: 80,
                  path: req.originalUrl,
                  method: req.method,
                  headers: Object.assign({}, req.headers, {
                    'host': testHost,
                    'origin': testHost,
                    'referer': testHost,
                    'Cookie': ""  //your login cookie here
                  })
                };

            var _req = http.request(options, function(_res) {

              var body = "";
              _res.on("data", function(chunk){
                body += chunk;
              })
              .on("end", function(){
                res.end(body);
              })
            }).on("error", function(){
              res.end();
            });
            _req.write(buf);
            _req.end();
        });
    }
  }
}

總結

上面不一樣工具下的動態數據代理可能存在必定的問題,就是在提供數據服務的響應實例返回的響應頭後被丟棄了,代理服務器生成的響應reponse直接將內容返回而沒有返回響應頭;通常狀況下都能知足要求,不能知足的能夠根據具體使用場景來具體修改。

上面講述的內容有什麼不妥之處,還請各位斧正!!!

參考文獻

相關文章
相關標籤/搜索