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

Xây dựng ứng dụng TODO có khả năng mở rộng với Remix và Redis Serverless

Trong bài viết này, chúng ta sẽ viết một ứng dụng TODO đơn giản sử dụng Remix và Redis Serverless (Upstash).

Remix là một framework web full-stack cho phép bạn tập trung vào giao diện người dùng và làm việc ngược lại qua các nguyên tắc cơ bản của web để cung cấp trải nghiệm người dùng nhanh, mượt mà và bền bỉ.

Tạo dự án Remix

Chạy lệnh sau:

npx create-remix@latest

Xây dựng ứng dụng TODO có khả năng mở rộng với Remix và Redis Serverless

Dự án đã sẵn sàng. Cài đặt phụ thuộc và chạy:

npm install
npm run dev

Xây dựng ứng dụng TODO có khả năng mở rộng với Remix và Redis Serverless

Giao diện người dùng

Chúng ta sẽ xây dựng một form đơn giản và danh sách cho các mục todo:

// app/routes/index.tsx
import type { ActionFunction, LoaderFunction } from "remix";
import { Form, useLoaderData, useTransition, redirect } from "remix";
import { useEffect, useRef } from "react";
import type { Todo } from "~/components/todo-item";
import TodoItem from "~/components/todo-item";

export const loader: LoaderFunction = async () => {
  // dữ liệu mẫu
  return [
    { id: 1, text: "Task 1", status: false },
    { id: 2, text: "Task 2", status: true },
  ];
};

export const action: ActionFunction = async ({ request }) => {
  // sẽ dùng cho tạo, cập nhật, xóa
};

export default function Index() {
  const transition = useTransition();
  const todos: Todo[] = useLoaderData();
  const isCreating = transition.submission?.method === "POST";
  const isAdding = transition.state === "submitting" && isCreating;

  // tách task hoàn thành và chưa hoàn thành
  const uncheckedTodos = todos.filter((todo) => !todo.status);
  const checkedTodos = todos.filter((todo) => todo.status);

  const formRef = useRef<HTMLFormElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // reset form sau khi tạo
    if (isAdding) return;
    formRef.current?.reset();
    inputRef.current?.focus();
  }, [isAdding]);

  return (
    <main className="container">
      {/* form tạo mới */}
      <Form ref={formRef} method="post">
        <input
          ref={inputRef}
          type="text"
          name="text"
          autoComplete="off"
          className="input"
          placeholder="Cần làm gì?"
          disabled={isCreating}
        />
      </Form>
      {/* task chưa hoàn thành */}
      <div className="todos">
        {uncheckedTodos.map((todo) => (
          <TodoItem key={todo.id} {...todo} />
        ))}
      </div>
      {/* task đã hoàn thành */}
      {checkedTodos.length > 0 && (
        <div className="todos todos-done">
          {checkedTodos.map((todo) => (
            <TodoItem key={todo.id} {...todo} />
          ))}
        </div>
      )}
    </main>
  );
}

Đây là component TODO:

// app/components/todo-item.tsx
import { Form } from "remix";

export type Todo = { id: string; text: string; status: boolean };

export default function TodoItem({ id, text, status }: Todo) {
  return (
    <div className="todo">
      <Form method="put">
        {/* input ẩn lưu dữ liệu todo */}
        <input type="hidden" name="todo" defaultValue={JSON.stringify({ id, text, status })} />
        {/* Remix forms giống form web truyền thống. Tôi thích điều này. */}
        <button type="submit" className="checkbox">
          {status && "✓"}
        </button>
      </Form>
      <span className="text">{text}</span>
    </div>
  );
}

Thêm file CSS app/styles/app.css:

