引言
在前端開發過程中,開發者通常都會遇到前端數據不能正常獲取的問題,這就需要開發者之間’想辦法‘搞到這些數據;開發過程中我們可能遇到的場景:
-
后端接口數據開發中暫時不可用,需要前端在自己本地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代理,主要利用其提供的相關事件完成,比如data
、end
和error
事件等,下面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直接將內容返回而沒有返回響應頭;一般情況下都能滿足要求,不能滿足的可以根據具體使用場景來具體修改。
上面講述的內容有什么不妥之處,還請各位斧正!!!