添加客户端路由

上一章节中,我们学习了如何为创建 UI 组件,并添加样式。

这一章节中,我们将会学习如何添加客户端路由

之前我们已经为联系人列表增加了 Archive 按钮,接下来我们添加一个客户端路由 /archives,访问这个路由时,只显示已存档的联系人,而原有的 / 继续显示所有联系人。

新建 src/routes/archives/page.tsx 文件:

macOS
Windows
mkdir -p src/routes/archives
touch src/routes/archives/page.tsx

添加如下代码:

src/archives/page.tsx
import { List } from 'antd';
import { Helmet } from '@modern-js/runtime/head';
import Item from '../../components/Item';

const getAvatar = (users: Array<{ name: string; email: string }>) =>
  users.map(user => ({
    ...user,
    avatar: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${user.name}`,
  }));

const getMockArchivedData = () =>
  getAvatar([
    { name: 'Thomas', email: 'w.kccip@bllmfbgv.dm' },
    { name: 'Chow', email: 'f.lfqljnlk@ywoefljhc.af' },
  ]);
function Index() {
  return (
    <div className="container lg mx-auto">
      <Helmet>
        <title>Archives</title>
      </Helmet>
      <List
        dataSource={getMockArchivedData()}
        renderItem={info => <Item key={info.name} info={info} />}
      />
    </div>
  );
}

export default Index;

这里使用了 React HelmetHelmet 组件,在 src/routes/page.tsx 中也添加 Helmet 组件:

import { Helmet } from '@modern-js/runtime/head';

function Index() {
  return (
    <div className="container lg mx-auto">
      <Helmet>
        <title>All</title>
      </Helmet>
      ...
    </div>
  );
}
INFO

Modern.js 默认集成了 react-helmet,也可以结合 SSR 使用,满足 SEO 需求。

因为现在有多个页面,都需要用到前面的 Utility Class,因此我们需要把样式文件移动到 src/routes/layout.tsx

import 'tailwindcss/base.css';
import 'tailwindcss/components.css';
import 'tailwindcss/utilities.css';
import '../styles/utils.css';

执行 pnpm run dev,访问 http://localhost:8080,可以看到完整的联系人,页面的标题是 All:

display1

访问 http://localhost:8080/archives,只会看到已存档的联系人,页面的标题是 Archives:

display

查看页面 HTML 源码,可以看到两个页面的内容是一样,是在客户端针对不同 URL 渲染不同内容。

接下来我们增加一个简单的导航栏,让用户能在两个列表之间切换

打开 src/routes/layout.tsx,在顶部导入 Radio 组件:

import { Radio } from 'antd';

然后将 UI 最顶部进行修改,增加一组单选框

1export default function Layout() {
2  return (
3    <div>
4      <div className="h-16 p-2 flex items-center justify-center">
5        <Radio.Group onChange={handleSetList} value={currentList}>
6          <Radio value="/">All</Radio>
7          <Radio value="/archives">Archives</Radio>
8        </Radio.Group>
9      </div>
10      <Outlet />
11    </div>
12  );
13}

然后我们来实现 currentListhandleSetList

引入三个 React Hook:useStateuseNavigateuseParams,以及 Ant Design 的事件类型定义:

import { useState } from 'react';
import { Radio, RadioChangeEvent } from 'antd';
import { Outlet, useLocation, useNavigate } from '@modern-js/runtime/router';

最后在 Layout 组件里增加局部状态和相关逻辑:

1export default function Layout() {
2  const navigate = useNavigate();
3  const location = useLocation();
4  const [currentList, setList] = useState(location.pathname || '/');
5  const handleSetList = (e: RadioChangeEvent) => {
6    const { value } = e.target;
7    setList(value);
8    navigate(value);
9  };
10  return (
11  ...
12}

到这里就已经完成了页面导航栏实现,执行 pnpm run dev 查看效果:

display2

点击导航栏中 Archives,可以看到单选框的选中状态和 URL 都会变化,页面没有刷新,只发生了 CSR。

通过 URL 访问两个页面,可以看到 HTML 内容是不同的,这是因为在 SSR 阶段页面就执行了客户端路由的逻辑,HTML 里已经包含了最终的渲染结果。