:root {
  --rounded: 0.25rem;
  --rounded-md: 0.375rem;
  --gray-50: rgb(249, 250, 251);
  --gray-100: rgb(243, 244, 246);
  --gray-200: rgb(229, 231, 235);
  --gray-300: rgb(209, 213, 219);
  --gray-400: rgb(156, 163, 175);
  --gray-500: rgb(107, 114, 128);
  --gray-600: rgb(75, 85, 99);
  --gray-700: rgb(55, 65, 81);
  --gray-800: rgb(31, 41, 55);
  --gray-900: rgb(17, 24, 39);
}
*, ::before, ::after { box-sizing: border-box; border: 0; padding: 0; }
button, input, optgroup, select, textarea {
  font-family: inherit; font-size: 100%; line-height: inherit; color: inherit; margin: 0; padding: 0;
}
button { cursor: pointer; background-color: white; }
html {
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: var(--gray-800);
}
.container { padding: 8rem 1rem 0; margin: 0 auto; max-width: 28rem; }
.input { width: 100%; padding: 0.75rem 1rem; background-color: var(--gray-100); border-radius: var(--rounded-md); }
.input::placeholder { color: var(--gray-400); }
.input:disabled { color: var(--gray-600); background-color: var(--gray-200); }
.todos { margin-top: 1.5rem; }
.todos.todos-done { background-color: var(--gray-100); color: var(--gray-500); border-radius: var(--rounded-md); }
.todo { display: flex; align-items: center; padding: 0.75rem; border-radius: var(--rounded-md); }
.todo + .todo { border-top: 1px solid var(--gray-100); }
.todo .checkbox {
  display: flex; align-items: center; justify-content: center;
  width: 1.25rem; height: 1.25rem; border-radius: var(--rounded);
  border: 1px solid var(--gray-300); box-shadow: 0 1px 1px 0 rgb(0 0 0 / 10%);
}
.todo .text { margin-left: 0.75rem; }

Import CSS trong root.tsx:

import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "remix";
import type { MetaFunction } from "remix";
import styles from "./styles/app.css";

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

export const meta: MetaFunction = () => {
  return { title: "Remix Todo App with Redis" };
}

export default function App() {
  // ...
}

Kết quả:

Xây dựng ứng dụng TODO có khả năng mở rộng với Remix và Redis Serverless

Chuẩn bị cơ sở dữ liệu

Lưu trữ dữ liệu trong Upstash Redis. Tạo cơ sở dữ liệu Upstash. Sử dụng client HTTP. Cài đặt:

npm install @upstash/redis

:::note Upstash tương thích với Redis API, bạn có thể dùng bất kỳ client Redis nào nhưng cần sửa code. :::

Thêm TODO mới bằng cách submit form. Lưu vào Redis Hash.

Sao chép UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN từ Upstash console.

// app/routes/index.tsx
// ...
import { Redis } from "@upstash/redis";

const redis = new Redis({
  url: "UPSTASH_REDIS_REST_URL",
  token: "UPSTASH_REDIS_REST_TOKEN",
});

export const action: ActionFunction = async ({ request }) => {
  const form = await request.formData();
  if (request.method === "POST") {
    const text = form.get("text");
    if (!text) return redirect("/");
    await redis.hset("remix-todo-example", {
      [Date.now().toString()]: { text, status: false },
    });
  }
  // fetch lại danh sách sau mỗi thao tác
  return redirect("/");
};

Liệt kê các mục:

// app/routes/index.tsx
export const loader: LoaderFunction = async () => {
  const res = await redis.hgetall<Record<string, object>>(DATABASE_KEY);
  const todos = Object.entries(res ?? {}).map(([key, value]) => ({
    id: key,
    ...value,
  }));
  // sắp xếp theo ngày (id=timestamp)
  return todos.sort((a, b) => parseInt(b.id) - parseInt(a.id));
};

Đã có chức năng 'tạo' và 'liệt kê'. Triển khai đánh dấu hoàn thành:

// app/routes/index.tsx
export const action: ActionFunction = async ({ request }) => {
  const form = await request.formData();
  // tạo
  if (request.method === "POST") { /* ... */ }
  // cập nhật
  if (request.method === "PUT") {
    const todo = form.get("todo");
    const { id, text, status } = JSON.parse(todo as string);
    await redis.hset("remix-todo-example", {
      [id]: { text, status: !status },
    });
  }
  return redirect("/");
};

Hoàn tất! Tôi dự định triển khai cùng ứng dụng với Next.js và SvelteKit để so sánh.

Hãy theo dõi chúng tôi trên Twitter và Discord.

Mã nguồn dự án

https://github.com/upstash/redis-examples/tree/master/remix-todo-app-with-redis

Trang Demo dự án

https://remix-todo-app-with-redis.vercel.app/