普通文組 2.5

快速筆記:如何對 remix 的 cookie 功能加上一點 TypeScript

筆記

根據 remix 官方的Cookie 範例,透過 createCookie 建立 cookieHelper 後,執行 cookieHelper.parse() 得到的回傳內容,其型別會是 any。但我想要在設定 request headers Set-Cookie 和取得 response headers Cookie 時借助一點 TypeScript 的力量,於是有了以下兩個功能 getsetAndSerialize

import { createCookie } from "@remix-run/node";

export const cookieHelper = createCookie("remixCookieJar");

const validKeys = ["bravoToken", "superToken"] as const;

type CookieKey = (typeof validKeys)[number];

export const get = async (key: CookieKey, cookies: string | null) => {
  const jar = await cookieHelper.parse(cookies);
  if (!jar) return "";
  return (jar[key] as string) || "";
};

export const setAndSerialize = async (
  key: CookieKey,
  value: string,
  cookies: string | null,
) => {
  const jar = await cookieHelper.parse(cookies);
  if (!jar) return "";
  jar[key] = value;
  return await cookieHelper.serialize(jar);
};

說明:

  • get: 目的是確保使用者從 cookie 取值時不會打錯字,比如目前只開放取用陣列 const validKeys = ['bravoToken', 'superToken'] as const; 提到的兩種鍵
  • setAndSerialize: 目的是確保使用者不會在對 cookie 賦值時打錯字,並順便回傳序列化後的資料

操作示範如下。首先在 remix action 使用 setAndSerialize 對 cookie 設定登入後回傳的 auth token,並透過 Set-Cookie 加進 response header 中:

import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { setAndSerialize } from "my-remix-app/app/cookies.server.ts";

export default function LoginPage() {
  return (
    <Form method="post">
      <button type="submit">Submit</button>
    </Form>
  );
}

export async function action({ request }: ActionFunctionArgs) {
  // do some login work...
  const authToken = "...";
  // assign auth token to cookie
  const headers = {
    "Set-Cookie": await setAndSerialize(
      "superToken",
      authToken,
      request.headers.get("Cookie"),
    ),
  };
  return redirect("/result", { headers });
}

而當我需要取得 auth token 時,就在 remix loader 使用 get 來取得 cookie 中的對應內容,並回傳的資料型別必定為字串,不需要再進行額外的型別驗證。可參考下方範例:

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { get } from "my-remix-app/app/cookies.server.ts";

export async function loader({ request }: LoaderFunctionArgs) {
  const superToken = await get("superToken", request.headers.get("Cookie"));
  return json({ superToken });
}

export default function ResultPage() {
  const { superToken } = useLoaderData<typeof loader>();
  return <p>{superToken ? superToken : "no super token"}</p>;
}

省事 🌚 害怕打錯字就是督促我多寫點抽象層的原動力。

最近在弄的專案會需要和同 domain 的不同產品共用 cookie,非 remix 建立的 cookie 就沒辦法透過 createCookie.get() / .set() 操作了。需要另覓他法。而考量到我需要在 remix loader 內(後端)處理 cookie,故選擇使用支援 Node.Js 環境的 cookie 套件。包一層抽象的程式碼如下:

import cookie from "cookie";

export const get3rdPartyCookie = async (
  key: string,
  cookies: string | null,
) => {
  const jar = cookie.parse(cookies || "");
  return jar[key] || "";
};

實際使用範例如下:

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { get3rdPartyCookie } from "my-remix-app/app/cookies.server.ts";

export async function loader({ request }: LoaderFunctionArgs) {
  const otherCookie = await get3rdPartyCookie(
    "notRemixCookieJar",
    request.headers.get("Cookie"),
  );
  return json({ otherCookie });
}

參考文件