findxc / blog

88 stars 5 forks source link

Remix 有什么不一样 #70

Open findxc opened 2 years ago

findxc commented 2 years ago

官网: Remix - Build Better Websites

路由的接口数据也能 prefetch 了!

如果你经常访问 React 官网 ,你会发现它跳转路由后新内容能马上展示,一点停顿都没有,因为当一个 Link 出现在视图中时,它就会去 prefetch 路由需要的资源,这样切换时资源都已经准备好了,直接渲染就行了。

React 官网是一个静态站点,一般我们日常开发的网站是动态的,需要在页面中去请求接口数据然后展示。如果我们做了按路由切割代码,那跳转路由时,会先去请求这个路由的 JS 代码,下载并执行完后,显示路由界面,然后请求接口数据,拿到数据后更新界面。

1603E74C-D48B-458C-8A81-FC6C675150DC

这里的 JS 文件是可以 prefetch 的,自行封装 Link 组件或者使用第三方库比如 quicklink ,这样跳转路由后可以马上显示路由界面,然后请求接口数据,会相对快一点。

那,接口数据能不能也 prefetch 呢?这样比如用户经常访问的路由,或者一些统计数据页面,就能更快呈现了。

React 在 2019 年的博客 Building Great User Experiences with Concurrent Mode and Suspense – React Blog 中写了一种思路,在配置路由时,额外配置一下这个路由需要请求什么数据,这样就可以在跳转路由之前去 prefetch 了。

现在, Remix 就支持这个功能了 ~ 😍😍😍

Link 设置属性 prefetch 为 intent ,这样用户 hover 或者 focus 这个 Link 时就会触发 prefetch ,跳转路由后新内容的展示会快一些。

AE0E6BE8-73D3-4F86-AFFC-A67421D4CF1E

嵌套的目录结构对应嵌套的界面,让开发更加模块化

有些框架、脚手架会使用约定式路由,比如 pages/aaa/bbb.jsx 对应的路由就是 /aaa/bbb ,这样目录结构就对应路由配置了,不用再手动配置。只是说当匹配路由 /aaa/bbb 时,界面只会渲染 pages/aaa/bbb.jsx ,和 pages/aaa.jsx 无关,而在 Remix 中,界面会和 pages/aaa.jsx 有关,界面会嵌套着展示。

React Router v6 实现了 Outlet 这个组件,使得界面的嵌套展示变得简单,在父路由组件中渲染 <Outlet /> 后,它会指代匹配的子路由。匹配的父路由会始终展示,然后子路由哪个匹配就展示哪个。

// React Router v6 Outlet 使用示例
function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route path="messages" element={<DashboardMessages />} />
        <Route path="tasks" element={<DashboardTasks />} />
      </Route>
    </Routes>
  )
}

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* 当路由是 /messages 时, <Outlet /> 会渲染为 <DashboardMessages> */}
      {/* 当路由是 /tasks 时, <Outlet /> 会渲染为 <DashboardTasks> */}
      {/* 当路由是 / 时, <Outlet /> 是 null */}
      <Outlet />
    </div>
  )
}

Remix 结合了约定式路由和 Outlet 来实现嵌套的目录结构对应嵌套的界面。

.
├── entry.client.jsx
├── entry.server.jsx
├── root.jsx
└── routes
    ├── index.jsx
    ├── jokes
    │   ├── $jokeId.jsx
    │   ├── index.jsx
    │   └── new.jsx
    └── jokes.jsx

这样有什么好处呢?目录结构的嵌套和界面的嵌套一致的行为,更加符合模块化开发的思想。

在 Remix 中,每个路由文件可以导出一个 loader 函数,表示需要从服务端获取什么数据,然后在客户端通过 useLoaderData 拿到数据。对, Remix 是一个服务端渲染框架。

// 某个路由文件,比如 app/routes/aaa/bbb.jsx
import { useLoaderData } from "remix"
// loader 会在服务端执行,可以直接请求数据库数据,或者用 fetch 发请求获取数据
export async function loader() {
  return fakeDb.invoices.findAll()
}
// 然后在客户端通过 useLoaderData 拿到 loader 的返回值
export default function Invoices() {
  const invoices = useLoaderData()
  // ...
}

