我们都知道,前后端交流靠的是网络接口,比如我要让前端隐藏按钮,就需要在接口返回里设计格式并传相应值给前端,苍穹也不例外,无非就是做了一层封装,下面我们从点击菜单打开一个单据列表来切入。
url | http://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":[{},[]]}] |
响应体 |
根据请求名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里)
这个方法逻辑很长,捡重点的看:
首先会获取FormView(从ThreadLocal里拿到SessionManager,再根据前端传过来的pageId获取)
我们肯定很好奇这个FormView是如何初始化的,毕竟这个类可太重要了,插件开发离不了它。这里的pageId是前端传过来的,是应用首页的,怎么确定的呢?这个pageId其实对应前端页面里元素的id,搜一下就能发现
这里我们先进到getView方法简单看下逻辑,同样是有缓存机制,所以直接看下面等于null时的代码,可以发现是从pageId拿到pageCache之后,再获取FormShowParameter对象(有过苍穹开发经验的同学应该比较熟悉这个类了),然后再通过其createView方法创建的view(主要逻辑就是反射创建kd.bos.mvc.form.FormView)
主要看下面的initView方法,会调用kd.bos.form.IFormView#initialize —> kd.bos.mvc.form.FormView#initiService
这里给view添加了很多服务,咱们捡重点的说:
IDataModel:我们在插件里面this.getModel时,拿到的就是这里createDataModel方法创建的FormDataModel对象,这里只是赋值了一些属性,里面的dataEntity(DynamicObject)并未初始化
IClientViewProxy:这个类是重中之重,看名字就知道是前端代理类,我们通过代码操控前端,比如this.getView().setVisible(),最后都是通过这个类添加了action(第一个入参就是开头说的a,第二个就是p),最终写入到响应里返回给前端,从而让前端去操作!!!
再就是initiPluginProxy方法,会创建并注册FormViewPluginProxy对象,此时会把单据上的表单插件初始化,当后续发生如点击事件时,都是会触发这个插件代理类的相关方法,就比如这里的this.fireInitialize()会调用proxy的fireInitialize方法,然后会遍历执行表单插件的initialize方法。
看到这好像创建view的逻辑并不复杂,比较关键的是前面的pageCache类,因为FormShowParameter是从它那get的,所以要看this.getPageCache(pageId)是怎么拿到pageCache的。
点进方法,依然是有缓存机制,重点看构造方法。
这里会先从cacheMap里,也就是本地缓存里拿旧的pageCache,有的话就保存之前的改动,然后从分布式缓存里获取(restore方法会触发this.cacheMap = cache.getAll(this.regionKey)),最后放入本地缓存。这里比较好理解,我们新建单据,中途系统重启后,依然还能继续操作,说明不止有本地缓存
这里都还是在取值,还是没有看到给pageCache put(FormShowParameter.class.getSimpleName())值的地方,这里先打住,后面会说到。
总结一下,FormView存放在SessionManager里,新建view会根据pageId拿到pageCache,从而拿到FormShowParameter对象去创建view,给view添加各种service和初始化插件类。
继续看kd.bos.mservice.form.FormServiceImpl#batchInvokeAction方法,拿到view后,会遍历前端传的params,先做些校验,再执行invokeAction方法,很正常,批量处理嘛,传参当然也要是批量传
此方法会根据key(即前端传的params里的key)是否有值,走不同的逻辑调用invokeMethod方法(即反射调用第二个入参对象的,名为methodName的方法),key就是控件id,没值会走view里注册的FormController的方法,这里我们点击的左侧菜单栏,有值,会走其控件类TreeMenu的treeMenuClick方法。
这个srv.registerListener()方法也挺关键的,会通过插件代理类触发表单插件的registerListener方法!
treeMenuClick方法的逻辑也很简单,就是遍历注册的listener,调用treeMenuClick方法。
到这里大家回想下给控件添加listener的代码,是不是就是在表单插件的registerListener方法里获取控件对象,给它添加listener,然后在接口方法里写业务逻辑?结合上图可以发现会先触发registerListener,再触发相应listener,所以必须得这么来。
首页单据的这个插件会先拿到菜单信息,再配置一些参数,调用OpenPageUtils.openApp方法。此方法逻辑过长,重点就是会根据rootview(也就是整个页面)里是否有这个菜单id的view,来决定是激活(view.activate)还是新打开(view.showForm),因为用户可能先打开菜单a,再打开菜单b,再点菜单a的时候,应该是切回a的tab而不会再新打开一个
就拿view.showForm方法来说,最终也会调用((IClientViewProxy)this.getService(IClientViewProxy.class)).addAction("showForm", config),这也就和开头接口的返回里的showForm对上了。
最后再简单看下这个addAction方法的逻辑,主要就是把入参对象放进这两个集合。
然后在kd.bos.mservice.form.FormServiceImpl#invokeAction方法里,view.getActionResult会调用ClientViewProxy#getActionResult方法把_actions全部返回,最后写入到响应中。
好了,到此为止,我们已经知道接口响应里第二个showForm是怎么来的了,是点击菜单触发了首页单据的控件类TreeMenu的treeMenuClick方法,继而触发了表单插件的treeMenuClick方法,最后触发了view.showForm。
最后再看看接口响应里第一个sendDynamicFormAction,此时我们可以在ClientViewProxy#addAction方法加一个条件断点:actionName.equals("sendDynamicFormAction"),然后重新打开菜单触发接口,跟着堆栈找出触发代码(这个技巧非常实用,尤其是查看标品逻辑时,总是难找到具体入口代码,可以像这样根据接口返回值反向去找!!!)
排查到是这里调用的,就是组装些数据:
其实有些朋友应该对sendDynamicFormAction有点印象的,比如单据A打开弹窗B,要在B的插件里去刷新A页面,就需要用到sendDynamicFormAction方法,不然刷新不了,实际上是因为前端传了一个pageId,接口返回里,默认都是针对这个pageId的div的操作,如果需要操作这个pageId之外的div,就需要用sendDynamicFormAction,里面的数据格式有pageId和actions,actions的格式其实和最外层的格式一致。
上面的说法可能有点抽象,还是以开头这个接口为例,点击菜单,右边界面会打开对应单据,还会在顶部导航栏新增一个tab,此时传参里的pageId是下图蓝色区域,右边界面在它里面,所以showForm在最外层响应里,而顶部导航栏不在它里面,在整个页面(也就是根view)里,所以sendDynamicFormAction里的pageId是根view的pageId,让其执行新增一个tab的操作。
这个接口就讲完了,写的有点长,下篇文章开始讲这个单据列表是如何加载的...
推荐阅读