一、创建 Remix 项目
npx create-remix@latest
yarn
yarn dev
二、目录结构
- 
app/ - 所有Remix应用程序代码的位置 
- 
app/entry.client.tsx- JavaScript 的第一部分,它将在应用程序在浏览器中加载时运行。我们使用此文件来补充我们的 React 组件。 
- 
app/entry.server.tsx- JavaScript 的第一部分,当请求到达您的服务器时,它将运行。Remix 处理加载所有必要的数据,您负责发回响应。我们将使用此文件将 React 应用呈现为字符串/流,并将其作为响应发送给客户端。 
- 
app/root.tsx- 应用程序放置根组件的位置。在此处呈现元素。 
- 
app/routes/- 所有"路由模块"的位置。Remix 使用此目录中的文件根据文件的名称为应用创建 URL 路由。 
- 
public/ - 静态资产(图像/字体/等)的位置 
- 
remix.config.js - Remix有一些配置选项,您可以在此文件中设置。 
三、路由(编程式/文件式)
1.这里使用文件式路由
app/routes/index.tsx
export default function IndexRoute() {
    return <div>Hello Index Route</div>;
}
2.
app/root.tsx
import { LiveReload, Outlet } from "remix";
export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <title>Remix: So great, it's funny!</title>
      </head>
      <body>
        <Outlet />
        {process.env.NODE_ENV === "development" ? (
          <LiveReload />
        ) : null}
      </body>
    </html>
  );
}
app/routes/jokes/index.tsx
export default function JokesIndexRoute() {
    return (
        <div>
            <p>Here's a random joke:</p>
            <p>
                I was wondering why the frisbee was getting bigger,
                then it hit me.
            </p>
        </div>
    )
}
app/routes/jokes/new.tsx
export default function NewJokeRoute() {
    return (
        <div>
            <p>Add your own hilarious joke</p>
            <form method="post">
                <div>
                    <label>
                        Name: <input type="text" name="name" />
                    </label>
                </div>
                <div>
                    <label>
                        Content: <textarea name="content" />
                    </label>
                </div>
                <div>
                    <button type="submit" className="button">
                        Add
                    </button>
                </div>
            </form>
        </div>
    );
}
3.动态路由(参数化路由)
app/routes/jokes/$jokeId.tsx
export default function JokeRoute() {
    return (
        <div>
            <p>Here's your hilarious joke:</p>
            <p>
                Why don't you find hippopotamuses hiding in trees?
                They're really good at it.
            </p>
        </div>
    );
}
四、样式
- 全局样式
app/root.tsx
import type { LinksFunction } from "remix";
import { Links, LiveReload, Outlet } from "remix";
import globalStylesUrl from "./styles/global.css";
import globalMediumStylesUrl from "./styles/global-medium.css";
import globalLargeStylesUrl from "./styles/global-large.css";
export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: globalStylesUrl
    },
    {
      rel: "stylesheet",
      href: globalMediumStylesUrl,
      media: "print, (min-width: 640px)"
    },
    {
      rel: "stylesheet",
      href: globalLargeStylesUrl,
      media: "screen and (min-width: 1024px)"
    }
  ];
};
export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <title>Remix: So great, it's funny!</title>
        <Links />
      </head>
      <body>
        <Outlet />
        {process.env.NODE_ENV === "development" ? (
          <LiveReload />
        ) : null}
      </body>
    </html>
  );
}
- 组件样式 (需要在root.tsx中调用组件才能渲染) 
app/routes/index.tsx
import type { LinksFunction } from "remix";
import { Link } from "remix";
import stylesUrl from "../styles/index.css";
export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: stylesUrl
    }
  ];
};
export default function Index() {
  return (
    <div className="container">
      <div className="content">
        <h1>
          Remix <span>Jokes!</span>
        </h1>
        <nav>
          <ul>
            <li>
              <Link to="jokes">Read Jokes</Link>
            </li>
          </ul>
        </nav>
      </div>
    </div>
  );
}
五、数据库 (这里使用Prisma ORM 和 SQLite 数据库)
Step1. 安装 Prisma
为 Prisma 安装 VSCode 插件
yarn add --dev prisma //用于在开发过程中与我们的数据库和schema进行交互
yarn add @prisma/client //用于在运行时对我们的数据库进行查询
Step2. 用 SQLite 初始化 Prisma
npx prisma init --datasource-provider sqlite
Step3. 为数据建模
prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
model Joke {
  id         String   @id @default(uuid())
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  name       String
  content    String
}
Step4. 推送数据模型到 SQLite
如果您的数据库搞砸了,您可以随时删除该prisma/dev.db文件并npx prisma db push再次运行。
npx prisma db push
Step5. 禁止 dev.db 提交到 github
.gitignore
/prisma/dev.db
.env
Step6. 测试数据填充数据库
prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function seed() {
  await Promise.all(
    getJokes().map(joke => {
      return db.joke.create({ data: joke });
    })
  );
}
seed();
function getJokes() {
  // shout-out to https://icanhazdadjoke.com/
  return [
    {
      name: "Road worker",
      content: `I never wanted to believe that my Dad was stealing from his job as a road worker. But when I got home, all the signs were there.`
    },
    {
      name: "Frisbee",
      content: `I was wondering why the frisbee was getting bigger, then it hit me.`
    },
    {
      name: "Trees",
      content: `Why do trees seem suspicious on sunny days? Dunno, they're just a bit shady.`
    },
    {
      name: "Skeletons",
      content: `Why don't skeletons ride roller coasters? They don't have the stomach for it.`
    },
    {
      name: "Hippos",
      content: `Why don't you find hippopotamuses hiding in trees? They're really good at it.`
    },
    {
      name: "Dinner",
      content: `What did one plate say to the other plate? Dinner is on me!`
    },
    {
      name: "Elevator",
      content: `My first time using an elevator was an uplifting experience. The second time let me down.`
    }
  ];
}
安装
esbuild-register为开发依赖项:
yarn add --dev esbuild-register
运行
seed.ts文件
node --require esbuild-register prisma/seed.ts
将此添加到
package.json,避免每次重置数据都必须记住该脚本
// ...
  "prisma": {
    "seed": "node --require esbuild-register prisma/seed.ts"
  },
  "scripts": {
// ...
Step7. 连接到数据库
prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
创建
app/utils/db.server.ts
import { PrismaClient } from "@prisma/client";
let db: PrismaClient;
declare global {
  var __db: PrismaClient | undefined;
}
//这是必需的,因为在开发中我们不想在每次更改时都重新启动服务器,
//但我们想确保我们也不会在每次更改时都创建到数据库的新连接。
if (process.env.NODE_ENV === "production") {
  db = new PrismaClient();
  db.$connect();
} else {
  if (!global.__db) {
    global.__db = new PrismaClient();
    global.__db.$connect();
  }
  db = global.__db;
}
export { db };
Step8. 限制数据获取数量
app/routes/jokes
type LoaderData = {
  jokeListItems: Array<{ id: string; name: string }>;
};
export const loader: LoaderFunction = async () => {
  const data: LoaderData = {
    jokeListItems: await db.joke.findMany({
      take: 5,
      select: { id: true, name: true },
      orderBy: { createdAt: "desc" }
    })
  };
  return data;
};
Step9. 数据库查询
app/routes/jokes/$jokeId.tsx
import type { LoaderFunction } from "remix";
import { Link, useLoaderData } from "remix";
import type { Joke } from "@prisma/client";
import { db } from "~/utils/db.server";
type LoaderData = { joke: Joke };
export const loader: LoaderFunction = async ({
  params
}) => {
  const joke = await db.joke.findUnique({
    where: { id: params.jokeId }
  });
  if (!joke) throw new Error("Joke not found");
  const data: LoaderData = { joke };
  return data;
};
export default function JokeRoute() {
  const data = useLoaderData<LoaderData>();
  return (
    <div>
      <p>Here's your hilarious joke:</p>
      <p>{data.joke.content}</p>
      <Link to=".">{data.joke.name} Permalink</Link>
    </div>
  );
}
随机查询
app/routes/jokes/index.tsx
import type { LoaderFunction } from "remix";
import { useLoaderData, Link } from "remix";
import type { Joke } from "@prisma/client";
import { db } from "~/utils/db.server";
type LoaderData = { randomJoke: Joke };
export const loader: LoaderFunction = async () => {
  const count = await db.joke.count();
  const randomRowNumber = Math.floor(Math.random() * count);
  const [randomJoke] = await db.joke.findMany({
    take: 1,
    skip: randomRowNumber
  });
  const data: LoaderData = { randomJoke };
  return data;
};
export default function JokesIndexRoute() {
  const data = useLoaderData<LoaderData>();
  return (
    <div>
      <p>Here's a random joke:</p>
      <p>{data.randomJoke.content}</p>
      <Link to={data.randomJoke.id}>
        "{data.randomJoke.name}" Permalink
      </Link>
    </div>
  );
}
Step10. 突变 (添加数据)
app/routes/new.tsx
import type { ActionFunction } from "remix";
import { redirect } from "remix";
import { db } from "~/utils/db.server";
export const action: ActionFunction = async ({
  request
}) => {
  const form = await request.formData();
  const name = form.get("name");
  const content = form.get("content");
  // we do this type check to be extra sure and to make TypeScript happy
  // we'll explore validation next!
  if (
    typeof name !== "string" ||
    typeof content !== "string"
  ) {
    throw new Error(`Form not submitted correctly.`);
  }
  const fields = { name, content };
  const joke = await db.joke.create({ data: fields });
  return redirect(`/jokes/${joke.id}`);
};
export default function NewJokeRoute() {
  return (
    <div>
      <p>Add your own hilarious joke</p>
      <form method="post">
        <div>
          <label>
            Name: <input type="text" name="name" />
          </label>
        </div>
        <div>
          <label>
            Content: <textarea name="content" />
          </label>
        </div>
        <div>
          <button type="submit" className="button">
            Add
          </button>
        </div>
      </form>
    </div>
  );
}
六、数据验证
app/routes/jokes/new.tsx
import type { ActionFunction } from "remix";
import { useActionData, redirect, json } from "remix";
import { db } from "~/utils/db.server";
function validateJokeContent(content: string) {
  if (content.length < 10) {
    return `That joke is too short`;
  }
}
function validateJokeName(name: string) {
  if (name.length < 2) {
    return `That joke's name is too short`;
  }
}
type ActionData = {
  formError?: string;
  fieldErrors?: {
    name: string | undefined;
    content: string | undefined;
  };
  fields?: {
    name: string;
    content: string;
  };
};
const badRequest = (data: ActionData) =>
  json(data, { status: 400 });
export const action: ActionFunction = async ({
  request
}) => {
  const form = await request.formData();
  const name = form.get("name");
  const content = form.get("content");
  if (
    typeof name !== "string" ||
    typeof content !== "string"
  ) {
    return badRequest({
      formError: `Form not submitted correctly.`
    });
  }
  const fieldErrors = {
    name: validateJokeName(name),
    content: validateJokeContent(content)
  };
  const fields = { name, content };
  if (Object.values(fieldErrors).some(Boolean)) {
    return badRequest({ fieldErrors, fields });
  }
  const joke = await db.joke.create({ data: fields });
  return redirect(`/jokes/${joke.id}`);
};
export default function NewJokeRoute() {
  const actionData = useActionData<ActionData>();
  return (
    <div>
      <p>Add your own hilarious joke</p>
      <form method="post">
        <div>
          <label>
            Name:{" "}
            <input
              type="text"
              defaultValue={actionData?.fields?.name}
              name="name"
              aria-invalid={
                Boolean(actionData?.fieldErrors?.name) ||
                undefined
              }
              aria-describedby={
                actionData?.fieldErrors?.name
                  ? "name-error"
                  : undefined
              }
            />
          </label>
          {actionData?.fieldErrors?.name ? (
            <p
              className="form-validation-error"
              role="alert"
              id="name-error"
            >
              {actionData.fieldErrors.name}
            </p>
          ) : null}
        </div>
        <div>
          <label>
            Content:{" "}
            <textarea
              defaultValue={actionData?.fields?.content}
              name="content"
              aria-invalid={
                Boolean(actionData?.fieldErrors?.content) ||
                undefined
              }
              aria-describedby={
                actionData?.fieldErrors?.content
                  ? "content-error"
                  : undefined
              }
            />
          </label>
          {actionData?.fieldErrors?.content ? (
            <p
              className="form-validation-error"
              role="alert"
              id="content-error"
            >
              {actionData.fieldErrors.content}
            </p>
          ) : null}
        </div>
        <div>
          <button type="submit" className="button">
            Add
          </button>
        </div>
      </form>
    </div>
  );
}
七、身份验证
Step1. prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
model User {
  id           String   @id @default(uuid())
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  username     String   @unique
  passwordHash String
  jokes        Joke[]
}
model Joke {
  id         String   @id @default(uuid())
  jokesterId String
  jokester   User     @relation(fields: [jokesterId], references: [id], onDelete: Cascade)
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  name       String
  content    String
}
Step2. 重置数据库
npx prisma db push