这个Remix框架到底有啥不一样,嵌套路由咋个整明白?

4,488字
19–28 分钟
in

Remix这货一出来就刷屏了,虽然2019年就悄悄诞生,但那时候还得掏钱订阅才能用。2021年团队搞到种子轮融资,直接开源免费,瞬间炸开了锅。简单说,Remix是个死磕服务器边缘的JavaScript框架,前端暂时抱着React大腿,但核心是把渲染扔到服务器上跑。边缘服务器这玩意儿就像把饭店开到小区楼下,取餐贼快还便宜。Remix把自己叫做“中心栈”框架,专门在服务器和客户端之间当翻译官,适配各种平台。

目录

部署那些事儿

Remix自己不带服务器,得自己找个宿主。支持Node.js或Deno环境,像Netlify边缘函数、DigitalOcean应用平台都能跑。整个编译过程用esbuild把请求转成对应平台能吃的格式,底层用的是Web Fetch API那套东西。

上手盘一个Remix项目,先得把环境支棱起来。电脑上装好Node.js(版本14以上),打开终端敲下面这串:

npx create-remix@latest my-remix-app

跟着提示选部署目标,比如选“Netlify Edge”或者“Node.js”。装完后进目录:

cd my-remix-app
npm run dev

浏览器打开http://localhost:3000,看到欢迎页面就算成了。想打包上线,跑npm run build,生成的生产代码在build/文件夹里。丢到对应平台前,得先配好平台需要的入口文件。比如Netlify平台,根目录下建个netlify.toml

[build]
  command = "npm run build"
  publish = "public"

[functions]

directory = “netlify/functions”

这里有个坑:不同平台的适配器不一样。创建项目时选错目标,后面改起来相当麻烦。要切换平台,得手动改remix.config.js里的serverModuleFormatserverBuildTarget,新手直接重新初始化项目更省事。

路由玩出花

Remix的路由系统是基于文件系统的,但最骚的操作是嵌套路由。传统路由点一下跳整个新页面,嵌套路由是在同一页面上换不同区块,像王者荣耀里换皮肤只换英雄外观,地图和血条不动。

根文件结构

每个Remix应用都从root.tsx起家。这个文件里放着整个页面的骨架:<html><head><body>,还有一个关键组件<Outlet />。这玩意儿就像插线板,所有子路由的内容都从这儿怼进去。<Scripts>组件也别漏掉,没它页面上的JavaScript就废了一半。

看个最基础的文件夹布局:

app/
├── routes/
│   ├── index.tsx      # 访问 / 时渲染
│   ├── about.tsx      # 访问 /about 时渲染
│   └── blog/
│       └── index.tsx  # 访问 /blog 时渲染
└── root.tsx

Outlet组件真香

<Outlet />是嵌套路由的灵魂。假设要做一个关于页面,里面还想分团队介绍、联系方式等子板块。先在app/routes/about.tsx里写布局:

import { Outlet } from "@remix-run/react";

export default function About() {
  return (
    <div>
      <h1>关于我们</h1>
      <p>这里是固定头部,所有子页面都看得见</p>
      <Outlet />  {/* 子路由的内容插这里 */}
    </div>
  );
}

然后在app/routes/about/目录下建team.tsxcontact.tsx。访问/about/team时,team.tsx的内容就会从<Outlet />位置冒出来。这种玩法让代码按模块拆分,出bug也只崩当前区块,其他部分照常运行,体验绝绝子。

动态路由搞起来

URL里带参数的情况,比如博客文章详情页,用$前缀声明。文件名里的$后面跟啥都行,Remix只认这个前缀。

app/
├── routes/
│   ├── blog/
│   │   ├── $postId.tsx   # 动态匹配 /blog/123、/blog/hello-world
│   │   └── index.tsx     # 匹配 /blog
│   └── index.tsx

$postId.tsx里拿到URL上的参数,得靠loader函数的params参数:

import { useLoaderData } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/node";

export async function loader({ params }: LoaderArgs) {
  // params.postId 就是URL上那个动态值
  return { postId: params.postId };
}

export default function BlogPost() {
  const { postId } = useLoaderData();
  return <article>正在展示文章编号:{postId}</article>;
}

