likes
comments
collection
share

深入浅出全栈框架Remix.js

作者站长头像
站长
· 阅读数 12

前言

最近公司刚好在使用 Remixjs来搭建全栈项目,而Remixjs的文档写的其实不太好,导致很多知识点只能去官网的demo里面看,现在项目告一段落后,在这里对相关的知识脉络进行梳理。

Remix简介

Remixjs 是由 React Router 原班团队打造,基于 TypeScript 与 React,内建 React Router V6 特性的全栈 Web 框架,Remixjs 的特性如下:

  • 追求速度,然后是用户体验(UX),支持任何 SSR/SSG 等
  • 基于 Web 基础技术,如 HTML/CSS 与 HTTP 以及 Web Fecth API,在绝大部分情况可以不依赖于 JavaScript 运行,所以可以运行在任何环境下,如 Web Browser、Cloudflare Workers、Serverless 或者 Node.js 等
  • 客户端与服务端一致的开发体验,客户端代码与服务端代码写在一个文件里,无缝进行数据交互,同时基于 TypeScript,类型定义可以跨客户端与服务端共用
  • 内建文件即路由、动态路由、嵌套路由、资源路由等
  • 干掉 Loading、骨架屏等任何加载状态,页面中所有资源都可以预加载(Prefetch),页面几乎可以立即加载
  • 告别以往瀑布式(Waterfall)的数据获取方式,数据获取在服务端并行(Parallel)获取,生成完整 HTML 文档,类似 React 的并发特性
  • 提供开发网页需要所有状态,开箱即用;提供所有需要使用的组件,包括 <Links><Link><Meta><Form><Script/> ,用于处理元信息、脚本、CSS、路由和表单相关的内容
  • 内建错误处理,针对非预期错误处理的 <ErrorBoundary>

文件路由

路由注册

基于文件即路由的理念,我们无需集中的维护一套路由定义,当我们创建了对应的文件之后,Remixjs 就为我们注册了对应的路由。

比如说我们在route文件夹中创建了以下文件:

深入浅出全栈框架Remix.js

那么这里就会注册对应的路由:

  • /notes
  • /notes/new
  • /notes/$noteId
  • /notes/admin
  • /notes/admin/magager

值得注意的是,如果同时注册了notes.tsxnotes._index.tsx两个文件,那么在路由匹配时会优先使用notes.tsx这个文件

如果你不喜欢用这种后缀特别长的文件名来注册路由,你还可以回到它的v1版本,使用嵌套目录的方式定义路由, 需要在remix.config.js中加入以下的配置。

+ const { createRoutesFromFolders } = require("@remix-run/v1-route-convention");
module.exports = {
  //...
  future: {
    // 需要注释v2的route配置
    // v2_routeConvention: true,
  },
  + routes(defineRoutes) {
  +   uses the v1 convention, works in v1.15+ and v2
  +   return createRoutesFromFolders(defineRoutes);
  + },
};

然后就可以使用文件嵌套的方式完成上面相同的路由

深入浅出全栈框架Remix.js

  • /notes
  • /notes/new
  • /notes/$noteId
  • /notes/admin
  • /notes/admin/magager

嵌套路由

Remix 最具特色的功能之一就是嵌套路由。在 Remix 中,一个页面通常包含多层级页面,每个子页面控制自身的 UI 展现,而且独立控制自身的数据加载和代码分割。

来看看官网的例子

深入浅出全栈框架Remix.js 上述页面的对应关系如下:

  • 整个页面模块为 / 、而对应到 /sales 则是右边的整块天蓝色内容、/sales/invoices 对应到黄色的部分、/sales/invoices/102000 则对应到右下角的红色部分

怎么做的呢,其实就是借助remix提供的<Outlet/>组件,在一级路由中放入此组件,表示在此处填充二级路由,而所填充的二级路由组件通过与url匹配所得。

比如我们有如下组件:

main.tsx

export default function IndexPage() {
  return (
    <p>
      一级路由
      <Outlet />
    </p>
  );
}

main.new.tsx

export default function IndexPage() {
  return (
    <p>
     二级路由/main/new
    </p>
  );
}

main.list.tsx

export default function IndexPage() {
  return (
    <p>
      二级路由/main/list
    </p>
  );
}

当访问/main/new时,main中的<Outlet />组件就会填充为main.new.tsx

同理,/main/list会填充为main.list.tsx

当然你还可以在二级路由中继续使用<Outlet/>来继续规划三级路由,这样分割带来的好处就是,整个路由分层,对应到整个页面的分层视图,而每个分层下的代码都是独立编写,视图渲染独立渲染,数据独立获取,错误独立展示,后面我们会详细说到。

页面渲染

页面加载

