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 | 服务器端包含 |
| 微前端 | 前端微服务架构 |
十一、总结 #
本章我们学习了:
- ESI概述:概念、工作原理、优势
- ESI语法:include、comment、remove、choose
- Varnish配置:启用ESI、条件启用
- 页面示例:完整页面、片段响应
- 缓存策略:不同片段不同TTL
- 高级用法:嵌套ESI、动态ESI
- ESI调试:日志、测试、错误处理
- 最佳实践:拆分原则、性能优化
掌握ESI后,让我们进入下一章,学习健康检查!
最后更新:2026-03-28