适合谁看
-
正在做鸿蒙系统直达入口的人
-
想把原生参数转成 Flutter 路由的人
-
不想让原生层直接耦合 Flutter path 的开发者
问题背景
鸿蒙系统入口最容易写坏的地方之一,就是直接把原生参数和 Flutter 路由绑死。例如:
-
原生层直接知道
/dish/:id -
Flutter 路由一改,原生配置也跟着改
这会让入口链越来越难维护。
错误做法 vs 正确做法:
| 做法 | 问题 | 结果 |
|---|---|---|
原生层直接写 /search |
和 Flutter 路由耦合 | 改路由要改原生配置 |
原生层用 pageId: "search" |
语义和实现解耦 | 改路由只改 Flutter 映射表 |
项目中的真实场景
食界探味当前的参数链涉及 4 层:
| 层 | 文件 | 职责 |
|---|---|---|
| 配置层 | insight_intent.json |
声明允许的 pageId 枚举 |
| 执行层 | InsightIntentExecutorImpl.ets |
校验 pageId 和 dishId |
| 插件层 | IntentNavigationPlugin.ets |
转发或暂存 pending |
| Flutter 层 | intent_navigation_channel.dart |
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 道校验:
| 校验 | 检查内容 | 失败处理 |
|---|---|---|
| 类型校验 | pageId 必须是 string |
返回 -1 |
| 白名单校验 | pageId 必须在 VALID_PAGE_IDS 中 |
返回 -1 |
| 详情页校验 | dish_detail 必须有 dishId |
返回 -1 |
| 参数完整性 | dishId 如果存在必须非空 |
返回 -1 |
这体现了"系统入口治理"的第一道门:
-
配置层定义允许的语义
-
执行器层做运行时合法性检查
-
这样进入插件层的,就不再是完全不受控的外部输入
三、插件层------只负责转发和暂存,不负责解释页面结构
到了 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 个设计要点:
| 要点 | 说明 |
|---|---|
映射表 _pageIdToRoute |
pageId → Flutter 路由的唯一真相来源 |
dish_detail 单独处理 |
详情页需要 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")
│
▼
用户看到牛肉咖喱详情页
关键代码位置
| 文件 | 作用 |
|---|---|
app/ohos/entry/src/main/resources/base/profile/insight_intent.json |
配置层 |
app/ohos/entry/src/main/ets/entryability/InsightIntentExecutorImpl.ets |
执行层 |
app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets |
插件层 |
app/lib/core/platform/intent_navigation_channel.dart |
Flutter 边界层 |
app/lib/app.dart |
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 --- 最终承接页面
只要这条链分层清楚,后面扩展鸿蒙系统直达入口会轻松很多。
