---
title: 使用约定式路由​​​​
---

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

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

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

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

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

<Tabs>
<TabItem value="macOS" label="macOS" default>

```bash
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
```

</TabItem>
<TabItem value="Windows" label="Windows">

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

</TabItem>
</Tabs>


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

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

执行命令后，项目结构如下：

```md
.
├── .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`命令, 按照以下选项选择，实现创建入口时，自动启用【 约定式路由 】：

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

#填写入口名称：
landing-page

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

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

# 是否启用应用状态管理?
❯ 否
```

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

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

```js
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` 是文档页，内容为：

```js
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 等。

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

```js
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](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/08/result.png)

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

![result1](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/08/result1.png)

我们同样可以开启/关闭 SSR 选项（[配置教程](/docs/apis/config/server/ssr)），并查看 HTML 源码。结果和【 自控式路由 】的 contacts 入口一样。

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

`pages/` 里面的文件虽然也是 React 组件，但它们是基于约定用于指定路由的特殊组件文件，对应的是 URL 中的路径，所以这里是一种例外情况，文件名/目录名跟 [URL 命名风格](https://geemus.gitbooks.io/http-api-design/content/en/requests/downcase-paths-and-attributes.html)保持一致更好，最好用 dash 命名风格（全小写，- 连字符分隔）。
:::

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

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

执行以下命令，新建以下文件：

<Tabs>
<TabItem value="macOS" label="macOS" default>

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

</TabItem>
<TabItem value="Windows" label="Windows">

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

</TabItem>
</Tabs>

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

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

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

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

```bash
pnpm add lodash
pnpm add -D @types/lodash
```

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

```js
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`，添加跳转到评论页的链接：

```js
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;
```

:::info 注
`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](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/08/result2.png)

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

![result3](https://lf3-static.bytednsdoc.com/obj/eden-cn/aphqeh7uhohpquloj/modern-js/docs/08/result3.png)

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

本小节中，我们学习了基本的【 约定式路由 】用法，除了 `_app.tsx` 外，我们还可以使用 [`_layout.tsx`](/docs/apis/hooks/mwa/src/pages#部分-layout) 实现这个访问路径下的公共 UI，可以用 [`404.tsx`](/docs/apis/hooks/mwa/src/pages#404-路由) 自定义 404 页面等。

---

> 本小节的代码可以在[这里查看](https://github.com/modern-js-dev/modern-js-examples/tree/main/tutorials/c08/hello-modern-2)。
