上一篇文章里,我记录了一个看起来很反直觉的问题:
A Tour of Go 课程页面明明存在大片视觉空白,Google AdSense 的 Auto Ads 也已经启用,adsbygoogle.js 同样正常加载,但普通的页面展示广告始终没有出现。
进一步查看页面结构后,我逐渐把问题收敛到了 A Tour of Go 本身比较特殊的 DOM 和布局方式上。
这时候,一个很自然的想法出现了:
既然现有 SPA 页面结构不利于广告展示,要不要干脆放弃 SPA,把每一节课程改成传统的独立页面?
单纯从 AdSense 的角度看,这个方案其实相当有吸引力。
但真正开始评估以后,我发现问题远没有“页面刷不刷新”这么简单。
最终决定保留 SPA,最大的原因甚至不是页面切换速度,而是:
自己的多语言版本还需要长期跟踪和同步官方 A Tour of Go 上游。如果为了广告把页面架构改成另一套实现,短期得到的是方便,长期增加的却可能是一笔持续累积的维护成本。
A Tour of Go 的“下一页”本来就不是整页刷新
首先需要确认一件最基本的事情:
官方 A Tour of Go 本身就是 SPA 式的课程导航。
为了确认这一点,我打开:
https://go.dev/tour/welcome/1然后在浏览器开发者工具的 Network 中只保留 HTML 请求,再点击“下一页”。
页面很快进入:
https://go.dev/tour/welcome/2课程正文也已经完全切换。
但是 Network 中没有出现新的 Document / HTML 请求。