通过嵌套路由,Remix 可以干掉几乎所有的加载状态、骨架屏,现在很多应用都是在前端组件里进行数据获取,获取前置数据之后,然后用前置数据去获取后置的数据,形成了一个瀑布式的获取形式,当数据量大的时候,页面加载就需要很长时间,所以绝大部分网站都会放一个加载的状态,如小菊花转圈圈,或者体验更好一点的骨架屏,如下:

深入浅出全栈框架Remix.js

深入浅出全栈框架Remix.js

可见虽然我们首屏拿到内容可能会慢一点,但是再也不需要加载状态。

页面更新

当用户单击链接时,Remix 不会往返服务器以获取整个文档和所有资源,而是简单地获取下一页的数据并更新 UI。因为Remix 对客户端导航进行了一些内置优化。它知道哪些布局将在两个 URL 之间保留,因此只需要获取正在更改的布局的数据。而完整的页面请求将需要在服务器上获取所有数据。

深入浅出全栈框架Remix.js

这种方法还可以缓存界面的状态,例如不会重置侧边栏导航的滚动位置。

数据预取

Remix 还可以在用户即将单击链接时预取页面的所有资源,因为浏览器在第一次加载的时候,就获取到了资产清单。它可以匹配链接的URL,读取清单,然后预取所有数据,JavaScript模块,甚至CSS资源以供下一页使用。这就是 Remix 应用程序即使在网络速度较慢的情况下也能快速感觉的方式。

深入浅出全栈框架Remix.js

错误处理 ErrorBoundary

基于嵌套路由的理念,页面在发生错误的时候,无需重新刷新,只需要在对应的错误的子路由展示错误信息,而页面的其他部分仍然可以正常工作。

深入浅出全栈框架Remix.js

正因为错误经常发生,且处理错误异常困难,包含客户端、服务端的各种错误,包含预期的、非预期的错误等,所以 Remix 内建了完善的错误处理机制,提供了类似 React 的 ErrorBoundary 的理念。

在 Remix 中,每个路由函数对应一个ErrorBoundary ,当出现这些非预期的错误时,就会激活这个函数,显示对应函数的表示错误信息的 UI,并且会将错误信息注入到useRouteError这个方法中。

import {
  useRouteError,
  isRouteErrorResponse,
} from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();

  // when true, this is what used to go to `CatchBoundary`
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>Oops</h1>
        <p>Status: {error.status}</p>
        <p>{error.data.message}</p>
      </div>
    );
  }

  // Don't forget to typecheck with your own logic.
  // Any value can be thrown, not just errors!
  let errorMessage = "Unknown error";
  if (isDefinitelyAnError(error)) {
    errorMessage = error.message;
  }

  return (
    <div>
      <h1>Uh oh ...</h1>
      <p>Something went wrong.</p>
      <pre>{errorMessage}</pre>
    </div>
  );
}

当我们没有在子路由中添加 ErrorBoundary函数时,一旦遇到错误,这些错误就会向更上一级的路由冒泡,直至最顶层的路由页面。

数据加载

Remixjs使用服务端渲染,用户可以通过loaderuseLoaderData在服务端预获取页面所需要的数据

export const loader = async () => {
  return json({ posts: await getPosts() });
};

export default function PostAdmin() {
  const { posts } = useLoaderData<typeof loader>();
  return (
    //...
  );
}

如果项目是包括服务端的,这里可以直接操作数据库获取数据

export async function getPosts() {
  return prisma.post.findMany();
}

数据提交

Remixjs不像nextjs那样,通过创建api文件夹会注册对应的接口路由,在Remixjs中如果需要操作服务端,或者调用其他服务的接口,需要通过表单submit或者useSubmit勾子方法,来触发组件中的action方法。

官方模版的登录组件为例

export default function LoginPage() {
  const [searchParams] = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") || "/notes";
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);
  
  //...

  return (
    <div className="flex min-h-full flex-col justify-center">
      <div className="mx-auto w-full max-w-md px-8">
        <Form method="post" className="space-y-6">
        //...
        // 这里是email和password的input输入框
        //...
          <button
            type="submit"
            className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
          >
            Log in
          </button>
        </Form>
      </div>
    </div>
  );
}

这里的表单实际上是Remixjs封装之后的表单,为了避免表单的默认行为,以及同步在服务端和客户端的表现形式,当用户点击提交后,Remixjs会以接口的形式调用组件的action方法


//...
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";

export const action = async ({ request }: ActionArgs) => {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
  const remember = formData.get("remember");
  
    if (typeof password !== "string" || password.length === 0) {
    return json(
      { errors: { email: null, password: "Password is required" } },
      { status: 400 }
    );
  }

  if (password.length < 8) {
    return json(
      { errors: { email: null, password: "Password is too short" } },
      { status: 400 }
    );
  }
  //...
  
  return createUserSession({
    redirectTo,
    remember: remember === "on" ? true : false,
    request,
    userId: user.id,
  });
};