缓存加速小妙招

虽然Remix必须跑在服务器上,但照样能蹭Jamstack的CDN缓存红利。静态站点生成器(SSG)是在构建时就把所有页面生成好,直接丢CDN上飞起。Remix不做传统SSG,但可以用HTTP头里的Cache-Control来缓存某个路由。

root.tsx或者任意路由组件里导出headers函数:

export function headers() {
  return {
    "Cache-Control": "public, s-maxage=3600",  // 缓存一小时
  };
}

更狠的操作是在remix.config.js旁边配一个headers文件。新建headers文件夹,里面放具体路由的缓存策略:

[[headers]]
  for = "/static/*"

[headers.values]

“Cache-Control” = “public, max-age=31536000, immutable”

注意嵌套路由的缓存策略有优先级:URL路径最长的那个路由说了算。比如/about/team会覆盖/about的缓存设置,别搞反了。

数据获取不烧脑

Remix里拿数据不用useStateuseEffect那一套,因为数据早就在服务器上煮好了。每个路由模块可以导出一个loader函数,它只在服务器端跑,返回的数据通过useLoaderData注入组件。

来整个活:调用猫片API,随机展示一只喵。

import { useLoaderData } from "@remix-run/react";

export async function loader() {
  const res = await fetch("https://cataas.com/cat?json=true");
  const json = await res.json();
  // 注意:cataas返回的json里有个_id字段
  return { catId: json._id };
}

export default function RandomCat() {
  const { catId } = useLoaderData();
  return (
    <img
      src={`https://cataas.com/cat/${catId}`}
      alt="一只随机喵星人"
    />
  );
}

动态路由里拿参数也方便,上面提过的params直接就能用。比如做一个标签页,URL里的$tag决定展示啥内容:

import { useLoaderData } from "@remix-run/react";

export async function loader({ params }) {
  // 假设params.tag = "funny"
  return { tag: params.tag };
}

export default function TagPage() {
  const { tag } = useLoaderData();
  return <div>当前标签:{tag},下面刷出一堆相关帖子</div>;
}

这里有个隐藏福利:loader函数里可以直接用原生fetch,不用装axios啥的,因为Node环境和浏览器环境都支持。但别在loader里调浏览器专属API,比如localStorage,服务器上没这玩意儿。

其他实用小工具

Remix还塞了几个辅助函数,每个路由都能单独定义。

Action处理表单

action函数专门对付表单提交,用的是标准FormData。写一个加待办事项的示例:

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

export async function action({ request }) {
  const formData = await request.formData();
  const title = formData.get("title");
  // 假装调数据库存起来
  const newId = Date.now();
  return redirect(`/todos/${newId}`);
}

export default function TodoForm() {
  return (
    <form method="post">
      <input name="title" type="text" />
      <button type="submit">添加待办</button>
    </form>
  );
}

Meta标签自定义

每个页面想换标题和描述,导出meta函数就行:

export function meta() {
  return {
    title: "关于我们 - 公司介绍",
    description: "这家公司成立于2020年,专搞前端框架",
  };
}

Links按需加载CSS

不想让全局CSS文件污染所有页面?用links函数只在当前路由引入样式表:

import stylesHref from "./about.css";

export function links() {
  return [
    { rel: "stylesheet", href: stylesHref },
    { rel: "icon", href: "/favicon.ico" },
  ];
}

这样只有访问/about相关页面时,CSS才会被加载,其他路由完全不受影响。

页面跳转用Link

代替<a>标签,用Remix的<Link>组件实现客户端路由,不刷新整个页面。还能加prefetch属性提前加载数据:

import { Link } from "@remix-run/react";

export default function NavBar() {
  return (
    <nav>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Link to="/blog" prefetch="intent">博客</Link>
    </nav>
  );
}

prefetch="intent"表示鼠标悬停或聚焦到链接时,偷偷在后台把目标页面的数据拉下来,点开瞬间加载,体验丝滑得像德芙。

上手Remix其实没啥门槛,把上面这些概念盘熟了,直接去啃官方那个Jokes示例应用或者K-Pop Stack,边抄边改,很快就能整个活出来。