跳转到主文档

使用约定式路由​​​​

上一小节中,我们学习了如何使用【 自控式路由 】方式实现客户端路由。

这一小节中,我们将在 landing-page 入口里,学习使用【 约定式路由 】。

我们首先将 landing-page 入口的标识 App.tsx 删除,换成另一种符合约定的标识 pages/

我们在终端执行以下代码修改 landing-page 文件内容:

rm src/landing-page/App.tsx src/landing-page/App.css
mkdir -p src/landing-page/pages/
touch src/landing-page/pages/_app.tsx src/landing-page/pages/index.tsx src/landing-page/pages/docs.tsx

pages/App.tsx 一样,都起到入口标识的作用,让 src/landing-page 被识别为入口。

如果存在 App.tsx 则优先使用 App.tsx 作为编译入口。而 pages/ 则默认启用【 约定式路由 】的客户端路由实现方式,pages/ 中的文件路径和文件内容,将被自动自动转换成客户端路由逻辑。

执行命令后,项目结构如下:

.
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── src/
│ ├── contacts/
│ │ ├── components/
│ │ │ ├── Avatar/
│ │ │ │ ├── index.stories.tsx
│ │ │ │ └── index.tsx
│ │ │ └── Item/
│ │ │ ├── index.test.tsx
│ │ │ └── index.tsx
│ │ ├── styles/
│ │ │ └── utils.css
│ │ ├── App.css
│ │ └── App.tsx
│ ├── landing-page/
│ │ └── pages/
│ │ ├── _app.tsx
│ │ ├── docs.tsx
│ │ └── index.tsx
│ ├── .eslintrc.json
│ └── modern-app-env.d.ts
├── .editorconfig
├── .gitignore
├── .npmrc
├── .nvmrc
├── README.md
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json

注:以上展示的是手动创建入口的方式。

以下介绍自动启用方式,开发者可在自己的项目中尝试:

在 modern.js 项目中,执行pnpm run new命令, 按照以下选项选择,实现创建入口时,自动启用【 约定式路由 】:

# 请选择你想要的操作:
❯ 新建「应用入口」

#填写入口名称:
landing-page

# 是否需要调整默认配置?
❯ 是

# 请选择客户端路由方式
自控路由
❯ 约定式路由
不启用

假设这个 landing-page 入口是一个简单的有导航栏的落地页,包含两个子页面:一个首页和一个文档页,我们用客户端路由的方式来区别这两个子页面。

我们先分别实现这个入口两个子页面的内容,src/landing-page/pages/index.tsx 是首页,内容为:

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

const Index = () => (
<>
<Helmet>
<title>Home</title>
</Helmet>
<p>
Welcome to <a href="/contacts">Contact List</a>!
</p>
</>
);

export default Index;

src/landing-page/pages/docs.tsx 是文档页,内容为:

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

const Docs = () => (
<>
<Helmet>
<title>Docs</title>
</Helmet>
<p>I am docs</p>
</>
);

export default Docs;

最后,src/landing-page/pages/_app.tsx 是下划线开头的,代表这是【 约定式路由 】中一个特殊的功能文件,为整个入口提供根组件,可以用于实现全局布局、公共 UI 等。

在这里,我们只实现一个简单的导航和结构:

import { ComponentType } from 'react';
import { NavLink } from '@modern-js/runtime/router';

const App = ({ Component, ...pageProps }: { Component: ComponentType }) => (
<div>
<h2>Nav:</h2>
<ul>
<li>
<NavLink
to="/"
exact={true}
activeStyle={{
color: '#f60',
}}>
Home
</NavLink>
</li>
<li>
<NavLink
to="/docs"
activeStyle={{
color: '#f60',
}}>
Docs
</NavLink>
</li>
</ul>
<h2>Content:</h2>
<Component {...pageProps} />
</div>
);

export default App;

执行 pnpm run dev,访问 http://localhost:8080/landing-page/,可以看到结果:

