快速筆記:如何對 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);
};

說明:

操作示範如下。首先在 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 });
}

參考文件