動態

詳情 返回 返回

前端本地開發,URL如何設置? - 動態 詳情

一、問題背景

前後端分離的 web 項目,前端本地開發的 URL 大多是“http+localhost+端口”,如 http://localhost:3000。用此 URL 開發,會遇到三類問題。一是跨域,二是 cookie 種植,三是調用外部服務需要真實域名。

跨域問題。如果你遇到跨域問題,大概率就會看過如下圖片,此時瀏覽器不讓你把請求發送到出去。

2024-01-06-10-50-51

為什麼會這樣?背後的原因是瀏覽器的同源策略(Same-origin policy)。URL 包括協議、域名和端口。同源策略是指兩個源相互之間只有同源,換句話説就是協議、域名和端口都一致,才能相互通信。這樣做本意是保證請求的安全,減少惡意攻擊,但也給開發帶來了麻煩。

cookie 種植問題。如果前後端的鑑權涉及 cookie,那 localhost 之類的域名會影響 cookie 的種植。因為瀏覽器能否正確種植下 cookie,受到 domain 的影響。

調用外部服務需要真實域名。當我們項目調用外部服務時,經常需要完整的真實域名,比如微信登錄。此時本地開發沒有完整的真實域名,就無法在本地聯調。

那麼,該如何解決這些問題呢?

二、解決方案

1. 後端配置 CORS

CORS(跨源資源共享)是一種基於 HTTP 請求頭的機制,該機制通過允許服務器標示除了它自己以外的其他源。因此,只要後端在 HTTP 請求的返回頭設置好相關配置,允許該域名通過,那麼瀏覽器就不會阻攔前端發送出請求。

比如下面是 python 服務端的配置,該服務除了自身的請求,也允許來自“http://localhost:3000”和“http://127.0.0.1:3000”的請求。

if ENV == 'dev':
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

使用此方法,有一些細節點要記住。1)對於後端服務,通常只需在測試環境才加入此 middleware,因為線上是用 nginx 代理,不需要此配置。2)對於前端,需要在請求頭添加{credentials: "include"},這樣才能傳送 cookie。

fetch(url, {credentials: "include"});

這種方法可以解決跨域問題,但從開發角度,有幾個缺點。一是此方案涉及前後端,增加了交流溝通成本;二是我認為更嚴重的缺點,那就是本地和線上的解決問題的思路不一致。線上用了 nginx 代理,而本地沒代理,結果就是線上和本地的代碼不一致,而這些不一致的地方就是容易出現 bug 的地方。並且每一次往線上發佈代碼,你不得不確認下不一致的地方有沒有問題,無形中增加了開發的負擔。

2. API 代理

另一種常見解決跨域的方法是 API 代理。既然跨域是由於瀏覽器的限制才會出現,那麼,只要不用瀏覽器發送請求就不會有跨域問題。因此,讓前端先將請求發送到(不會跨域)的代理服務器,再由代理服務器請求真正後端就成了有效的解決方案。

proxy

使用 React、Vue 等前端框架搭建的前端,在本地開發時,通常是有一個本地服務器,而此服務器就可以配置 API 代理。比如 nextjs,只需在 nextjs.config.js 中配置 rewrites:

const nextConfig = {
  async rewrites() {
    const rules =
      process.env.NODE_ENV === "development"
        ? [
            {
              source: "/api/:path*",
              destination: `http://127.0.0.1:8000/:path*`,
            },
          ]
        : [];
    return rules;
  },
};

使用代理時,從前端發送到後端的請求,就不需要在 url 前面添加後端域名 process.env.BASEURL,而是前端請求統一加上/api 即可(所有帶有/api 的請求都會通過此處被轉發到後端)。

const url = `/api${api}`;

這樣子配置之後,不僅不需要後端配置 CORS,也不需要前端在請求接口時,區分開發和線上不同的後端域名,更加簡潔。

最終流程:

2024-01-06-11-31-49

whistle

不過如果單純使用 proxy,本地開發的 URL 還是 http://localhost:3000 的話,那還會出現 cookie 種植和調用外部服務需要真實域名這兩個問題。該怎麼辦呢?

你還可以使用 whistle。whistle 是一個基於 Node 實現的跨平台 web 調試代理工具,類似的工具有 Windows 平台上的 Fiddler,主要用於查看、修改 HTTP、HTTPS、Websocket 的請求、響應。

在本地開啓 whistle 服務之後,只需要填入域名和代理的域名以及端口,就實現真實域名關聯到本地帶有端口的前後端服務。

2024-01-06-10-49-13

當然,光是在 whistle 這樣配置還不夠。當你在瀏覽器輸入域名之後,瀏覽器並不知道要先將請求發送到 whistle 代理服務器,而是直接發送到互聯網上。因此,配合 whistle,需要一個瀏覽器代理插件,通常是 Proxy SwitchyOmega。

使用 Proxy SwitchyOmega 很簡單,在 whistle 的文檔中就有詳細説明。在下載插件之後,只需配置 Proxy SwitchyOmega 要代理 8899 端口(因為 whistle 在本地的代理端口是 8899。),當你開啓該情景模式 proxy 之後,通過瀏覽器發送的請求,就會先到達 whistle,從而正確代理。

2024-01-06-10-50-13

最終流程:

2024-01-06-11-32-38

nginx

whistle 很靈活,大部分前端本地開發時的 URL 配置問題都能夠解決。但它還有一個缺點,那就是和線上環境不完全一致。作為 web 開發多年,我的一個感受是本地環境與線上環境越類似越好。既然線上是使用 nginx 代理,那本地也用 nginx,就能省卻許多環境問題。但是我們大家也都知道,本地開啓 nginx 並不方便。一來 nginx 的服務列表沒有一個可視化工具,我們經常忘記該服務是開啓還是關閉;二來 nginx 配置文件也很分散,我們不容易知道配置的內容。所以,許多人本地開發是不會開啓 nginx。

不過有了 dokcer 之後,上述兩個問題都不見了,因為你可以通過 docker 部署 nginx。你只需將 nginx.conf 和 docker-compose 寫在一個統一的地方。那麼是否開啓本地 nginx,以及開啓之後 nginx 有哪些能力,就變成一行命令的問題,非常簡單。
配置好 nginx.conf,以及相關的 nginx 證書;然後寫好 docker-compose.yml 文件;在當前文件夾運行 docker-compose 就能一鍵開啓本地 nginx。

以下是一個例子。所有來自www.example.com的請求,都會被代理到本地3000端口(假設本地前端3000端口);所有帶有/api的請求都會被代理到8000端口(假設本地的後端服務在8000端口)。這樣配置之後,就可以在本地用nginx代理URL。

# nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    # access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;
    client_max_body_size 15M;

    server {
        listen 80;
        server_name www.example.com;

        return 301 https://www.example.com$request_uri;
    }

    server {
        listen 80;
        server_name example.com;

        return 301 https://www.example.com$request_uri;
    }

    server {
        listen 443 ssl;
        server_name example.com;

        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        return 301 https://www.example.com$request_uri;
    }

    server {
        listen 443 ssl;
        server_name www.example.com;
        # 本地https證書
        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;


        # 前端代理
        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_pass http://host.docker.internal:3000;
        }

        # 後端代理
        location /api/ {
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Origin $http_origin;
            proxy_set_header X-Forwarded-Host $server_name;
            proxy_read_timeout 300s;

            add_header 'Access-Control-Allow-Origin' $http_origin always;
            add_header 'Access-Control-Allow-Credentials' 'true' always;
            add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'authorization,Content-Type,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Range';

            # 其他的配置...
            if ($request_method = 'OPTIONS') {
               return 204;
            }

            rewrite ^/api/(.*) /$1  break;
            proxy_pass http://host.docker.internal:8000;
        }
    }
}
# docker-compose.yml
# 後端部署
version: "3.8"

services:
  nginx:
    image: nginx:1.25-perl
    restart: always
    volumes:
      - ./nginx/conf:/etc/nginx
    ports:
      - "80:80"
      - "443:443"
      - "8443:8443"
    networks:
      - shared-network

networks:
  shared-network:
    external: true

當然,如果你在本地使用 nginx,還得修改 hosts,不然在瀏覽器的請求不會到達本地 nginx。有一些工具也能幫你快速切換本地 hosts 的狀態,我用 Mac 電腦,使用的工具是 iHost。

最終流程:

2024-01-06-11-33-16

總結

本文討論了前端開發時,如何設置本地的 URL。如果目標是“保持本地環境和線上環境越相似越好”,那麼可以使用 docker 開啓本地 nginx 服務的方式;如果需要更靈活的代理,可以使用 whistle+Proxy SwitchyOmega。

參考

Using HTTP cookies - HTTP | MDN

Cross-Origin Resource Sharing (CORS) - HTTP | MDN

Same-origin policy - Security on the web | MDN

Proxy SwitchyOmega

關於 whistle · GitBook

iHosts - /etc/hosts editor on the Mac App Store

Add a new 評論

Some HTML is okay.