
入職新公司以來,第一個月接手vue項目,第二個月接手angularjs項目,第三個月加入react重構(gòu)項目。心生感嘆:業(yè)務(wù)驅(qū)動式學(xué)習(xí)是一種高效率的學(xué)習(xí)方式,保持好奇心,在業(yè)務(wù)中快速成長!
新項目中在package.json中有一個proxy字段,這是我從來沒接觸過的,因此就有了此文的誕生,我使用create-react-app 新建了一個最原始狀態(tài)的項目,對proxy字段與create-react-app之間的糾葛展開了學(xué)習(xí)。
在npm-configuration中,對proxy有如下解釋:
默認(rèn)值為null,類型為url,一個為了發(fā)送http請求的代理。如果HTTP__PROXY或者h(yuǎn)ttp_proxy環(huán)境變量已經(jīng)設(shè)置好了,那么proxy設(shè)置將被底層的請求庫實現(xiàn)。
這個proxy字段目前我只了解到可以與create-react-app的react-scripts結(jié)合使用:Proxying API Requests in Development,react-scripts應(yīng)該是基于HTTP_PROXY環(huán)境變量做了一些封裝。
閱讀完本文,你將有一以下收獲:
- 如何更優(yōu)雅地為前端項目配置代理Proxy服務(wù)器
- 復(fù)現(xiàn)之前啃《HTTP權(quán)威指南》代理相關(guān)的知識
- 對easy-mock的使用限制有了新的認(rèn)識
- 對process.env可以直接在React層展示感到震驚
- 了解到對process.env可以進(jìn)行擴(kuò)展的dotenv和expand-env兩個庫
主要分為3部分:
- 開發(fā)過程中的Proxy API 請求設(shè)置
- 手動配置proxy
- 環(huán)境變量式配置Proxy
開發(fā)過程中的Proxy API 請求設(shè)置
注意:這個特性可以在react-scripts@2.3以及更高版本中使用。
人們通常從將服務(wù)于后端實現(xiàn)的host和port,同樣也為前端react應(yīng)用提供服務(wù)。
例如,在一個應(yīng)用部署后,生產(chǎn)配置類似下面這樣:
/ -靜態(tài)服務(wù)器返回React應(yīng)用和index.html
/todos -靜態(tài)服務(wù)器返回React應(yīng)用和index.html
/api/todos -服務(wù)器會使用后端實現(xiàn)去處理所有/api/*的請求
但其實這樣的設(shè)置不是必須的。然而,如果你確實有一個這樣的設(shè)置,在不考慮重定向它們到其他的host和port開發(fā)環(huán)境下,那么寫出像fetch('/api/todos')這樣的請求時正常的。
為了告訴開發(fā)環(huán)境的服務(wù)器去代理任何開發(fā)環(huán)境中未知的請求到我們自己的api服務(wù)器,添加一個proxy到package.json的字段,例如:
"proxy":"http://localhost:4000"
使用這種形式的話,當(dāng)你在開發(fā)環(huán)境中使用fecth('api/todos')的時候,開發(fā)環(huán)境的服務(wù)器將識別出這不是一個靜態(tài)資源,然后將代理轉(zhuǎn)發(fā)你的請求到http://localhost:4000/api/todos 作為一個回調(diào)。生產(chǎn)環(huán)境服務(wù)器只能代理沒有text/html在Accept頭中的請求。
方便的是,這就避免了CORS問題以及類似像下面這樣的錯誤信息。
Fetch API cannot load http://localhost:4000/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
要知道proxy只有在開發(fā)環(huán)境中會有副作用,而且類似/api/todos 這樣的URL在生產(chǎn)環(huán)境中是否指向正確取決于我們。你不需要使用/api前綴。任何沒有text/html請求頭的未識別的請求將會被代理到配置的服務(wù)器。
proxy選項支持HTTP,HTTPS以及WebSocket連接。
如果proxy選項還不夠靈活的話,你可以去做自定義:
- 自己配置代理(未實驗)
- 服務(wù)器端開啟CORS(親測,express和koa均可實現(xiàn),koa可以直接使用koa-cors)
- 使用環(huán)境變量注入正確的服務(wù)器以及端口到應(yīng)用(未實驗)
工科男的執(zhí)著:)
為了更好的說明問題,我們來做一次本地實驗:
- 啟動服務(wù)
npx creat-react-app my-app
cd my-app
npm run start
- 引入axios并發(fā)送請求
npm i axios --save
componentDidMount(){
axios.get('/foo')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
請求發(fā)送:"http://localhost:3000/foo"
錯誤信息:404
我們?yōu)閜ackage.json新增proxy服務(wù)器:
"proxy":"http://0.0.0.89:7300"
ctrl + s 熱更新react代碼后,沒有生效,依舊報404的錯誤。
npm run start 重啟本地服務(wù)后,代理服務(wù)器生效,返回正常的數(shù)據(jù)。
實現(xiàn)了自動將"http://localhost:3000" 請求轉(zhuǎn)發(fā)到"http://0.0.0.89:7300" 的服務(wù)器。
不知道聰明的你們發(fā)現(xiàn)沒有,我們并沒有遇到CORS問題,因為在瀏覽器眼里,我們還是將請求發(fā)送到"http://localhost:3000" 中的,它并不知道creat-react-app已經(jīng)將請求轉(zhuǎn)發(fā)到了"http://0.0.0.89:7300" 這個所謂的會觸發(fā)瀏覽器CORS安全策略的其他Origin。
天真的瀏覽器:

請求發(fā)送路徑:
"http://localhost:3000" →"http://0.0.0.89:7300/foo"
響應(yīng)返回路徑:
"http://0.0.0.89:7300/foo" →"http://localhost:3000"
備注:
1.此處需要重新運(yùn)行npm run start 重啟本地服務(wù),否則在package.json中設(shè)置的proxy不會被檢測到并生效。
2.此處的服務(wù)器可以是公司內(nèi)網(wǎng)某臺虛擬機(jī)上的啟動的node服務(wù),也可以是easy-mock等mock服務(wù)器(僅支持公司內(nèi)網(wǎng)部署版,大搜車公網(wǎng)線上服務(wù)器不支持)。
因此我們得出一個結(jié)論:
creat-react-app腳手架可以結(jié)合package.json中的proxy實現(xiàn)請求轉(zhuǎn)發(fā)。
實驗成功!
手動配置proxy
注意:這個特性可以在react-scripts@1.0.0以及更高版本中使用。
如果proxy的默認(rèn)配置不夠靈活,可以在package.json自定義一個像下面這樣形式的對象。
你也可以http-proxy-middleware或者h(yuǎn)ttp-proxy去實現(xiàn)。
{
“proxy”:{
"/api":{
"target":"<url>",
"ws":true
}
}
}
所有與這個路徑相互匹配的請求將被代理轉(zhuǎn)發(fā)。這包括了text/html類型的請求,這種類型是標(biāo)準(zhǔn)proxy選項不支持的。
如果你需要配置多個代理,你需要在定義幾個入口。匹配規(guī)則還是那樣,這樣你才能使用正則匹配多個路徑。
{
// ...
"proxy": {
// Matches any request starting with /api
"/api": {
"target": "<url_1>",
"ws": true
// ...
},
// Matches any request starting with /foo
"/foo": {
"target": "<url_2>",
"ssl": true,
"pathRewrite": {
"^/foo": "/foo/beta"
}
// ...
},
// Matches /bar/abc.html but not /bar/sub/def.html
"/bar/[^/]*[.]html": {
"target": "<url_3>",
// ...
},
// Matches /baz/abc.html and /baz/sub/def.html
"/baz/.*/.*[.]html": {
"target": "<url_4>"
// ...
}
}
// ...
}
工科男的執(zhí)著,繼續(xù)來做一個實驗:
依然使用上面的my-app項目,proxy配置如下:
"proxy":{
"/api": {
"target": "http://0.0.0.89:7300",
"ws": true
},
"/foo": {
"target": "http://0.0.11.22:8848",
"ws": true,
"pathRewrite": {
"^/foo": "/foo/beta"
}
}
}
代碼如下:
axios.get('/api')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios.get('/foo')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
執(zhí)行結(jié)果:
api接口和之前一致,我們這里主要看重定向的foo接口。
請求發(fā)送路徑:
"http://localhost:3000" →"http://0.0.11.22:8848/foo" →"http://0.0.11.22:8848/foo/beta"
響應(yīng)返回路徑:
"http://0.0.11.22:8848/foo/beta" →"http://localhost:3000"
可以配置對個代理,我們此處使用的是"http://0.0.0.89:7300" 和"http://0.0.11.22:8848" 這個兩臺代理服務(wù)器,其中
"http://0.0.0.89:7300" 提供了api接口,"http://0.0.11.22:8848" 提供了foo接口。而且我們可以在代理服務(wù)器上重定向接口。
因此我們得出一個結(jié)論:
creat-react-app腳手架可以結(jié)合package.json中的proxy,可以配置對個代理,而且我們可以在代理服務(wù)器上重定向接口。
實驗成功!
環(huán)境變量式配置proxy
這個功能在react-scripts@0.2.3及更高本版中適用。
react的項目可以使用已經(jīng)聲明好的環(huán)境變量,這些變量就像是在你的js文件中定義的本地變量一樣。默認(rèn)情況下,已經(jīng)有NODE_ENV默認(rèn)環(huán)境變量,以及其他的以REACT_APP_為前綴的環(huán)境變量。
環(huán)境變量在構(gòu)建期間是被嵌入進(jìn)去的。因為Create React App提供了靜態(tài)的HTML/CSS/JS打包,不能在runtime時被讀取到。為了在runtime期間讀取到環(huán)境變量,你需要還在HTML到服務(wù)器的內(nèi)存,并且在運(yùn)行時替換占位符,就像這里描述的這樣:Injecting Data from the Server into the Page。另外你可以在任何你更改他們的時間里重新構(gòu)建應(yīng)用。
你需要使用REACT_APP_創(chuàng)建通用的環(huán)境變量。除了NODE_ENV之外的任何其他的變量將被忽略,這是為了避免exposing a private key on the machine that could have the same name。運(yùn)行期間,只要你修改了環(huán)境變量,就需要重啟開發(fā)服務(wù)器。
這些環(huán)境變量將被定義在process.env。例如,有一個名叫REACT_APP_SECRET_CODE的環(huán)境變量,它可以通過process.env.REACT_APP_SECRET_CODE暴露在我們的javascript文件中。
我們這里同樣也有一個內(nèi)建的叫做NODE_ENV的環(huán)境變量。你可以通過process.env.NODE_ENV去讀取它。當(dāng)你運(yùn)行npm start時,NODE_ENV的值是development,當(dāng)你運(yùn)行npm test時,NODE_ENV的值是test,而且當(dāng)你運(yùn)行npm run build構(gòu)建生產(chǎn)環(huán)境的包的時候,它通常是production。你不能的手動覆蓋NODE_ENV。這樣可以預(yù)防開發(fā)者錯把開發(fā)環(huán)境的代碼部署到生產(chǎn)環(huán)境。
這些環(huán)境變量可以用于根據(jù)項目的部署位置或使用超出版本控制的敏感數(shù)據(jù)來有條件地顯示信息。
首先,你需要一個已經(jīng)定義的環(huán)境變量。例如,你想在form表單中控制一個secret變量。
render(){
return (
<div>
<small>你的應(yīng)用運(yùn)行在<b>{process.env.NODE_ENV}</b>模式。</small>
<form>
<input type="hidden" defaultValue={process.env.REACT_APP_SECRET_CODE} />
</form>
</div>
);
}
在構(gòu)建期間,process.env.REACT_APP_SECRET_CODE將會被環(huán)境變量中的當(dāng)前值替代。謹(jǐn)記NODE_ENV是自動設(shè)置的變量。
當(dāng)你在瀏覽器查看input時,它已經(jīng)被設(shè)置成了abcde(或者是空)。
上面的表單從環(huán)境變量中搜索一個名叫REACT_APP_SECRET_CODE的變量。為了使用這個值,我們需要將其定義在環(huán)境中。使用兩種方式可以做到,一種是在shell中定義,一種是.env文件中。
可以通過NODE_ENV去對一些操作進(jìn)行控制:
if(process.env.NODE_ENV !== 'production'){
analytics.disable();
}
當(dāng)你使用npm run build編譯app時,將會使文件變得更小。
在HTML中引用環(huán)境變量:
注意:這個特性在react-scripts@0.9.0以及更高版本中使用。
你可以在public/index.html中獲取到以REACT_APP_為前綴的環(huán)境變量。例如:
<title>%REACT_APP_WEBSITE_NAME%</title>
注意事項:
- 除了內(nèi)建變量(NODE_ENV和PUBLIC_URL),變量名必須以REACT_APP_開頭才能正常工作。
- 構(gòu)建期間環(huán)境變量可以被注入進(jìn)去。如果你想在運(yùn)行期間注入它們,采用這個方法:Generating Dynamic <meta> Tags on the Server
在shell中添加臨時的環(huán)境變量
對于不同的操作系統(tǒng),環(huán)境變量的設(shè)置是不同的。但是更加需要注意的是,這是創(chuàng)建變量的方式僅僅是當(dāng)前shell session窗口有效。
Linux和macOS(Bash)
REACT_APP_SECRECT_CODE=abcdef npm start
還有一種創(chuàng)建.env文件定義環(huán)境變量的方式。
.env文件將被檢如源代碼控制。
其他.env文件將怎么使用?
這個特性僅在react-scripts@1.0.0及更高中使用
使用dotenv可以將.env中的值注入到process.env中。例如:
require('dotenv').config()
在項目的根目錄定義一個.env文件并鍵入如下內(nèi)容:
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
然后就可以在process.env中訪問到了:
const db = require('db')
db.connect({
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS
})
.env: Default.
.env.local: Local overrides. 加載除了test之外的環(huán)境變量。
.env.development, .env.test, .env.production: 公用的環(huán)境變量。
.env.development.local, .env.test.local, .env.production.local:本地的環(huán)境變量。
左邊的比右邊的優(yōu)先級高:
npm start: .env.development.local, .env.development, .env.local, .env
npm run build: .env.production.local, .env.production, .env.local, .env
npm test: .env.test.local, .env.test, .env (note .env.local is missing)
如何將系統(tǒng)環(huán)境變量擴(kuò)展到我們項目下的.env文件使用:
使用dotenv-expand
REACT_APP_VERSION=$npm_package_version
# also works:
# REACT_APP_VERSION=${npm_package_version}
在.env文件內(nèi)部也可以使用變量:
DOMAIN=www.example.com
REACT_APP_FOO=$DOMAIN/foo
REACT_APP_BAR=$DOMAIN/bar
工科男的執(zhí)著:)
簡單做個實驗:
touch .env
code .env
鍵入:
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
代碼:
console.log("process.env.DB_HOST-->%s,process.env.DB_USER-->%s,process.env.DB_PASS-->%s",process.env.DB_HOST,process.env.DB_USER,process.env.DB_PASS)
實驗結(jié)果:
process.env.DB_HOST-->undefined,process.env.DB_USER-->undefined,process.env.DB_PASS-->undefined
實驗結(jié)果并不總是令人滿意,問題在于不知道在何處require('dotenv').config(),可能需要在node層引入,也可能需要借助webpack之類的工具,使得view層能訪問到。
實驗失敗。
做一下總結(jié):
- 開發(fā)過程中的Proxy API 請求設(shè)置(默認(rèn)選型,滿足大多數(shù)情況下需求)
- 手動配置Proxy (可實現(xiàn)多代理,重定向)
- 環(huán)境變量式配置Proxy (臨時變量方式簡單易用,.env方式較為復(fù)雜,可以使用配置文件代替)
努力成為優(yōu)秀的前端工程師!
期待和大家交流,共同進(jìn)步,歡迎大家加入我創(chuàng)建的與前端開發(fā)密切相關(guān)的技術(shù)討論小組:
- SegmentFault技術(shù)圈:ES新規(guī)范語法糖
- SegmentFault專欄:趁你還年輕,做個優(yōu)秀的前端工程師
- 知乎專欄:趁你還年輕,做個優(yōu)秀的前端工程師
- Github博客: 趁你還年輕233的個人博客
- 前端開發(fā)QQ群:660634678
微信公眾號: 人獸鬼 / excellent_developers
努力成為優(yōu)秀前端工程師!
