引入:
我们经常在Liferay网站中编辑文章,而且它常常会弹出我们非常熟悉的富文本编辑器,其实稍微有经验的人一看就知道它是一个加强版的CKEditor,为什么我们确定这个控件是ckeditor而不是其他富文本编辑器控件呢?这个控件是如何被集成在我们的页面中的呢?这是我们要解决的问题。
调试分析:
我们还是从原点开始,(小插曲:这也受益于我中学时代数学老师的教的一句话,学习就是用已知的东西解释未知的东西,而学习的过程就是建立从已知知识到未知知识的桥梁,要让A->B的每一步都满足A是B的充分条件,而不要想当然,只有这样,知识体系结构才足够牢固)
我们肯定从页面点击开始,当点击下图中图标按钮最右边那个带绿色加号的图标时:
它会发出如下请求,我们可以从web工具中看出来,这个对应请求参数的struts_action是/journal/edit_article
所以我们去struts-config.xml中去找action mapping:
这里可以看到,它会把请求转发到portlet.journal.edit_article 这个key对应的页面中:
我们去tiles-def.xml中找到这个key 对应的页面,结果是edit_article.jsp
因为我们的目标是富文本编辑器,所以我们必须在edit_article.jsp中找到对应的代码,我一看这个页面,傻眼了,它全是用的自定义标记写的,要看内容我不得不单独调试标记对应的java代码。 而且这里面充斥着scriptlet,所以我很难直接命中代码片段。我只能用排除法。
显然,在页面上"New Web Content"
对应的从Web调试器返回的代码是:
它一定满足以下代码(edit_article.jsp的第173行):
而这个<table>元素从第173行一直延伸到了第311行,所以我们的目标代码就在这从173-311行这些代码之间。(旁白:这就是缩小目标范围,也是警察甄别犯罪嫌疑人的常用方法)但是这段代码非常复杂,各种逻辑判断,我用肉眼已经没办法准确的判断走的流程了,只能借助调试器了。
首先,调试器在第88行到第93行会去获取当前languageId,这里是"en_US", 然后toLanguageId为""
然后当运行到第107行mainSections时候,我眼前一亮,因为以往经验,为了让jsp片断能更好的重用,多数是以section的形式来存放一个一个小的页面片断的,稍微初级点的人一般会在页面上用N个 jsp:include来引入这些页面片断,稍微高级点的人则会定义一个页面片断数组,然后把所有片断页面的名字都存放在这个数组中,至少我以前经常这么做,所以感觉很熟悉。
结果果然和我猜想一致(旁白:果然经验和感觉很重要啊)因为我们是点“绿色加号”页面图标进来的,所以它的mainSections的取值就是上图第107行的PropsValues.JOURNAL_ARTICLE_FORM_ADD指定的值,我们去portal.properties中找,果然找到了,而且从注释上看也证实了我们的猜想:
结合调试信息:
所以我们确定,要在这里添加多个jsp片断的内容,包括content.jsp,abstract.jsp等。并且categorySections:
然后因为我们的toLanguageId为"",所以符合第281行的条件Validator.isNull(toLanguageId)==true:
所以我们断定,这个标记对应的jsp片断就是应该包含若干jsp页面,它要包含的页面名字数组放在刚才生成的categorySections,而页面的路径应该是jspPath指定的路径,我们现在来证明这个猜想:
为此,我们去liferay-ui.tld中找到这个标记对应的处理类:
所以,它对应的标记处理类是FormNavigatorTag类:
而这个类的getPage()方法会吧页面转到/html/taglib/ui/form_navigator/page.jsp中:
我们看到,它会在第48行循环的吧所有的sectionJsp从传入的categorySections入参中读取出来,,然后分别拼接页面路径前缀(jspPath),最后在第56行,分别包含这些页面内容插入到当前页面中。因为我们刚才已经分析了,这些页面列表都在categorySection入参中,而jspPath前缀也传递进来,叫/html/portlet/journal/article,所以一切谜底都解开了,他们会从webapps/ROOT/html/portlet/journal/article中吧相应的页面片断包含进来。
我们现在来看第一个页面content.jsp,这个页面还是比较简单的, 它是一个典型的自顶向下结构:
结合页面很容易就把每块区域和对应的代码联系起来:
其中Structure:Default 和Template: None 这些所在的容器对应的是conent.jsp第211行的article-structure-template-toolbar
接下来Default language:所在的容器对应的是content.jsp第347行article-translation-toolbar
接下来Title(Required)所在容器对应的是content.jsp第480行journal-article-general-fields:
接下来的Content以及富文本编辑器对应的是content.jsp的第488行journal-article-container:
很快的, 我们就在journal-article-container下面找到了富文本编辑器对应的代码,它也是用liferay-ui标记库中的标记写的:
为此,我们又回到liferay-ui.tld中找input-editor对应的标记处理类为InputEditorTag:
从InputEditorTag类的调试信息中,我们终于发现,它使用ckeditor作为富文本编辑器的内容:(长叹一下:终于找到目标了)
从调试信息中看到,它会在108行包含一个页面叫/html/js/editor/ckeditor.jsp。
我们进入这个页面继续调试后发现,这个ckeditor使用的配置信息(存于变量ckeditorConfigFileName中)位于文件ckconfig.jsp中:
稍微瞄了一眼ckconfig.jsp,发现它提供了ckeditor很多配置参数包括定制显示的按钮,甚至包括样式,这也能解释为什么我们看到的ckeditor和我们看到原版的有一定的区别.
第62-68行定义了ckeditor的主要样式,让其绝对定位:
然后第74行指定了ckeditor控件的具体呈现,原来它是封装在一段叫ckeditor.js的javascript代码中的,它的路径在/html/js/editor/ckeditor/ckeditor.js中
最后,在我们用ckeditor编辑完文章后,liferay会自动收集这个控件产生的内容和数据,并且进一步处理。
总结:
页面调试经验:
我从事框架研究,架构很多年了,虽然自己没怎么写过jsp页面:)。
我看过很多同事,对于这种复杂页面嵌套页面的调试,他们的做法是:直接打开Firebug然后对照Dom树去找对应的jsp文件,但是这样效果不好,一来是现在主流网站多数用了很多前端框架,比如tile框架,还大量使用自定义标记库,jstl等,所以你很难直接从最终的Dom找到对应的jsp片断来满足你要求。二来是这样泛搜经常搜不到结果或者搜到很多结果,你无法判断到底哪个片断才是你所需要的, 也许这些片断彼此都很相似。
我的经验是:对于简单的,你直接一目了然就可以看到页面,页帧的嵌套包含关系,那么只要go-through一下就可以了,但是对于复杂的,尤其是页面中嵌套许多<c:if><c:choose>这种分支,判断的,你也许无法直接判断走那个分支,那么必须通过调试器。java调试工具对于jsp页面的调试支持并不完美,但是至少其中的scriptlet和expression部分是可以敲断点调试的,而条件分支,判断,循环中用到的变量值往往来自于这些scriptlet和 expression,所以知道这些代码段中出现的变量的值可以帮助我们正确的选择分支。还有就是如果页面很乱很杂,那么就可以先尽量排除不想干代码,而把目标代码局限在很小的范围中,然后断点打的细一点,就很容易搞定了。在我写着文章时候,虽然我猜想可能是用ckeditor,但我没查证过,我完全是边写博客边调试,然后结果就自动出来了。
本文中的知识:
本文其实大多数我的篇幅都是在如何进行调试和从各种复杂文件包含,引入中准确定位我们的目标的。知识本身不是很多,主要就是以下几点:
(1)Liferay在编辑博客内容时使用的富文本编辑器是一个加强版本的ckeditor
ckeditor基本知识可以参见http://ckeditor.com/
(2)这个ckeditor其实封装在一段js脚本中,其内容在/html/js/editor/ckeditor/ckeditor.js中。
(3)Liferay对于ckeditor进行了很多配置和定制,这些配置内容放在/html/js/editor/ckeditor_diffs/ckconfig.jsp中
(4)Liferay使用了大量自定义标记库,其中和页面显示的很多都放在liferay-ui.tld中定义。