然后可以在方法中做一些服务端的操作,包括数据校验数据库访问等,然后返回接口的处理结果,或者直接使用Redirect组件跳转到其他页面。

然后我们可以通过useActionData来得到接口的返回值,包括错误信息


export const action = async ({ request }: ActionArgs) => {

  const actionData = useActionData<typeof action>();
  //...

  useEffect(() => {
    if (actionData?.errors?.email) {
      emailRef.current?.focus();
    } else if (actionData?.errors?.password) {
      passwordRef.current?.focus();
    }
  }, [actionData]);
  
  //...
  return (
      // ...
      {actionData?.errors?.body ? (
          <div className="pt-1 text-red-700" id="body-error">
            {actionData.errors.body}
          </div>
        ) : null}
  )
}

当然,也可以方法的形式进行数据提交:

export default function Login() {
  const submit = useSubmit();

  const handleSubmit = (email,password) => {
    submit({ email,password}{ method: "post" });
  }
  
  //...

指定接口路由

除此之外,如果你想复用action中的逻辑,比如很多组件都可以退出登录,但是只想写一遍逻辑,那么我们可以专门定义一个组件来管理这个逻辑,然后在提交数据时,可以提交到特定的路由

只需要在使用表单或者useSubmit提交时,指定action属性即可

logout.tsx

import type { ActionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";

import { logout } from "~/session.server";

export const action = async ({ request }: ActionArgs) => logout(request);

export const loader = async () => redirect("/");

使用

<Form action="/logout" method="post">
//...
</Form>

或者

submit({}, { method: "post",action:'/logout' });

与服务端集成

Remixjs其实就是一个js处理器,可以非常方便的与其他服务器项目进行集成

引用官网就是:

While Remix runs on the server, it is not actually a server. It's just a handler that is given to an actual JavaScript server. It's built on the Web Fetch API instead of Node.js. This enables Remix to run in any Node.js server like VercelNetlifyArchitect, etc. as well as non-Node.js environments like Cloudflare Workers and Deno Deploy.

通过Remixjs提供的createRequestHandler方法实现,如果要在express中集成remixjs:

app.all(
  "*",
  createRequestHandler({
        build: require("./build"),
        getLoadContext: () => ({
          loaders: {
            usersById: createUsersByIdLoader(),
          },
        }),
      })
);
  • build代表remix打包资源的目录
  • getLoadContext代表注入到组件 loader 方法中的上下文

比如在这个地方注入之后,就可以在loader方法中获取到

export const loader = async ({ context }: LoaderArgs) => {
  const users = await context.loaders.usersById.loadMany([
    "ef3fcb93-0623-4d10-adbf-4dd865d6688c",
    "2cbad877-2da6-422d-baa6-c6a96a9e085f",
  ]);
  return json({ users });
};

React RSC

Remixjs默认开启了React的流式渲染,并且默认是导出了配置文件(可以删掉)

  • entry.client.tsx
  • entry.sever.tsx

其他基础组件

同时在路由函数所在文件里,可以通过声明 linkmetalinksheaders 等函数来声明对应的功能:

  • links 变量函数:表示此页面需要加载的资源,如 CSS、图片等
import type { LinksFunction } from "remix";
import stylesHref from "../styles/something.css";

export let links: LinksFunction = () => {
  return [
    // add a favicon
    {
      rel: "icon",
      href: "/favicon.png",
      type: "image/png"
    },

    // add an external stylesheet
    {
      rel: "stylesheet",
      href: "https://example.com/some/styles.css",
      crossOrigin: "true"
    },
  ];
};
  • links 函数:声明需要 Prefetch 的页面,当用户点击之前就加载好资源
export function links() {
  return [{ page: "/posts/public" }];
}
  • meta 函数:与 <``Meta``> 组件类似,声明页面需要的元信息
import type { MetaFunction } from "remix";

export let meta: MetaFunction = () => {
  return {
    title: "Josie's Shake Shack", // <title>Josie's Shake Shack</title>
    description: "Delicious shakes", // <meta name="description" content="Delicious shakes">
    "og:image": "https://josiesshakeshack.com/logo.jpg" // <meta property="og:image" content="https://josiesshakeshack.com/logo.jpg">
  };
};
  • headers 函数:定义此页面发送 HTTP 请求时,带上的请求头信息
export function headers({ loaderHeaders, parentHeaders }) {
  return {
    "X-Stretchy-Pants": "its for fun",
    "Cache-Control": "max-age=300, s-maxage=3600"
  };
}

最后

感谢你能看到这里,文章写了Remixjs的一些基础特性,希望对你有用,后续会在使用过后继续更新。当然,都看了这么久了,不妨给笔者点个赞再走呢,这对我很重要。

参考链接

remix.run/docs/en/mai…