1. 先理解AWC 4.x的菜单体系
做AWC定制开发的人,早晚都会碰到这么一件事:业务方提了一个需求,要在系统某个角落加一个菜单入口,点进去还有二级菜单,甚至三级。听起来不复杂,真动手时才发现,AWC的菜单体系和你想象的不太一样,它既有配置驱动的部分,也有代码注册的部分,两套逻辑混在一起,搞不懂底层关系,就会陷入“我明明加了配置,页面上就是不显示”的困境。
先说一个基本判断:AWC 4.x里的菜单不是简单的“在某个文件里写一行菜单名”就能搞定的。它背后是命令(command)、菜单项(menu item)、命令组(command group)三个层次的概念。命令决定“点击后执行什么逻辑”,菜单项决定“在界面上长什么样、出现在哪里”,命令组决定“它归属于哪个系统功能分组”。这三层在配置里是分开定义的,再通过ID关联起来。理解了这条链路,后面所有操作都是顺着走的。
1.1 三种菜单形态,先明确你要做哪一种
AWC界面上的菜单大体可以分成三类,它们的定制定位完全不同,如果一开始就搞混,后面容易白忙一场。
第一类是主菜单栏。就是页面顶部那一条,通常有“文件”“视图”“编辑”这类系统级入口,有些版本还允许自定义顶层菜单,把二级、三级菜单挂下去。AWC的主菜单栏本身也是一个命令组容器,往里塞自定义菜单项是可行的。
第二类是上下文菜单。在业务对象上右键弹出的那个菜单,比如在“物料”或“工单”上右键,会出现“打开”“属性”“上传文件”等入口。这类菜单的特点是“跟着对象走”,所以配置的时候必须绑定对象类型,否则菜单永远不出现。
第三类是工具条或卡片菜单。出现在某些窗口的工具区域或者数据集卡片上的按钮组,它们也属于命令组的一种形态,只是呈现方式不同。
我这次要做的实际上是把一个自定义多级菜单挂到“系统命令组”里去,说到底就是第一类和第三类的交叉场景:入口放在系统已有的命令组附近,但内容是我们自己的多级菜单。AWC的灵活性在于,它允许你通过扩展机制(扩展点)往系统命令组追加内容,而不是非得去改系统自带的那份只读配置文件。
1.2 菜单项、命令、命令组三者的关系
为了说清楚后面怎么做,这里先把三个概念掰开揉碎。
命令(command)是AWC里最小的执行单元。它对应一个JavaScript函数或者一个调用后端服务的动作,比如“打开窗口”“执行搜索”“下载文件”。每个命令有唯一的ID和对应的handler逻辑。
菜单项(menuItem)是界面上真正渲染出来的条目。它有一个text属性(显示文本)、icon属性(图标)、以及action属性(指向某个命令ID)。菜单项可以嵌套,就是说一个菜单项下可以挂多个子菜单项(children),这正好是我们做多级菜单的基础。
命令组(commandGroup)则是一个命名的容器,里面可以放置多个命令或菜单项的引用。系统自己有一堆预定义好的命令组,例如“主页功能组”“搜索结果工具组”“更多操作组”等等。我们的目标就是把自己的菜单项,注入到某个系统预定义好的命令组里,从而实现“自定义菜单出现在系统区域”的效果。
实际操作中有一种很常见的做法:先新定义一个一级菜单项,底下挂上若干二级菜单项,每个二级菜单项再指向具体命令,最后把这个一级菜单项追加到某个系统命令组的末尾。这样既保留了系统原有功能,又能在指定位置看到自己的入口,互不干扰。
2. 多级菜单是怎么来的?
很多人第一次看到“多级菜单”会以为要写一堆HTML嵌套列表,其实AWC里不需要那样做。它自带一个菜单渲染机制,只需要在配置的菜单项定义里,给某个菜单项挂上一层children数组,界面上的下拉菜单就会自动渲染出多级效果。
2.1 用children属性实现层级嵌套
我一直觉得AWC把菜单层级这件事做得挺简单的,就是递归渲染。一个菜单项的定义里有children字段,children里的每一项又是标准菜单项结构,可以继续挂children。想挂几层就挂几层,没有硬性限制。
举例来说,假设业务场景是“文件处理”,我们想做一个“批量工具”的一级菜单,下面分“批量上传”“批量下载”“批量归档”三个二级菜单。配置结构大体是:
var bulkToolMenu = { id: "BulkToolMenu", text: "批量工具", icon: "cmd-upl", children: [ { id: "BulkUploadMenuItem", text: "批量上传", action: "executeBulkUploadCommand" }, { id: "BulkDownloadMenuItem", text: "批量下载", action: "executeBulkDownloadCommand" }, { id: "BulkArchiveMenuItem", text: "批量归档", action: "executeBulkArchiveCommand" } ] };这里的action指向的是命令ID,说明真正执行逻辑的还是命令层。这个结构直接挂在某个命令组的items数组里,前端引擎渲染时发现该菜单项有children,就会渲染出一个带展开箭头的父菜单,鼠标hover或点击后就展示下级菜单。
如果是三级菜单,只需要在某个二级菜单项下面继续挂children即可。例如“批量下载”下面再拆成“下载当前版本”“下载历史版本”,把“批量下载”的children数组里放两条子项就行。
2.2 两种实现路线,按场景选
有些项目里,我们也遇到过不想动太多配置文件的情况,那就走JavaScript扩展注册的路线。区别在于:配置文件方式适合“静态菜单”,菜单结构和命令关系相对固定;代码注册方式可以在运行期动态组装菜单项,比如根据用户角色动态隐藏某些二级菜单,这个灵活性是配置文件做不到的。
我个人的建议是:菜单本身比较固定,就用配置文件,管理起来直观;菜单需要根据数据或权限动态变化,就用代码注册。在真实项目中往往是混合使用:先定义一堆基础命令,再在代码里组装菜单结构,最后注册到命令组。
这里必须提醒一句,无论是哪种方式,命令的定义是绕不开的。菜单项只是一个壳,真正干活的还是命令Handler。所以开发顺序应该是“先命令,后菜单,再挂组”,这个顺序别搞反了,否则就会出现菜单点过去没反应,或者甚至不显示。
3. 实操:把自定义菜单挂到系统命令组
这一节是全文的重头戏,我按实际操作的顺序走一遍,把每个步骤的必要性讲明白。这里以某跨平台系统模拟项目X为背景,需求是在AWC 4.x的“更多”命令组里,加入两个层级的自定义菜单,入口文本是“项目工具”,下面挂“导入清单”和“导出报告”,分别指向两个自研命令。
3.1 环境准备,看看要动哪些文件
AWC 4.x的定制通常发生在客户化配置目录,要修改的是扩展JSON文件和对应的JavaScript文件。在动手前,先确认安装环境里有如下结构和能力:
- 客户化配置根目录(一般叫customization之类)
- 配置发布机制(修改完配置后需要通过某种同步手段发布到运行实例)
- 日志输出端口,用于排查前端报错
很多新手第一次碰到AWC,第一反应是去修改系统安装目录下的原始JSON。这个操作通常行不通,因为系统每次发布配置时会把原始配置覆盖掉。正确的做法是在客户化目录中追加自定义文件,把修改放在系统能识别到的“扩展配置”层面。
3.2 定义一个完整的多级菜单命令
第一步,定义一个命令。假设我们要做的是“导入清单”功能,它在AWC里对应一段查询逻辑。用JavaScript注册一个命令:
baseCommand.extend({ id: "ImportChecklistCmd", action: function(ctx) { // 这里写导入清单的业务逻辑 showMessage("开始导入清单..."); } });然后定义第二个命令“导出报告”,逻辑类似:
baseCommand.extend({ id: "ExportReportCmd", action: function(ctx) { // 导出报告的业务逻辑 showMessage("开始导出报告..."); } });命令定义完,接下来定义菜单项并组合层级。这里我把一级菜单叫“项目工具”,把两个命令挂成二级菜单:
var projectToolsMenu = { id: "ProjectToolsMenu", text: "项目工具", children: [ { id: "ImportChecklistMenuItem", text: "导入清单", action: "ImportChecklistCmd" }, { id: "ExportReportMenuItem", text: "导出报告", action: "ExportReportCmd" } ] };注意,这里的一级菜单本身没有绑action,因为它只管展开,点击不触发业务逻辑。这是多级菜单的一个常见特点,父菜单通常不绑定具体命令,子菜单才绑定。
3.3 注册到系统命令组并发布
菜单结构有了,现在还差“把菜单塞进系统命令组”这一步。AWC里通常有一个扩展注册区,可以通过类似addCommandGroupItem之类的方法把菜单项追加到指定的系统命令组里。在模拟项目X里,目标命令组是“更多操作”对应的系统命令组,它的ID一般可以在系统配置文件里查到。
注册代码看起来是这样:
configuration.regCommandGroupItem("ProjectToolsMenu", "系统更多命令组ID", 3);第三个参数表示插入位置,在真实操作中,位置参数不是必填且在不同版本表现略有差异,最稳妥的做法是先不传位置,让它追加到最后,等能显示了再调整顺序。这个习惯能省掉很多“怎么显示位置不对”的调试时间。
配置同步到运行端后,如果一切正常,在“更多”相关区域就会看到“项目工具”菜单,鼠标悬停后展开“导入清单”和“导出报告”两个子菜单。
3.4 权限控制与加载时机的细节
菜单能显示出来只是第一步,真正上生产前还要考虑两件事:谁能看到这个菜单,以及什么时候注册生效。
权限控制可以走两条路。一是利用AWC自带的权限表达式,在菜单项里配置可见性条件;二是在代码里动态判断当前用户角色,动态决定是否把菜单项加入命令组。两种方式各有利弊,前者配置简单、适合粗粒度控制;后者灵活,适合细粒度控制,但需要额外开发维护。
加载时机主要指客户化脚本的执行顺序。AWC在启动时会加载一批配置和扩展脚本,如果你的注册代码在系统命令组尚未就绪时执行,就会出现“你明明注册了,但菜单未显示且无报错”的现象。这个问题的排查很耗时,我通常会在注册代码前后打印日志,确认注册时机是否早于系统布局渲染。
4. 踩坑记录与排查速查
AWC菜单开发最折磨人的不是功能逻辑复杂,而是“配置看着没问题,页面就是没有变化”。我把这几年遇到过的典型问题梳理了一遍,做成一个排查速查表,供你们直接照着查。
4.1 菜单不显示的五个常见原因
第一个原因,配置文件根本没被加载。AWC的客户化文件有严格的命名和目录要求,放错位置、文件名不匹配,都会被静默忽略。检查方法很简单,在脚本里加一行临时日志,看发布后能不能在控制台看到输出。看不到,就说明你的整个客户化脚本都没加载,不用再往下查菜单了。
第二个原因,命令组ID写错了。系统命令组的ID在不同版本可能不同,同一个“更多”命令组,在某个版本叫这个名字,在另一个版本可能叫另一个名字。正确做法是打开系统原始配置文件,搜索界面显示文本对应的命令组定义,把官方ID复制过来,别凭记忆写。
第三个原因,菜单项缺少必填属性。AWC对菜单项渲染有基础属性要求,比如text不能缺、id不能重复。如果视力扫描没发现问题,建议把菜单定义用JSON格式检查工具过一遍,语法错误比逻辑错误更隐蔽。
第四个原因,层级结构闭合不对。多级菜单完全依赖children嵌套,如果你在写大括号时少了一个},第二级菜单会整个失效,一级菜单虽然渲染但变成一个点了没反应的死节点。这类问题很难通过肉眼发现,最好用格式化工具统一整理。
第五个原因,命令Handler没注册成功。菜单项绑定了某个action,但对应的command根本没被扩展进去,表现结果同样是“菜单无反应”或者干脆不渲染。排查方式是在注册代码里手动调用一下命令,看能不能弹出业务提示,不能的话基本就是命令层的问题。
4.2 权限和缓存带来的“幽灵问题”
再讲两个很隐蔽的坑,一个跟权限有关,一个跟缓存有关,它们都能让一行看起来正确的代码失效。
权限问题多发于启用了AWC安全策略的环境。你在客户化脚本里定义的菜单,在“开发账号”下看是正常的,换一个业务账号登录就消失了。原因是平台根据当前用户上下文去计算菜单可见性,如果自定义菜单没有绑定任何权限表达式,部分环境会默认不给普通用户显示。这个不属于代码错误,而是权限模型差异。处理方式是显式地在菜单定义里加上一个“全员可见”的权限配置,或者指定目标角色组。
缓存问题则是发布配置后,前端还在使用旧配置。最常见的情况就是:配置发布完,浏览器刷新后菜单没出现,但换台电脑或者用无痕模式打开,菜单出现了。这就是缓存命中。AWC前端对配置是有缓存的,发布后需要清理浏览器缓存或者启用某种强制刷新机制,具体方式因部署方式而异。我个人的建议是,在开发验证阶段,始终用无痕窗口打开页面,能减少很多假“Bug”。
4.3 排查工具与一条实操经验
排查前端问题,最依赖的就是控制台的日志输出。AWC的JavaScript框架支持在浏览器控制台查看错误信息,很多菜单不显示的问题,其实控制台已经打出了原因,只是没有经验的开发者不知道看哪里。我建议在客户化脚本里通过合适的方式输出关键日志,例如检查命令注册成功时输出一条info,菜单注册完成时输出一条info,这样发布后打开控制台,按时间顺序就能看出哪一步没走到。
脚本加载顺序也是一个容易被忽略的因素。如果注册代码在框架基础组件初始化之前运行,你的命令可能还没来得及挂到全局命名空间,导致后续注册失败。处理方式是在脚本外层套一个初始化时机判断,确保基础组件存在后再执行注册逻辑。类似“等组件就绪再注册”的思路在很多前端框架里都有,AWC也遵循这个套路。
5. 后续还能怎么扩展
菜单做好之后,你会发现这条技术路线还能延伸到不少相近场景。比如把同一套菜单结构复用到一个自定义的工作区页面,或者在特定业务对象的上下文菜单里再挂一套层级。只要理解了命令组和菜单项的原理,扩展起来边际成本很低。
在后面处理类似需求时,我更倾向于把菜单定义单独拆成一个模块,命令层归命令层,菜单层归菜单层,再把注册入口独立出来。这样的话,如果业务方今天说“加一个菜单入口”,我只需要在菜单层加配置;明天说“入口不要了”,把注册那一行注释掉即可,不用在业务代码里到处找入口。
我踩过几次坑之后最深的体会是,AWC的定制开发,逻辑本身不难,难在一个“托”字——它托在系统的各种约束上,命令要先注册、菜单要挂载到正确的组、权限模型要匹配、缓存要清理干净。任何一个环节没对齐,都会让结果像“薛定谔的猫”一样不确定。
最后分享一个很小的技巧:不要直接改系统自带的命令组定义文件,一定要走扩展注册的方式。虽然改系统文件一时爽,但版本升级之后,你的改动会被覆盖得干干净净,而扩展注册是随客户化配置整体发布的,升级后依然存在。这个取舍,做过一次版本升级的人都会懂。