> Symfony中文手册 > 使用Edge Side Include

使用Edge Side Include

Gateway cache是一种很好的方式,能够令你的网站表现更佳。但它们有一个缺点:只能缓存整个页面。如果不想缓存全部页面,或者页面中的某些部分是“更为动态”的,你就没办法了。幸运的是,Symfony为这些场景提供了一种解决方案,基于一种被称为ESI的技术,或者叫Edge Side Includes。Akamai在十年前写了这个协议,允许页面的特定部分拥有一种不同于整个页面的缓存策略。

ESI协议,描述了你可以嵌入页面的标签,用来与gateway cache进行通信。只有一个标签被Symfony实现,即include,因为这是在Akamai上下文之外唯一有用的。

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE HTML>
<html>
    <body>
        <!-- ... some content -->
 
        <!-- Embed the content of another page here -->
        <esi:include src="Http://..." />
 
        <!-- ... more content -->
    </body>
</html>

注意,例子中每个ESI标记都有一个fully-qualified的URL(译注:绝对完整路径)。一个ESI标记,展现的是一个可以通过给定URL取出的页面片段。

当一个请求被处理时,gateway cache从其缓存中取出整个页面,或者通过后台程序取出。如果响应中包含了一或多个ESI标记,它们都将被以相同方式处理。也就是说,gateway cache既从缓存中也通过再次请求后台程序来取出页面中的包容片段。当所有的ESI标记被解析之后,网关缓存合并每一个片段到页面中,然后发送最终内容到客户端。

在Symfony中使用ESI ¶

首先,要使用ESI必须先在程序级配置中开启它:

1
2
3
4
# app/config/config.yml
framework:
    # ...
    esi: { enabled: true }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/symfony"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <framework:config>
        <!-- ... -->
        <framework:esi enabled="true" />
    </framework:config>
</container>
1
2
3
4
5
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'esi' => array('enabled' => true),
));

现在,假设你有一个页面,它是相对静态的,除了底部的一些新闻播报。使用ESI,你可以将新闻播报的缓存独立于页面其他部分之外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Controller/DefaultController.php
 
// ...
class DefaultController extends Controller
{
    public function aboutAction()
    {
        $response = $this->render('static/about.html.twig');
        // set the shared max age - which also marks the response as public
        // 设置共享最大周期 - 它将标记响应为public
        $response->setSharedMaxAge(600);
 
        return $response;
    }
}

本例中,全页缓存是10分钟的周期。接下来,在模板中内嵌action来包容新闻播报。这可以通过renderhelper来实现。(参考Embedding Controller以了解细节)

当嵌入的内容从另外的页面(或者是控制器)过来之后,Symfony使用标准的render helper来配置ESI标签:

1
2
3
4
5
6
7
8
{# app/Resources/views/static/about.html.twig #}
 
{# you can use a controller reference #}
{# 在此引用一个控制器 #}
{{ render_esi(controller('AppBundle:News:latest', { 'maxPerPage': 5 })) }}
 
{# ... or a URL 或者是一个URL #}
{{ render_esi(url('latest_news', { 'maxPerPage': 5 })) }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/Resources/views/static/about.html.php -->
 
<!-- you can use a controller reference -->
<?php echo $view['actions']->render(
    new \Symfony\Component\HttpKernel\Controller\ControllerReference(
        'AppBundle:News:latest',
        array('maxPerPage' => 5)
    ),
    array('strategy' => 'esi')
) ?>
 
<!-- ... or a URL -->
<?php echo $view['actions']->render(
    $view['router']->url('latest_news', array('maxPerPage' => 5)),
    array('strategy' => 'esi'),
) ?>

通过使用esi render(借助render_esi Twig函数),你告之Symfony这个action应当作为一个ESI标记来渲染。你可能好奇为何要使用helper而不是自己书写ESI标记。这是因为,使用helper可以令你的程序正常运行,哪怕并gateway cache根本没有被安装。

下面你将会看到,你传入的maxPerPage变量是可以作为你的控制器参数来使用的(也就是$maxPerPage)。被render_esi传入的变量,也将成为缓存的键的一部分,以便你能够为“变量-值”组合设置独立的缓存。

当使用默认的render函数(或者设置renderer为inline)时,Symfony在发送响应到客户端之前,会合并被包容的页面内容到主要页面中。但是如果你使用esi renderer(也就是调用renderer_esi),并且Symfony检测到它要调用支持ESI的gateway cache的话,程序会生成一个ESI包容标签。但如果没有gateway cache或者缓存不支持ESI,Symfony将直接合并页面内容到主要页面,就像你使用render时的程序行为一样。

Symfony之所以能够检测到网关缓存是否支持ESI,全靠另外一个被Symfony反向代理支持得极好的的Akamai协议。

内嵌的action,现在已经可以指定他的缓存规则了,完全独立于宿主页面之外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Controller/NewsController.php
namespace AppBundle\Controller;
 
// ...
class NewsController extends Controller
{
    public function latestAction($maxPerPage)
    {
        // ...
        $response->setSharedMaxAge(60);
 
        return $response;
    }
}

在ESI的加持下,全页缓存是600秒的有效期,但是新闻播送部分的缓存,将仅持续60秒。

当(在模板中)使用控制器的引用时,ESI标签会引用这个内嵌action作为一个可以访问的URL,这样的话,网关缓存才能够把它单独地从页面中取出,而有别于其他内容。Symfony负责为任何一个controller reference生成独立URL,并且能够正确发送路由,多亏了FragmentListener,这个监听必须在你的配置文件中开启:

1
2
3
4
# app/config/config.yml
framework:
    # ...
    fragments: { path: /_fragment }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://symfony.com/schema/dic/framework"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
 
    <!-- ... -->
    <framework:config>
        <framework:fragments path="/_fragment" />
    </framework:config>
</container>
1
2
3
4
5
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'fragments' => array('path' => '/_fragment'),
));

ESI renderer的一个特别先进之处在于,你可以令程序尽可能的动态化,同时还能尽可能少地(让请求)命中程序。

片段监听(fragment listener )只对签名请求负责。而请求只在使用fragment renderer和render_esi Twig函数时才会被签名。

一旦你使用ESI,记得始终要用s-maxage指令来代替max-age。因为浏览器只接收聚合资源(aggregated resource),它并不对sub-component子组件保持认知,因此它并不遵守max-age指令,进而缓存了整个页面。显然你不希望那样。

render_esi helper还支持以下两个常用选项:

alt

在ESI标记中使用alt属性,你就可以指定一个可选URL,它将在src找不到时被使用。


ignore_errors

如果设为true,一个onerror属性会被添加到ESI标记中,该属性的值是continue,用于指示在failure事件中,gateway cache静默移除ESI标记。