李恒道 发表于 2022-12-17 20:03:09

【Remix教程翻译】开发者博客(四)

# 嵌套路由
现在我们仅从数据库读取内容,这不是一个真正的解决方案,我们需要一种创建新博客文章的方法,我们去操作这个方法。
让我们为应用创建一个管理的部分
首先,我们在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
```js
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
结果如下
![图片.png](data/attachment/forum/202212/17/171640uyyiieusfuuosuyt.png)
# Index路由
让我们用admin的index路由来代替占位符,耐心等待,我们在这里引入了嵌套路由,您的路由文件会编程页面的UI组件嵌套
为admin.tsx创建一个文件夹,里面有一个索引
app/routes/posts/admin/index.tsx
```js
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
```js
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路由中添加表单
```js
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的任意位置
```js
export async function createPost(post) {
return prisma.post.create({ data: post });
}
```
createPost从new路由的action调用
app/routes/posts/admin/new.tsx
```js
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
```js
export declare type ActionArgs = DataFunctionArgs;
```
发现是DataFunctionArgs
继续看
```js
/**
* 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
![图片.png](data/attachment/forum/202212/17/180515ucc09cweppcb43b5.png)
我们在网页端找一下这个字段
```js
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添加类型
```js
import type { Post } from "@prisma/client";

export async function createPost(
post: Pick<Post, "slug" | "title" | "markdown">
) {
return prisma.post.create({ data: post });
}
```
Pick是一个类型工具,选择第二个参数的字符串属性构造一个新的类型对象
```js
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
    : T;
};
```

我们继续看remix教程
无论是否使用Typescript,当用户未在某个字段提供值的时候,我们都会遇到问题,TS目前也是报错状态,所以让我们创建帖子之前添加一些验证,验证是否包含我们所需的内容,如果不包含则报告一个错误
![图片.png](data/attachment/forum/202212/17/183509awubdvlxy3wkjypg.png)
我们开始解决错误吧!
app/routes/posts/admin/new.tsx
```js
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
```js
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解决这个问题。
```js
//...
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
```js
export const action = async ({ request }: ActionArgs) => {
// TODO: remove me
await new Promise((res) => setTimeout(res, 1000));

// ...
};
```
添加一些等待的 UI 使用useTransition
```js
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 来增强它的用户体验。
# 结语
官方的第一篇终于看个七七八八也操作完了...


页: [1]
查看完整版本: 【Remix教程翻译】开发者博客(四)