ESI边缘包含 #

一、ESI概述 #

1.1 什么是ESI #

ESI (Edge Side Includes) 是一种标记语言,允许在边缘(CDN/代理服务器)组装网页片段。通过ESI,可以将页面拆分为多个独立缓存的片段。

1.2 ESI工作原理 #

text
┌─────────────────────────────────────────────────────────┐
│                    ESI工作原理                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   请求 ──► Varnish ──► 主页面(缓存)                      │
│                    │                                    │
│                    ├──► 片段1(缓存) ──┐                  │
│                    │                  │                  │
│                    ├──► 片段2(缓存) ──┼──► 组装响应      │
│                    │                  │                  │
│                    └──► 片段3(缓存) ──┘                  │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.3 ESI优势 #

优势 说明
片段缓存 不同片段独立缓存,提高命中率
个性化 静态部分长缓存,动态部分短缓存
性能提升 减少后端请求,降低响应时间
灵活组装 动态组合页面内容

二、ESI语法 #

2.1 基本include标签 #

html
<esi:include src="/fragment/header"/>

2.2 包含属性 #

html
<!-- 基本包含 -->
<esi:include src="/fragment/header"/>

<!-- 指定alt备用URL -->
<esi:include src="/fragment/header" alt="/fragment/header-fallback"/>

<!-- 超时处理 -->
<esi:include src="/fragment/header" timeout="5"/>

<!-- 继续处理 -->
<esi:include src="/fragment/header" onerror="continue"/>

2.3 注释标签 #

html
<esi:comment text="This is a comment"/>

2.4 移除标签 #

html
<esi:remove>
    <!-- ESI不可用时的替代内容 -->
    <div>Header content</div>
</esi:remove>

2.5 条件处理 #

html
<esi:choose>
    <esi:when test="$(HTTP_USER_AGENT) =~ /Mobile/">
        <esi:include src="/fragment/mobile-header"/>
    </esi:when>
    <esi:otherwise>
        <esi:include src="/fragment/desktop-header"/>
    </esi:otherwise>
</esi:choose>

2.6 变量使用 #

html
<!-- 使用HTTP头变量 -->
<esi:include src="/fragment/user?user_id=$(HTTP_COOKIE{user_id})"/>

<!-- 使用查询参数 -->
<esi:include src="/fragment/page?lang=$(QUERY_STRING{lang})"/>

三、Varnish ESI配置 #

3.1 启用ESI #

vcl
sub vcl_backend_response {
    # 启用ESI处理
    set beresp.do_esi = true;
    
    # 或基于条件启用
    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        set beresp.do_esi = true;
        unset beresp.http.Surrogate-Control;
    }
}

3.2 基于Content-Type启用 #

vcl
sub vcl_backend_response {
    # HTML页面启用ESI
    if (beresp.http.Content-Type ~ "text/html") {
        set beresp.do_esi = true;
    }
}

3.3 基于URL启用 #

vcl
sub vcl_backend_response {
    # 特定路径启用ESI
    if (bereq.url ~ "^/pages/" || bereq.url ~ "^/articles/") {
        set beresp.do_esi = true;
    }
}

四、ESI页面示例 #

4.1 完整页面示例 #

html
<!DOCTYPE html>
<html>
<head>
    <title>ESI Example</title>
</head>
<body>
    <!-- 头部片段 - 全站共享,长缓存 -->
    <esi:include src="/fragments/header"/>
    
    <!-- 导航片段 - 基于用户状态 -->
    <esi:include src="/fragments/navigation"/>
    
    <!-- 主内容 - 页面特定,中等缓存 -->
    <main>
        <esi:include src="/fragments/article/123"/>
    </main>
    
    <!-- 侧边栏 - 个性化内容 -->
    <aside>
        <esi:include src="/fragments/recommendations"/>
    </aside>
    
    <!-- 页脚片段 - 全站共享,长缓存 -->
    <esi:include src="/fragments/footer"/>
