鸿蒙如何让 pageId 和 dishId 这类原生参数安全地进入 Flutter 路由
适合谁看
-
正在做鸿蒙系统直达入口的人
-
想把原生参数转成 Flutter 路由的人
-
不想让原生层直接耦合 Flutter path 的开发者
问题背景
鸿蒙系统入口最容易写坏的地方之一,就是直接把原生参数和 Flutter 路由绑死。例如:
-
原生层直接知道
/dish/:id -
Flutter 路由一改,原生配置也跟着改
这会让入口链越来越难维护。
错误做法 vs 正确做法:
|
做法 |
问题 |
结果 |
|---|---|---|
|
原生层直接写 |
和 Flutter 路由耦合 |
改路由要改原生配置 |
|
原生层用 |
语义和实现解耦 |
改路由只改 Flutter 映射表 |
项目中的真实场景
食界探味当前的参数链涉及 4 层:
|
层 |
文件 |
职责 |
|---|---|---|
|
配置层 |
|
声明允许的 pageId 枚举 |
|
执行层 |
|
校验 pageId 和 dishId |
|
插件层 |
|
转发或暂存 pending |
|
Flutter 层 |
|
pageId → Flutter 路由映射 |
核心实现
一、配置层——在 insight_intent.json 把入口语义收窄
系统入口不是从代码开始的,而是从入口配置开始的。当前项目在 insight_intent.json 里先定义了允许进入应用的核心参数:
{
"insightIntents": [{
"intentName": "JumpFunctionPage",
"inputParams": [{
"properties": {
"pageId": {
"type": "string",
"enum": [
{ "value": "search", "displayName": "搜索美食" },
{ "value": "wish_box", "displayName": "心愿单" },
{ "value": "ingredients", "displayName": "食材探索" },
{ "value": "explore", "displayName": "探索美食" },
{ "value": "dish_detail", "displayName": "查看菜品详情" }
]
}
}
}]
}]
}
这一步保证了:
-
原生层和 Flutter 层讨论的是同一组稳定入口语义
-
不会变成"系统传什么,应用就随便接什么"
-
pageId是业务语义,不是 Flutter 路由路径
二、执行层——在 InsightIntentExecutorImpl 做第一轮参数校验
到了 InsightIntentExecutorImpl.ets,当前项目做了更明确的代码检查:
const VALID_PAGE_IDS: string[] = [
'search', 'wish_box', 'ingredients', 'explore', 'dish_detail'
];
private jumpFunctionPage(param: Record<string, Object>): Promise<insightIntent.ExecuteResult> {
return new Promise((resolve) => {
// 1. 类型校验
if (typeof param?.pageId !== 'string') {
resolve(makeResult(-1, 'pageId type error'));
return;
}
const pageId = param.pageId as string;
const dishId = typeof param?.dishId === 'string' ? param.dishId : undefined;
// 2. 白名单校验
if (!VALID_PAGE_IDS.includes(pageId)) {
resolve(makeResult(-1, `unknown pageId: ${pageId}`));
return;
}
// 3. 详情页参数校验
if (pageId === 'dish_detail' && (!dishId || dishId.length === 0)) {
resolve(makeResult(-1, 'dishId type error'));
return;
}
// 4. 转发到插件层
const plugin = IntentNavigationPlugin.getInstance();
if (plugin !== null) {
plugin.navigateToPage(pageId, dishId);
} else {
IntentNavigationPlugin.setPendingNavigation(pageId, dishId);
}
resolve(makeResult(0, 'success'));
});
}
执行层的 4 道校验:
|
校验 |
检查内容 |
失败处理 |
|---|---|---|
|
类型校验 |
|
返回 |
|
白名单校验 |
|
返回 |
|
详情页校验 |
|
返回 |
|
参数完整性 |
|
返回 |
这体现了"系统入口治理"的第一道门:
-
配置层定义允许的语义
-
执行器层做运行时合法性检查
-
这样进入插件层的,就不再是完全不受控的外部输入
三、插件层——只负责转发和暂存,不负责解释页面结构
到了 IntentNavigationPlugin.ets,当前项目没有让插件层去理解:
-
/search -
/dish/:id -
是否走
go -
是否走
push
插件层只做两件事:
navigateToPage(pageId: string, dishId?: string): void {
if (this.channel) {
// 1. Flutter 已 ready,直接推送
const args = new Map<string, Object>();
args.set('pageId', pageId);
if (dishId) args.set('dishId', dishId);
this.channel.invokeMethod('onIntentNavigation', args);
} else {
// 2. Flutter 未 ready,先存到 pendingNavigation
IntentNavigationPlugin.pendingNavigation = { pageId, dishId };
}
}
插件层不做的事情:
|
不做 |
为什么 |
|---|---|
|
不解释路由路径 |
路由是 Flutter 的事 |
|
不判断 go/push |
跳转策略是 Flutter 的事 |
|
不处理页面层级 |
页面结构是 Flutter 的事 |
这个设计避免了最常见的耦合:原生层直接知道 Flutter 路由结构。
四、Flutter 边界层——把稳定语义翻译成真正路由
真正的路由翻译发生在 intent_navigation_channel.dart:
static const _pageIdToRoute = <String, String>{
'search': '/search',
'ai_assistant': '/ai-assistant',
'wish_box': '/wish-box',
'ingredients': '/ingredients',
'explore': '/explore',
};
static const _shellRoutes = <String>{
'/explore', '/inspiration', '/collection', '/profile',
};
static void _navigate(_NavigationPayload payload) {
// 特殊处理:详情页
if (payload.pageId == 'dish_detail') {
final dishId = payload.dishId;
if (dishId == null || dishId.isEmpty) return;
_router?.go('/explore');
scheduleMicrotask(() {
_router?.push('/dish/$dishId');
});
return;
}
// 通用处理:普通页面
final route = _pageIdToRoute[payload.pageId];
if (route == null) return;
if (_shellRoutes.contains(route)) {
_router?.go(route);
} else {
_router?.go('/explore');
scheduleMicrotask(() {
_router?.push(route);
});
}
}
Flutter 层的 3 个设计要点:
|
要点 |
说明 |
|---|---|
|
映射表 |
pageId → Flutter 路由的唯一真相来源 |
|
|
详情页需要 go+push,不能简单 go |
|
Shell 路由判断 |
Tab 页面用 go,非 Tab 页面用 go+push |
Flutter 层不仅负责"把 pageId 变成 route",还负责:
-
区分壳路由页和独立详情页
-
区分
go和push -
区分冷启动和热跳转时更稳的跳法
五、GoRouter 最终承接页面
到了 app.dart,真正定义页面结构的是 GoRouter:
GoRoute(
path: '/search',
builder: (context, state) => const SearchScreen(),
),
GoRoute(
path: '/dish/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DishDetailScreen(dishId: id);
},
),
完整链路:
insight_intent.json(配置层)
→ 声明允许的 pageId 枚举
│
▼
InsightIntentExecutorImpl(执行层)
→ 校验 pageId 类型、白名单、dishId
│
▼
IntentNavigationPlugin(插件层)
→ 转发或暂存 pending
│
▼
intent_navigation_channel.dart(Flutter 边界层)
→ pageId → Flutter 路由映射
│
▼
GoRouter(路由层)
→ 最终承接页面
六、参数安全传递的完整流程
用户在鸿蒙搜索点击"查看牛肉咖喱详情"
│
▼
鸿蒙系统:pageId="dish_detail", dishId="beef-curry-001"
│
▼
InsightIntentExecutorImpl:
├─ 类型校验 → pageId 是 string ✓
├─ 白名单校验 → "dish_detail" 在 VALID_PAGE_IDS 中 ✓
├─ 详情页校验 → dishId="beef-curry-001" 非空 ✓
├─ 转发到 IntentNavigationPlugin
│
▼
IntentNavigationPlugin:
├─ channel 可用 → invokeMethod('onIntentNavigation', {pageId: "dish_detail", dishId: "beef-curry-001"})
│
▼
intent_navigation_channel.dart:
├─ _parseArguments() → pageId="dish_detail", dishId="beef-curry-001"
├─ _navigate() → pageId == "dish_detail"
├─ _router?.go('/explore')
├─ scheduleMicrotask(() { _router?.push('/dish/beef-curry-001'); })
│
▼
GoRouter:
├─ 匹配 '/dish/:id' → id="beef-curry-001"
├─ DishDetailScreen(dishId: "beef-curry-001")
│
▼
用户看到牛肉咖喱详情页
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
配置层 |
|
|
执行层 |
|
|
插件层 |
|
|
Flutter 边界层 |
|
|
GoRouter 路由层 |
常见坑
-
原生层直接写 Flutter route path — 改路由要改原生配置
-
参数既不校验,也没有白名单 — 非法参数直接到 Flutter
-
dishId这类附加参数没有单独检查 — dish_detail 没有 dishId 会崩溃 -
Flutter 层收到参数后又重新写一套不同语义 — pageId 应该是稳定的业务语义
-
插件层顺手做了过多路由判断 — 插件只转发,不做路由决策
-
壳路由页和详情页没有分开处理 — 返回栈会很怪
-
VALID_PAGE_IDS和 insight_intent.json 的 enum 不一致 — 两边必须同步 -
Flutter 路由映射表没有覆盖所有 pageId — 未知 pageId 会被忽略
可复用模板
参数安全传递模板
insight_intent.json 定义稳定 pageId(枚举)
→ InsightIntentExecutor 校验 pageId / dishId(白名单 + 类型)
→ IntentNavigationPlugin 转发或暂存(不理解路由)
→ intent_navigation_channel.dart 做路由翻译(映射表)
→ GoRouter 最终承接页面(页面结构唯一来源)
鸿蒙侧参数校验模板
const VALID_PAGE_IDS: string[] = ['home', 'profile', 'settings'];
private jumpToPage(param: Record<string, Object>): Promise<insightIntent.ExecuteResult> {
return new Promise((resolve) => {
if (typeof param?.pageId !== 'string') {
resolve(makeResult(-1, 'pageId type error'));
return;
}
const pageId = param.pageId as string;
if (!VALID_PAGE_IDS.includes(pageId)) {
resolve(makeResult(-1, `unknown pageId: ${pageId}`));
return;
}
// 转发到插件层
const plugin = IntentPlugin.getInstance();
if (plugin) plugin.navigateToPage(pageId);
else IntentPlugin.setPendingNavigation(pageId);
resolve(makeResult(0, 'success'));
});
}
Flutter 侧路由映射模板
static const _pageIdToRoute = <String, String>{
'home': '/home',
'profile': '/profile',
'settings': '/settings',
};
static void _navigate(NavigationPayload payload) {
final route = _pageIdToRoute[payload.pageId];
if (route == null) return;
if (_shellRoutes.contains(route)) {
_router?.go(route);
} else {
_router?.go('/home');
scheduleMicrotask(() => _router?.push(route));
}
}
参数安全检查清单
配置层:
□ pageId 是否有 enum 限制?
□ displayName 和 keywords 是否填写?
执行层:
□ pageId 类型是否校验?
□ pageId 白名单是否校验?
□ dishId 等附加参数是否校验?
□ VALID_PAGE_IDS 和 enum 是否一致?
插件层:
□ 是否只转发,不做路由决策?
□ pending 机制是否正确?
Flutter 层:
□ _pageIdToRoute 是否覆盖所有 pageId?
□ dish_detail 等特殊页面是否单独处理?
□ go 和 push 是否正确区分?
本篇总结
鸿蒙系统入口参数应该先在原生层被收成稳定语义,再进入 Flutter 路由层。pageId 和 dishId 这种设计的价值,就在于把入口语义和页面实现解耦:
-
配置层 —
insight_intent.json定义允许的 pageId 枚举 -
执行层 —
InsightIntentExecutorImpl做白名单和参数校验 -
插件层 —
IntentNavigationPlugin只转发,不做路由决策 -
Flutter 边界层 —
intent_navigation_channel.dart做 pageId → 路由映射 -
GoRouter — 最终承接页面
只要这条链分层清楚,后面扩展鸿蒙系统直达入口会轻松很多。
更多推荐



所有评论(0)