1. 程式人生 > >前端工程化之動態資料代理

前端工程化之動態資料代理

引言

在前端開發過程中,開發者通常都會遇到前端資料不能正常獲取的問題,這就需要開發者之間’想辦法‘搞到這些資料;開發過程中我們可能遇到的場景:

  • 後端介面資料開發中暫時不可用,需要前端在自己本地mock介面資料進行開發

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

  • 解決線上問題,需要本地開啟服務訪問線上資料

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

  • 本地服務訪問某個具體環境的資料時需要帶上某些具體認證資訊,如cookie資訊等

  • .....

類似這樣的場景可能還有其他的情況,其實他們歸結到一個問題就是:http代理。我們可以使用http代理來解決前端開發過程中資料獲取的問題,下面就來講講各個工具中http代理的動態實現,其實原理都是一樣的。

http代理

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

http代理可以分為 普通代理隧道代理。首先說明一下,我們這裡只講述http普通代理。

何為普通代理?

http客戶端向代理伺服器傳送http報文,代理伺服器做一箇中間的處理,比如處理一下請求或者連結,然後向伺服器傳送請求,並將收到的響應轉發給客戶端。

其實,普通的http代理更多扮演’中間人‘的角色,對於客戶端來說,它是服務端;對於真正要連結服務端來說它是客戶端,它負責在客戶端和伺服器兩端來回傳送http報文。可以借用上文中的一幅圖來說明:
408483-20170109210136791-536319893.png

普通代理其實又可以分為兩種情況:

正向代理

正向代理通俗的說就是客戶端要訪問真正的伺服器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指令;

1、首先需要在server.conf檔案中定義rewrite規則。

rewrite ^\/api /mock/mock.js

上面rewrite規則表面,當前本地服務的所有以/api開頭的介面pathname都會經過根目錄的mock目錄下的mock.js進行重寫。

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

這一步可以完成很多重要的作用,例如一個場景就是本地開啟的服務想訪問測試環境或者線上環境同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直接將內容返回而沒有返回響應頭;一般情況下都能滿足要求,不能滿足的可以根據具體使用場景來具體修改。

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

參考文獻

  • Http模組
  • http 模組【學習Nodejs】
  • 用 node.js 做 HTTP 正向 & 反向代理
  • dora-plugin-proxy
  • [webpack] webpack-dev-server介紹及配置
  • webpack dev server.cn