这意味着,“下一页”并不是:
浏览器请求 /tour/welcome/2
↓
服务器返回完整 HTML
↓
浏览器重新加载页面而更接近:
现有页面
↓
Angular 处理导航
↓
切换课程数据与视图
↓
URL 更新
↓
继续使用当前 Document从用户体验来看,这种方式非常自然。
课程切换快,不需要每次重新加载整个站点。
代码编辑器、课程目录以及其他前端状态也都处于同一个应用生命周期中。
但这恰好也给 AdSense 带来了麻烦。
对普通网站很自然的广告生命周期,在 SPA 中并不存在
对于传统多页面网站,广告的逻辑其实很简单。
用户打开第一篇文章:
GET /article/1
↓
完整 HTML
↓
AdSense 初始化
↓
广告请求点击下一篇:
GET /article/2
↓
新的完整 HTML
↓
AdSense 再初始化
↓
新的广告请求每次页面加载,本身就构成了一次天然的生命周期边界。
但是 SPA 并不是这样。
在 A Tour of Go 中:
/tour/basics/1
↓
点击下一页
↓
/tour/basics/2虽然用户已经认为自己进入了另一个页面,但浏览器中的那个 Document 并没有重新创建。
这也就意味着:
不能简单期待 AdSense 像传统网站那样,在每次点击“下一页”以后自然重新开始一次完整页面广告流程。
所以,当时摆在面前的大致有两条路线。
路线一:放弃 SPA
第一条路线最直接。
把现在的课程导航改成真正的页面跳转:
/tour/basics/1
↓
整页加载
↓
/tour/basics/2
↓
整页加载
↓
/tour/basics/3这样做以后,至少有几个问题会明显简单。
AdSense 更简单
每一次课程切换都是一次新的页面访问。
广告脚本、页面内容和广告请求,都可以跟着页面重新加载。
不再需要额外考虑:
SPA 路由切换以后,应该在什么时候销毁旧广告、什么时候创建新广告?
统计更简单
Google Analytics、百度统计之类的统计系统面对传统页面也更加自然。
每次新 Document 就是一条新的页面访问。
SPA 下则需要主动处理虚拟 pageview。
SEO 理论上也更直观
每一个 URL 都由服务器返回自己的完整 HTML:
/tour/basics/1
/tour/basics/2
/tour/methods/1搜索引擎不需要等待客户端 JavaScript 执行以后才能看到真正的课程内容。
从这些角度看,放弃 SPA 确实很诱人。
如果这个项目只是一个我完全从头实现的网站,也许真的会认真考虑这么做。
但 A Tour of Go 多语言项目还有另一个非常重要的约束。
这个项目不是一套完全独立的 Tour
我的目标一直不是重新实现一个“A Tour of Go 类似网站”。
而是在官方 A Tour of Go 基础上做持续维护的多语言版本。
官方当前的课程内容主要位于:
golang/website/_content/tour/里面包括:
basics/
concurrency/
flowcontrol/
generics/
methods/
moretypes/
welcome/
static/
template/
basics.article
concurrency.article
flowcontrol.article
...
_content/tour/。课程内容、静态资源和模板都在这个上游目录中持续维护。这张截图里还有一个很值得注意的细节:
一些上游课程文件刚刚在几天内发生过修改。
例如可以看到:
yesterday
5 days ago
last week也就是说,A Tour of Go 并不是一份已经冻结、以后再也不会改变的源码。
上游还会继续修改:
- 课程正文;
- 示例代码;
- 模板;
- 静态资源;
- 前端行为;
- Go 版本相关内容。
这时候,“自己把 SPA 改成传统多页面”就不再只是一个一次性开发工作了。
它会变成:
从此以后,每一次同步 upstream,都要考虑自己的分叉架构还能不能继续兼容。
我的项目甚至正式记录 upstream revision
这种上游关系也不是口头上的“偶尔参考”。
go-tour-i18n 本身就正式记录了 upstream baseline。
项目状态中会保存类似:
golang/website master@645042eb...当上游发生变化以后,还会经过自己的同步流程更新这个 baseline。
production bundle 同样会在 release.json 中写入:
upstream_commit=645042eb...
这意味着对于这个项目来说:
upstream revision
↓
source 同步
↓
TranslationUnit 状态
↓
重新翻译 / 审核(如需要)
↓
build
↓
publish
↓
production本身就是长期工程流程的一部分。
所以我最终评估 SPA 问题时,最关心的已经不是:
改成普通页面难不难?
而是:
几年以后,我还愿不愿意一直维护这个与 upstream 不同的版本?
两种路线重新放到一起比较
当时最终的判断,大致可以整理成下面这个表格。
| 维度 | 保留 SPA | 放弃 SPA |
|---|---|---|
| 与上游架构接近程度 | 高 | 明显降低 |
| 后续同步 upstream | 改动相对集中 | 需要持续适配自己的页面架构 |
| “下一页”体验 | 原生 SPA 快速切换 | 每页整页刷新 |
| 编辑器与课程交互 | 更接近上游 | 需要重新确认状态和初始化行为 |
| AdSense | 需要主动管理 SPA 广告生命周期 | 每次页面加载天然重新执行 |
| 页面统计 | 需要处理 SPA pageview | 普通 pageview 更自然 |
| SEO | 需要额外解决客户端渲染问题 | 服务器独立页面更直接 |
| 初期开发 | 广告和 SEO 处理更复杂 | 看起来更直接 |
| 长期维护 | 与 upstream 差异较小 | 自定义差异持续累积 |
如果只看其中某一行:
AdSense那我很可能会选择放弃 SPA。
但如果把最后一行:
长期维护也放进去,结论就开始发生变化。
一次性复杂度和长期复杂度不是一回事
这里其实涉及一个我越来越重视的工程问题。
有些方案看起来“简单”,只是因为它把复杂度推迟到了以后。
比如放弃 SPA。
第一次实现的时候,也许只需要:
修改下一页链接
↓
服务器直接返回对应页面
↓
让浏览器重新加载看起来很简单。
但是以后上游一旦修改 Tour 的:
- 路由;
- Angular controller;
- editor 生命周期;
- lesson 数据结构;
- template;
- navigation;
- Playground 行为;
自己的非 SPA 实现就需要逐项判断:
这个修改还能不能直接同步?
哪一部分已经不能用了?
是否又需要为自己的架构重新实现一次?
随着时间推移,两套代码之间的距离只会越来越大。
这就是所谓的维护分叉。
而另一条路线虽然一开始麻烦:
保留 SPA,然后单独解决 SPA 广告生命周期。
但它的复杂度主要集中在自己新增的广告层。
理想状态可以是:
上游 Tour
↓
尽量保持原结构
+
自己的 locale 层
+
自己的广告生命周期层而不是:
上游 Tour
↓
持续转换
↓
自己的另一套页面框架这两种架构在项目维护几年以后,差异会非常大。
广告应该适应项目,而不是项目适应广告
这也是最终让我决定保留 SPA 的核心原因。
Google AdSense 对这个项目来说很重要。
它关系到站点后续能不能覆盖一部分服务器、域名和维护成本。
但是广告终究还是附加能力。
A Tour of Go 这个项目最核心的东西仍然是:
- Go 官方课程内容;
- 与上游保持同步;
- 多语言翻译;
- Playground;
- 课程交互;
- 长期可维护性。
如果为了广告方便,把这些基础架构大幅修改,优先级其实反了。
所以最后确定的原则变成了:
不为了 AdSense 放弃 SPA。
进一步来说:
尽量让广告实现适配 A Tour of Go,而不是让 A Tour of Go 为广告重新设计。
但保留 SPA 并不会自动解决广告问题
当然,做出这个决定并不意味着问题解决了。
恰恰相反。
选择保留 SPA,相当于主动接受了接下来必须解决的问题:
用户进入课程页
↓
显示一个广告
↓
点击“下一页”
↓
Document 不刷新
↓
旧广告怎么办?
↓
新课程是否需要新的广告?
↓
什么时候重新请求?
↓
怎样避免广告节点不断累积?而且还有一个更加基础的问题:
广告到底应该插在哪里?
原来的 Auto Ads 连页面上那么大的视觉空白都没有使用。
如果由应用自己明确提供广告位,又应该放到:
- 左侧正文下面?
- 右侧编辑器下面?
- 两栏外部?
- footer 上面?
- footer 下面?
Google AdSense 后台的页面预览,后来恰好给了一个很有意思的答案。
它确实找到了一处“可以放广告”的地方。
但那个位置却让我意识到:
Google 认为可以放的位置,不一定就是我希望用户看到广告的位置。
于是问题又进入了下一阶段:
既然决定保留 SPA,课程广告到底应该放在哪里,又应该由谁管理它的生命周期?
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 在线学习:A Tour of Go 简体中文版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前第一阶段已完成简体中文版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。

