【5.0苍穹源码浅析】二、接口详解之点击菜单原创
金蝶云社区-丁梦洋
丁梦洋
5人赞赏了该文章 257次浏览 未经作者许可,禁止转载编辑于2024年07月07日 12:04:00

我们都知道,前后端交流靠的是网络接口,比如我要让前端隐藏按钮,就需要在接口返回里设计格式并传相应值给前端,苍穹也不例外,无非就是做了一层封装,下面我们从点击菜单打开一个单据列表来切入。

urlhttp://127.0.0.1:8881/ierp/form/batchInvokeAction.do?appId=a2h6_testapp&f=a2h6_testapp_apphome&ac=treeMenuClick
请求体pageId: a2h6_testapprootcb5ff168a37c40c9b46573efcba87825

appId: a2h6_testapp

params: [{"key":"navigationbar","methodName":"treeMenuClick","args":["root","1844121403830305792"],"postData":[{},[]]}]

响应体image.png

根据请求名batchInvokeAction推测应该是个批量请求,所以接口返回结构是个大的list,里面的每个对象都是具体让前端做的事,a应该是action的缩写,p可能是parameter的缩写?里面是前端做这件事需要的一些参数。

第一个sendDynamicFormAction看起来结构比较深,有点复杂,先按下不表。第二个showForm比较好理解,点击菜单会打开单据页面嘛,p里面的就是单据的一些信息,比如标识,宽高,页面有哪些元素...


现在我们进代码里看看,根据上一篇【5.0苍穹源码浅析】一、项目启动与请求分发 (kingdee.com)可知,此接口会进入kd.bos.web.actions.FormAction#batchInvokeAction方法,先是一堆校验,然后调用FormService的batchInvokeAction方法(有一说一,这和以前mvc项目挺像的,这个action类就是controller,只做些校验,业务逻辑在service里

image.png

这个方法逻辑很长,捡重点的看:

首先会获取FormView(从ThreadLocal里拿到SessionManager,再根据前端传过来的pageId获取)

image.png

我们肯定很好奇这个FormView是如何初始化的,毕竟这个类可太重要了,插件开发离不了它。这里的pageId是前端传过来的,是应用首页的,怎么确定的呢?这个pageId其实对应前端页面里元素的id,搜一下就能发现

image.png

这里我们先进到getView方法简单看下逻辑,同样是有缓存机制,所以直接看下面等于null时的代码,可以发现是从pageId拿到pageCache之后,再获取FormShowParameter对象(有过苍穹开发经验的同学应该比较熟悉这个类了),然后再通过其createView方法创建的view(主要逻辑就是反射创建kd.bos.mvc.form.FormView)

image.png

主要看下面的initView方法,会调用kd.bos.form.IFormView#initialize —> kd.bos.mvc.form.FormView#initiService

image.png

这里给view添加了很多服务,咱们捡重点的说:

  • IDataModel:我们在插件里面this.getModel时,拿到的就是这里createDataModel方法创建的FormDataModel对象,这里只是赋值了一些属性,里面的dataEntity(DynamicObject)并未初始化

  • IClientViewProxy:这个类是重中之重,看名字就知道是前端代理类,我们通过代码操控前端,比如this.getView().setVisible(),最后都是通过这个类添加了action(第一个入参就是开头说的a,第二个就是p),最终写入到响应里返回给前端,从而让前端去操作!!!

    image.png

再就是initiPluginProxy方法,会创建并注册FormViewPluginProxy对象,此时会把单据上的表单插件初始化,当后续发生如点击事件时,都是会触发这个插件代理类的相关方法,就比如这里的this.fireInitialize()会调用proxy的fireInitialize方法,然后会遍历执行表单插件的initialize方法。

看到这好像创建view的逻辑并不复杂,比较关键的是前面的pageCache类,因为FormShowParameter是从它那get的,所以要看this.getPageCache(pageId)是怎么拿到pageCache的。

点进方法,依然是有缓存机制,重点看构造方法。

image.png

这里会先从cacheMap里,也就是本地缓存里拿旧的pageCache,有的话就保存之前的改动,然后从分布式缓存里获取(restore方法会触发this.cacheMap = cache.getAll(this.regionKey)),最后放入本地缓存。这里比较好理解,我们新建单据,中途系统重启后,依然还能继续操作,说明不止有本地缓存

