如何理解 package.json 中的 proxy 欄位?
入職新公司以來,第一個月接手vue專案,第二個月接手angularjs專案,第三個月加入react重構專案。心生感嘆:業務驅動式學習是一種高效率的學習方式,保持好奇心,在業務中快速成長! 新專案中在package.json中有一個proxy欄位,這是我從來沒接觸過的,因此就有了此文的誕生,我使用create-react-app 新建了一個最原始狀態的專案,對proxy欄位與create-react-app之間的糾葛展開了學習。
在npm-configuration中,對proxy有如下解釋:
預設值為null,型別為url,一個為了傳送http請求的代理。如果HTTP__PROXY或者http_proxy環境變數已經設定好了,那麼proxy設定將被底層的請求庫實現。
這個proxy欄位目前我只瞭解到可以與create-react-app的react-scripts結合使用:Proxying API Requests in Development,react-scripts應該是基於HTTP_PROXY環境變數做了一些封裝。
閱讀完本文,你將有一以下收穫:
- 如何更優雅地為前端專案配置代理Proxy伺服器
- 復現之前啃《HTTP權威指南》代理相關的知識
- 對easy-mock的使用限制有了新的認識
- 對process.env可以直接在React層展示感到震驚
- 瞭解到對process.env可以進行擴充套件的dotenv和expand-env兩個庫
主要分為3部分:
- 開發過程中的Proxy API 請求設定
- 手動配置proxy
- 環境變數式配置Proxy
開發過程中的Proxy API 請求設定
注意:這個特性可以在[email protected]以及更高版本中使用。
人們通常從將服務於後端實現的host和port,同樣也為前端react應用提供服務。 例如,在一個應用部署後,生產配置類似下面這樣:
/ -靜態伺服器返回React應用和index.html
/todos -靜態伺服器返回React應用和index.html
/api/todos -伺服器會使用後端實現去處理所有/api/*的請求
複製程式碼
但其實這樣的設定不是必須的。然而,如果你確實有一個這樣的設定,在不考慮重定向它們到其他的host和port開發環境下,那麼寫出像fetch('/api/todos')這樣的請求時正常的。
為了告訴開發環境的伺服器去代理任何開發環境中未知的請求到我們自己的api伺服器,新增一個proxy到package.json的欄位,例如:
"proxy":"http://localhost:4000"
複製程式碼
使用這種形式的話,當你在開發環境中使用fecth('api/todos')的時候,開發環境的伺服器將識別出這不是一個靜態資源,然後將代理轉發你的請求到http://localhost:4000/api/todos 作為一個回撥。生產環境伺服器只能代理沒有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只有在開發環境中會有副作用,而且類似/api/todos 這樣的URL在生產環境中是否指向正確取決於我們。你不需要使用/api字首。任何沒有text/html請求頭的未識別的請求將會被代理到配置的伺服器。
proxy選項支援HTTP,HTTPS以及WebSocket連線。 如果proxy選項還不夠靈活的話,你可以去做自定義:
- 自己配置代理(未實驗)
- 伺服器端開啟CORS(親測,express和koa均可實現,koa可以直接使用koa-cors)
- 使用環境變數注入正確的伺服器以及埠到應用(未實驗)
工科男的執著:) 為了更好的說明問題,我們來做一次本地實驗:
- 啟動服務
npx creat-react-app my-app
cd my-app
npm run start
複製程式碼
- 引入axios併發送請求
npm i axios --save
複製程式碼
componentDidMount(){
axios.get('/foo')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
複製程式碼
請求傳送:"http://localhost:3000/foo" 錯誤資訊:404
我們為package.json新增proxy伺服器:
"proxy":"http://0.0.0.89:7300"
複製程式碼
ctrl + s 熱更新react程式碼後,沒有生效,依舊報404的錯誤。
npm run start 重啟本地服務後,代理伺服器生效,返回正常的資料。
實現了自動將"http://localhost:3000" 請求轉發到"http://0.0.0.89:7300" 的伺服器。
不知道聰明的你們發現沒有,我們並沒有遇到CORS問題,因為在瀏覽器眼裡,我們還是將請求傳送到"http://localhost:3000" 中的,它並不知道creat-react-app已經將請求轉發到了"http://0.0.0.89:7300" 這個所謂的會觸發瀏覽器CORS安全策略的其他Origin。
天真的瀏覽器:
請求傳送路徑: "http://localhost:3000" →"http://0.0.0.89:7300/foo"
響應返回路徑: "http://0.0.0.89:7300/foo" →"http://localhost:3000" 備註: 1.此處需要重新執行npm run start 重啟本地服務,否則在package.json中設定的proxy不會被檢測到並生效。 2.此處的伺服器可以是公司內網某臺虛擬機器上的啟動的node服務,也可以是easy-mock等mock伺服器(僅支援公司內網部署版,大搜車公網線上伺服器不支援)。
因此我們得出一個結論:
creat-react-app腳手架可以結合package.json中的proxy實現請求轉發。
實驗成功!
手動配置proxy
注意:這個特性可以在[email protected]以及更高版本中使用。
如果proxy的預設配置不夠靈活,可以在package.json自定義一個像下面這樣形式的物件。 你也可以http-proxy-middleware或者http-proxy去實現。
{
“proxy”:{
"/api":{
"target":"<url>",
"ws":true
}
}
}
複製程式碼
所有與這個路徑相互匹配的請求將被代理轉發。這包括了text/html型別的請求,這種型別是標準proxy選項不支援的。
如果你需要配置多個代理,你需要在定義幾個入口。匹配規則還是那樣,這樣你才能使用正則匹配多個路徑。
{
// ...
"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>"
// ...
}
}
// ...
}
複製程式碼
工科男的執著,繼續來做一個實驗:
依然使用上面的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);
});
複製程式碼
執行結果: api介面和之前一致,我們這裡主要看重定向的foo介面。
請求傳送路徑: "http://localhost:3000" →"http://0.0.11.22:8848/foo" →"http://0.0.11.22:8848/foo/beta"
響應返回路徑: "http://0.0.11.22:8848/foo/beta" →"http://localhost:3000"
可以配置對個代理,我們此處使用的是"http://0.0.0.89:7300" 和"http://0.0.11.22:8848" 這個兩臺代理伺服器,其中 "http://0.0.0.89:7300" 提供了api介面,"http://0.0.11.22:8848" 提供了foo介面。而且我們可以在代理伺服器上重定向介面。
因此我們得出一個結論:
creat-react-app腳手架可以結合package.json中的proxy,可以配置對個代理,而且我們可以在代理伺服器上重定向介面。
實驗成功!
環境變數式配置proxy
這個功能在[email protected]及更高本版中適用。
react的專案可以使用已經宣告好的環境變數,這些變數就像是在你的js檔案中定義的本地變數一樣。預設情況下,已經有NODE_ENV預設環境變數,以及其他的以REACT_APP_為字首的環境變數。
**環境變數在構建期間是被嵌入進去的。**因為Create React App提供了靜態的HTML/CSS/JS打包,不能在runtime時被讀取到。為了在runtime期間讀取到環境變數,你需要還在HTML到伺服器的記憶體,並且在執行時替換佔位符,就像這裡描述的這樣:Injecting Data from the Server into the Page。另外你可以在任何你更改他們的時間裡重新構建應用。
你需要使用REACT_APP_建立通用的環境變數。除了NODE_ENV之外的任何其他的變數將被忽略,這是為了避免exposing a private key on the machine that could have the same name。執行期間,只要你修改了環境變數,就需要重啟開發伺服器。
這些環境變數將被定義在process.env。例如,有一個名叫REACT_APP_SECRET_CODE的環境變數,它可以通過process.env.REACT_APP_SECRET_CODE暴露在我們的javascript檔案中。
我們這裡同樣也有一個內建的叫做NODE_ENV的環境變數。你可以通過process.env.NODE_ENV去讀取它。當你執行npm start時,NODE_ENV的值是development,當你執行npm test時,NODE_ENV的值是test,而且當你執行npm run build構建生產環境的包的時候,它通常是production。**你不能的手動覆蓋NODE_ENV。**這樣可以預防開發者錯把開發環境的程式碼部署到生產環境。
這些環境變數可以用於根據專案的部署位置或使用超出版本控制的敏感資料來有條件地顯示資訊。
首先,你需要一個已經定義的環境變數。例如,你想在form表單中控制一個secret變數。
render(){
return (
<div>
<small>你的應用執行在<b>{process.env.NODE_ENV}</b>模式。</small>
<form>
<input type="hidden" defaultValue={process.env.REACT_APP_SECRET_CODE} />
</form>
</div>
);
}
複製程式碼
在構建期間,process.env.REACT_APP_SECRET_CODE將會被環境變數中的當前值替代。謹記NODE_ENV是自動設定的變數。
當你在瀏覽器檢視input時,它已經被設定成了abcde(或者是空)。 上面的表單從環境變數中搜索一個名叫REACT_APP_SECRET_CODE的變數。為了使用這個值,我們需要將其定義在環境中。使用兩種方式可以做到,一種是在shell中定義,一種是.env檔案中。
可以通過NODE_ENV去對一些操作進行控制:
if(process.env.NODE_ENV !== 'production'){
analytics.disable();
}
複製程式碼
當你使用npm run build編譯app時,將會使檔案變得更小。
在HTML中引用環境變數:
注意:這個特性在[email protected]以及更高版本中使用。
你可以在public/index.html中獲取到以REACT_APP_為字首的環境變數。例如:
注意事項:
- 除了內建變數(NODE_ENV和PUBLIC_URL),變數名必須以REACT_APP_開頭才能正常工作。
- 構建期間環境變數可以被注入進去。如果你想在執行期間注入它們,採用這個方法:Generating Dynamic Tags on the Server
在shell中新增臨時的環境變數 對於不同的作業系統,環境變數的設定是不同的。但是更加需要注意的是,這是建立變數的方式僅僅是當前shell session視窗有效。
Linux和macOS(Bash)
REACT_APP_SECRECT_CODE=abcdef npm start
複製程式碼
還有一種建立.env檔案定義環境變數的方式。
.env檔案將被檢如原始碼控制。
其他.env檔案將怎麼使用?
這個特性僅在[email protected]及更高中使用 使用dotenv可以將.env中的值注入到process.env中。例如:
require('dotenv').config()
複製程式碼
在專案的根目錄定義一個.env檔案並鍵入如下內容:
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之外的環境變數。
.env.development, .env.test, .env.production: 公用的環境變數。
.env.development.local, .env.test.local, .env.production.local:本地的環境變數。
複製程式碼
左邊的比右邊的優先順序高:
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)
複製程式碼
如何將系統環境變數擴充套件到我們專案下的.env檔案使用:
使用dotenv-expand
REACT_APP_VERSION=$npm_package_version
# also works:
# REACT_APP_VERSION=${npm_package_version}
複製程式碼
在.env檔案內部也可以使用變數:
DOMAIN=www.example.com
REACT_APP_FOO=$DOMAIN/foo
REACT_APP_BAR=$DOMAIN/bar
複製程式碼
工科男的執著:) 簡單做個實驗:
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)
複製程式碼
實驗結果:
process.env.DB_HOST-->undefined,process.env.DB_USER-->undefined,process.env.DB_PASS-->undefined
複製程式碼
實驗結果並不總是令人滿意,問題在於不知道在何處require('dotenv').config(),可能需要在node層引入,也可能需要藉助webpack之類的工具,使得view層能訪問到。
實驗失敗。 做一下總結:
- 開發過程中的Proxy API 請求設定(預設選型,滿足大多數情況下需求)
- 手動配置Proxy (可實現多代理,重定向)
- 環境變數式配置Proxy (臨時變數方式簡單易用,.env方式較為複雜,可以使用配置檔案代替)
努力成為優秀的前端工程師!如何理解package.json中的proxy欄位?