一、创建 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