16 页面内容组装

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

现在,包含节点对象的可呈现数组,已经返回来了。这个页面是怎么构件出来的呢?让我们回到函数menu_execute_active_handler上面来。

$page_callback_result = MENU_ACCESS_DENIED;

$page_callback_result = MENU_NOT_FOUND;

这两种特殊情况我们就不分析了。往下看。

  if ($deliver) {

    $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item['delivery_callback'] : NULL;

    drupal_deliver_page($page_callback_result, $default_delivery_callback);

  }

  else {

    return $page_callback_result;

  }

我们没有向menu_execute_active_handler传递参数,所以这里的$deliverTRUE,将会执行if语句里面的代码。


Drupal版本:

16.1 drupal_deliver_html_page

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们在node_menu里面,并没有定义'delivery callback',所以这里的$default_delivery_callback将会使用Drupal的默认值。$page_callback_result就是刚才返回的包含节点对象的呈现数组。现在,让我们来看一下drupal_deliver_page,是怎么将这个数组转换成整个页面,并返回给浏览器的。

function drupal_deliver_page($page_callback_result, $default_delivery_callback = NULL) {

  if (!isset($default_delivery_callback) && ($router_item = menu_get_item())) {

    $default_delivery_callback = $router_item['delivery_callback'];

  }

  $delivery_callback = !empty($default_delivery_callback) ? $default_delivery_callback : 'drupal_deliver_html_page';

  // Give modules a chance to alter the delivery callback used, based on

  // request-time context (e.g., HTTP request headers).

  drupal_alter('page_delivery_callback', $delivery_callback);

  if (function_exists($delivery_callback)) {

    $delivery_callback($page_callback_result);

  }

  else {

    // If a delivery callback is specified, but doesn't exist as a function,

    // something is wrong, but don't print anything, since it's not known

    // what format the response needs to be in.

    watchdog('delivery callback not found', 'callback %callback not found: %q.', array('%callback' => $delivery_callback, '%q' => $_GET['q']), WATCHDOG_ERROR);

  }

}

实际上我们并没有在任何地方设置$delivery_callback,所以将会使用默认的'drupal_deliver_html_page'。这里的

drupal_alter('page_delivery_callback', $delivery_callback);

用来允许第三方模块修改这里的$delivery_callback,但是实际上,我基本上没有见过哪个第三方模块实现了这个钩子函数。Drupal提供了太多的钩子函数,有些可能从来没有被使用过。

function_exists负责检查函数是否存在,drupal_deliver_html_page这个函数是存在的,所以这里实际调用的是:

drupal_deliver_html_page($page_callback_result);

为什么不直接调用drupal_deliver_html_page呢?我们要的就是html页面啊。Drupal核心的开发者是这样考虑的,这个地方留个接口,这样第三方模块就可以实现,比如说:

drupal_deliver_json_page

drupal_deliver_xml_page

也就是说,Drupal不仅仅支持HTML,返回有可能还是jsonxml等其它格式。我们这里只用到了html,所以让我们来看一下默认的具体实现。

/**

 * Packages and sends the result of a page callback to the browser as HTML.

 *

 * @param $page_callback_result

 *   The result of a page callback. Can be one of:

 *   - NULL: to indicate no content.

 *   - An integer menu status constant: to indicate an error condition.

 *   - A string of HTML content.

 *   - A renderable array of content.

 *

 * @see drupal_deliver_page()

 */

