Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Tích hợp Upstash Redis vào Remix để quản lý Feature Flag hiệu quả

Remix là cách tiếp cận mới về fullstack React framework, tập trung vào web standards hiện có và gắn kết chặt chẽ frontend với backend. Sự gắn kết chặt chẽ này là làn gió mới khi thấy việc load data vào React components hay xử lý data submit từ form đơn giản đến nhường nào.

Trong bài này chúng ta sẽ thấy sức mạnh của Remix bằng cách tạo một hệ thống Feature Flag management đơn giản dùng Upstash Redis làm database.

Full source code có thể tìm thấy ở đây.

Thiết lập

Tạo Remix app mới chạy npx create-remix@latest chọn deployment environment ưa thích. Tôi chọn Vercel nhưng không ảnh hưởng tutorial này.

Có 2 cách kết nối Upstash Redis: Thứ nhất qua TCP connection, dùng bất kỳ Redis client library chuẩn nào. Thứ hai qua REST API của Upstash. Chúng ta chọn option 2 vì nó available trong tất cả serverless environments, như Cloudflare Workers environment mà Remix có thể deploy. Upstash có package mô phỏng Redis commands thực tế, dễ biết gọi function nào.

Cần cách lưu 2 environment variables để kết nối Upstash Redis. Remix không có development env var support out of the box, nhưng thực hiện được bằng cách thêm dotenv làm dev dependency.

npm add --save-dev dotenv

Trong file .env (nên thêm vào .gitignore) setup 2 env vars cần thiết kết nối Upstash Redis. Package @upstash/redis tự detect các biến này nên không cần connect trong code. Giá trị này tìm thấy trong Upstash dashboard sau khi tạo Redis database mới.

UPSTASH_REDIS_REST_URL="https://..."
UPSTASH_REDIS_REST_TOKEN="..."

Cập nhật script dev để dotenv load env vars. Các script khác giữ nguyên.

{
  "scripts": {
    "dev": "node -r dotenv/config node_modules/.bin/remix dev"
  }
}

Lưu trữ Feature Data

Feature flags có thể phức tạp, với rollout plans cho phần trăm userbase, enabled cho groups user cụ thể, nhưng cũng có thể đơn giản chỉ "on" và "off". Chúng ta lưu feature flags dùng Hash data type của Redis. Data sẽ giống JSON dưới, "1" là enabled/on, "0" là disabled/off.

{
  "chart": "1",
  "graph": "0"
}

Để truy cập và thao tác data, dùng 4 commands/functions Redis cung cấp:

  • hgetall lấy tất cả keys (features) và values (enabled/disabled).
  • hset enable hoặc disable feature flag cụ thể.
  • hdel xóa feature flag cụ thể.
  • hmget lấy multiple nhưng specific feature flag values cùng lúc.

Quản lý Features

Xây dựng trang tại /features chịu trách nhiệm tạo và quản lý (enable/disable/delete) features hiện có. Chi tiết AddFeature và FeatureList khi thảo luận load data và write data.

// app/routes/features.tsx
export default function Features() {
  return (
    <div>
      <h1>Features</h1>
      <AddFeature />
      <FeatureList />
    </div>
  );
}

Data Loaders

Data loader là exported function trong Remix tên loader chạy trên server và trả về data available cho React component qua hook.

Bắt đầu với trang tạo và quản lý feature flags, muốn trả về all features. Trả về dạng array of pairs:

[
  ["graph", true],
  ["chart", false]
]

Bắt đầu với TypeScript type definition, rồi function loadAllFeatures dùng hgetall từ @upstash/redis.

import { Redis } from "@upstash/redis";
type LoaderData = { features: Array<[string, boolean]> };

const loadAllFeatures = async () => {
  const redis = Redis.fromEnv();
  const data = await redis.hgetall("features");
  const features: Array<[string, boolean]> = [];
  for (let i = 0; i < data.length; i += 2) {
    features.push([data[i], data[i + 1] === "1"]);
  }
  return features.sort((a, b) => {
    if (a[0] > b[0]) return 1;
    if (a[0] < b[0]) return -1;
    return 0;
  });
};

Exported loader function gọi loadAllFeatures, trả về features để pass vào React component.

export const loader: LoaderFunction = async (): Promise<LoaderData> => {
  // Thêm authentication/authorization ở đây
  const features = await loadAllFeatures();
  return { features };
};

Chi tiết React component sau, nhưng để truy cập data từ loader function, dùng Remix hook useLoaderData.

