上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖
楼主: 李恒道 - 

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

[复制链接]
  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6764

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6764

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2022-12-17 20:03:09 | 显示全部楼层 | 阅读模式

    嵌套路由

    现在我们仅从数据库读取内容,这不是一个真正的解决方案,我们需要一种创建新博客文章的方法,我们去操作这个方法。
    让我们为应用创建一个管理的部分
    首先,我们在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
    结果如下
    图片.png

    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
    图片.png
    我们在网页端找一下这个字段

    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目前也是报错状态,所以让我们创建帖子之前添加一些验证,验证是否包含我们所需的内容,如果不包含则报告一个错误
    图片.png
    我们开始解决错误吧!
    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 来增强它的用户体验。

    结语

    官方的第一篇终于看个七七八八也操作完了...

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表