嵌套路由
现在我们仅从数据库读取内容,这不是一个真正的解决方案,我们需要一种创建新博客文章的方法,我们去操作这个方法。
让我们为应用创建一个管理的部分
首先,我们在posts的index路由的管理部门添加一个链接
app/routes/posts/index.tsx
// ...
<Link to="admin" className="text-red-600 underline">
Admin
</Link>
// ...
你可以把这个添加到任何地方,我将她添加到h1标签的下方
tips
你是否注意到prop属性是admin,却跳转到了/post/admin
再Remix框架中,你可以使用相对链接
在posts目录创建admin路由
app/routes/posts/admin.tsx
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { getPosts } from "~/models/post.server";
export const loader = async () => {
return json({ posts: await getPosts() });
};
export default function PostAdmin() {
const { posts } = useLoaderData<typeof loader>();
return (
<div className="mx-auto max-w-4xl">
<h1 className="my-6 mb-2 border-b-2 text-center text-3xl">
Blog Admin
</h1>
<div className="grid grid-cols-4 gap-6">
<nav className="col-span-4 md:col-span-1">
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link
to={post.slug}
className="text-blue-600 underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</nav>
<main className="col-span-4 md:col-span-3">
...
</main>
</div>
</div>
);
}
你应该已经轻车熟路,现在,你有一个外观不错的页面,左侧是帖子,右侧是占位符,如果你单机Admin链接,会带领你跳转到http://localhost:3000/posts/admin
结果如下
Index路由
让我们用admin的index路由来代替占位符,耐心等待,我们在这里引入了嵌套路由,您的路由文件会编程页面的UI组件嵌套
为admin.tsx创建一个文件夹,里面有一个索引
app/routes/posts/admin/index.tsx
import { Link } from "@remix-run/react";
export default function AdminIndex() {
return (
<p>
<Link to="new" className="text-blue-600 underline">
Create a New Post
</Link>
</p>
);
}
如果你刷新,目前你不会看到他,当他的URL匹配时,每个app/routes/posts/admin/文件夹内的路由将会在app/routes/posts/admin.tsx的内部立即渲染,你可以控制admin.tsx的哪一部分来让子路由进行渲染。
添加Outlet组件在admin.tsx
app/routes/posts/admin.tsx
import { json } from "@remix-run/node";
import {
Link,
Outlet,
useLoaderData,
} from "@remix-run/react";
import { getPosts } from "~/models/post.server";
export const loader = async () => {
return json({ posts: await getPosts() });
};
export default function PostAdmin() {
const { posts } = useLoaderData<typeof loader>();
return (
<div className="mx-auto max-w-4xl">
<h1 className="my-6 mb-2 border-b-2 text-center text-3xl">
Blog Admin
</h1>
<div className="grid grid-cols-4 gap-6">
<nav className="col-span-4 md:col-span-1">
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link
to={post.slug}
className="text-blue-600 underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</nav>
<main className="col-span-4 md:col-span-3">
<Outlet />
</main>
</div>
</div>
);
}
请在此稍等几分钟,index路由可能第一眼会让你困惑,但是当你知道url匹配到父路由的时候,index就会渲染到outlet内部
也许这会有点帮助,让我们添加/posts/admin/new路由,看看当我们点击链接的时候会发生什么
app/routes/posts/admin/new.tsx
export default function NewPost() {
return <h2>New Post</h2>;
}
现在单击索引中的链接,outlet就会立即切换到new路由
动作
我们现在要做一个非常严肃的事情,我们要在new路由中创建一个表单用于创建新的文章
首先在new路由中添加表单
import { Form } from "@remix-run/react";
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
export default function NewPost() {
return (
<Form method="post">
<p>
<label>
Post Title:{" "}
<input
type="text"
name="title"
className={inputClassName}
/>
</label>
</p>
<p>
<label>
Post Slug:{" "}
<input
type="text"
name="slug"
className={inputClassName}
/>
</label>
</p>
<p>
<label htmlFor="markdown">Markdown:</label>
<br />
<textarea
id="markdown"
rows={20}
name="markdown"
className={`${inputClassName} font-mono`}
/>
</p>
<p className="text-right">
<button
type="submit"
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
>
Create Post
</button>
</p>
</Form>
);
}
如果你和我们一样喜欢html,你应该会感到非常激动,如果你已经做过非常多的<form onSubmit>
和<button onClick>
,你会被HTML所震撼。
对于这样的功能,你只需要在用户界面使用这样一个表单去获取数据,以及一个后端进行处理的行为,在Remix中,这就是你全部要做的。
让我们先创建一个知道如何保存文章的基本代码在post.ts模块中
添加createPost函数在app/models/post.server.ts的任意位置
export async function createPost(post) {
return prisma.post.create({ data: post });
}
createPost从new路由的action调用
app/routes/posts/admin/new.tsx
import type { ActionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { createPost } from "~/models/post.server";
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const title = formData.get("title");
const slug = formData.get("slug");
const markdown = formData.get("markdown");
await createPost({ title, slug, markdown });
return redirect("/posts/admin");
};
这里是action 的request 是从ActionArgs进行解构
我们去看一下ActionArgs
export declare type ActionArgs = DataFunctionArgs;
发现是DataFunctionArgs
继续看
/**
* The arguments passed to ActionFunction and LoaderFunction.
*/
export interface DataFunctionArgs {
request: Request;
context: AppLoadContext;
params: Params;
}
可以看到有上下文context,params参数,request请求三个
我们继续看原来的代码
Remix和浏览器会做其他事情,单击提交按钮,并观察我们列出来的帖子会自动更新。
在HTML中,输入name的属性将自动发送到后端,并且请求中的formData,哦,别忘了,request和formData对象都是来自于网络规范的,因此,如果你想了解关于他们中的任何信息,请前往MDN,https://developer.mozilla.org/en-US/docs/Web/API/Request/formData
补充
这里可以看到响应头有一个x-remix-redirect
我们在网页端找一下这个字段
async function checkRedirect(response) {
if (isRedirectResponse(response)) {
let url = new URL(response.headers.get("X-Remix-Redirect"), window.location.origin);
if (url.origin !== window.location.origin) {
await new Promise(() => {
window.location.replace(url.href);
});
} else {
return new TransitionRedirect(url.pathname + url.search + url.hash, response.headers.get("X-Remix-Revalidate") !== null);
}
}
return null;
}
可以发现网页端在接收到请求响应之后判断是否存在这个属性,如果存在则直接进行跳转的代码,我们继续
Typescript又疯了,让我们添加一些类型。
在app/models/post.server.ts添加类型
import type { Post } from "@prisma/client";
export async function createPost(
post: Pick<Post, "slug" | "title" | "markdown">
) {
return prisma.post.create({ data: post });
}
Pick是一个类型工具,选择第二个参数的字符串属性构造一个新的类型对象
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
我们继续看remix教程
无论是否使用Typescript,当用户未在某个字段提供值的时候,我们都会遇到问题,TS目前也是报错状态,所以让我们创建帖子之前添加一些验证,验证是否包含我们所需的内容,如果不包含则报告一个错误
我们开始解决错误吧!
app/routes/posts/admin/new.tsx
import type { ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { createPost } from "~/models/post.server";
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const title = formData.get("title");
const slug = formData.get("slug");
const markdown = formData.get("markdown");
const errors = {
title: title ? null : "Title is required",
slug: slug ? null : "Slug is required",
markdown: markdown ? null : "Markdown is required",
};
const hasErrors = Object.values(errors).some(
(errorMessage) => errorMessage
);
if (hasErrors) {
return json(errors);
}
await createPost({ title, slug, markdown });
return redirect("/posts/admin");
};
这次我们没有返回重定向,我们实际上返回了错误的json,这些错误可以通过useActionData提供给组件,类似于useLoaderData函数,但是数据是来自于表单提交之后。
向 UI 添加验证消息
app/routes/posts/admin/new.tsx
import type { ActionArgs } from "@remix-run/node";
import { redirect, json } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
// ...
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
export default function NewPost() {
const errors = useActionData<typeof action>();
return (
<Form method="post">
<p>
<label>
Post Title:{" "}
{errors?.title ? (
<em className="text-red-600">{errors.title}</em>
) : null}
<input type="text" name="title" className={inputClassName} />
</label>
</p>
<p>
<label>
Post Slug:{" "}
{errors?.slug ? (
<em className="text-red-600">{errors.slug}</em>
) : null}
<input type="text" name="slug" className={inputClassName} />
</label>
</p>
<p>
<label htmlFor="markdown">
Markdown:{" "}
{errors?.markdown ? (
<em className="text-red-600">
{errors.markdown}
</em>
) : null}
</label>
<br />
<textarea
id="markdown"
rows={20}
name="markdown"
className={`${inputClassName} font-mono`}
/>
</p>
<p className="text-right">
<button
type="submit"
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
>
Create Post
</button>
</p>
</Form>
);
}
此时TypeScript 仍然存在错误,因为有人可以用非字符串值调用我们的 API或不存在,所以让我们添加一些invariant解决这个问题。
//...
import invariant from "tiny-invariant";
// ..
export const action = async ({ request }: ActionArgs) => {
// ...
invariant(
typeof title === "string",
"title must be a string"
);
invariant(
typeof slug === "string",
"slug must be a string"
);
invariant(
typeof markdown === "string",
"markdown must be a string"
);
await createPost({ title, slug, markdown });
return redirect("/posts/admin");
};
渐进式增强
为了真正的乐趣,请在您的开发工具中禁用 JavaScript并尝试一下。因为 Remix 是建立在 HTTP 和 HTML 的基础之上的,所以这一切都可以在浏览器中没有 JavaScript 的情况下运行🤯但这不是重点。很酷的是,这意味着我们的 UI 可以适应网络问题。但我们真的很喜欢在浏览器中使用 JavaScript,当我们拥有它时,我们可以做很多很酷的事情,所以在继续之前确保重新启用 JavaScript,因为我们将需要它来逐步增强接下来是用户体验。
让我们减缓响应并向我们的表单添加一些“等待 UI”。
用假延迟减慢我们的行动
app/routes/posts/admin/new.tsx
export const action = async ({ request }: ActionArgs) => {
// TODO: remove me
await new Promise((res) => setTimeout(res, 1000));
// ...
};
添加一些等待的 UI 使用useTransition
import type { ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import {
Form,
useActionData,
useTransition,
} from "@remix-run/react";
// ..
export default function NewPost() {
const errors = useActionData<typeof action>();
const transition = useTransition();
const isCreating = Boolean(transition.submission);
return (
<Form method="post">
{/* ... */}
<p className="text-right">
<button
type="submit"
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
disabled={isCreating}
>
{isCreating ? "Creating..." : "Create Post"}
</button>
</p>
</Form>
);
}
您刚刚实现了支持 JavaScript 的渐进增强!通过我们所做的,体验比浏览器本身可以做的更好。许多应用程序使用 JavaScript 来实现(实际上有一些应用程序确实需要 JavaScript 才能工作),但我们已经将工作体验作为基础,并且只是使用 JavaScript 来增强它的用户体验。
结语
官方的第一篇终于看个七七八八也操作完了...