这样在父路由中可以请求父路由自己的数据,在子路由中请求子路由的数据,而不是在页面最外层组件去统一获取所有的数据然后用 props 传递下去,父子路由之间更加独立。

补充1,如果父路由希望传数据给子路由,在父路由渲染时加上 context 属性,比如 <Outlet context={{name: 'xc'}} /> ,然后在子路由可以通过 useOutletContext 即可拿到数据。

补充2, useMatches 可以拿到当前匹配的所有路由,当你希望根据不同路由使用不同 layout 时,或者希望渲染面包屑时,可以在最外层路由中去处理。再就是每个路由文件可以导出一个 handle ,这个值在 useMatches 返回值中能拿到,也就是每个路由可以有一些自定义配置。

// routes/xxx.jsx
export const handle = {
  aaa: '',
  bbb: '',
}

补充3, Remix 默认 app/routes 下的文件都是路由文件,如果希望把 css 也放在 routes 下,可以在 remix.config.js 中配置 ignoredRouteFiles: ['.*', '*.css']

使用表单来提交数据,禁用 JS 网站也能正常使用

Remix 中的 Form 组件,如果浏览器没禁用 JS ,那就还是用 JS 来提交数据,如果禁用了 JS ,就会用原生 form 的方式提交数据。

import { json, useActionData, Form } from 'remix'

// 和 loader 一样, action 会在服务端执行,可以去修改数据库或者用 fetch 发请求
// 当客户端有表单数据提交时就会触发 action
export async function action({ request }) {
  const body = await request.formData()
  const name = body.get('visitorsName')
  return json({ message: `Hello, ${name}` })
}

export default function Invoices() {
  // 用 useActionData 来拿到 action 的返回值, Remix 真的很喜欢 hooks
  const data = useActionData()
  return (
    <Form method="post">
      <p>
        <label>
          What is your name?
          <input type="text" name="visitorsName" />
        </label>
      </p>
      <p>
        {/* 当点击时,会触发上面的 action */}
        <button type="submit">submit</button>
      </p>
      <p>{data ? data.message : 'Waiting...'}</p>
    </Form>
  )
}

下面是禁用 JS 后,提交表单请求的请求头和响应头。

补充1,如果希望手动提交表单数据,可以使用 useSubmit ,或者更加灵活的 useFetcher , useFetcher 除了可以 submit 数据外,也可以 load 数据。

补充2,由于路由文件中只能导出一个 action ,那如果界面中有多个表单呢?在 How do I handle multiple forms in one route? 中有解释。我们可以给提交按钮加上属性 name="type" value="create" ,然后在 action 中取出 type 值,根据不同 type 值做不同处理。(文档中是设的 name="action" ,但是实测会报错,所以我改为了 name="type"

当有数据提交成功后,会自动更新当前界面数据

想象一下,我们正在开发一个某某列表页,然后用户删除了一项数据,我们是不是得写代码重新请求一下列表页来保证界面和数据库数据一致?

在 Remix 中,它会自动帮我们处理。因为 Remix 知道当前 URL 匹配的所有路由组件,然后每个路由组件有自己的 loader ,当有表单数据提交成功后, Remix 会自动去执行所有的 loader ,开发者不用再手动处理了。

再就是, Remix 提供了 useTransition 来让开发者知道当前是否有数据在提交,这样你可以在提交按钮上展示 loading 或者别的啥处理,而不用手动设置变量表示数据是否在提交中了。

所以当你使用 Remix 后,你的业务代码很可能会更简洁。

写在结尾

Remix 官网的 title 是 Remix - Build Better Websites ,如果你要问我 Remix 和其它某某框架有什么不一样,我会说,它们从追求开始就不一样了,所以到最后包含的功能、适用的场景才会不一样。

然后我发现想象力真的很重要,在读 React 那篇博客之前,我从来没想过路由页面的接口数据也可以 prefetch ,就觉得进入页面了再请求呗, hhh ,太局限了。只有不断追求更好,才能突破当下。