</body>
</html>

4.2 片段响应 #

Header片段 (/fragments/header):

html
<header>
    <div class="logo">My Website</div>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <a href="/contact">Contact</a>
    </nav>
</header>

Navigation片段 (/fragments/navigation):

html
<nav class="user-nav">
    <esi:choose>
        <esi:when test="$(HTTP_COOKIE{logged_in}) == 'true'">
            <a href="/profile">Profile</a>
            <a href="/logout">Logout</a>
        </esi:when>
        <esi:otherwise>
            <a href="/login">Login</a>
            <a href="/register">Register</a>
        </esi:otherwise>
    </esi:choose>
</nav>

五、ESI缓存策略 #

5.1 不同片段不同TTL #

vcl
sub vcl_backend_response {
    # 主页面
    if (bereq.url == "/") {
        set beresp.ttl = 5m;
        set beresp.do_esi = true;
    }
    
    # 头部片段 - 长缓存
    if (bereq.url ~ "^/fragments/header") {
        set beresp.ttl = 1d;
    }
    
    # 导航片段 - 短缓存(用户相关)
    if (bereq.url ~ "^/fragments/navigation") {
        set beresp.ttl = 1m;
    }
    
    # 内容片段 - 中等缓存
    if (bereq.url ~ "^/fragments/article") {
        set beresp.ttl = 1h;
    }
    
    # 推荐片段 - 极短缓存(个性化)
    if (bereq.url ~ "^/fragments/recommendations") {
        set beresp.ttl = 10s;
    }
    
    # 页脚片段 - 长缓存
    if (bereq.url ~ "^/fragments/footer") {
        set beresp.ttl = 1d;
    }
}

5.2 片段缓存键 #

vcl
sub vcl_hash {
    hash_data(req.url);
    
    # 导航片段基于登录状态
    if (req.url ~ "^/fragments/navigation") {
        if (req.http.Cookie ~ "logged_in=true") {
            hash_data("logged_in");
        } else {
            hash_data("anonymous");
        }
    }
    
    # 推荐片段基于用户ID
    if (req.url ~ "^/fragments/recommendations") {
        if (req.http.Cookie ~ "user_id") {
            hash_data(regsub(req.http.Cookie, ".*user_id=([^;]+).*", "\1"));
        }
    }
    
    return (lookup);
}

六、ESI高级用法 #

6.1 嵌套ESI #

html
<!-- 主页面 -->
<esi:include src="/fragments/layout"/>

<!-- layout片段 -->
<div class="layout">
    <esi:include src="/fragments/sidebar"/>
    <esi:include src="/fragments/content"/>
</div>

6.2 动态ESI #

vcl
# 后端返回动态ESI标签
# 后端可以根据条件生成不同的ESI include

6.3 ESI与AJAX结合 #

html
<!-- ESI作为降级方案 -->
<esi:include src="/fragments/weather"/>
<esi:remove>
    <!-- ESI不可用时使用AJAX加载 -->
    <div id="weather" data-src="/api/weather"></div>
    <script>
        fetch(document.getElementById('weather').dataset.src)
            .then(r => r.text())
            .then(html => document.getElementById('weather').innerHTML = html);
    </script>
</esi:remove>

七、ESI调试 #

7.1 查看ESI处理 #

bash
# 查看ESI处理日志
varnishlog -q "ESI"

# 查看片段请求
varnishlog -q "ReqURL ~ /fragments/"

7.2 测试ESI #

bash
# 测试主页面
curl -v http://localhost:6081/

# 测试片段
curl -v http://localhost:6081/fragments/header

7.3 ESI错误处理 #

vcl
sub vcl_backend_response {
    # ESI片段错误处理
    if (bereq.is_bgfetch && beresp.status >= 500) {
        # 后台获取失败,返回错误片段
        set beresp.status = 200;
        synthetic({"<esi:comment text="ESI fragment failed"/>"});
    }
}

