前端工程化之动态数据代理

引言

在前端开发过程中,开发者通常都会遇到前端数据不能正常获取的问题,这就需要开发者之间’想办法‘搞到这些数据;开发过程中我们可能遇到的场景:

  • 后端接口数据开发中暂时不可用,需要前端在自己本地mock接口数据进行开发

  • 重构一个已有的前端功能,在测试环境开发功能,这时可能需要使用测试环境提供的数据来进行开发

  • 解决线上问题,需要本地开启服务访问线上数据

  • 访问某个服务资源时,用另一个服务器上的资源提供服务

  • 本地服务访问某个具体环境的数据时需要带上某些具体认证信息,如cookie信息等

  • .....

类似这样的场景可能还有其他的情况,其实他们归结到一个问题就是:http代理。我们可以使用http代理来解决前端开发过程中数据获取的问题,下面就来讲讲各个工具中http代理的动态实现,其实原理都是一样的。

http代理

http代理的具体原理就不在本文中讲述了,具体可以参考这篇文章HTTP 代理原理及实现(一)

http代理可以分为 普通代理隧道代理。首先说明一下,我们这里只讲述http普通代理。

何为普通代理?

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指令;

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直接将内容返回而没有返回响应头;一般情况下都能满足要求,不能满足的可以根据具体使用场景来具体修改。

上面讲述的内容有什么不妥之处,还请各位斧正!!!

参考文献

posted @ 2017-01-20 19:27  wonyun  阅读(5816)  评论(1编辑  收藏  举报