const FeatureList = () => {
  const { features } = useLoaderData<LoaderData>();
  return (
    <ul>
      {features.map(([feature, active]) => (
        <li key={feature}>{/* coming soon */}</li>
      ))}
    </ul>
  );
};

Form Actions

Đã thấy load data, nhưng chưa có feature flags trong DB! Form actions xử lý data. Export function tên action. Giống loader, chạy trên server và thường trả về json data component truy cập qua hook khác, hoặc redirect sang trang khác.

action function dưới xử lý 4 loại action: tạo feature, enable/disable feature, xóa feature. Dùng switch statement gọi Redis function/command phù hợp.

export const action: ActionFunction = async ({ request }) => {
  // Thêm authentication/authorization ở đây
  const formData = await request.formData();
  const feature = formData.get("feature") as string;
  const action = formData.get("_action") as string;
  if (!feature || feature.length === 0) {
    return json({ error: "Please provide a feature" });
  }
  switch (action) {
    case "create":
    case "enable":
      await redis.hset("features", { [feature]: 1 });
      break;
    case "disable":
      await redis.hset("features", { [feature]: 0 });
      break;
    case "delete":
      await redis.hdel("features", feature);
      break;
  }
  return redirect("/features");
};

Khi thành công, redirect về trang hiện tại. Điều này trigger page reload, gọi loader function và cập nhật hiển thị cho user.

Để thêm feature flag mới, component AddFeature dùng Remix Form component submit data đến action function. Chỉ định method post và prop replace để không thêm trang mới vào history browser mỗi lần tạo feature flag.

const AddFeature = () => {
  return (
    <Form method="post" replace>
      <input type="hidden" name="_action" value="create" />
      <input type="text" name="feature" required placeholder="name" />
      <button type="submit">Add</button>
    </Form>
  );
};

Sau khi tạo feature, muốn show all current feature flags để manage. Mỗi feature flag hiển thị 2 forms: Một enable/disable, một delete.

Lưu ý 2 hidden fields: _action để action function biết action gì, và feature gửi flag name muốn modify.

const FeatureList = () => {
  const { features } = useLoaderData<LoaderData>();
  return (
    <ul>
      {features.map(([feature, active]) => (
        <li key={feature}>
          <Form method="post" replace>
            <input type="hidden" name="_action" value={active ? "disable" : "enable"} />
            <input type="hidden" name="feature" value={feature} />
            <button type="submit" className="btn-naked">
              {active ? "💪" : "🦾"}
            </button>
          </Form>
          <span>{feature}</span>
          <Form method="post" replace>
            <input type="hidden" name="_action" value="delete" />
            <input type="hidden" name="feature" value={feature} />
            <button type="submit">Delete</button>
          </Form>
        </li>
      ))}
    </ul>
  );
};

Sử dụng Features

Có feature flags trong Upstash Redis, nhưng vô ích nếu không toggle functionality on/off dựa trên flags. Dùng loader function load specific features từ DB dùng hmget, rồi manipulate data để đúng structure.

Muốn load ["chart", "graph", "fake"] flags, Redis trả về ["1", "0", null]... nhớ nếu flag không tồn tại, value là null, tôi include fake flag để demo.

type LoaderData = { features: Record<string, boolean> };

const loadFeatures = async (keys: Array<string>) => {
  const data = await redis.hmget("features", ...keys);
  const features = keys.reduce<Record<string, boolean>>((acc, key, index) => {
    acc[key] = data[index] === "1";
    return acc;
  }, {});
  return features;
};

export const loader: LoaderFunction = async (): Promise<LoaderData> => {
  const features = await loadFeatures(["chart", "graph"]);
  return { features };
};

Truy cập loaded data trong component, dùng Remix useLoaderData hook. Chọn cách functionality website thay đổi dựa trên flag enabled/disabled.

export default function Index() {
  const { features } = useLoaderData<LoaderData>();
  return (
    <div>
      <h1>Dashboard</h1>
      {features.chart ? <h2>Chart</h2> : <h2>No Chart</h2>}
      {features.graph ? <h2>Graph</h2> : <h2>No Graph</h2>}
    </div>
  );
}

Kết luận

Trong bài này chúng ta thấy cách dùng Upstash Redis tạo feature flag system đơn giản trong Remix, tận dụng data loader và form action server-side functions. Cho phép giữ backend và frontend của trang cụ thể gắn kết chặt, iterate nhanh mà không cần setup GraphQL API riêng và override form submission events trên frontend. Remix leaning vào web standards về cách forms submit data.