使用约定式路由
上一小节中,我们学习了如何使用【 自控式路由 】方式实现客户端路由。
这一小节中,我们将在 landing-page 入口里,学习使用【 约定式路由 】。
我们首先将 landing-page
入口的标识 App.tsx
删除,换成另一种符合约定的标识 pages/
。
我们在终端执行以下代码修改 landing-page
文件内容:
- macOS
- Windows
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
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
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/
,可以看到结果:
点击 Nav 里的链接,URL 变为 http://localhost:8080/landing-page/docs
,内容会变为:
我们同样可以开启/关闭 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 提供了一种特殊的文件命名方式来实现。
执行以下命令,新建以下文件:
- macOS
- Windows
mkdir -p src/landing-page/pages/comments/\[commentTitle\]
touch src/landing-page/pages/comments/\[commentTitle\]/index.tsx
mkdir -p src/landing-page/pages/comments/[commentTitle]
ni 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/
,可以看到效果如下:
点击评论链接,跳转到 http://localhost:8080/landing-page/comments/i-am-a-comment-title
,效果如下:
URL 中的动态部分,在代码中被转换成了标题内容,修改最后部分 URL 后重新访问,标题内容同样发生改变。
本小节中,我们学习了基本的【 约定式路由 】用法,除了 _app.tsx
外,我们还可以使用 _layout.tsx
实现这个访问路径下的公共 UI,可以用 404.tsx
自定义 404 页面等。
本小节的代码可以在这里查看。