React動態切換本地代理地址(devServer.proxy)
背景需求
在本地跑React專案時,呼叫的介面往往是跨域的,一般常用的是webpack-dev-server
提供的prxoy
代理功能。然而在每次切換代理環境後,都需要重新跑專案,對於開發人員來說太麻煩了。如果能夠在切換代理環境後,不用重跑專案,豈不是提升了開發體驗和減少了專案編譯的時間呢?
● webpack.devServer.config.js
'use strict'; const fs = require('fs'); // ... module.exports = function(proxy, allowedHost) { return { // ... proxy: { '/test': { target: 'http://47.115.13.227:8001', changeOrigin: true, }, '/user': { target: 'http://47.115.13.227:8002', changeOrigin: true, }, }, }; };
需求分析
根據專案的背景需求,分析了一下原因,之所以需要重跑專案,是因為修改配置後,webpack不會重新讀取我們修改後的代理配置檔案(webpack.devServer.config.js)。
那麼,這麼一分析下來話,想要解決這個問題,有兩種思路:
- 讓webpack監聽我們代理的配置檔案(webpack.devServer.config.js),一旦檔案有修改就重新熱載入;
- 讓webpack實時讀取配置檔案中的
proxy
配置,能夠在每次代理的時候實時讀取,不用每次重新載入。
基於這兩種思路,我去查閱了下webpack的官方文件。
查閱文件
devServer.proxy
在查閱了webpack官網中devServer
是用了http-proxy-middleware包去實現的,並且給出了它的官方文件
http-proxy-middleware
在http-proxy-middleware
中發現了這樣一個router
配置引數,意思是可以針對特殊的一些請求重新定位/代理
- option.router: object/function, re-target option.target for specific requests.
// Use `host` and/or `path` to match requests. First match will be used. // The order of the configuration matters. router: { 'integration.localhost:3000' : 'http://localhost:8001', // host only 'staging.localhost:3000' : 'http://localhost:8002', // host only 'localhost:3000/api' : 'http://localhost:8003', // host + path '/rest' : 'http://localhost:8004' // path only } // Custom router function (string target) router: function(req) { return 'http://localhost:8004'; } // Custom router function (target object) router: function(req) { return { protocol: 'https:', // The : is required host: 'localhost', port: 8004 }; } // Asynchronous router function which returns promise router: async function(req) { const url = await doSomeIO(); return url; }
其中router
可以傳遞函式並且支援async函式,那麼意味著,是不是webpack能夠實時讀取proxy
的配置呢。
驗證想法
為了驗證這個API,我先搭建了兩個node服務,再通過配置webpack.devServer.config.js中的proxy中動態的請求代理地址引數去驗證想法。
服務埠8001
如下,搭建埠為8001
的node服務有以下功能:
-
/getRouterProxyUrl
隨機返回8001
和8002
埠的代理地址, -
/test
,返回8001 succesed get test word!
const http = require('http');
const server8001 = http.createServer(function(req, res) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-type,Content-Length,Authorization,Accept,X-Requested-Width");
res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
let proxyUrl = '';
if (req.url == "/getRouterProxyUrl") {
if (Math.random() * 10 > 5) {
proxyUrl = 'http://47.115.13.227:8001';
} else {
proxyUrl = 'http://47.115.13.227:8002';
}
res.writeHead(200, {
'Content-type': 'text/plain;charset=UTF8',
});
res.end(proxyUrl);
} else if (req.url == "/test") {
res.writeHead(200, { 'Content-type': 'text/plain;charset=UTF8' });
res.end('8001 succesed get test word!');
} else {
res.writeHead(200, { 'Content-type': 'text/plain;charset=UTF8' });
res.end(`8001 hello,your request url is ${req.url}`);
}
console.debug(new Date(), `8001 req.url:${req.url}`);
console.debug(new Date(), `8001 proxyUrl:${proxyUrl}`);
});
server8001.listen('8001', function() {
console.log((new Date()) + 'Server is listening on port:', 8001);
})
服務埠8002
如下,埠為8002
的node服務有以下功能:
-
/test
返回8002 succesed get test word!
const http = require('http');
const server8002 = http.createServer(function(req, res) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-type,Content-Length,Authorization,Accept,X-Requested-Width");
res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
if (req.url == "/test") {
res.writeHead(200, { 'Content-type': 'text/plain;charset=UTF8' });
res.end('8002 succesed get test word!');
} else {
res.writeHead(200, { 'Content-type': 'text/plain;charset=UTF8' });
res.end(`8002 hello,your request url is ${req.url}`);
}
console.debug(new Date(), `8002 req.url:${req.url}`);
});
server8002.listen('8002', function() {
console.log((new Date()) + 'Server is listening on port:', 8002);
})
配置proxy
webpack.devServer.config.js檔案如下,通過getFecth
去請求動態的代理地址,router
中拿到getFecth
中請求到的代理地址再返回
'use strict';
const fs = require('fs');
const http = require('http');
// ...
const getFetch = () => {
return new Promise((resolve, reject) => {
http.get('http://47.115.13.227:8001/getRouterProxyUrl', res => {
let todo = '';
res.on('data', chunk => {
todo += chunk;
});
res.on('end', () => {
resolve(todo);
});
res.on('error', (error) => {
reject(error);
});
});
});
};
module.exports = function(proxy, allowedHost) {
return {
// ...
proxy: {
'/test': {
target: 'http://localhost:3000',
changeOrigin: true,
router: async function() {
const url = await getFetch();
return url;
},
},
}
};
};
前端請求
fetch('/test')
.then(response => {
if (!response.ok) {
throw 'Error';
}
return response.text();
})
.then(res => console.debug(res))
.catch(err => console.error(err));
請求報錯500 Internal Server Error
前端請求後,發現報了500 Internal Server Error 的錯誤
為了排除服務端的錯誤,使用postman對8001
埠的/getRouterProxyUrl
和/test
,還有8002埠的/test
,均發起請求驗證了下,都能正常返回正常的響應資料!
這就很納悶,可能還是在router代理地址這裡除了問題,但是通過打console.log
,發現getFetch
是能正常返回資料的。再根據錯誤提示,可以大致判斷是在router
裡邊呼叫介面導致出錯的。
TypeError: Cannot read property 'split' of undefined
at required (D:\workspace\Web\react-app\node_modules\requires-port\index.js:13:23)
at Object.common.setupOutgoing (D:\workspace\Web\react-app\node_modules\http-proxy\lib\http-proxy\common.js:101:7)
at Array.stream (D:\workspace\Web\react-app\node_modules\http-proxy\lib\http-proxy\passes\web-incoming.js:127:14)
at ProxyServer.<anonymous> (D:\workspace\Web\react-app\node_modules\http-proxy\lib\http-proxy\index.js:81:21)
at middleware (D:\workspace\Web\react-app\node_modules\http-proxy-middleware\lib\index.js:46:13)
at handle (D:\workspace\Web\react-app\node_modules\webpack-dev-server\lib\Server.js:322:18)
at D:\workspace\Web\react-app\node_modules\webpack-dev-server\lib\Server.js:330:47
at Layer.handle_error (D:\workspace\Web\react-app\node_modules\express\lib\router\layer.js:71:5)
at trim_prefix (D:\workspace\Web\react-app\node_modules\express\lib\router\index.js:315:13)
at D:\workspace\Web\react-app\node_modules\express\lib\router\index.js:284:7
- option.router: object/function, re-target option.target for specific requests.
// Asynchronous router function which returns promise
router: async function(req) {
const url = await doSomeIO();
return url;
}
這裡 await doSomeIO()
引起了我的注意,這個函式命名是不是意味著這裡非同步的路由函式只能是做一些I/O操作,並不支援呼叫介面呢?抱著這個疑問,我再查了下資料
有沒有可能是router返回的引數不正確,非同步函式中不應該是返回string字串。
於是程式碼改為如下,在router函式中呼叫非同步介面,測試後是不報錯的。
router: function() {
getFetch();
return {
protocol: 'http:', // The : is required
host: '47.115.13.227',
port: 8001,
};
},
然後再把router改為非同步函式,在裡邊呼叫getFetch
,測試後是報錯的!難道router
不支援非同步函式???離離原上譜!
router: async function() {
getFetch();
return {
protocol: 'http:', // The : is required
host: '47.115.13.227',
port: 8001,
};
},
http-proxy-middleware版本問題
我再去查了下資料https://github.com/chimurai/http-proxy-middleware/issues/153
發現http-proxy-middleware是在0.21.0版本才支援async router
,那麼我們再檢查下webpack中webpack-dev-server的版本
好傢伙,webpack-dev-server
裡邊引用的http-proxy-middleware
中介軟體是0.19.1版本,我說試了半天咋沒有用。那這個async router
在咱們專案裡就不能用了,要用還得升級下中介軟體的版本。
支援I/O操作
正當想放棄的時候,剛剛中介軟體文件提到的router一個用法async doSomeIO
,要不試試I/O
操作,看下在router裡邊呼叫檔案流能否成功。
- test.env.json
{
"protocol": "http:",
"host": "47.115.13.227",
"port": 8002
}
- proxy
'/test': {
target: 'http://47.115.13.227:8001',
changeOrigin: true,
router: function() {
const envStr = fs.readFileSync('./test.env.json', 'utf-8');
const { protocol, host, port } = JSON.parse(envStr);
return {
protocol,
host,
port,
};
},
}
在頁面裡點選呼叫fetch("/test")
,發現請求通了,並且是從埠為8002
的伺服器返回的結果!
果然可以做I/O
操作,那如果在不重啟專案的情況下,修改test.env.json
的代理配置,把port
改為8001
,再繼續呼叫fetch("/test")
,請求的結果會變成8001
埠伺服器返回的嗎?
{
"protocol": "http:",
"host": "47.115.13.227",
"port": 8001
}
修改完test.env.json
的配置後,繼續呼叫fetch("/test")
,發現請求的結果變成了8001
埠伺服器返回的了!
到這一步,就驗證了咱們最初的想法——“希望能夠在修改代理環境後,不用重新跑專案即可”,是可行的!
實現程式碼
- test.env.json
{
"protocol": "http:",
"host": "47.115.13.227",
"port": 8001
}
- webpack.devServer.config.js
'use strict';
const fs = require('fs');
// ...
const getEvnFilesJSON = (context) =>{
// const = {req,url,methods } = context;
// ...可根據req,url,methods來獲取不同的代理環境
const envStr = fs.readFileSync('./test.env.json, 'utf-8');
const { protocol, host, port } = JSON.parse(envStr);
return { protocol, host, port }
};
module.exports = function(proxy, allowedHost) {
return {
// ...
proxy: {
'/test': {
target: 'http://47.115.13.227:8001',
changeOrigin: true,
router: getEvnFilesJSON
},
},
};
};
總結
- webpack的web-dev-server是基於http-proxy-middleware實現的
- http-proxy-middleware中
options.router
的async router
功能是在0.21.0版本開始支援的 - http-proxy-middleware中
options.router
功能支援I/O操作
後續有時間可以用Electron開發一個管理React本地devServer.Proxy的工具!像SwitchHosts一樣!