image.png

这里都还是在取值,还是没有看到给pageCache put(FormShowParameter.class.getSimpleName())值的地方,这里先打住,后面会说到。


总结一下,FormView存放在SessionManager里,新建view会根据pageId拿到pageCache,从而拿到FormShowParameter对象去创建view,给view添加各种service和初始化插件类。


继续看kd.bos.mservice.form.FormServiceImpl#batchInvokeAction方法,拿到view后,会遍历前端传的params,先做些校验,再执行invokeAction方法,很正常,批量处理嘛,传参当然也要是批量传

image.png

此方法会根据key(即前端传的params里的key)是否有值,走不同的逻辑调用invokeMethod方法(即反射调用第二个入参对象的,名为methodName的方法),key就是控件id,没值会走view里注册的FormController的方法,这里我们点击的左侧菜单栏,有值,会走其控件类TreeMenu的treeMenuClick方法。

这个srv.registerListener()方法也挺关键的,会通过插件代理类触发表单插件的registerListener方法!

image.png

treeMenuClick方法的逻辑也很简单,就是遍历注册的listener,调用treeMenuClick方法。

image.png

到这里大家回想下给控件添加listener的代码,是不是就是在表单插件的registerListener方法里获取控件对象,给它添加listener,然后在接口方法里写业务逻辑?结合上图可以发现会先触发registerListener,再触发相应listener,所以必须得这么来。


首页单据的这个插件会先拿到菜单信息,再配置一些参数,调用OpenPageUtils.openApp方法。此方法逻辑过长,重点就是会根据rootview(也就是整个页面)里是否有这个菜单id的view,来决定是激活(view.activate)还是新打开(view.showForm),因为用户可能先打开菜单a,再打开菜单b,再点菜单a的时候,应该是切回a的tab而不会再新打开一个

image.png

就拿view.showForm方法来说,最终也会调用((IClientViewProxy)this.getService(IClientViewProxy.class)).addAction("showForm", config),这也就和开头接口的返回里的showForm对上了。

最后再简单看下这个addAction方法的逻辑,主要就是把入参对象放进这两个集合。

image.png

然后在kd.bos.mservice.form.FormServiceImpl#invokeAction方法里,view.getActionResult会调用ClientViewProxy#getActionResult方法把_actions全部返回,最后写入到响应中。

image.png

好了,到此为止,我们已经知道接口响应里第二个showForm是怎么来的了,是点击菜单触发了首页单据的控件类TreeMenu的treeMenuClick方法,继而触发了表单插件的treeMenuClick方法,最后触发了view.showForm。


最后再看看接口响应里第一个sendDynamicFormAction,此时我们可以在ClientViewProxy#addAction方法加一个条件断点:actionName.equals("sendDynamicFormAction"),然后重新打开菜单触发接口,跟着堆栈找出触发代码(这个技巧非常实用,尤其是查看标品逻辑时,总是难找到具体入口代码,可以像这样根据接口返回值反向去找!!!

排查到是这里调用的,就是组装些数据:

image.png

其实有些朋友应该对sendDynamicFormAction有点印象的,比如单据A打开弹窗B,要在B的插件里去刷新A页面,就需要用到sendDynamicFormAction方法,不然刷新不了,实际上是因为前端传了一个pageId,接口返回里,默认都是针对这个pageId的div的操作,如果需要操作这个pageId之外的div,就需要用sendDynamicFormAction,里面的数据格式有pageId和actions,actions的格式其实和最外层的格式一致


上面的说法可能有点抽象,还是以开头这个接口为例,点击菜单,右边界面会打开对应单据,还会在顶部导航栏新增一个tab,此时传参里的pageId是下图蓝色区域,右边界面在它里面,所以showForm在最外层响应里,而顶部导航栏不在它里面,在整个页面(也就是根view)里,所以sendDynamicFormAction里的pageId是根view的pageId,让其执行新增一个tab的操作。

image.png


这个接口就讲完了,写的有点长,下篇文章开始讲这个单据列表是如何加载的...

赞 5