@linyjs/plugin-docs 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/docs/api-reference/index.md +3 -0
- package/docs/faq.md +4 -1
- package/docs/guides/authentication.md +313 -240
- package/docs/guides/client-hooks.md +626 -0
- package/docs/guides/client-module.md +5 -2
- package/docs/guides/client-routes.md +631 -0
- package/docs/guides/dependency-injection.md +3 -0
- package/docs/guides/permissions.md +124 -53
- package/docs/guides/server-module.md +2 -0
- package/docs/guides/shared-module-interface.md +117 -51
- package/docs/guides/state-management.md +905 -0
- package/docs/guides/ui-slots.md +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# 前端页面路由
|
|
2
|
+
|
|
3
|
+
本指南详细介绍 linyjs 插件前端页面路由的完整使用方式,包括路由定义、路由参数、路由守卫、数据预加载、嵌套路由以及 `useClientRoute` 路由功能 Hook。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
linyjs 前端路由基于 `IRouteService` 服务接口声明,框架在模块加载时自动调用 `getRoutes()` 收集所有路由并注册到路由注册表中。路由系统采用适配器模式,底层可对接 react-router、微信小程序等不同实现,消费端代码保持不变。
|
|
8
|
+
|
|
9
|
+
### 路由声明流程
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
插件客户端模块
|
|
13
|
+
└── IRouteService (usageType: 'route')
|
|
14
|
+
└── getRoutes() → IRoute[]
|
|
15
|
+
└── 框架收集到 clientResult.routes
|
|
16
|
+
└── SSR 中间件 / CSR 路由器 匹配并渲染
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 路由定义
|
|
20
|
+
|
|
21
|
+
### IRoute 接口
|
|
22
|
+
|
|
23
|
+
每条路由是一个 `IRoute` 对象:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
interface IRoute {
|
|
27
|
+
path: string // 路由路径(如 '/'、'/:id'、'/settings')
|
|
28
|
+
component: IComponent // 对应的 React 组件
|
|
29
|
+
key?: string // 路由唯一标识(用于缓存、调试)
|
|
30
|
+
children?: IRoute[] // 子路由(嵌套路由)
|
|
31
|
+
guard?: IRouteGuard // 路由守卫
|
|
32
|
+
loader?: IRouteLoader // 数据预加载函数
|
|
33
|
+
meta?: {
|
|
34
|
+
title?: string // 页面标题
|
|
35
|
+
requiresAuth?: boolean // 是否需要登录
|
|
36
|
+
layout?: IComponent // 布局组件
|
|
37
|
+
[key: string]: any // 其他自定义元数据
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 基本路由
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import type { IRouteService, IRouteDef } from '@linyjs/client-module-interface';
|
|
46
|
+
import React from 'react';
|
|
47
|
+
|
|
48
|
+
function HomePage() {
|
|
49
|
+
return <div>首页内容</div>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class MyRouteService implements IRouteService {
|
|
53
|
+
getRoutes() {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
path: '/',
|
|
57
|
+
component: HomePage,
|
|
58
|
+
key: 'home',
|
|
59
|
+
meta: { title: '首页' }
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const myRouteDef: IRouteDef = {
|
|
66
|
+
serviceTag: 'myRoute',
|
|
67
|
+
serviceImpl: MyRouteService,
|
|
68
|
+
meta: { usageType: 'route' }
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### basePath 路由前缀
|
|
73
|
+
|
|
74
|
+
`IRouteService.basePath` 用于为所有路由添加统一前缀,避免在每个路由的 `path` 中重复书写:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
class KanbanRouteService implements IRouteService {
|
|
78
|
+
basePath = '/kanban'; // 所有路由都会自动添加 /kanban 前缀
|
|
79
|
+
|
|
80
|
+
getRoutes() {
|
|
81
|
+
return [
|
|
82
|
+
// 实际路径:/kanban
|
|
83
|
+
{
|
|
84
|
+
path: '/',
|
|
85
|
+
component: KanbanBoardPage,
|
|
86
|
+
key: 'kanban-board',
|
|
87
|
+
meta: { title: '看板' }
|
|
88
|
+
},
|
|
89
|
+
// 实际路径:/kanban/settings
|
|
90
|
+
{
|
|
91
|
+
path: '/settings',
|
|
92
|
+
component: ProjectSettingsPage,
|
|
93
|
+
key: 'kanban-settings',
|
|
94
|
+
meta: { title: '项目设置' }
|
|
95
|
+
},
|
|
96
|
+
// 实际路径:/kanban/tasks/:id
|
|
97
|
+
{
|
|
98
|
+
path: '/tasks/:id',
|
|
99
|
+
component: TaskDetailPage,
|
|
100
|
+
key: 'kanban-task-detail',
|
|
101
|
+
meta: { title: '任务详情' }
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> 💡 **最终路径** = `basePath` + `route.path`。如果未设置 `basePath`,则最终路径就是 `route.path`。
|
|
109
|
+
|
|
110
|
+
## 路由参数
|
|
111
|
+
|
|
112
|
+
### 动态路由参数
|
|
113
|
+
|
|
114
|
+
使用 `:paramName` 语法定义动态路由参数:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// 路由定义
|
|
118
|
+
{
|
|
119
|
+
path: '/users/:id',
|
|
120
|
+
component: UserDetail,
|
|
121
|
+
key: 'user-detail'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 路由匹配:/users/42 → params = { id: '42' }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 在组件中获取参数
|
|
128
|
+
|
|
129
|
+
通过 `useClientRoute('useParams')` 获取路由参数:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
133
|
+
|
|
134
|
+
function UserDetail() {
|
|
135
|
+
const useParams = useClientRoute('useParams');
|
|
136
|
+
const params = useParams();
|
|
137
|
+
const { id } = params;
|
|
138
|
+
|
|
139
|
+
return <div>用户 ID: {id}</div>;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 多参数路由
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// 路由定义
|
|
147
|
+
{
|
|
148
|
+
path: '/groups/:groupId/tasks/:taskId',
|
|
149
|
+
component: TaskDetail,
|
|
150
|
+
key: 'task-detail'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 路由匹配:/groups/5/tasks/12 → params = { groupId: '5', taskId: '12' }
|
|
154
|
+
|
|
155
|
+
// 组件中获取
|
|
156
|
+
function TaskDetail() {
|
|
157
|
+
const useParams = useClientRoute('useParams');
|
|
158
|
+
const { groupId, taskId } = useParams();
|
|
159
|
+
|
|
160
|
+
return <div>分组 {groupId} 的任务 {taskId}</div>;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 路由守卫(Guard)
|
|
165
|
+
|
|
166
|
+
路由守卫用于在进入或离开路由前进行权限校验。
|
|
167
|
+
|
|
168
|
+
### IRouteGuard 接口
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
interface IRouteGuard {
|
|
172
|
+
/** 进入前校验,返回 false 阻止进入,返回 string 重定向到该路径 */
|
|
173
|
+
canEnter?(params: { location: Location }): boolean | string | Promise<boolean | string>
|
|
174
|
+
/** 离开前校验 */
|
|
175
|
+
canLeave?(params: { location: Location }): boolean | Promise<boolean>
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 使用示例
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
class AuthGuard implements IRouteGuard {
|
|
183
|
+
async canEnter({ location }) {
|
|
184
|
+
// 检查用户是否已登录
|
|
185
|
+
const token = localStorage.getItem('token');
|
|
186
|
+
if (!token) {
|
|
187
|
+
return '/login'; // 未登录,重定向到登录页
|
|
188
|
+
}
|
|
189
|
+
return true; // 允许进入
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async canLeave({ location }) {
|
|
193
|
+
// 编辑页面离开前确认
|
|
194
|
+
if (location.pathname.includes('/edit')) {
|
|
195
|
+
return confirm('有未保存的更改,确定要离开吗?');
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 在路由中使用
|
|
202
|
+
{
|
|
203
|
+
path: '/admin',
|
|
204
|
+
component: AdminPanel,
|
|
205
|
+
key: 'admin',
|
|
206
|
+
guard: new AuthGuard(),
|
|
207
|
+
meta: { title: '管理后台', requiresAuth: true }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 守卫返回值
|
|
212
|
+
|
|
213
|
+
| 返回值 | 行为 |
|
|
214
|
+
|--------|------|
|
|
215
|
+
| `true` | 允许导航 |
|
|
216
|
+
| `false` | 阻止导航 |
|
|
217
|
+
| `string`(路径) | 重定向到该路径 |
|
|
218
|
+
| `Promise<boolean \| string>` | 异步校验,完成后按上述规则处理 |
|
|
219
|
+
|
|
220
|
+
## 数据预加载(Loader)
|
|
221
|
+
|
|
222
|
+
Loader 允许在渲染组件前预加载数据,适用于 SSR 和 CSR 两种模式。
|
|
223
|
+
|
|
224
|
+
### IRouteLoader 类型
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
type IRouteLoader = (params: {
|
|
228
|
+
request?: Request // SSR 模式下的请求对象
|
|
229
|
+
params: Record<string, string> // 路由参数
|
|
230
|
+
location: Location // 当前位置信息
|
|
231
|
+
}) => Promise<any> | any
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 使用示例
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
function ArticleList({ loaderData }: { loaderData?: any }) {
|
|
238
|
+
const { articles } = loaderData || { articles: [] };
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div>
|
|
242
|
+
{articles.map(article => (
|
|
243
|
+
<div key={article._id}>{article.title}</div>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 路由定义
|
|
250
|
+
{
|
|
251
|
+
path: '/articles',
|
|
252
|
+
component: ArticleList,
|
|
253
|
+
key: 'article-list',
|
|
254
|
+
loader: async ({ params }) => {
|
|
255
|
+
const res = await fetch('/api/articles');
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
return { articles: data };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### SSR 中的 Loader
|
|
263
|
+
|
|
264
|
+
在 SSR 模式下,框架会在服务端执行 `loader` 函数,并将返回的数据序列化到 HTML 中,客户端 hydrate 时直接使用,避免重复请求:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
{
|
|
268
|
+
path: '/articles/:id',
|
|
269
|
+
component: ArticleDetail,
|
|
270
|
+
key: 'article-detail',
|
|
271
|
+
loader: async ({ params }) => {
|
|
272
|
+
// SSR 时在服务端执行,客户端直接复用结果
|
|
273
|
+
const res = await fetch(`https://localhost:3578/api/articles/${params.id}`);
|
|
274
|
+
return { article: await res.json() };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## 嵌套路由
|
|
280
|
+
|
|
281
|
+
嵌套路由允许在父路由的组件内渲染子路由。
|
|
282
|
+
|
|
283
|
+
### 定义嵌套路由
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
class BlogRouteService implements IRouteService {
|
|
287
|
+
basePath = '/blog';
|
|
288
|
+
|
|
289
|
+
getRoutes() {
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
path: '/',
|
|
293
|
+
component: BlogLayout, // 布局组件,包含 <Outlet /> 或子路由渲染区域
|
|
294
|
+
key: 'blog-layout',
|
|
295
|
+
children: [
|
|
296
|
+
{
|
|
297
|
+
path: '',
|
|
298
|
+
component: ArticleList, // /blog
|
|
299
|
+
key: 'article-list'
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: ':id',
|
|
303
|
+
component: ArticleDetail, // /blog/:id
|
|
304
|
+
key: 'article-detail'
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 路由匹配规则
|
|
314
|
+
|
|
315
|
+
对于嵌套路由,框架会递归匹配。最终路径 = `basePath` + `parentPath` + `childPath`:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
basePath = '/blog'
|
|
319
|
+
parent.path = '/'
|
|
320
|
+
child.path = ':id'
|
|
321
|
+
|
|
322
|
+
→ 最终匹配路径:/blog/:id
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## meta 元数据
|
|
326
|
+
|
|
327
|
+
`meta` 字段允许为路由附加自定义元数据:
|
|
328
|
+
|
|
329
|
+
### 标准字段
|
|
330
|
+
|
|
331
|
+
| 字段 | 类型 | 说明 |
|
|
332
|
+
|------|------|------|
|
|
333
|
+
| `title` | `string` | 页面标题,框架会自动设置 `<title>` |
|
|
334
|
+
| `requiresAuth` | `boolean` | 是否需要登录才能访问 |
|
|
335
|
+
| `layout` | `IComponent` | 布局组件,包裹在页面组件外层 |
|
|
336
|
+
|
|
337
|
+
### 自定义字段
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
{
|
|
341
|
+
path: '/dashboard',
|
|
342
|
+
component: Dashboard,
|
|
343
|
+
key: 'dashboard',
|
|
344
|
+
meta: {
|
|
345
|
+
title: '仪表盘',
|
|
346
|
+
requiresAuth: true,
|
|
347
|
+
icon: '📊', // 自定义:菜单图标
|
|
348
|
+
order: 10, // 自定义:菜单排序
|
|
349
|
+
permission: 'dashboard:view' // 自定义:权限标识
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### layout 布局组件
|
|
355
|
+
|
|
356
|
+
通过 `meta.layout` 指定布局组件,框架会将页面组件作为 children 传入:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
function AdminLayout({ children, currentPage, pages }: {
|
|
360
|
+
children: React.ReactNode;
|
|
361
|
+
currentPage?: any;
|
|
362
|
+
pages?: any[];
|
|
363
|
+
}) {
|
|
364
|
+
return (
|
|
365
|
+
<div className="admin-layout">
|
|
366
|
+
<aside className="sidebar">{/* 侧边栏 */}</aside>
|
|
367
|
+
<main className="content">{children}</main>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 路由定义
|
|
373
|
+
{
|
|
374
|
+
path: '/admin',
|
|
375
|
+
component: AdminPage,
|
|
376
|
+
key: 'admin',
|
|
377
|
+
meta: {
|
|
378
|
+
title: '管理后台',
|
|
379
|
+
layout: AdminLayout // 指定布局组件
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## useClientRoute — 路由功能 Hook
|
|
385
|
+
|
|
386
|
+
框架通过 `useClientRoute` 统一暴露路由相关的组件和 Hooks,确保切换路由适配器后消费端代码不变。
|
|
387
|
+
|
|
388
|
+
### 可用的 Key
|
|
389
|
+
|
|
390
|
+
| Key | 返回类型 | 说明 |
|
|
391
|
+
|-----|----------|------|
|
|
392
|
+
| `'NavLink'` | `React.ComponentType` | 导航链接组件(支持 active 状态) |
|
|
393
|
+
| `'Link'` | `React.ComponentType` | 普通链接组件 |
|
|
394
|
+
| `'useNavigate'` | `() => (to, options?) => void` | 编程式导航 Hook |
|
|
395
|
+
| `'useParams'` | `() => Record<string, string>` | 获取路由参数 Hook |
|
|
396
|
+
| `'useLocation'` | `() => Location` | 获取当前位置 Hook |
|
|
397
|
+
| `'useSearchParams'` | `() => [URLSearchParams, setter]` | 获取查询参数 Hook |
|
|
398
|
+
|
|
399
|
+
### 使用示例
|
|
400
|
+
|
|
401
|
+
#### NavLink 导航链接
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
405
|
+
|
|
406
|
+
function NavBar() {
|
|
407
|
+
const NavLink = useClientRoute('NavLink');
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<nav>
|
|
411
|
+
<NavLink to="/" end>首页</NavLink>
|
|
412
|
+
<NavLink to="/articles">文章</NavLink>
|
|
413
|
+
<NavLink to="/kanban">看板</NavLink>
|
|
414
|
+
</nav>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### 编程式导航
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
423
|
+
|
|
424
|
+
function LoginForm() {
|
|
425
|
+
const useNavigate = useClientRoute('useNavigate');
|
|
426
|
+
const navigate = useNavigate();
|
|
427
|
+
|
|
428
|
+
const handleLogin = async () => {
|
|
429
|
+
// 登录逻辑...
|
|
430
|
+
if (success) {
|
|
431
|
+
navigate('/dashboard'); // 导航到仪表盘
|
|
432
|
+
// navigate('/dashboard', { replace: true }); // 替换历史记录
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return <button onClick={handleLogin}>登录</button>;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### 获取路由参数
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
444
|
+
|
|
445
|
+
function ArticleDetail() {
|
|
446
|
+
const useParams = useClientRoute('useParams');
|
|
447
|
+
const { id } = useParams();
|
|
448
|
+
|
|
449
|
+
return <div>文章 ID: {id}</div>;
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### 获取当前位置
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
457
|
+
|
|
458
|
+
function Breadcrumb() {
|
|
459
|
+
const useLocation = useClientRoute('useLocation');
|
|
460
|
+
const location = useLocation();
|
|
461
|
+
|
|
462
|
+
return <span>当前路径: {location.pathname}</span>;
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### 获取查询参数
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { useClientRoute } from '@linyjs/client-module-interface';
|
|
470
|
+
|
|
471
|
+
function ArticleList() {
|
|
472
|
+
const useSearchParams = useClientRoute('useSearchParams');
|
|
473
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
474
|
+
|
|
475
|
+
const page = searchParams.get('page') || '1';
|
|
476
|
+
const limit = searchParams.get('limit') || '10';
|
|
477
|
+
|
|
478
|
+
const handlePageChange = (newPage: number) => {
|
|
479
|
+
setSearchParams({ page: String(newPage), limit });
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div>
|
|
484
|
+
<p>第 {page} 页,每页 {limit} 条</p>
|
|
485
|
+
<button onClick={() => handlePageChange(Number(page) + 1)}>下一页</button>
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
> ⚠️ **注意**:`useClientRoute` 返回的值取决于路由适配器的实现。如果返回 `undefined`,请检查是否已正确设置 `RouterProvider`。
|
|
492
|
+
|
|
493
|
+
## 完整示例
|
|
494
|
+
|
|
495
|
+
以下是一个完整的看板插件路由定义示例:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import type { IModule, IRouteDef, IRouteService } from '@linyjs/client-module-interface';
|
|
499
|
+
import KanbanBoardPage from './pages/kanban-board-page.js';
|
|
500
|
+
import ProjectSettingsPage from './pages/project-settings-page.js';
|
|
501
|
+
import TaskDetailPage from './pages/task-detail-page.js';
|
|
502
|
+
|
|
503
|
+
class KanbanRouteService implements IRouteService {
|
|
504
|
+
basePath = '/kanban';
|
|
505
|
+
|
|
506
|
+
getRoutes() {
|
|
507
|
+
return [
|
|
508
|
+
{
|
|
509
|
+
path: '/',
|
|
510
|
+
component: KanbanBoardPage,
|
|
511
|
+
key: 'kanban-board',
|
|
512
|
+
meta: { title: '看板', requiresAuth: true }
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
path: '/settings',
|
|
516
|
+
component: ProjectSettingsPage,
|
|
517
|
+
key: 'kanban-settings',
|
|
518
|
+
meta: { title: '项目设置', requiresAuth: true }
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
path: '/tasks/:id',
|
|
522
|
+
component: TaskDetailPage,
|
|
523
|
+
key: 'kanban-task-detail',
|
|
524
|
+
meta: { title: '任务详情', requiresAuth: true },
|
|
525
|
+
loader: async ({ params }) => {
|
|
526
|
+
const res = await fetch(`/api/kanban/tasks/${params.id}`);
|
|
527
|
+
return { task: await res.json() };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const kanbanRouteDef: IRouteDef = {
|
|
535
|
+
serviceTag: 'kanbanRoute',
|
|
536
|
+
serviceImpl: KanbanRouteService,
|
|
537
|
+
meta: { usageType: 'route' }
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
export const kanbanClientModule: IModule = {
|
|
541
|
+
name: 'kanbanClient',
|
|
542
|
+
serviceDefs: [kanbanRouteDef]
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
export default kanbanClientModule;
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## 调试技巧
|
|
549
|
+
|
|
550
|
+
### 查看已注册的路由
|
|
551
|
+
|
|
552
|
+
在开发模式下,框架会打印已加载的客户端路由:
|
|
553
|
+
|
|
554
|
+
```
|
|
555
|
+
[SSR] Loaded client routes: [ '/', '/articles', '/kanban', '/kanban/settings', '/kanban/tasks/:id' ]
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 路由匹配日志
|
|
559
|
+
|
|
560
|
+
SSR 中间件会输出路由匹配详情:
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
[SSR Middleware] Incoming request: GET /kanban
|
|
564
|
+
[SSR Middleware] Attempting to match route for: /kanban
|
|
565
|
+
[SSR Middleware] Match result: /
|
|
566
|
+
[SSR Middleware] Route matched, starting render pipeline...
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
如果看到 `Match result: null`,说明路由未注册或路径不匹配。
|
|
570
|
+
|
|
571
|
+
## 常见问题
|
|
572
|
+
|
|
573
|
+
### 1. 路由 404 — 页面显示找不到
|
|
574
|
+
|
|
575
|
+
**原因**:客户端模块未成功加载,或路由路径不匹配。
|
|
576
|
+
|
|
577
|
+
**排查步骤**:
|
|
578
|
+
1. 检查控制台是否有模块加载错误日志
|
|
579
|
+
2. 检查 `[SSR] Loaded client routes` 输出中是否包含目标路径
|
|
580
|
+
3. 确认 `basePath` + `path` 的最终路径与浏览器访问的 URL 一致
|
|
581
|
+
4. 如果服务端模块加载失败(如依赖注入错误),可能阻断客户端模块加载
|
|
582
|
+
|
|
583
|
+
### 2. 路由参数获取不到
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// ❌ 错误:直接从 props 获取
|
|
587
|
+
function UserDetail(props) {
|
|
588
|
+
const id = props.match.params.id; // 可能 undefined
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ✅ 正确:使用 useClientRoute
|
|
592
|
+
function UserDetail() {
|
|
593
|
+
const useParams = useClientRoute('useParams');
|
|
594
|
+
const { id } = useParams();
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 3. basePath 重复
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// ❌ 错误:path 中重复了 basePath
|
|
602
|
+
class MyRouteService implements IRouteService {
|
|
603
|
+
basePath = '/kanban';
|
|
604
|
+
getRoutes() {
|
|
605
|
+
return [
|
|
606
|
+
{ path: '/kanban', component: Board } // 最终路径变成 /kanban/kanban
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ✅ 正确:path 不含 basePath
|
|
612
|
+
class MyRouteService implements IRouteService {
|
|
613
|
+
basePath = '/kanban';
|
|
614
|
+
getRoutes() {
|
|
615
|
+
return [
|
|
616
|
+
{ path: '/', component: Board } // 最终路径是 /kanban
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## 相关文档
|
|
623
|
+
|
|
624
|
+
- 💻 [客户端模块开发](./client-module.md) — 模块整体结构
|
|
625
|
+
- 🪝 [公共 Hooks 使用](./client-hooks.md) — `useClientComponent`、`useClientHook`、`useClientService`
|
|
626
|
+
- 🔒 [权限管理](./permissions.md) — 路由守卫与权限结合
|
|
627
|
+
- 🔧 [API 参考](../api-reference/index.md) — IRoute、IRouteService 接口定义
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
**上一篇**: [客户端模块开发](./client-module.md) | **下一篇**: [公共 Hooks 使用](./client-hooks.md)
|
|
@@ -565,6 +565,9 @@ class MyController {
|
|
|
565
565
|
|
|
566
566
|
- 📖 [服务端模块开发](./server-module.md) — 控制器、中间件、定时任务
|
|
567
567
|
- 📦 [shared-module-interface 指南](./shared-module-interface.md) — 类型定义包使用说明
|
|
568
|
+
- 💻 [客户端模块开发](./client-module.md) — 模块整体结构
|
|
569
|
+
- 🛤️ [前端页面路由](./client-routes.md) — 路由定义与使用
|
|
570
|
+
- 🪝 [公共 Hooks 使用](./client-hooks.md) — 跨模块资源消费
|
|
568
571
|
- 💾 [数据持久化](./data-persistence.md) — MongoDB 服务使用
|
|
569
572
|
- 🔒 [权限管理](./permissions.md) — 权限服务使用
|
|
570
573
|
- 🔧 [API 参考](../api-reference/index.md) — 接口类型定义
|