function drupal_deliver_html_page($page_callback_result) {

  // Emit the correct charset HTTP header, but not if the page callback

  // result is NULL, since that likely indicates that it printed something

  // in which case, no further headers may be sent, and not if code running

  // for this page request has already set the content type header.

  if (isset($page_callback_result) && is_null(drupal_get_http_header('Content-Type'))) {

    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');

  }

 

  // Send appropriate HTTP-Header for browsers and search engines.

  global $language;

  drupal_add_http_header('Content-Language', $language->language);

 

  // Menu status constants are integers; page content is a string or array.

  if (is_int($page_callback_result)) {

    // @todo: Break these up into separate functions?

    switch ($page_callback_result) {

      case MENU_NOT_FOUND:

        // Print a 404 page.

        drupal_add_http_header('Status', '404 Not Found');

 

        watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);

 

        // Check for and return a fast 404 page if configured.

        drupal_fast_404();

 

        // Keep old path for reference, and to allow forms to redirect to it.

        if (!isset($_GET['destination'])) {

          $_GET['destination'] = $_GET['q'];

        }

 

        $path = drupal_get_normal_path(variable_get('site_404', ''));

        if ($path && $path != $_GET['q']) {

          // Custom 404 handler. Set the active item in case there are tabs to

          // display, or other dependencies on the path.

          menu_set_active_item($path);

          $return = menu_execute_active_handler($path, FALSE);

        }

 

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {

          // Standard 404 handler.

          drupal_set_title(t('Page not found'));

          $return = t('The requested page "@path" could not be found.', array('@path' => request_uri()));

        }

 

        drupal_set_page_content($return);

        $page = element_info('page');

        print drupal_render_page($page);

        break;

 

      case MENU_ACCESS_DENIED:

        // Print a 403 page.

        drupal_add_http_header('Status', '403 Forbidden');

        watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);

 

        // Keep old path for reference, and to allow forms to redirect to it.

        if (!isset($_GET['destination'])) {

          $_GET['destination'] = $_GET['q'];

        }

 

        $path = drupal_get_normal_path(variable_get('site_403', ''));

        if ($path && $path != $_GET['q']) {

          // Custom 403 handler. Set the active item in case there are tabs to

          // display or other dependencies on the path.

          menu_set_active_item($path);

          $return = menu_execute_active_handler($path, FALSE);

        }

 

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {

          // Standard 403 handler.

          drupal_set_title(t('Access denied'));

          $return = t('You are not authorized to access this page.');

        }

 

        print drupal_render_page($return);

        break;

 

      case MENU_SITE_OFFLINE:

        // Print a 503 page.

        drupal_maintenance_theme();

        drupal_add_http_header('Status', '503 Service unavailable');

        drupal_set_title(t('Site under maintenance'));

        print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message',

          t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))));

        break;

    }

  }

  elseif (isset($page_callback_result)) {

    // Print anything besides a menu constant, assuming it's not NULL or

    // undefined.

    print drupal_render_page($page_callback_result);

  }

 

  // Perform end-of-request tasks.

  drupal_page_footer();

}

最前面的两端代码,是用来添加http header的(drupal_add_http_header),我们不用去管它。

再往下是对$page_callback_result做了整数判断。

