开发者社区> 余二五> 正文

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,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
给DataGrid单元行添加双击事件
现在我需要做到的功能是当我单击DataGrid某行时显示相对应选中的数据信息,在双击此相同行时弹出删除对话框,应该怎么做呢。由于单击问题很简单就不再阐述了,下面我说一下双击事件是怎么实现的。       这里用到了DataGrid的ItemDataBound事件,我们可以把下面的代码加入到所需的程序中就可实现双击的功能。
671 0
添加非oracle用户到dba, oinstall组
    oracle用户所拥有的权限比较大,因此有些时候需要使用非oracle用户来完成相关数据库管理工作。尤其是多个人维护系统或数据库时,有必要为其添加不同的用户,然后将这些用户添加到dba组。
1209 0
【更正】“给自定义控件(Web Control)添加事件的几种方法”有一个不太准确的地方。
    给自定义控件(Web Control)添加事件的几种方法。前两种方法可以不实现IPostBackEventHandler           上一篇写了一下如何在自定义控件里面添加事件,由简单的开始,一步一步实现了几种添加事件的方式,由于当时只给自定义控件添加了一种外部事件,测试的时候没有什么问题,但是后来在写分页控件的时候,我给分页控件加了两种外部事件,然后测试的时候就出现了一个问题,本来只想调用外部的一种事件,结果外部的两种事件都被调用了。
757 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
30461 0
给自定义控件(Web Control)添加事件的几种方法。前两种方法可以不实现IPostBackEventHandler
    写自定义控件已经好久了,也有几个用得时间比较长的,但是对于“事件”一直是比较模糊,没有很详细的理解。          最近升级分页控件,由于原来使用的是VB.net(在VB.net里面添加一个事件是比较容易的),现在想改用C#,而原来的方法又写得比较笨拙,想换一个更简洁一点的方法,所以不得不重新认识一下事件。
765 0
填报表中也可以添加 html 事件
填报,HTML事件,自定义js
913 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
21380 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23641 0
Mysql、SqlServer和Oracle 添加修改删除字段
MySql:添加单列:ALTER TABLE 表名 ADD 列名 数据类型添加多列:ALTER TABLE 表名 ADD 列名1 数据类型1,Add 列名2 数据类型2修改单列数据类型:ALTER TABLE 表名 CHANGE COLUMN 列名 数据类型同时修改多列数据类型:ALTE...
657 0
+关注
20377
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载