result

点击 Nav 里的链接,URL 变为 http://localhost:8080/landing-page/docs,内容会变为:

result1

我们同样可以开启/关闭 SSR 选项(配置教程),并查看 HTML 源码。结果和【 自控式路由 】的 contacts 入口一样。

Modern.js Lint 规则集要求 React 组件的文件名或目录名都用 Pascal 命名风格,跟组件本身的名字保持一致,首字母大写。

pages/ 里面的文件虽然也是 React 组件,但它们是基于约定用于指定路由的特殊组件文件,对应的是 URL 中的路径,所以这里是一种例外情况,文件名/目录名跟 URL 命名风格保持一致更好,最好用 dash 命名风格(全小写,- 连字符分隔)。

接下来我们再实现一个简单的评论详情页,因为评论会有很多条,所以评论详情页的 URL 包含动态部分,例如:http://localhost:8080/landing-page/docs/comments/:commentTitle/

对于像 :commentTitle 这样的动态部分,Modern.js 提供了一种特殊的文件命名方式来实现。

执行以下命令,新建以下文件:

mkdir -p src/landing-page/pages/comments/\[commentTitle\]
touch src/landing-page/pages/comments/\[commentTitle\]/index.tsx

landing-page 入口中的文件结构变成:

.
└── pages/
├── comments/
│ └── [commentTitle]/
│ └── index.tsx
├── _app.tsx
├── docs.tsx
└── index.tsx

[commentTitle] 将会被 Modern.js 转换成动态路由。

我们首先安装文件中需要的依赖:

pnpm add lodash
pnpm add -D @types/lodash

pages/comments/[commentTitle]/index.tsx 的内容为:

import { Helmet } from '@modern-js/runtime/head';
import { Link } from '@modern-js/runtime/router';
import _ from 'lodash';

const Index = ({ match: { params } }: any) => {
const title = _.startCase(params.commentTitle);
return (
<>
<Helmet>
<title>Comment: {title}</title>
</Helmet>
<p>
<Link to="/docs/">{'< Back'}</Link>
</p>
<h1>Subject: {title}</h1>
<p>Post: I am a comment</p>
</>
);
};

export default Index;

修改 docs.tsx,添加跳转到评论页的链接:

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

const Docs = () => (
<>
<Helmet>
<title>Docs</title>
</Helmet>
<p>I am docs</p>
<p>
<Link to="/comments/i-am-a-comment-title">[Random comment]</Link>
</p>
</>
);

export default Docs;

pages/App.tsx 一样,应该专注于路由的组织,具体的 UI 实现和业务逻辑,一旦复杂到要拆分成多个文件的程度,应该放到 pages/ 外面,在同一个应用入口目录中,用 feature 目录role 目录的方式来组织。

例如使用 src/landing-page/docs/components/Article/index.tsx 来组织功能特性。

landing-page/docs/landing-page/pages/docs/ 不同,前者是 feature 目录,将修改频率不同或由不同人负责开发的业务逻辑,封装在同一个黑盒里,这种目录下的问题与具体路由解耦。

pages/ 目录里的组件文件,负责把这些 feature 组件组织到一起,通过特定的路由结构提供给用户。

因此 pages/ 目录里基本上不应该有组件之外的文件,也不应该有过于复杂和具体的实现。

执行 pnpm run dev,访问 http://localhost:8080/landing-page/docs/,可以看到效果如下:

result2

点击评论链接,跳转到 http://localhost:8080/landing-page/comments/i-am-a-comment-title,效果如下:

result3

URL 中的动态部分,在代码中被转换成了标题内容,修改最后部分 URL 后重新访问,标题内容同样发生改变。

本小节中,我们学习了基本的【 约定式路由 】用法,除了 _app.tsx 外,我们还可以使用 _layout.tsx 实现这个访问路径下的公共 UI,可以用 404.tsx 自定义 404 页面等。


本小节的代码可以在这里查看