if (is_int($page_callback_result)) {

什么时候会是整数呢,当MENU_NOT_FOUNDMENU_ACCESS_DENIEDMENU_SITE_OFFLINE的时候,会是整数。我们这里面返回的是数组,不是整数,我们这里也就跳过了这三种特殊情况,继续往下看代码。

  elseif (isset($page_callback_result)) {

    // Print anything besides a menu constant, assuming it's not NULL or

    // undefined.

    print drupal_render_page($page_callback_result);

  }

 

  // Perform end-of-request tasks.

  drupal_page_footer();


Drupal版本:

16.2 页面数组的合成

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

这是我们这里实际执行的语句。用来合成页面的是drupal_render_page函数。这个时候,传递过来的$page_callback_result,只是一个包含节点对象的呈现数组,节点外面的区域、区块是怎么加进来的呢?这是很多初学者的疑问。让我们来看这个函数的定义:

/**

 * Renders the page, including all theming.

 *

 * @param $page

 *   A string or array representing the content of a page. The array consists of

 *   the following keys:

 *   - #type: Value is always 'page'. This pushes the theming through

 *     page.tpl.php (required).

 *   - #show_messages: Suppress drupal_get_message() items. Used by Batch

 *     API (optional).

 *

 * @see hook_page_alter()

 * @see element_info()

 */

function drupal_render_page($page) {

  $main_content_display = &drupal_static('system_main_content_added', FALSE);

 

  // Allow menu callbacks to return strings or arbitrary arrays to render.

  // If the array returned is not of #type page directly, we need to fill

  // in the page with defaults.

  if (is_string($page) || (is_array($page) && (!isset($page['#type']) || ($page['#type'] != 'page')))) {

    drupal_set_page_content($page);

    $page = element_info('page');

  }

 

  // Modules can add elements to $page as needed in hook_page_build().

  foreach (module_implements('page_build') as $module) {

    $function = $module . '_page_build';

    $function($page);

  }

  // Modules alter the $page as needed. Blocks are populated into regions like

  // 'sidebar_first', 'footer', etc.

  drupal_alter('page', $page);

 

  // If no module has taken care of the main content, add it to the page now.

  // This allows the site to still be usable even if no modules that

  // control page regions (for example, the Block module) are enabled.

  if (!$main_content_display) {

    $page['content']['system_main'] = drupal_set_page_content();

  }

 

  return drupal_render($page);

}

这个时候会组装$page这个数组,开始的时候,只包含节点部分,注意往下执行的时候,粗体部分的代码,定义了hook_page_build钩子函数,允许其它模块向$page数组追加内容。难道block模块实现了这个钩子函数?


Drupal版本:

16.3 通过hook_page_build组装区域区块

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

让我们来试一下,打开block.module文件,搜索block_page_build,还真找到了。真聪明,你猜对了。我们来看一下block模块的具体实现。

/**

 * Implements hook_page_build().

 *

 * Renders blocks into their regions.

 */

function block_page_build(&$page) {

  global $theme;

 

  // The theme system might not yet be initialized. We need $theme.

  drupal_theme_initialize();

 

  // Fetch a list of regions for the current theme.

  $all_regions = system_region_list($theme);

 

  $item = menu_get_item();

  if ($item['path'] != 'admin/structure/block/demo/' . $theme) {

    // Load all region content assigned via blocks.

    foreach (array_keys($all_regions) as $region) {

      // Assign blocks to region.

      if ($blocks = block_get_blocks_by_region($region)) {

        $page[$region] = $blocks;

      }

    }

    // Once we've finished attaching all blocks to the page, clear the static

    // cache to allow modules to alter the block list differently in different

    // contexts. For example, any code that triggers hook_page_build() more

    // than once in the same page request may need to alter the block list

    // differently each time, so that only certain parts of the page are

    // actually built. We do not clear the cache any earlier than this, though,

    // because it is used each time block_get_blocks_by_region() gets called

    // above.

    drupal_static_reset('block_list');

  }

  else {

    // Append region description if we are rendering the regions demo page.

    $item = menu_get_item();

    if ($item['path'] == 'admin/structure/block/demo/' . $theme) {

      $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));

      foreach ($visible_regions as $region) {

        $description = '<div class="block-region">' . $all_regions[$region] . '</div>';

        $page[$region]['block_description'] = array(

          '#markup' => $description,

          '#weight' => 15,

        );

      }

      $page['page_top']['backlink'] = array(

        '#type' => 'link',

        '#title' => t('Exit block region demonstration'),

        '#href' => 'admin/structure/block' . (variable_get('theme_default', 'bartik') == $theme ? '' : '/list/' . $theme),

        // Add the "overlay-restore" class to indicate this link should restore

        // the context in which the region demonstration page was opened.

        '#options' => array('attributes' => array('class' => array('block-demo-backlink', 'overlay-restore'))),

        '#weight' => -10,

      );

    }

  }

}

Block模块,在这里会找到当前主题的所有的区域,对于每个区域,会加载这个区域里面的所有的当前可用的区块。以区域的机读名字为键,把区域里面的内容追加到$page数组上来。

代码里面的

