終於理解了 Next.js 中的 Cookie

王大冶發表於2024-11-24
  • CSS技巧與案例詳解
  • vue2與vue3技巧合集
  • VueUse原始碼解讀

Cookie 是瀏覽器中默默存在的資料塊。雖然有些 Cookie 會侵犯使用者隱私,但其他一些則試圖透過跟蹤使用者的瀏覽習慣、偏好等來改善瀏覽體驗。Cookie 在許多場景都很有用,包括身份驗證、改善使用者體驗和加快響應時間。

在本文中,我們將探討如何在 Next.js 的伺服器元件和客戶端元件以及中介軟體中管理 Cookie。我們還將介紹兩個可以在 Next.js Pages Router 中設定 Cookie 的包,並將它們應用到實際用例中。

要跟隨本教程學習,可以前往此 GitHub 倉庫。

https://github.com/GeoBrodas/cookies-with-nextjs

Cookie:福祉還是禍害?

Cookie 是 Web 應用程式放置在使用者計算機上的小資料塊,用於儲存狀態資訊,如使用者偏好或會話管理,也用於跟蹤目的。

近年來,關於 Cookie 的討論引發了很多爭議,因為它們既有優點也有缺點。

一方面,Cookie 可以幫助你輕鬆儲存使用者的個性化資料,讓你能更好地為每個使用者定製使用者體驗。另一方面,Cookie 跟蹤使用者線上行為的能力引發了隱私擔憂。

因此,現在有了一些標準和法規,要求 Web 應用程式披露其 Cookie 使用情況,並讓使用者選擇是否退出。

鑑於 Next.js 的多功能性,有多種方法可以管理 Cookie。在此之前,我們只需要在頁面、API 路由和中介軟體中管理 Cookie。然而,Next 13 中新增的伺服器元件引入了更多新技術。

客戶端 vs 伺服器端 Cookie

很多人問的一個問題是客戶端和伺服器端 Cookie 是否有區別。

Cookie 可以透過客戶端和伺服器端操作建立。伺服器端 Cookie 通常是透過 HTTP 標頭建立和訪問的。無論如何建立,Cookie 都儲存在使用者的瀏覽器中,可以直接在客戶端訪問。

但是,httpOnly Cookie 是個例外。如果你建立啟用了 httpOnly 屬性的 Cookie,這些 Cookie 就不能透過客戶端操作直接訪問,從而降低XSS攻擊的風險。

在伺服器元件中處理 Cookie:Next.js App Router

我們首先探討如何在使用 Next.js App Router 的伺服器元件中訪問和修改 Cookie。要繼續,請執行以下命令建立一個新的 Next 應用:

npx create-next-app@latest next-cookie

在安裝過程中,選擇你偏好的配置。不過,別忘了為"use App router?"選項選擇"Yes"。完成基本設定後

如何獲取 Cookie

要在伺服器元件中讀取傳入的請求 Cookie 值,我們使用 cookies().get(cookieName) 方法,如下所示:

// app/page.js

import { cookies } from "next/headers";

const Home = () => {
  const cookieStore = cookies();
  const userId = cookieStore.get("user_id");

  return <>{userId && <p>User ID: {userId}</p>}</>;
};

export default Home;

在本例中,如果沒有儲存帶有user_id標籤的 cookie,我們將看到一個空白螢幕。但是,如果有多個 cookie 與此標記匹配,userId將被設定為第一個匹配,並顯示在瀏覽器上。

要獲取與某個名稱匹配的所有 cookie,我們可以使用cookies().getAll()方法,如下所示:

// app/page.js

import { cookies } from "next/headers";

const Home = () => {
  const cookieStore = cookies();
  const userId = cookieStore.getAll("user_id");

  return (
    <>
      {userId.length > 0 &&
        userId.map((cookie) => (
          <div key={cookie.name}>
            <p>Name: {cookie.name}</p>
            <p>Value: {cookie.value}</p>
          </div>
        ))}
    </>
  );
};

export default Home;

在這個更新的示例中,如果有多個帶有user_id標記的 cookie,我們就會遍歷它們,並顯示每個 cookie 的名稱和值。

如何設定 cookie

我們可以透過 cookies().set() 方法設定新的 Cookie。但是,由於 HTTP 不允許在流式傳輸開始後設定 Cookie,我們只能在伺服器操作(Server Actions)或 API 路由中修改 Cookie 值(設定和刪除)。以下是一個例子:

// app/page.js

import { cookies } from "next/headers";

const Home = () => {
  async function createThemeCookie(formData) {
    "use server";

    const selectedTheme = formData.get("theme");
    cookies().set("theme", selectedTheme);
  }

  return (
    <>
      <form action={createThemeCookie}>
        <select name="theme">
          <option value="dark">Dark Theme</option>
          <option value="light">Light Theme</option>
        </select>
        <button type="submit">Create Theme Cookie</button>
      </form>
    </>
  );
};

export default Home;

在這個例子中,我們用"use server"語句標記了設定 Cookie 的函式,將其指定為伺服器操作。然後我們渲染了一個表單,允許使用者選擇首選主題。使用者提交表單後,我們獲取他們選擇的主題值並將其設定到 theme cookie 中。

此外,我們可以使用以下語法在設定新 Cookie 時傳入額外選項:

cookies().set({
  name: "theme",
  value: "light || dark",
  httpOnly: true,
  path: "/",
  maxAge: 60 * 60 * 24 * 365 * 1000,
  expires: new Date(Date.now() + 60 * 60 * 24 * 365 * 1000),
});

這樣,我們可以建立一個 httpOnly cookie 並設定 cookie 的路徑、最大年齡和過期日期。

如果你使用的 Next.js 版本低於v14,在使用伺服器操作時可能會遇到以下錯誤:

Error: 
  × To use Server Actions, please enable the feature flag in your Next.js config.

要修復這個問題,只需更新你的 next.config.js 檔案以啟用實驗性的 serverActions,如下所示:

const nextConfig = {
  experimental: {
    serverActions: true,
  },
  // . . .
};

module.exports = nextConfig;

但是,如果你使用的是 Next.js v14 或更新版本,則不會遇到此類錯誤。

如何刪除 Cookie

我們可以使用 cookies().delete(name) 方法刪除 Cookie。但是,和設定 Cookie 一樣,我們只能在伺服器操作或 API 路由中使用此方法,如下所示:

// app/page.js

import { cookies } from "next/headers";

const Home = () => {
  async function deleteThemeCookie(formData) {
    "use server";

    cookies().delete("theme");
  }

  return (
    <>
      <form action={deleteThemeCookie}>
        <button type="submit">Delete Theme Cookie</button>
      </form>
    </>
  );
};

export default Home;

透過上面這個例子,當使用者點選按鈕提交表單時,theme cookie 將從瀏覽器中刪除。

Next.js 路由處理程式和 API 路由中的 Cookie

在 Next.js 路由處理程式(即 /app 目錄的 API 路由等價物)中,我們可以自由使用我們剛剛介紹的所有 cookie 方法,而無需建立伺服器操作。你可以在下面看到一個例子:

// app/api/route.js

import { cookies } from "next/headers";
export async function GET(request) {
  const cookieStore = cookies();

  // 獲取 Cookie
  const myCookie = cookieStore.get("cookieName");

  // 獲取所有 Cookie
  const myCookies = cookieStore.getAll("cookieName");

  // 設定 Cookie
  cookies().set("cookieName", "value");

  // 刪除 Cookie
  cookies().delete("cookieName");

  return new Response("Hello, World!", {
    status: 200,
  });
}

另外,你也可以在路由處理程式和 API 路由中使用傳統的 Web API 從請求中讀取 cookie,如下所示:

// app/api/route.js 或 pages/api/index.js

export async function GET(request) {
  let theme = request.cookies.get("theme");
  return new Response(JSON.stringify(theme));
}

在這個例子中,我們直接從請求物件中獲取 theme cookie 並將其作為 API 響應返回。

在 Next.js Pages Router 中處理 Cookie

現在,讓我們看看如何在經典的 Next.js Pages Router 中管理 Cookie。首先,建立一個新的 Next.js 應用程式,並確保在配置過程中選擇 pages router。

在 Next.js 中使用 react-cookie

我們要探索的第一個包是 react-cookie。這個包旨在幫助你在 React 應用程式中載入和儲存 cookie。為了嘗試一下,我們將建立一個簡單的應用程式來跟蹤註冊使用者。

使用以下程式碼安裝 react-cookie:

npm install react-cookie

要開始使用這些 Hook,在 _app.tsx 檔案中新增 CookiesProvider 元件,如下所示:

import type { AppProps } from 'next/app';
import { CookiesProvider } from 'react-cookie';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <CookiesProvider>
      <Component {...pageProps} />
    </CookiesProvider>
  );
}

export default MyApp;

我們首先新增一個 useEffect Hook 來在載入時獲取所有 cookie:

import { useCookies } from 'react-cookie';

const Home: NextPage = () => {
  const [cookies, setCookie, removeCookie] = useCookies(['user']);

  useEffect(() => {
    console.log('Cookies: ', cookies);
  }, [cookies]);

  return (
  <div>...</div>
)}

目前,你應該還看不到任何 cookie。所以,我們建立一個使用 setCookie() 函式來設定 cookie 的函式:

import { useRouter } from 'next/router';

//...在預設函式內部
const router = useRouter();

const setCookieHandler = () => {
  setCookie('new-user', true, {
    path: '/',
  });

  router.replace("/");
};

setCookie() 函式接受三個引數:鍵名、鍵值和一些配置選項。這些選項包括 MaxAge、Path、Domain、expires 等。在這種情況下使用了 path 選項,允許程式從任何位置訪問該 cookie。

如你所見,我們還使用了 useRouter() Hook 透過 replace() 方法重新載入頁面,以避免在歷史堆疊中新增 URL 條目。這樣看起來就像頁面重新渲染了一樣!

隨著我們繼續前進,請記住本教程僅專注於演示特定包的功能。因此,我們將假設你理解認證流程等概念。

要了解更多關於 Next.js 中的認證資訊,請參考這個關於 SuperTokens 的指南。你也可以在這篇文章中回顧認證流程。

將函式繫結到按鈕

接下來,讓我們將這個函式繫結到一個按鈕上。輸入以下程式碼:

{!cookies['user'] && (
  <button onClick={setCookieHandler} className={styles.button}>
    Complete new user registration!
  </button>
)}

在這種情況下,只有當 cookie 存在時按鈕才會渲染。執行開發伺服器來看看這個效果。你可以透過按 Control+Shift+J 開啟開發者工具,然後選擇 Application 部分來直觀地看到這個 cookie。

現在,我們刪除 cookie 以允許使用者退出。首先,編寫另一個函式:

const removeCookieHandler = () => {
  removeCookie('new-user');

  router.replace("/");
};

接下來,將它繫結到另一個只有在 cookie 可用時才會渲染的按鈕上。這意味著什麼?如果使用者已註冊,cookie 就會可用。程式碼看起來是這樣的:

{cookies['new-user'] && (
  <button onClick={removeCookieHandler} className={styles.resetbutton}>
    Reset new user registration
  </button>
)}

下面是它在應用程式中的樣子:

使用 cookies-next 包

接下來,我們來看看如何使用 cookies-next 包。這個包更適合 Next.js 生態系統,因為它可以在任何地方使用 - 無論是在 Pages Router 還是 App Router 中,在客戶端,透過 getServerSideProps 在伺服器端,甚至在 Next.js API 路由中。

以下是這兩個包的對比:

image.png

  • react-cookie 更加流行,提供簡單易用的 API 並與 React 框架高度相容
  • cookies-next 是專門為 Next.js 建立的相對較新的包,提供伺服器端渲染功能和改進的安全措施

另一個關於 cookies-next 令人驚喜的事實(這個是給那些關心包大小的開發者) - 它比 react-cookie 的包體積更小。這使得它在你的下一個專案中更具吸引力! 🎉

按照慣例,讓我們首先用以下命令安裝 cookies-next:

npm install cookies-next

cookies-next 包內建了類似於 react-cookie 包的功能。這些功能可用於設定和刪除 cookie。讓我們用以下程式碼建立用於設定和刪除 cookie 的處理函式:

// 新增 cookie
const addCookie = () => {
  setCookie('user', true, {
    path: '/',
  });
  router.replace('/');
};

// 刪除 cookie
const removeCookie = () => {
  deleteCookie('user', {
    path: '/',
  });
  router.replace('/');
};

完成後,你可以透過將其繫結到在 cookie 存在時渲染的不同按鈕來測試它。除了 getServerSideProps 和 API 路由外,你還可以在應用程式的伺服器端使用 cookies-next 包。

讓我們看一個例子,使用者收到一些資訊,對其進行驗證,然後設定一個 cookie 來表示資訊的合法性,所有這些都在 API 路由上完成。

實現 API 路由

繼續在 ./pages/api/verify-otp.ts 中建立一個新的 API 路由。在檔案中,用以下程式碼建立一個基本的處理函式:

export default function handler (
  req: NextApiRequest,
  res: NextApiResponse
) {
  return;  
}

我們將設定 cookie 來表示使用者的可信度,並在特定時間後過期。更具體地說,如果有某種驗證(比如用於檢查憑證的資料庫或某些 OTP 邏輯),它就會過期。處理函式如下:

if (
    req.method === 'POST' // 只允許 POST 請求
  ) {
  // 從請求體中獲取用於驗證的憑證
  const { name } = req.body;

  // OTP 驗證邏輯

  // 設定 cookie
  setCookie('authorize', true, {
    req,
    res,
    maxAge: 60 * 60 * 24 * 7, // 1 周
    path: '/',
  });

  // 響應狀態和訊息
  return res.status(200).json({
    message: `${name} is authorized to access`,
    authorize: true,
    code: '20-0101-2092',
  });
}

在這裡,cookie 會在一週後過期,並要求使用者重新驗證。在驗證成功後,API 會返回一個狀態為 200 的訊息,其中包含可以在前端顯示的相關資料。

從前端訪問 API 路由

現在,我們嘗試從前端訪問這個路由。該函式只能在使用者首次註冊時觸發。使用以下程式碼建立函式:

const verifyOTP = async (name: string) => {
  const response = await fetch('/api/verify-otp', {
    method: 'POST',
    body: JSON.stringify({ name }),
  });

  const data = await response.json();

  if (data.authorize) {
    setAuthorization(true);
    setLaunchCode(data.code);
  } else {
    setAuthorization(false);
    alert('Invalid OTP');
  }
};

我們可以使用 useState Hook 來儲存來自 API 路由的資料,並基於 isAuthorized 變數有條件地渲染按鈕。使用以下程式碼:

const [isAuthorized, setAuthorization] = useState(false);
const [launchCode, setLaunchCode] = useState('');

完成這些後,試試到目前為止寫的程式碼。你可以透過開啟開發者工具並選擇 Application 部分來檢查 cookie 是否存在。

image.png

Next.js 中介軟體中的 Cookie

Next.js 中介軟體設計在 Pages Router 和 App Router 中是一致的。因此,在中介軟體中處理 cookie 的方式對兩者都是相同的。例如,我們可以透過中介軟體請求獲取和刪除 cookie,如下所示:

// middleware.js

export function middleware(request) {
  // 獲取 Cookie
  let cookie = request.cookies.get("cookieName");
  console.log(cookie);
  // 獲取所有 Cookie
  const allCookies = request.cookies.getAll();
  console.log(allCookies);
  // 刪除 Cookie
  request.cookies.delete("cookieName");
}

要在中介軟體中設定新的 cookie,我們還可以利用 NextResponse API,如下所示:

// middleware.js

import { NextResponse } from "next/server";

export function middleware(request) {
  const response = NextResponse.next();
  // 設定 cookies
  response.cookies.set("foo", "bar");
  // 或者
  response.cookies.set({
    name: "foo",
    value: "bar",
    path: "/",
    // . . .
  });
  return response;
}

這樣,cookie 就會全域性設定在使用者的瀏覽器中,並可以在我們之前演示的 Next.js 頁面中訪問。

常見的 Next.js Cookie 問題及解決方案

sameSite 功能是 cookie 的一個重要屬性,但它也可能在生產級應用程式中造成問題:

image.png

sameSite 功能僅表明是否可以透過具有不同源的其他網站檢索 cookie。理想情況下,這應該是準確的,因為它只提供了一層防禦跨站攻擊的保護。

為了確定方案和域名的最後部分是否匹配,sameSite 瀏覽器機制會分析目標 URI 和來自客戶端的請求:
image.png

由於 sameSite 引數預設設定為 true,如果你是一個經常使用其他智慧手機並透過開發伺服器託管在本地主機上的私有 IP 地址連線的開發者,cookie 將不會被註冊。

domain 屬性是 cookie 的一個關鍵但有時容易混淆的元素。這個屬性決定了哪些域可以訪問該 cookie。如果沒有指定域,cookie 的預設域分配將是最初生成它的域。

這就是為什麼在多個子域試圖訪問相同 cookie 的情況下,建議設定 domain 屬性:

image.png

結論

Cookie 對於 Web 開發至關重要。react-cookie 和 cookies-next 包由於其獨特的特性和優勢,非常適合各種使用場景。

react-cookie 更受歡迎,提供簡單易用的 API 和與 React 框架的良好相容性。相比之下,cookies-next 是一個專門為 Next.js 建立的相對較新的包,提供伺服器端渲染功能和改進的安全措施。

另一個令人驚喜的事實 - 這是給所有注重包大小的開發者的 - 就是與 react-cookie 相比,cookies-next 的包體積更小。這本質上使它在你的下一個專案中更具吸引力! 🎉

首發於公眾號 大遷世界,歡迎關注。📝 每週一篇實用的前端文章 🛠️ 分享值得關注的開發工具 ❓ 有疑問?我來回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章