八、ESI最佳实践 #

8.1 页面拆分原则 #

片段类型 缓存策略 更新频率
头部/页脚 长缓存 很少更新
导航 短缓存 偶尔更新
主内容 中等缓存 经常更新
个性化 极短缓存 实时更新

8.2 性能优化 #

vcl
sub vcl_backend_response {
    # ESI片段并行获取
    # Varnish自动并行处理ESI include
    
    # 限制ESI嵌套深度
    if (bereq.esi_level > 5) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
    }
}

8.3 错误处理 #

html
<!-- 使用alt属性提供备用 -->
<esi:include src="/fragments/header" alt="/fragments/header-simple" onerror="continue"/>

<!-- 使用esi:remove提供降级 -->
<esi:include src="/fragments/weather"/>
<esi:remove>
    <div>Weather unavailable</div>
</esi:remove>

九、完整配置示例 #

vcl
vcl 4.1;

import std;

# 后端定义
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

# 请求处理
sub vcl_recv {
    # 片段请求处理
    if (req.url ~ "^/fragments/") {
        # 移除不必要的Cookie
        if (req.url ~ "^/fragments/(header|footer)") {
            unset req.http.Cookie;
        }
        return (hash);
    }
    
    return (hash);
}

# 缓存键
sub vcl_hash {
    hash_data(req.url);
    
    # 导航片段基于登录状态
    if (req.url ~ "^/fragments/navigation") {
        if (req.http.Cookie ~ "logged_in=true") {
            hash_data("logged_in");
        }
    }
    
    # 推荐片段基于用户
    if (req.url ~ "^/fragments/recommendations") {
        if (req.http.Cookie ~ "user_id") {
            hash_data(regsub(req.http.Cookie, ".*user_id=([^;]+).*", "\1"));
        }
    }
    
    return (lookup);
}

# 后端响应处理
sub vcl_backend_response {
    # 主页面启用ESI
    if (beresp.http.Content-Type ~ "text/html" && 
        bereq.url !~ "^/fragments/") {
        set beresp.do_esi = true;
        set beresp.ttl = 5m;
    }
    
    # 片段缓存策略
    if (bereq.url ~ "^/fragments/header") {
        set beresp.ttl = 1d;
    } elseif (bereq.url ~ "^/fragments/footer") {
        set beresp.ttl = 1d;
    } elseif (bereq.url ~ "^/fragments/navigation") {
        set beresp.ttl = 1m;
    } elseif (bereq.url ~ "^/fragments/article") {
        set beresp.ttl = 1h;
    } elseif (bereq.url ~ "^/fragments/recommendations") {
        set beresp.ttl = 10s;
    }
    
    # 设置Grace
    set beresp.grace = 1h;
}

# 响应处理
sub vcl_deliver {
    # 添加ESI处理标记
    if (obj.esi > 0) {
        set resp.http.X-ESI = "processed";
    }
}

十、ESI局限性 #

10.1 不支持的功能 #

  • 不支持ESI全部规范
  • 不支持esi:inline
  • 不支持复杂的条件表达式
  • 不支持POST请求中的ESI

10.2 替代方案 #

方案 说明
AJAX 客户端动态加载片段
Server-Side Include 服务器端包含
微前端 前端微服务架构

十一、总结 #

本章我们学习了:

  1. ESI概述:概念、工作原理、优势
  2. ESI语法:include、comment、remove、choose
  3. Varnish配置:启用ESI、条件启用
  4. 页面示例:完整页面、片段响应
  5. 缓存策略:不同片段不同TTL
  6. 高级用法:嵌套ESI、动态ESI
  7. ESI调试:日志、测试、错误处理
  8. 最佳实践:拆分原则、性能优化

掌握ESI后,让我们进入下一章,学习健康检查!

最后更新:2026-03-28