if ($item['path'] == 'admin/structure/block/demo/' . $theme) {

这是一种非常特殊的情况,只有在区块的管理界面,显示一个主题的演示区域的时候,才会用到,所以里面的代码,我们这里不用深究。


Drupal版本:

16.4 使用drupal_render呈现页面数组

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

现在,整个$page数组已经构建完成了,Drupal是怎么把它转为HTML页面的呢?注意这里的这个数组的类型是page,这个和表单元素数组,是一样的。Drupal6里面的表单数组,在Drupal7下,概念上做了进一步的扩充,除了表单元素以外,区块、节点、页面都是以呈现数组的形式出现。所有的呈现数组,经过drupal_render函数处理,就会转换成对应HTML形式。我们来看一下drupal_render的定义。

function drupal_render(&$elements) {

  // Early-return nothing if user does not have access.

  if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {

    return;

  }

 

  // Do not print elements twice.

  if (!empty($elements['#printed'])) {

    return;

  }

 

  // Try to fetch the element's markup from cache and return.

  if (isset($elements['#cache'])) {

    $cached_output = drupal_render_cache_get($elements);

    if ($cached_output !== FALSE) {

      return $cached_output;

    }

  }

 

  // If #markup is set, ensure #type is set. This allows to specify just #markup

  // on an element without setting #type.

  if (isset($elements['#markup']) && !isset($elements['#type'])) {

    $elements['#type'] = 'markup';

  }

 

  // If the default values for this element have not been loaded yet, populate

  // them.

  if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {

    $elements += element_info($elements['#type']);

  }

 

  // Make any final changes to the element before it is rendered. This means

  // that the $element or the children can be altered or corrected before the

  // element is rendered into the final text.

  if (isset($elements['#pre_render'])) {

    foreach ($elements['#pre_render'] as $function) {

      if (function_exists($function)) {

        $elements = $function($elements);

      }

    }

  }

 

  // Allow #pre_render to abort rendering.

  if (!empty($elements['#printed'])) {

    return;

  }

 

  // Get the children of the element, sorted by weight.

  $children = element_children($elements, TRUE);

 

  // Initialize this element's #children, unless a #pre_render callback already

  // preset #children.

  if (!isset($elements['#children'])) {

    $elements['#children'] = '';

  }

  // Call the element's #theme function if it is set. Then any children of the

  // element have to be rendered there.

  if (isset($elements['#theme'])) {

    $elements['#children'] = theme($elements['#theme'], $elements);

  }

  // If #theme was not set and the element has children, render them now.

  // This is the same process as drupal_render_children() but is inlined

  // for speed.

  if ($elements['#children'] == '') {

    foreach ($children as $key) {

      $elements['#children'] .= drupal_render($elements[$key]);

    }

  }

 

  // Let the theme functions in #theme_wrappers add markup around the rendered

  // children.

  if (isset($elements['#theme_wrappers'])) {

    foreach ($elements['#theme_wrappers'] as $theme_wrapper) {

      $elements['#children'] = theme($theme_wrapper, $elements);

    }

  }

 

  // Filter the outputted content and make any last changes before the

  // content is sent to the browser. The changes are made on $content

  // which allows the output'ed text to be filtered.

  if (isset($elements['#post_render'])) {

    foreach ($elements['#post_render'] as $function) {

      if (function_exists($function)) {

        $elements['#children'] = $function($elements['#children'], $elements);

      }

    }

  }

 

  // Add any JavaScript state information associated with the element.

  if (!empty($elements['#states'])) {

    drupal_process_states($elements);

  }

 

  // Add additional libraries, CSS, JavaScript an other custom

  // attached data associated with this element.

  if (!empty($elements['#attached'])) {

    drupal_process_attached($elements);

  }

 

  $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';

  $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';

  $output = $prefix . $elements['#children'] . $suffix;

 

  // Cache the processed element if #cache is set.

  if (isset($elements['#cache'])) {

    drupal_render_cache_set($output, $elements);

  }

 

  $elements['#printed'] = TRUE;

  return $output;

}

这里面注意缓存的相关代码,Drupal缓存无处不在。另外需要注意的是,呈现数组,就是一个树状结构,一个枝干下面会有子枝干,子枝干下面又有子枝干,一直递归下去,最后是树叶。将呈现数组转为HTML的时候,也是这样递归调用的,最先转成HTML的,就是最小的单元组成部分,慢慢地合成,最后整个呈现数组彻底转为HTML


Drupal版本:

16.5 drupal_page_footer负责收尾工作

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

Drupal执行完HTTP请求后,调用drupal_page_footer

/**

 * Performs end-of-request tasks.

 *

 * This function sets the page cache if appropriate, and allows modules to

 * react to the closing of the page by calling hook_exit().

 */

function drupal_page_footer() {

  global $user;

 

  module_invoke_all('exit');

 

  // Commit the user session, if needed.

  drupal_session_commit();

 

  if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) {

    drupal_serve_page_from_cache($cache);

  }

  else {

    ob_flush();

  }

 

  _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);

  drupal_cache_system_paths();

  module_implements_write_cache();

  system_run_automated_cron();

}

这里提供了一个hook_exit钩子函数,允许第三方模块在HTTP请求结束时,与Drupal进行交互。然后就是会话提交,页面缓存设置。这里的ob_flush,用来刷新输出缓冲区,可以看作引导指令页面头部阶段ob_start()的一个回应。

再往下面就是注册表检查、缓存、自动运行定时任务。我们看到,定时任务的运行,是在HTTP请求结束后,运行的,这样就不会影响返回当前页面的性能。

通过阅读Drupal核心的代码,我们弄明白了,Drupal一个普通节点页面的生成过程。如果你还有不明白的地方,可以多看几遍,把每个函数都认真的读一遍,遇到不懂的函数,查一下文档。


Drupal版本: