Liferay 从Dockbar 添加Portlet的事件细节研究

简介:

 Part 1: 从页面点击"Add"过程找出事件处理函数:

在Liferay中,当我们从左边选择一个Portlet并且添加的时候,会触发一系列的动作,并且最终把这个Portlet显示在页面上,现在我们就对这个神秘的过程进行窥测。

在页面上,为了找到我们点击Add之后绑定的事件处理函数,我们先找到这段代码对应的jsp页面在/html/portlet/layout_configuration/view_category.jsp中:

 
 
  1. <div 
  2.     class="lfr-portlet-item lfr-archived-setup" 
  3.     id="<portlet:namespace />portletItem<%= portletItem.getPortletItemId() %>" 
  4.     instanceable="<%= portletInstanceable %>" 
  5.     plid="<%= plid %>" 
  6.     portletId="<%= portlet.getPortletId() %>" 
  7.     portletItemId="<%= portletItem.getPortletItemId() %>" 
  8.     title="<%= HtmlUtil.escape(portletItem.getName()) %>" 
  9. > 
  10.     <p><%= HtmlUtil.escape(portletItem.getName()) %> <a href="javascript:;"><liferay-ui:message key="add" /></a></p> 
  11. </div> 

 

因为昨天我们研究过,这个文本任意变动都不会影响到添加Portlet事件的触发(参见http://supercharles888.blog.51cto.com/609344/908773)文章,所以我们确定点击事件和显示内容无关,而页面上除了这个文本以外任何地方点击都无效(不触发添加Portlet事件),由此看来,我们的事件最终是绑定到<a>元素的,因为这是它唯一和其他部分不同的地方。

 

最终,我们在/html/js/liferay/layout_configuration.js 中找到了事件绑定关联的地方,它在_loadContent方法中:

 
 
  1. _loadContent: function() { 
  2.                     var instance = this
  3.  
  4.                     Liferay.fire('initLayout'); 
  5.  
  6.                     instance.init(); 
  7.  
  8.                     Util.addInputType(); 
  9.  
  10.                     Liferay.on('closePortlet', instance._onPortletClose, instance); 
  11.  
  12.                     instance._portletItems = instance._dialogBody.all('div.lfr-portlet-item'); 
  13.  
  14.                     var portlets = instance._portletItems; 
  15.  
  16.                     instance._dialogBody.delegate( 
  17.                         'mousedown'
  18.                         function(event) { 
  19.                             var link = event.currentTarget; 
  20.                             var portlet = link.ancestor('.lfr-portlet-item'); 
  21.  
  22.                             instance._addPortlet(portlet); 
  23.                         }, 
  24.                         'a' 
  25.                     ); 

 

从这里我们看出来,它先把左边对话框中所有的div.lfr-portlet-item都加进来作为portletItem,而我们的每个portletItem刚好位于类选择器lfr-portlet-item中,所以被选中。然后它用delegate方法吧为父元素绑定监听器监听子元素,在这里就是为lfr-portlet-item中绑定监听器来监听子元素a(锚点-port)【关于delegate我一直不太明白,还好同事js高手帮我解决了这个疑惑】,(小插曲,这里我不得不感慨Liferay框架设计者的精妙思路,因为页面上有多个portlet,每个portlet最后都有一个a元素,因为左边对话框元素是固定的,但是对话框中包含的portlet数量是不固定的,所以直接绑定事件有很大的困难,不如索性还是让对话框当监听器,让其委托其子元素中的a来触发事件,这样就算<a>再多,也不会影响事件的触发)所以现在,每个<a>当被鼠标按下去,就会触发事件处理函数_addPortlet(portlet),这就是我们关联到的事件处理函数:

 
 
  1. _addPortlet: function(portlet, options) { 
  2.                 var instance = this
  3.  
  4.                 var portletMetaData = instance._getPortletMetaData(portlet); 
  5.  
  6.                 if (!portletMetaData.portletUsed) { 
  7.                     var plid = portletMetaData.plid; 
  8.                     var portletId = portletMetaData.portletId; 
  9.                     var portletItemId = portletMetaData.portletItemId; 
  10.                     var isInstanceable = portletMetaData.instanceable; 
  11.  
  12.                     if (!isInstanceable) { 
  13.                         instance._disablePortletEntry(portletId); 
  14.                     } 
  15.  
  16.                     var beforePortletLoaded = null
  17.                     var placeHolder = A.Node.create('<div class="loading-animation" />'); 
  18.  
  19.                     if (options) { 
  20.                         var item = options.item; 
  21.  
  22.                         item.placeAfter(placeHolder); 
  23.                         item.remove(true); 
  24.  
  25.                         beforePortletLoaded = options.beforePortletLoaded; 
  26.                     } 
  27.                     else { 
  28.                         var layoutOptions = Layout.options; 
  29.  
  30.                         var firstColumn = Layout.getActiveDropNodes().item(0); 
  31.  
  32.                         if (firstColumn) { 
  33.                             var dropColumn = firstColumn.one(layoutOptions.dropContainer); 
  34.                             var referencePortlet = Layout.findReferencePortlet(dropColumn); 
  35.  
  36.                             if (referencePortlet) { 
  37.                                 referencePortlet.placeBefore(placeHolder); 
  38.                             } 
  39.                             else { 
  40.                                 if (dropColumn) { 
  41.                                     dropColumn.append(placeHolder); 
  42.                                 } 
  43.                             } 
  44.                         } 
  45.                     } 
  46.  
  47.                     var portletOptions = { 
  48.                         beforePortletLoaded: beforePortletLoaded, 
  49.                         plid: plid, 
  50.                         placeHolder: placeHolder, 
  51.                         portletId: portletId, 
  52.                         portletItemId: portletItemId 
  53.                     }; 
  54.  
  55.                     Liferay.Portlet.add(portletOptions); 
  56.                 } 
  57.             } 

 

从这段函数我们可以看出,它在04行通过_getPortletMetaData方法,先获得portlet的元数据,比如(instancable,plid,portletId,portletItemId等信息)。然后06行判断这个portlet是否未被使用,如果没有,则第07-10行从元数据的json对象中分类出instancable,plid等信息,然后12行做出是否instancable的判断来决定这个portlet是否可以被重复添加。然后第17行创建一个placeholder用于页面上加载新portlet的容器,然后进行一系列判断和检查,最终第47-53行,吧所有这个portlet有关的信息放入一个json对象叫portletOptions,然后第55行调用Liferay.Portlet.add方法来渲染这个portlet.

 

这个Liferay.Portlet.add方法定义在/html/js/liferay/portlet.js文件中:

 
 
  1. Liferay.provide( 
  2.     Portlet, 
  3.     'add'
  4.     function(options) { 
  5.         var instance = this
  6.  
  7.         Liferay.fire('initLayout'); 
  8.  
  9.         var plid = options.plid || themeDisplay.getPlid(); 
  10.         var portletId = options.portletId; 
  11.         var portletItemId = options.portletItemId; 
  12.         var doAsUserId = options.doAsUserId || themeDisplay.getDoAsUserIdEncoded(); 
  13.  
  14.         var placeHolder = options.placeHolder; 
  15.  
  16.         if (!placeHolder) { 
  17.             placeHolder = A.Node.create('<div class="loading-animation" />'); 
  18.         } 
  19.         else { 
  20.             placeHolder = A.one(placeHolder); 
  21.         } 
  22.  
  23.         var positionOptions = options.positionOptions; 
  24.         var beforePortletLoaded = options.beforePortletLoaded; 
  25.         var onComplete = options.onComplete; 
  26.  
  27.         var container = null
  28.  
  29.         if (Liferay.Layout && Liferay.Layout.INITIALIZED) { 
  30.             container = Liferay.Layout.getActiveDropContainer(); 
  31.         } 
  32.  
  33.         if (!container) { 
  34.             return
  35.         } 
  36.  
  37.         var portletPosition = 0; 
  38.         var currentColumnId = Util.getColumnId(container.attr('id')); 
  39.  
  40.         if (options.placeHolder) { 
  41.             var column = placeHolder.get('parentNode'); 
  42.  
  43.             if (!column) { 
  44.                 return
  45.             } 
  46.  
  47.             placeHolder.addClass('portlet-boundary'); 
  48.  
  49.             portletPosition = column.all('.portlet-boundary').indexOf(placeHolder); 
  50.  
  51.             currentColumnId = Util.getColumnId(column.attr('id')); 
  52.         } 
  53.  
  54.         var url = themeDisplay.getPathMain() + '/portal/update_layout'
  55.  
  56.         var data = { 
  57.             cmd: 'add'
  58.             dataType: 'json'
  59.             doAsUserId: doAsUserId, 
  60.             p_l_id: plid, 
  61.             p_p_col_id: currentColumnId, 
  62.             p_p_col_pos: portletPosition, 
  63.             p_p_id: portletId, 
  64.             p_p_i_id: portletItemId, 
  65.             p_p_isolated: true
  66.             p_v_g_id: themeDisplay.getParentGroupId() 
  67.         }; 
  68.  
  69.         var firstPortlet = container.one('.portlet-boundary'); 
  70.         var hasStaticPortlet = (firstPortlet && firstPortlet.isStatic); 
  71.  
  72.         if (!options.placeHolder && !options.plid) { 
  73.             if (!hasStaticPortlet) { 
  74.                 container.prepend(placeHolder); 
  75.             } 
  76.             else { 
  77.                 firstPortlet.placeAfter(placeHolder); 
  78.             } 
  79.         } 
  80.  
  81.         if (themeDisplay.isFreeformLayout()) { 
  82.             container.prepend(placeHolder); 
  83.         } 
  84.  
  85.         data.currentURL = Liferay.currentURL; 
  86.  
  87.         return instance.addHTML( 
  88.             { 
  89.                 beforePortletLoaded: beforePortletLoaded, 
  90.                 data: data, 
  91.                 onComplete: onComplete, 
  92.                 placeHolder: placeHolder, 
  93.                 url: url 
  94.             } 
  95.         ); 
  96.     }, 
  97.     ['aui-base'
  98. ); 

从这里可以看出,它09-14行中从options中取出所有的信息,然后为渲染的内容添加一组样式类,最终发送到的目的地是var url = themeDisplay.getPathMain() + '/portal/update_layout'

因为我们在文章http://supercharles888.blog.51cto.com/609344/905695中已经给出了themeDisplay.getPathMain()代表的是'c',所以最终发送到的请求目的地是host:port/c/portal/update_layout,并且携带所有必要数据(56-67)行,然后吧在页面上嵌入一段url,吧从服务器端返回的内容进行填充,并且在第87-94行用数据并且调用Liferay.addHTML方法进行渲染。

对比浏览器调试器的信息,我们可以清楚的看到客户端发送到服务器端的json对象的请求内容刚好匹配56-67行:

 

Part 2: 分析这个携带json数据发送到/c/portal/update_layout的过程的幕后。

首先,因为我们Liferay服务器已经正确加载,而且Liferay的MainServlet是基于Struts中央控制器的,所以这个/c/portal/update_layout必须会进入到Struts框架:

对照struts-config.xml的url-mapping的设定:

 
 
  1. <action path="/portal/update_layout" type="com.liferay.portal.action.UpdateLayoutAction" /> 

我们可以看到,这个最终是UpdateLayoutAction类来处理这个请求:

 

这个UpdateLayoutAction是标准的Struts的Action并且它的直接父类是JSONAction:

它的execute方法最终会调用getJSON方法,先从json参数中获取cmd参数和获得portletId:

 
 
  1. String cmd = ParamUtil.getString(request, Constants.CMD); 
  2.  
  3.         String portletId = ParamUtil.getString(request, "p_p_id"); 

 

因为我们的cmd参数的内容是"add",所以它会去执行以下代码:

 
 
  1. if (cmd.equals(Constants.ADD)) { 
  2.             String columnId = ParamUtil.getString(request, "p_p_col_id"null); 
  3.             int columnPos = ParamUtil.getInteger(request, "p_p_col_pos", -1); 
  4.  
  5.             portletId = layoutTypePortlet.addPortletId( 
  6.                 userId, portletId, columnId, columnPos); 
  7.  
  8.             if (layoutTypePortlet.isCustomizable() && 
  9.                 layoutTypePortlet.isCustomizedView() && 
  10.                 !layoutTypePortlet.isColumnDisabled(columnId)) { 
  11.  
  12.                 updateLayout = false
  13.             } 
  14.         } 

这段代码的内容就是02-03行获取p_p_col_id参数和p_p_col_pos参数。然后第05-06行用LayoutTypePortlet的addPorttletId方法吧这个portletId添加到Layout中(参见LayoutTypePortletImpl的addPortletId定义),现在这个portlet就在Layout中可用了。

 

执行完这段代码后,继续会去执行以下代码:

 
 
  1. if (cmd.equals(Constants.ADD) && (portletId != null)) { 
  2.             addPortlet(mapping, form, request, response, portletId); 
  3.         } 

它会去调用UpdateLayoutAction的addPortlet方法:

 
 
  1. protected void addPortlet( 
  2.             ActionMapping mapping, ActionForm form, HttpServletRequest request, 
  3.             HttpServletResponse response, String portletId) 
  4.         throws Exception { 
  5.  
  6.         // Run the render portlet action to add a portlet without refreshing. 
  7.  
  8.         Action renderPortletAction = (Action)InstancePool.get( 
  9.             RenderPortletAction.class.getName()); 
  10.  
  11.         // Pass in the portlet id because the portlet id may be the instance id. 
  12.         // Namespace the request if necessary. See LEP-4644. 
  13.  
  14.         long companyId = PortalUtil.getCompanyId(request); 
  15.  
  16.         Portlet portlet = PortletLocalServiceUtil.getPortletById( 
  17.             companyId, portletId); 
  18.  
  19.         DynamicServletRequest dynamicRequest = null
  20.  
  21.         if (portlet.isPrivateRequestAttributes()) { 
  22.             String portletNamespace = 
  23.                 PortalUtil.getPortletNamespace(portlet.getPortletId()); 
  24.  
  25.             dynamicRequest = new NamespaceServletRequest( 
  26.                 request, portletNamespace, portletNamespace); 
  27.         } 
  28.         else { 
  29.             dynamicRequest = new DynamicServletRequest(request); 
  30.         } 
  31.  
  32.         dynamicRequest.setParameter("p_p_id", portletId); 
  33.  
  34.         String dataType = ParamUtil.getString(request, "dataType"); 
  35.  
  36.         if (dataType.equals("json")) { 
  37.             JSONObject jsonObject = JSONFactoryUtil.createJSONObject(); 
  38.  
  39.             StringServletResponse stringResponse = new StringServletResponse( 
  40.                 response); 
  41.  
  42.             renderPortletAction.execute( 
  43.                 mapping, form, dynamicRequest, stringResponse); 
  44.  
  45.             populatePortletJSONObject( 
  46.                 request, stringResponse, portlet, jsonObject); 
  47.  
  48.             response.setContentType(ContentTypes.TEXT_JAVASCRIPT); 
  49.  
  50.             ServletResponseUtil.write(response, jsonObject.toString()); 
  51.         } 
  52.         else { 
  53.             renderPortletAction.execute( 
  54.                 mapping, form, dynamicRequest, response); 
  55.         } 
  56.     } 

从这里可以看出,它实际是调用RenderPortletAction的execute方法来对这个portlet进行渲染。如何渲染呢?我们继续跟进。

 

 
 
  1. public ActionForward execute( 
  2.             ActionMapping mapping, ActionForm form, HttpServletRequest request, 
  3.             HttpServletResponse response) 
  4.         throws Exception { 
  5.  
  6.         ServletContext servletContext = (ServletContext)request.getAttribute( 
  7.             WebKeys.CTX); 
  8.  
  9.         String ajaxId = request.getParameter("ajax_id"); 
  10.  
  11.         long companyId = PortalUtil.getCompanyId(request); 
  12.         User user = PortalUtil.getUser(request); 
  13.         Layout layout = (Layout)request.getAttribute(WebKeys.LAYOUT); 
  14.         String portletId = ParamUtil.getString(request, "p_p_id"); 
  15.  
  16.         Portlet portlet = PortletLocalServiceUtil.getPortletById( 
  17.             companyId, portletId); 
  18.  
  19.         String queryString = null
  20.         String columnId = ParamUtil.getString(request, "p_p_col_id"); 
  21.         int columnPos = ParamUtil.getInteger(request, "p_p_col_pos"); 
  22.         int columnCount = ParamUtil.getInteger(request, "p_p_col_count"); 
  23.         boolean staticPortlet = ParamUtil.getBoolean(request, "p_p_static"); 
  24.         boolean staticStartPortlet = ParamUtil.getBoolean( 
  25.             request, "p_p_static_start"); 
  26.  
  27.         if (staticPortlet) { 
  28.             portlet = (Portlet)portlet.clone(); 
  29.  
  30.             portlet.setStatic(true); 
  31.             portlet.setStaticStart(staticStartPortlet); 
  32.         } 
  33.  
  34.         if (ajaxId != null) { 
  35.             response.setHeader("Ajax-ID", ajaxId); 
  36.         } 
  37.  
  38.         WindowState windowState = WindowStateFactory.getWindowState( 
  39.             ParamUtil.getString(request, "p_p_state")); 
  40.  
  41.         PortalUtil.updateWindowState( 
  42.             portletId, user, layout, windowState, request); 
  43.  
  44.         PortalUtil.renderPortlet( 
  45.             servletContext, request, response, portlet, queryString, columnId, 
  46.             new Integer(columnPos), new Integer(columnCount), true); 
  47.  
  48.         return null
  49.     } 

 从这里我们可以看出,从06-31行只是设置一些参数,然后从第38-42行获取并且更新窗口状态(最大,最小,中等),然后最后44行调用PortalUtil的renderPortlet方法来渲染Portlet:

 

我们继续跟进到PortalUtil的renderPortlet,经过一系列封装之后,它会调用PortalImpl的renderPortlet方法:

 
 
  1. public String renderPortlet( 
  2.             ServletContext servletContext, HttpServletRequest request, 
  3.             HttpServletResponse response, Portlet portlet, String queryString, 
  4.             String columnId, Integer columnPos, Integer columnCount, 
  5.             String path, boolean writeOutput) 
  6.         throws IOException, ServletException { 
  7.  
  8.         queryString = GetterUtil.getString(queryString); 
  9.         columnId = GetterUtil.getString(columnId); 
  10.  
  11.         if (columnPos == null) { 
  12.             columnPos = Integer.valueOf(0); 
  13.         } 
  14.  
  15.         if (columnCount == null) { 
  16.             columnCount = Integer.valueOf(0); 
  17.         } 
  18.  
  19.         request.setAttribute(WebKeys.RENDER_PORTLET, portlet); 
  20.         request.setAttribute(WebKeys.RENDER_PORTLET_QUERY_STRING, queryString); 
  21.         request.setAttribute(WebKeys.RENDER_PORTLET_COLUMN_ID, columnId); 
  22.         request.setAttribute(WebKeys.RENDER_PORTLET_COLUMN_POS, columnPos); 
  23.         request.setAttribute(WebKeys.RENDER_PORTLET_COLUMN_COUNT, columnCount); 
  24.  
  25.         if (path == null) { 
  26.             path = "/html/portal/render_portlet.jsp"
  27.         } 
  28.  
  29.         RequestDispatcher requestDispatcher = 
  30.             servletContext.getRequestDispatcher(path); 
  31.  
  32.         ..

 

可以发现,它最终是调用/html/portal/render_portlet.jsp去渲染页面。

 

 
 
  1. ...
  2. else { 
  3.     if (useDefaultTemplate) { 
  4.         renderRequestImpl.setAttribute(WebKeys.PORTLET_CONTENT, stringResponse.getString()); 
  5. > 
  6.  
  7.         <tiles:insert template='<%= StrutsUtil.TEXT_HTML_DIR + "/common/themes/portlet.jsp" %>' flush="false"> 
  8.             <tiles:put name="portlet_content" value="<%= StringPool.BLANK %>" /> 
  9.         </tiles:insert> 
  10.  
  11.     } 
  12.     else { 
  13.         stringResponse.writeTo(pageContext.getOut()); 
  14.     } 
  15. ...
     

而这页面,最终会去调用Tiles框架来进行渲染:比如为了解析07-09行的<tiles>标记,因为定义了它的tld:

 
 
  1. <%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %> 

所以就可以去web.xml找它的标记库定义:

 
 
  1. <taglib> 
  2.             <taglib-uri>http://struts.apache.org/tags-tiles</taglib-uri> 
  3.             <taglib-location>/WEB-INF/tld/struts-tiles.tld</taglib-location> 
  4.         </taglib> 

然后从去struts-tiles.tld去找到其标记库的定义,然后就可以找到解析类了(这里看出页面上<tiles:insert>会被InsertTag类所消费),后续不再一一展开:

 
 
  1. <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd"> 
  2. <taglib> 
  3. <tlibversion>1.2</tlibversion> 
  4. <jspversion>1.1</jspversion> 
  5. <shortname>tiles</shortname> 
  6. <uri>http://struts.apache.org/tags-tiles</uri> 
  7. <tag> 
  8. <name>insert</name> 
  9. <tagclass>org.apache.struts.taglib.tiles.InsertTag</tagclass> 
  10. <bodycontent>JSP</bodycontent> 
  11. <attribute> 
  12. <name>template</name> 
  13. <required>false</required> 
  14. <rtexprvalue>true</rtexprvalue> 
  15. </attribute> 
  16. <attribute> 
  17. <name>component</name> 
  18. <required>false</required> 
  19. <rtexprvalue>true</rtexprvalue> 
  20. </attribute> 
  21. <attribute> 
  22. <name>page</name> 
  23. <required>false</required> 
  24. <rtexprvalue>true</rtexprvalue> 
  25. </attribute> 
  26. <attribute> 
  27. <name>definition</name> 
  28. <required>false</required> 
  29. <rtexprvalue>true</rtexprvalue> 
  30. </attribute> 
  31. <attribute> 
  32. <name>attribute</name> 
  33. <required>false</required> 
  34. <rtexprvalue>false</rtexprvalue> 
  35. </attribute> 
  36. <attribute> 
  37. <name>name</name> 
  38. <required>false</required> 
  39. <rtexprvalue>true</rtexprvalue> 
  40. ..

 





本文转自 charles_wang888 51CTO博客,原文链接:http://blog.51cto.com/supercharles888/909883,如需转载请自行联系原作者

目录
相关文章
|
6月前
|
XML 存储 JavaScript
sap.ui.core.Configuration 的作用简析
sap.ui.core.Configuration 的作用简析
25 1
深入探究Camunda监听器
执行监听器与任务监听器
998 1
深入探究Camunda监听器
|
存储 Java 索引
Web阶段:第十三章:EL表达式
Web阶段:第十三章:EL表达式
Web阶段:第十三章:EL表达式
|
SQL XML Java
Web阶段:第十四章:JSTL标签库
Web阶段:第十四章:JSTL标签库
Web阶段:第十四章:JSTL标签库
|
前端开发 JavaScript
谈一谈Web中的事件
谈一谈Web中的事件
83 0
谈一谈Web中的事件
|
前端开发 JavaScript Java
WEB核心【response对象(重点),案例:读取WEB工程资源文件】第十一章
本章:主要介绍response对象详情,案例SE 和 EE 读取web工程的资源文件
WEB核心【response对象(重点),案例:读取WEB工程资源文件】第十一章
|
Shell
SAP UI5 application component运行时加载的奥秘
Created by Jerry Wang, last modified on Aug 09, 2015 shell openSomething getServiceConfig parseShellHash Hash exec initiateApplication AppConfiguration.getMetadata
89 0
SAP UI5 application component运行时加载的奥秘
|
Web App开发
【更正】“给自定义控件(Web Control)添加事件的几种方法”有一个不太准确的地方。
    给自定义控件(Web Control)添加事件的几种方法。前两种方法可以不实现IPostBackEventHandler           上一篇写了一下如何在自定义控件里面添加事件,由简单的开始,一步一步实现了几种添加事件的方式,由于当时只给自定义控件添加了一种外部事件,测试的时候没有什么问题,但是后来在写分页控件的时候,我给分页控件加了两种外部事件,然后测试的时候就出现了一个问题,本来只想调用外部的一种事件,结果外部的两种事件都被调用了。
855 0
|
应用服务中间件 容器 数据格式