第一个Remix应用 #

一、应用概述 #

我们将创建一个简单的待办事项应用,包含以下功能:

  • 显示待办事项列表
  • 添加新的待办事项
  • 标记完成状态
  • 删除待办事项

二、创建项目 #

bash
npx create-remix@latest todo-app --template remix
cd todo-app
npm run dev

三、构建首页 #

3.1 修改根组件 #

编辑 app/root.tsx

tsx
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";

import stylesheet from "~/tailwind.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

export default function App() {
  return (
    <html lang="zh-CN">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body className="bg-gray-100 min-h-screen">
        <div className="container mx-auto px-4 py-8">
          <Outlet />
        </div>
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

3.2 创建首页路由 #

编辑 app/routes/_index.tsx

tsx
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";

export const meta: MetaFunction = () => {
  return [
    { title: "待办事项应用" },
    { name: "description", content: "一个简单的待办事项管理应用" },
  ];
};

export default function Index() {
  return (
    <div className="max-w-2xl mx-auto">
      <h1 className="text-3xl font-bold text-center mb-8">
        待办事项应用
      </h1>
      <nav className="flex justify-center gap-4 mb-8">
        <Link
          to="/todos"
          className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
        >
          查看待办事项
        </Link>
      </nav>
      <p className="text-center text-gray-600">
        欢迎使用 Remix 待办事项应用!
      </p>
    </div>
  );
}

四、创建待办事项页面 #

4.1 创建数据模型 #

创建 app/models/todo.server.ts

tsx
export interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

let todos: Todo[] = [
  {
    id: "1",
    title: "学习 Remix",
    completed: false,
    createdAt: new Date(),
  },
  {
    id: "2",
    title: "构建第一个应用",
    completed: false,
    createdAt: new Date(),
  },
];

export async function getTodos(): Promise<Todo[]> {
  return todos;
}

export async function createTodo(title: string): Promise<Todo> {
  const todo: Todo = {
    id: Date.now().toString(),
    title,
    completed: false,
    createdAt: new Date(),
  };
  todos.push(todo);
  return todo;
}

export async function toggleTodo(id: string): Promise<Todo | null> {
  const todo = todos.find((t) => t.id === id);
  if (todo) {
    todo.completed = !todo.completed;
    return todo;
  }
  return null;
}

export async function deleteTodo(id: string): Promise<boolean> {
  const index = todos.findIndex((t) => t.id === id);
  if (index > -1) {
    todos.splice(index, 1);
    return true;
  }
  return false;
}

4.2 创建待办事项列表页面 #

创建 app/routes/todos._index.tsx

tsx
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, Form, Link } from "@remix-run/react";
import { getTodos } from "~/models/todo.server";

export const meta: MetaFunction = () => {
  return [{ title: "待办事项列表" }];
};

export async function loader({}: LoaderFunctionArgs) {
  const todos = await getTodos();
  return json({ todos });
}

export default function TodosIndex() {
  const { todos } = useLoaderData<typeof loader>();

  return (
    <div className="max-w-2xl mx-auto">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-2xl font-bold">待办事项列表</h1>
        <Link
          to="/todos/new"
          className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
        >
          添加新事项
        </Link>
      </div>

      {todos.length === 0 ? (
        <p className="text-center text-gray-500 py-8">
          暂无待办事项,点击上方按钮添加。
        </p>
      ) : (
        <ul className="space-y-3">
          {todos.map((todo) => (
            <li
              key={todo.id}
              className="flex items-center justify-between bg-white p-4 rounded shadow"
            >
              <div className="flex items-center gap-3">
                <Form method="post" action="/todos/toggle">
                  <input type="hidden" name="id" value={todo.id} />
                  <button
                    type="submit"
                    className={`w-5 h-5 rounded border ${
                      todo.completed
                        ? "bg-green-500 border-green-500"
                        : "border-gray-300"
                    }`}
                  />
                </Form>
                <span
                  className={
                    todo.completed ? "line-through text-gray-400" : ""
                  }
                >
                  {todo.title}
                </span>
              </div>
              <Form method="post" action="/todos/delete">
                <input type="hidden" name="id" value={todo.id} />
                <button
                  type="submit"
                  className="text-red-500 hover:text-red-700"
                >
                  删除
                </button>
              </Form>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

4.3 创建添加待办事项页面 #

创建 app/routes/todos.new.tsx

tsx
import type { ActionFunctionArgs, MetaFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, Link } from "@remix-run/react";
import { createTodo } from "~/models/todo.server";

export const meta: MetaFunction = () => {
  return [{ title: "添加待办事项" }];
};

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");

  if (typeof title !== "string" || title.trim() === "") {
    return redirect("/todos/new");
  }

  await createTodo(title.trim());
  return redirect("/todos");
}

export default function NewTodo() {
  return (
    <div className="max-w-2xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">添加待办事项</h1>
      <Form method="post" className="bg-white p-6 rounded shadow">
        <div className="mb-4">
          <label className="block text-gray-700 mb-2">
            待办事项标题
          </label>
          <input
            type="text"
            name="title"
            className="w-full border rounded px-3 py-2"
            placeholder="输入待办事项..."
            required
          />
        </div>
        <div className="flex gap-3">
          <button
            type="submit"
            className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
          >
            添加
          </button>
          <Link
            to="/todos"
            className="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400"
          >
            取消
          </Link>
        </div>
      </Form>
    </div>
  );
}

4.4 创建Action处理 #

创建 app/routes/todos.toggle.ts

tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { toggleTodo } from "~/models/todo.server";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const id = formData.get("id");

  if (typeof id === "string") {
    await toggleTodo(id);
  }

  return redirect("/todos");
}

创建 app/routes/todos.delete.ts

tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { deleteTodo } from "~/models/todo.server";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const id = formData.get("id");

  if (typeof id === "string") {
    await deleteTodo(id);
  }

  return redirect("/todos");
}

五、添加样式 #

5.1 安装 Tailwind CSS #

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

5.2 配置 Tailwind #

编辑 tailwind.config.js

javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

5.3 创建CSS文件 #

创建 app/tailwind.css

css
@tailwind base;
@tailwind components;
@tailwind utilities;

六、运行应用 #

bash
npm run dev

访问 http://localhost:5173,你将看到:

  1. 首页有导航链接
  2. 待办事项列表页面显示所有事项
  3. 可以添加新的待办事项
  4. 可以切换完成状态
  5. 可以删除待办事项

七、项目结构总览 #

text
todo-app/
├── app/
│   ├── models/
│   │   └── todo.server.ts    # 数据模型
│   ├── routes/
│   │   ├── _index.tsx        # 首页
│   │   ├── todos._index.tsx  # 待办列表
│   │   ├── todos.new.tsx     # 添加页面
│   │   ├── todos.toggle.ts   # 切换状态
│   │   └── todos.delete.ts   # 删除处理
│   ├── root.tsx              # 根组件
│   └── tailwind.css          # 样式文件
├── public/
├── package.json
└── vite.config.ts

八、学到的核心概念 #

8.1 路由 #

  • 文件系统路由:文件名决定URL路径
  • _index.tsx:路由的默认页面

8.2 Loader #

  • 服务端数据加载函数
  • 使用 useLoaderData 获取数据

8.3 Action #

  • 处理表单提交
  • 使用 formData 获取表单数据

8.4 Form #

  • 原生表单增强
  • 自动处理表单提交

九、下一步 #

恭喜你完成了第一个 Remix 应用!接下来你可以:

  1. 添加数据库持久化存储
  2. 添加用户认证
  3. 添加表单验证
  4. 部署到生产环境

让我们继续学习 Remix 的更多功能!

最后更新:2026-03-28