动态路由参数
我们现在来创建一个实际的路由来进行帖子的查看,我们应该让以下URL可用
/posts/my-first-post
/posts/90s-mixtape
我们可以在url中使用路径地址匹配,而不是为每一个动态路径创建一个路由地址,Remix将解析并且传递给我们内容,方便我们访问动态地址可以阅览帖子。
我们现在访问到app/routes/posts/创建一个$slug.tsx
然后贴入以下内容
export default function PostSlug() {
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">
Some Post
</h1>
</main>
);
}
可以看到目前已经有了内容
然后添加loader函数的代码来获取参数
注意,这里的loader是服务器的函数,只由服务端来执行
如果在服务端执行渲染,则不会发送请求
如果在客户端执行渲染,则会发送到服务端请求对应的参数
另外我们可以观察一下laoder函数的签名LoaderFunction
/**
* A function that loads data for a route.
*/
export interface LoaderFunction {
(args: DataFunctionArgs): Promise<Response> | Response | Promise<AppData> | AppData;
}
export interface DataFunctionArgs {
request: Request;
context: AppLoadContext;
params: Params;
}
可以看到,函数参数是一个对象,包含了请求,上下文以及参数,而返回推荐是一个Promise,Appdata是any类型,代表我们也可以返回一个任意类型。
现在我们通过slug来获取帖子的内容
在post文件里增加一个getPost函数
修改app/models/post.server.ts
import { prisma } from "~/db.server";
export async function getPosts() {
return prisma.post.findMany();
}
export async function getPost(slug: string) {
return prisma.post.findUnique({ where: { slug } });
}
tips
如果看到 TypeScript 警告,例如TS2305: Module '"@prisma/client"' has no exported member 'Post'.,尝试重新启动编辑器
我们接下来继续尝试编写slug.tsc代码
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getPost } from "~/models/post.server";
export const loader: LoaderFunction = async ({ params }) => {
const post = await getPost(params.slug!);
return json({ post });
};
export default function PostSlug() {
const { post } = useLoaderData();
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">{post.title}</h1>
</main>
);
}
这个时候已经可以从服务器来拉取数据了
现在我们补充一下Typescript的类型
我们修改一下$slug的代码
注意要在app\models\post.server.ts中导出一下Post的类型
export type { Post } from "@prisma/client";
然后就可以
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import type { Post } from "~/models/post.server";
import { getPost } from "~/models/post.server";
type LoaderData = { post: Post };
export const loader: LoaderFunction = async ({ params }) => {
invariant(params.slug, `params.slug is required`);
const post = await getPost(params.slug);
invariant(post, `Post not found: ${params.slug}`);
return json<LoaderData>({ post });
};
export default function PostSlug() {
const { post } = useLoaderData<LoaderData>();
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">{post.title}</h1>
</main>
);
}
有几点需要注意
invariant是为了判断是否目前条件为真,我们可以看一下他的代码
可以看到就是判断一下条件,如果不符合就throw new Error一下,触发异常,这时候返回的页面就也会显示一个异常的页面了
var isProduction = process.env.NODE_ENV === 'production';
var prefix = 'Invariant failed';
function invariant(condition, message) {
if (condition) {
return;
}
if (isProduction) {
throw new Error(prefix);
}
var provided = typeof message === 'function' ? message() : message;
var value = provided ? "".concat(prefix, ": ").concat(provided) : prefix;
throw new Error(value);
}
但是为什么加上这个函数Ts就不报错了?
我们去看一下他的定义
export default function invariant(condition: any, message?: string | (() => string)): asserts condition;
可以看到有一个asserts condition
这个是TS的类型条件断言
如果condition传入的条件为真,则继续执行,并且类型会判断为通过该条件的类型,如果为假则报出异常
我们在这里首先params.slug的类型为string|undefined,通过第一个的时候则完全变成了string,然后开始搜寻文章,通过第二个断言的时候判断是否存在返回内容,如果不存在则也进行断言~
其次是useLoaderData必须使用泛型传参,不再可以根据教程里使用as,如果使用as则必须通过as any as xxx做双重断言才可以正确设置类型,具体原因涉及类型推断,理解使用即可。
const { post } = useLoaderData<LoaderData>();
我们已经使用了invariant来处理post,以后我们将更好的处理404页面,让我们继续
我们现在将Markdown解析为html,有很多mk解析器,这里我们选择了较为上手的marked
输入
npm add marked
如果需要Ts类型提醒则再安装
npm add @types/marked -D
然后重新运行npm run dev
编写slug.tsx代码
还是比较简单的,就不叙述了
这里的dangerouslySetInnerHTML是innerHTML的替代品
为了提醒开发者这个属性只能传入安全的数据,防止XSS注入
import type { LoaderFunction } from "@remix-run/node";
import { marked } from "marked";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import type { Post } from "~/models/post.server";
import { getPost } from "~/models/post.server";
type LoaderData = { post: Post; html: string };
export const loader: LoaderFunction = async ({ params }) => {
invariant(params.slug, `params.slug is required`);
const post = await getPost(params.slug);
invariant(post, `Post not found: ${params.slug}`);
const html = marked(post.markdown);
return json<LoaderData>({ post, html });
};
export default function PostSlug() {
const { post, html } = useLoaderData<LoaderData>();
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: html }} />
</main>
);
}
那么我们就完成了最基本的博客创建
结语
撒花~