一種方便的跨域開發解決方案

創宇前端發表於2018-03-21

一種方便的跨域開發解決方案
現在越來越多的 Web 專案都採取前後端分離的開發方式,也就是在開發過程中前端工程執行在一個 node server 上,同時提供 REST API 的後端工程作為獨立的服務執行在另一個 server 上,這樣前後端通過 HTTP 請求進行通訊的時候就會遇到跨域的問題,跨域是怎麼一回事大家應該都瞭解,這裡就不再贅述。以往解決跨域問題比較常用的方法就是給服務端配置 CORS,但是這種方式會帶來一些不便:

  1. 要修改後端程式碼,如果後端專案執行在自己機器上還好,如果是執行在別的伺服器上,甚至是其他團隊在維護,那麼修改一段程式碼就會比較麻煩,並且還要確保這些程式碼不會被髮布到生產環境中去。
  2. 每次啟動瀏覽器的時候要輸入一長串程式碼來關閉瀏覽器的安全策略,比如 Chrome 瀏覽器的命令如下: open -a /Applications/Google\ Chrome.app --args --disable-web-security --user-data-dir 不僅麻煩,萬一一不小心在這種模式下訪問了一些敏感的資料,還會帶來安全隱患。
  3. 開發模式下前端調介面要帶上測試後端伺服器的 URL,因此釋出到生產環境之前還要把它去掉。

總之就是比較麻煩,體會不到那種脫了褲子就上,完事提上褲子就走的爽快感,我說的是上廁所。所以今天就是要介紹一種更加簡單安全的解決方案,同時我們會深入去了解其中的原理是什麼。

首先,用 create-react-app 建立一個前端專案,假如你的前端專案執行的地址是 http://localhost:3000,與此同時提供 API 的後端專案執行的地址是 http://localhost:4000,你要做的只是在前端工程的 package.json 檔案中新增這樣一行配置:

"proxy": "http://localhost:4000"
複製程式碼

然後你就會神奇地發現,從前端頁面發出的 HTTP 請求,雖然訪問的依然是 3000 埠,但是會被自動轉發到 4000 埠的後端伺服器並得到正確的響應,於此同時訪問頁面的請求卻不會被轉發,依然能夠被前端路由捕獲,這樣我們就完全不需要再考慮如何處理跨域的問題了。問題是解決了,但是又出現了 2 個問題縈繞在我的心中:

  1. package.json 中的 proxy 引數是作用在什麼地方的?
  2. 是怎麼樣做到把訪問頁面的請求和訪問 REST API 的請求區分開的?

帶著這樣的疑問我們一起去看看 create-react-app 的原始碼是怎樣寫的,首先在前端專案中的 package.json 裡我們能看到,專案啟動執行的指令碼是 react-script start,所以我們開啟檔案 create-react-app/packages/react-scripts/scripts/start.js(為何直接能定位到這個檔案,以及 react-script 這個命令是如何註冊的,屬於其他知識點,本文不展開說明,有疑問的童鞋可以去這裡 學習一個),我們看到有以下程式碼:

// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
// Serve webpack assets generated by the compiler over a web sever.
const serverConfig = createDevServerConfig(
    proxyConfig,
    urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
複製程式碼

此處的 paths.appPackageJson 的宣告在 create-react-app/packages/react-scripts/config/paths.js 中:

module.exports = {
  appPackageJson: resolveApp('package.json'),
};
複製程式碼

因此我們就知道,這裡的 proxySetting 就是我們之前在前端工程的 package.json 中定義的 proxy 的值,然後我們看到,proxySetting 被用來生成了 serverConfig,最終 serverConfig 作為配置引數建立了 WebpackDevServer 例項。webpack-dev-server 是一個用於啟動 webpack 的測試伺服器,並且提供了諸如 HMR 等方便開發的功能,因此我們就得出第一個結論:前端工程的 package.json 中定義的 proxy 值,是作用於 WebpackDevServer,最終通過 WebpackDevServer 進行的轉發

讓我們繼續試圖解答第二個問題——是怎麼樣做到把訪問頁面的請求和訪問 REST API 的請求區分開的?我們看到 proxySetting 首先是被傳入 prepareProxy 方法得到 proxyConfig,然後在 createDevServerConfig 方法中返回了一個物件,並且物件的 proxy 欄位的值為 proxyConfig,最終該物件就是 webpack-dev-server 的配置項,在 webpack-dev-server 文件 中可以看到 proxy 的作用就是做一層代理,把從頁面來的請求轉發到另一個地址,因此關鍵就在於 proxyConfig 的配置是怎麼樣的,於是目光轉移到 prepareProxy 方法,prepareProxy 方法的定義在 create-react-app/packages/react-dev-utils/WebpackDevServerUtils.js 中,在這裡我們可以看到首先是對 proxy 進行了型別和格式的檢測,然後如果 proxy 是一個格式正確的字串,就返回一個只有一個物件元素的陣列,在這個物件中的 context 欄位中出現瞭如下的判斷:

context: function(pathname, req) {
    return (
        req.method !== 'GET' ||
        (mayProxy(pathname) &&
         req.headers.accept &&
         req.headers.accept.indexOf('text/html') === -1)
    );
}
複製程式碼

在這裡我們看到有對 req.headers.accept 進行判斷,req.headers.accept 用於表示瀏覽器通過這次 HTTP 請求希望獲取到的內容型別,因此如果 accept 中帶有 text/html 則說明本次請求獲取的是一個 document,因此就不應該被轉發到後端,這一堆判斷邏輯用一幅圖表示出來如下:

一種方便的跨域開發解決方案
在這裡 context 的含義在 webpack-dev-server 文件中是找不到的,它的說明出現在 http-proxy-middleware 中,context 支援傳入一個 function 用於自定義轉發的邏輯,只在返回值為 true 時才轉發請求,因此該代理將只會轉發 ajax 或者 fetch 發出的 HTTP 請求 。

除了文中提到的這種最簡單的配置,webpack-dev-server 的 proxy 還支援多種配置方式以同時滿足多種代理規則,感興趣的同學可以去文件裡面瞭解更多細節。

相關文章