20. 渲染上下文RenderContext、渲染器renderer
当drupal的控制器返回渲染数组的时候,系统会派发视图事件,渲染数组被main_content_view_subscriber(主内容视图订阅器)处理,它根据请求的格式,将系统流程定向到对应的格式渲染器,系统默认提供了四个格式渲染器,他们被定义在容器的main_content_renderers参数里面,其中html格式对应的是服务id。为“main_content_renderer.html”的html格式渲染器,我们得到的html格式页面几乎都是它渲染的,它将渲染分为两个步骤:先渲染body标签,然后渲染html标签,其中我们将html的渲染称为根渲染。
具体的工作是由渲染器完成的,它负责将渲染数组转换为html字符串,本篇的主题就是这个渲染器。
要理解它是怎么工作的需要先理解一些知识:
首先是渲染数组,关于它可阅读本系列的《云客Drupal8源码分析之渲染数组(render array)》
其次是渲染上下文,下面介绍一下渲染上下文:
渲染上下文
每次渲染时,渲染数组的渲染过程是从根元素开始的,递归到所有的子元素,它对应着html页面结构,由父元素渲染到子元素,这个过程中有两个问题需要解决:
1:缓存问题,为了提高性能,每个渲染数组只渲染一次,除非变的无效,否则不会再次渲染,这里子元素的缓存性质将影响到父元素,比如子元素一小时后失效,那么父元素的缓存时间也不能超过一小时。
2:附件问题,每个元素都可以定义自己要用到的js或css等附件,需要收集所有的这些附件,以添加到最终的页面上,这个收集过程就伴随渲染过程实现的。
为了解决以上两个问题,系统定义了一个BubbleableMetadata对象(Drupal\Core\Render\BubbleableMetadata),字面翻译为“可冒泡元数据”,每个BubbleableMetadata对象都对应一个元素(或称为渲染数组,可含有子元素),它包含着该元素的缓存数据和附件数据,但根据它的用途,云客这里将它称为“渲染跟踪元数据”,为什么取了BubbleableMetadata这么个名字呢?为什么是冒泡?这跟渲染数组的遍历算法有关系,如果学过计算机数据结构这门课程会比较熟悉冒泡遍历,没学过?没关系后面会讲清楚。
对渲染数组的渲染(遍历)是从根元素开始的,它可能有很多层子元素,每一层也可能有多个并列子元素,渲染并不是将父层渲染完后再开始渲染子层(这叫做广度优先算法),而是使用深度优先算法(将一个元素和它的所有子元素渲染完才会开始渲染兄弟元素),如果把“渲染”比作一个人,它将在元素间不停进出或平移,他的行动轨迹完全符合堆栈数据结构,实际上系统就设置了一个堆栈对象来表示这个轨迹,每个元素对应着一个BubbleableMetadata,这个人在进行渲染工作的时候,只需要将元素对应的BubbleableMetadata对象在堆栈对象中压入、弹出、合并就能解决上面两个问题,这个堆栈对象就是本主题说的渲染上下文对象:Drupal\Core\Render\RenderContext,当渲染进入一个子元素的时候压入一个空的BubbleableMetadata对象到堆栈中,渲染完后更新这个对象,当退出这个元素的时候将BubbleableMetadata和堆栈中前一个BubbleableMetadata合并,这个合并动作就是冒泡,它将子元素的缓存数据和附件数据带到父元素,现在明白为什么叫做冒泡了吧,渲染过程中缓存数据逐级向上传递,附件也全部收集到一起。
这里需要注意的是:“渲染”这个“人”在渲染完某个元素,更新当前的BubbleableMetadata时,也可能依据BubbleableMetadata将渲染结果进行缓存,这样每个元素只渲染一次,每个满足缓存条件的子元素都被独立缓存了,这样大大提高了系统性能。
介绍完上面的准备知识后我们来看一下渲染器是怎么工作的:
渲染器
渲染器的服务id是:renderer
类:Drupal\Core\Render\Renderer
实现了渲染器接口:Drupal\Core\Render\RendererInterface
有以下构造参数,以服务id指出:
controller_resolver:解析渲染数组中的回调,见控制器解析器
theme.manager:主题系统,进行主题渲染
plugin.manager.element_info:用于获取元素的默认值
render_placeholder_generator:产生占位符,占位符请看渲染数组主题
render_cache:执行渲染缓存
request_stack:请求堆栈,用于获取当前请求的渲染上下文,它以请求对象为数组键,保存在属性数组里。
以及必须的渲染上下文及占位符条件配置数组。
drupal的渲染系统是一个很大的系统,各种对象配合工作,由于篇幅原因本主题不介绍以上对象的内容,但不用担心,他们不影响本主题的理解,他们将在后续主题中介绍,现在只需要知道他们的作用足矣。
渲染器的核心方法是doRender,在这个方法中能明白渲染的主要工作,下面是对这个方法的流程说明,请对照着程序看:
见Drupal\Core\Render\Renderer::doRender
由于要自我调用渲染子数组,所以其第二个参数$is_root_call表明当前调用是否处于自我递归调用中,仅内部使用,并不是指在渲染根html元素
这里以变量:$elements做为渲染数组来说明:
如果$elements为空直接返回空串
先进行权限检查
如果没有设置$elements['#access']时,看有无访问控制回调$elements['#access_callback'],如果有执行回调,将结果赋值给$elements['#access']
如果$elements['#access']全等为false,则返回空串,如果为AccessResultInterface对象,将其缓存依赖添加到渲染数组,以便权限正确,访问结果不允许的话也返回空串。
如果$elements['#printed']不为空,说明已经被渲染过了,直接返回空(笔者认为返回$elements['#markup']更好)。
准备开始渲染实质性工作
取得上下文堆栈对象,它用于跟踪各级渲染数组的缓存数据和附件数据,压入一个初始的BubbleableMetadata对象,字面翻译是“可冒泡元数据”,按用途和直观原因笔者这里翻译为:“渲染跟踪元数据”,是如何跟踪的呢?伴随渲染递归过程,从最里层子元素冒泡到根父元素。
如果有$elements['#cache']['keys']属性或者是根渲染,则合并预配置的必要缓存上下文。
如果设置有$elements['#cache']['keys']则尝试从渲染缓存里面获取渲染结果,渲染缓存里面的内容都是没有经过占位符替换的,所以如果是递归子元素完毕后的根渲染,则返回前要进行占位符替换。
如果存在$elements['#type']则附加元素的默认值,其中如果设置了$elements['#defaults_loaded']不为空将阻止加载,这在某些方面是有用的,比如表单API里面的form_builder()。
判断是否设置延迟构建回调$elements['#lazy_builder'],并检查它的合法性;它的作用就是延迟构建渲染数组,其值只能是两个元素组成的数组:回调及回调的参数,参数可以是NULL或者标量类型值;当指定延迟构建时,渲染数组不能也不应该有子渲染数组,子内容应该由延迟构建回调产生。
判断是否需要运用自动占位符机制,如果需要,则设置$elements['#create_placeholder']为真,当$elements['#create_placeholder']有设置且为真时$elements['#lazy_builder']必须存在,因为需要用它的结果去替换占位符。
当$elements['#create_placeholder']有设置且为真时对$elements创建占位符。
如果存在$elements['#lazy_builder']则调用它来构建新元素以代替原元素,将原元素的缓存数据设置到新元素,设置$elements['#lazy_builder_built']为TRUE以表示已经调用了延迟构建。
如果设置了$elements['#pre_render']则调用它们,允许其修改渲染数组。
如果$elements['#markup']或$elements['#plain_text']不为空,则处理它们,$elements['#plain_text']优先级更高,保证有一个安全的$elements['#markup'],其值为markup对象。
规范化当前元素的渲染跟踪元数据
检查$elements['#printed']的设置,允许$elements['#pre_render']终止渲染。
如果存在$elements['#states']则处理状态信息。
查找出子元素(子渲染数组),如果$elements['#children']不存在则设置它的初始值为空字符串,它代表子元素的html内容字符串。
如果设置了$elements['#theme'],则说明渲染数组需要由主题系统去渲染,调用主题系统进行渲染,这也将渲染子元素,并将结果赋值给$elements['#children'],可以设置$elements['#render_children']防止调用主题渲染。
当没有被主题系统渲染或设置了$elements['#render_children']防止主题渲染,此时当$elements['#children']为空,则进行子渲染,自我调用,这里是递归渲染的开端。
如果存在$elements['#markup'],将它追加到$elements['#children']前面,如果是主题渲染的话,此工作应该由主题系统完成。
如果存在$elements['#theme_wrappers']且没有设置$elements['#render_children'],则由主题系统执行主题包装渲染,它通常在已经渲染的子元素外面包装一些其他元素。
如果存在$elements['#post_render']则执行这些回调。
如果有设置前缀$elements['#prefix']或后缀$elements['#suffix']则运用它们,最终形成渲染后的$elements['#markup'],它是一个Markup对象。
执行渲染上下文堆栈的更新,具体是将代表本元素的BubbleableMetadata(渲染跟踪元数据)变为最新,因为上面许多过程可以改变这些值,比如#pre_render、#post_render、#lazy_builder等等。
缓存已经被渲染的渲染数组,在以上整个过程中$elements['#cache']['keys']不允许被改变,需要不同的缓存id可以使用缓存上下文去实现。
如果是递归渲染结束后的根渲染,则进行占位符替换,将真正的占位内容值渲染后替换占位变量。
执行冒泡:$context->bubble(),这是渲染跟踪元数据冒泡的关键,也是渲染上下文堆栈对象发挥作用的核心。
最后标记$elements['#printed']为真表示已经渲染过了,返回$elements['#markup'],它代表渲染后的字符串值,以markup对象的方式存在。
补充说明
其实drupal8的所有控制器都是包装在一个渲染上下文中执行的,目的是解决在控制器中直接渲染内容丢失BubbleableMetadata的问题,有望在drupal9中通过禁止在控制器中直接渲染解决。
渲染器进行根渲染后js和css附件仅通过占位tokens代替,在后续流程中才会真正附加这些附件。
doRender等方法的$is_root_call参数代表当前调用是否为一个递归调用,并不表示在进行html文档的根渲染,它是一个编程技巧,仅内部使用,在递归过程中不会进行占位符替换。
渲染器的$contextCollection属性,它是SplObjectStorage对象,保存请求堆栈中每个请求对应的请求上下文堆栈对象,注意它是静态属性,这意味着不管多少个渲染器实例,渲染上下文保证每个请求只对应一个,在executeInRenderContext方法中会临时设置一个上下文,用完即毁,然后复原之前的上下文。
强调一下在渲染过程中$elements['#cache']['keys']不允许被改变。