AMP 全称 Accelerated Mobile Pages ,是由 Google 提出的一种移动端页面的规范。相比普通 HTML 而言,最大的特点是对页面中可用元素进行了严格的限制,以确保高性能。此外,Google 还对 AMP 页面提供了高速缓存,如果从 Google 搜索中打开 AMP 页面,速度非常快,几乎是秒开。

前几天将我的博客完全迁移到了 AMP,在此也做一个记录。

作为单独页面存在的 AMP

在很长一段时间内,我都以为 AMP 只适用于移动端某些特殊场景,至少文章页应该是适用的,于是自然而然就想到了博客应该是非常适合 AMP 的场景。因为我的博客使用的是著名的静态博客程序 Hexo,所以也就很自然地想到了,会不会有人已经写好了 Hexo 的 AMP 插件?搜了一下,果然没有失望,找到了一个名为 hexo-generator-amp的插件。

这个插件不会修改已有的文章页面,而是会为文章页面再生成一个合法的 AMP 页面。这两个页面可以互相引用,表示一个是普通页面,一个是 AMP 页面。这种方式也是 Google 搜索等平台认可的 AMP 生成方式。

按照插件的文档,使用起来还是比较简单的,具体的方式就不写了,直接参考文档即可。有几个值得注意的点:

  1. 一定要修改文章页的模板,添加link[rel=amphtml]链接,要不然搜索无法找到 AMP 页面
  2. AMP 页面会自动在head中添加canonical链接回原有的文章页

这个插件支持自己修改模板。鉴于我对它的样式并不是很满意,没有修改的欲望,于是也就没有去改它。如果你需要修改 AMP 模板的话,可以在网址中添加#development=1,然后在控制台中查看 AMP 验证结果。

最后效果:

Google搜索显示AMP

从 Google 搜索的时候能看到明显的“AMP”和闪电标记

AMP使用Google域名缓存

打开后就能看到这个插件提供的默认模板长什么样。

另外可以看到,从搜索中直接点击时,访问的域名是 Google 的,这便是经常被说到的,Google 对 AMP 页面提供了高速缓存。按照 Google 的说法,这种状态下,页面的逻辑仍然会运行,此时页面既是在 Google 那里的,也是在网站作者的控制下的。

hexo-generator-amp默认样式

这是插件默认模板的 footer,可以说是相当大,而且个人不太喜欢。但不管怎么说,我的博客有 AMP 页面了,并且能在 Google 搜索结果中被标识出来,点击时还能秒开,这确实是一种非常不错的使用体验。

全站 AMP

后来我看到了《澄清对AMP的十个误解》这篇文章,才知道原来 AMP 既不是移动端的专属应用,也不是能力非常受限的技术。因此萌生了将博客整站都改成 AMP 的想法。

首先,既然全站都要改成 AMP 了,那么为文章页再单独生成一个 AMP 页面就显得不必要了,因此我去掉了上面说的 hexo-generator-amp 插件,而使用完全手工修改的方式来改造。

1. 照葫芦画瓢

前面说过,当在地址栏中加上#development=1时,浏览器控制台就会出现 AMP 的验证信息。于是我想着那就先打开这个验证信息,然后跟着错误一个一个改吧,于是也就直接访问了http://localhost:4000/#development=1,结果控制台空空如也。通过查询文档,才知道原来要打开 AMP 验证,至少还是得做一点前置工作的,最起码你得让浏览器知道“我是打算变成 AMP 的,请按 AMP 的标准来要求我”。于是按照文档一一照做:

  • Doctype 是必须要有:无需改动
  • 包含一个顶层的<html ⚡>或者<html amp>:打开 Hexo 模板,给html加上闪电属性
  • 包含<head><body>:无需改动
  • <head>的第一个子节点是<meta charset="utf-8">:无需改动
  • <head>的第二个子节点是<script async src="https://cdn.ampproject.org/v0.js"></script>:照做
  • <head>中通过<link rel="canonical" href="$SOME_URL">指向非 AMP 版本的页面,如果只有 AMP 版本则指向自身:照做,指向自身
    1
    link(rel="canonical",href=url_for(page.path))
  • <head>中包含<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">:无需改动
  • 包含一段 AMP 必须有的代码:照做,注意这里不要对代码进行格式化,按原样一行复制下来就好,否则会验证不通过

做完回头看一下,其实改动并不是很多。然后再次刷新浏览器,就能看到验证信息了:

AMP验证截图

在图上,我们可以看到大概有这样几个问题:

  • 使用link[rel-stylesheet]加载了一个 CDN 域名上的字体
  • script 标签不允许(3次)
  • imgsrc属性缺失
  • img不允许出现,可能需要amp-img

对这些问题一一进行解释和解决。

2. 样式表

第一个问题,它说我加载了一个 CDN 域名上的字体,这是怎么回事呢?同样,首先查看文档,发现文档上说,只允许加载以下几个域名的自定义字体:

但下面也说,在自定义 CSS 中,可以使用@font-face来引用字体,这种方式不受域名限制。

我的博客主题确实用到了自定义字体,而且确实是在我自己的 CSS 中定义的,而我的字体托管域名并不是上面那几个域名,所以肯定是无法加载了。于是我将字体的定义从CSS中移到了<head>中:

1
2
3
4
5
6
7
8
9
<style>
@font-face {
font-family: 'sourcesanspro';
src: url('//toobug.s.f2er.info/font/sourcesanspro.woff2') format('woff2'),
url('//toobug.s.f2er.info/font/sourcesanspro.woff') format('woff');
font-weight: normal;
font-style: normal;
}
</style>

满心希望地刷新了一下,结果发现这个错误依然存在。在花了三十分钟百思不得其解之后,我终于想到了去翻一下 AMP 关于样式的说明,文档中明确说:“AMP pages can’t include external stylesheets, with the exception of custom fonts”,即 AMP 页面中不允许加载外部样式,除了自定义字体。

再明确一下,AMP 中不允许使用 CSS 样式表加载样式。唯一可以用 CSS 来加载的只有字体,而用来加载字体的 CSS 必须是上面的三个网址之一。

那么解决方式就简单了,使用<style amp-custom>将样式文件内联进来即可,具体到 jade 模板中,只要include一下就好:

1
2
style(amp-custom)
include ../../source/css/apollo.css

3. 脚本

AMP 页面中不允许以我们熟悉的方式引入脚本,或者也可以先简单理解为不允许使用脚本。而我的页面上使用了 3 个脚本:

  1. 用于将 HTTP 访问跳转到 HTTPS 的脚本
  2. 用于图片 lazyload 的脚本
  3. Google Analytics 统计脚本

第 1 个有一定历史原因,因为最早将博客托管在 Gitlab.com 上,不支持自动 HTTPS 跳转,所以只能使用脚本。现在使用了自己的服务器,可以直接使用301跳转,并支持 HSTS,所以这个脚本直接去掉即可。

第 2 个是自己写的一个简单的图片 lazyload 的脚本,即构建时将imgsrc属性换成data-src,然后在图片滚动到当前视野中时再加载。因为 AMP 并不支持img,且amp-img有 lazyload 的特性,所以直接去掉。(关于图片的问题下文详述。)

第 3 个,GA 统计的脚本,AMP 有官方的组件可以支持,通过查看文档,只要先引入amp-analytics组件脚本,然后将 GA 的代码替换掉就可以解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 在head区 AMP 脚本之前引入 -->
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>

<!-- 将统计脚本替换成如下代码 -->
<amp-analytics type="googleanalytics" id="UA-XXXXXXXX">
<script type="application/json">
{
"vars": {
"account": "UA-XXXXXXXX"
},
"triggers": {
"trackPageview": {
"on": "visible",
"request": "pageview"
}
}
}
</script>
</amp-analytics>

至此,脚本的问题解决了。

4. 图片

AMP 有一个很大的特点,就是强调页面的静态布局。

举个例子,当浏览器加载一张图片时,如果图片没有被显式指定宽高,此时图片的占位大小是不确定的,因此浏览器会先对后面的内容进行排版,等图片加载完之后再回来重新计算图片占的位置,此时就会造成页面布局的变化。

而 AMP 强调页面布局应该是确定的,因此不允许像上面这样的页面布局变化存在。针对图片,AMP 中需要使用<amp-img>元素来替代<img>,并且强制要求一定要指定图片的布局方式和宽高。

首先第一张要处理的图是博客顶部的 Logo,直接在模板中将它修改成<amp-img>

1
2
a.logo-link(href=url_for())
amp-img(layout="fixed",width="60",height="60",src=theme.logo)

这里我使用了layout="fixed",表示这张图片的大小是固定的。

接下来要处理的文章正文中的图片,我利用了 Hexo 主题的勾子(Script)特性。按官方文档的说法,只要在与source同级的目录创建一个scripts目录,里面的脚本就会被执行。但是我是将scripts目录放到了主题的根目录下,同样会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var imageSize = require('image-size');
var path = require('path');

hexo.extend.filter.register('after_render:html', (source) => {
return source.replace(/<img src="(.+?)"/g, function(str, src){
var imagePath = path.join(process.cwd(), 'source' , src);
var size = imageSize(imagePath);
if(!size){
size = {
width: 800,
height: 500
};
}
return `<amp-img layout="responsive" width="${size.width}" height="${size.height}" src="//toobug.s.f2er.info${src}"`;
});
});

这段代码注册了一个勾子,在渲染 HTML 之后执行,所以可以对 HTML 中的<img>进行替换。

因为<amp-img>要求必须指定宽高,因此使用了image-size这个 npm 模块来获取图片宽高。最后将<img>替换成<amp-img>即可。因为是文章正文中的图片,layout指定了responsive,这样就可以在不同宽度下自适应。最后我在替换的时候顺手加了上 CDN 的前缀。

至此,首页就已经改造完成了,刷新一下,看到错误信息已经没有了。

5. 评论框

进入文章详情页,仍然会有一个使用了脚本的提示,这是因为引入了 disqus 评论组件。

1
2
3
4
5
6
7
8
9
var disqus_shortname = '#{theme.disqus}';
var disqus_identifier = '#{page.path}';
var disqus_title = '#{page.title}';
var disqus_url = '#{config.url}/#{page.path}';
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();

首先动用搜索,看看 disqus 官方是否向 GA 那样有提供官方组件。结论是……没有。但是,找到一篇官方的 AMP 页面下使用指南

事实上,这篇指南写得并不是十分清楚,并且照做的话会有一些错误信息,也不能正常显示和发布评论。在尝试好几次并做了反复修改之后,才终于弄懂它的含义。它的大致原理是使用amp-iframe组件,将评论放在一个独立的 iframe 中去。这是利用了 AMP 的规则,虽然 AMP 页面不允许有脚本存在,但是可以通过amp-iframe来包含页面,将脚本放到这个单独的页面中即可。

第一步,我们要准备一下这个被嵌入的页面,它将包含主要的 disqus 相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!-- 用于显示评论框和评论列表的容器 -->
<div id="disqus_thread"></div>
<script>
// 监听disqus组件传递的消息
// 其中有一个是`resize`,是disqus用于告诉当前页面
// “我加载完了,我的尺寸是XXX”
window.addEventListener('message', receiveMessage, false);
function receiveMessage(event)
{
if (event.data) {
var msg;
try {
msg = JSON.parse(event.data);
} catch (err) {
// Do nothing
}
if (!msg)
return false;

if (msg.name === 'resize') {
// 向 AMP 页面发送消息,要求重新设置 amp-iframe的高度
window.parent.postMessage({
sentinel: 'amp',
type: 'embed-size',
height: msg.data.height
}, '*');
}
}
}
</script>
<script>
// 用于解析url参数
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
// 通过url参数获取当前 AMP 页面对应的文章相关参数
// 并赋值给page变量,稍后disqus将读取page变量
var disqus_config = function () {
this.page.title = decodeURIComponent(getQueryVariable("title"));
this.page.url = decodeURIComponent(getQueryVariable("url"));
this.page.identifier = decodeURIComponent(getQueryVariable("identifier"));
};

// 引入disqus脚本
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');

s.src = '//toobug.disqus.com/embed.js';

s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>

我已经在代码中标上了注释,这里面有几个关键点需要理解:

  1. 这个页面将会被amp-iframe引用,因此 AMP 页面是父页面,本页面是指上面的这段代码所在页面
  2. disqus 的脚本在加载后,会将一个新的 iframe 写到容器#disqus_thread
  3. disqus 会从新的 iframe 中发消息,告知本页面自己的状态,其中一种消息是尺寸
  4. disqus 需要知道 AMP 页面对应的 url、标题等信息,我们通过本页面的location.search获取
  5. amp-iframe 可以接受本页面的消息,动态设置高度

第二步,需要将这个页面放到一个单独的域名上,不能和 AMP 页面同一个域名。刚好我使用了一个 CDN ,有独立的域名,因此下面就直接用 CDN 的域名进行引入。

第三步,使用amp-iframe将这个页面引入,并且在 url 中传入文章的相关参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<amp-iframe
width="600"
height="140"
layout="responsive"
sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"
resizable
src="https://toobug.s.f2er.info/amp/disqus/toobug.html?title=#{page.title}&url=#{config.url}/#{page.path}&identifier=#{page.path}">
<div
overflow
tabindex=0
role=button
aria-label="Disqus Comments"
>Disqus Comments</div>
</amp-iframe>

值得注意的点:

  1. layoutresponsive以便自适应宽度
  2. sandbox需要写明权限,否则可能导致评论无法操作
  3. 要有resizable属性,这样会接受 iframe 中页面的消息,重新计算高度
  4. 要有div[overflow]子元素,否则会有报错“Overflow element must be defined for resizable frames”

这样就完成了 disqus 评论的改造。

amp-iframe加载中

图:amp-iframe加载中

disqus加载中

图:disqus加载中

disqus评论

图:disqus评论正常显示

回顾一下完整的原理:

  • 使用amp-iframe来包含评论的逻辑
  • 接受 disqus 的消息,如果是高度变更了,那么向 AMP 页面发送一个消息,要求重新计算高度
  • 通过amp-iframesrc参数来指定disqus相关的参数

改造完之后发现一个疑似 AMP 的 bug:在移动端 Chrome 上访问时,评论框显示不完整,也就是说,高度调整并没有完成。按照文档中的说法,说这个高度调整不一定是立即完成,AMP 会判断需要调整的时候再做调整。估计是这个判断什么时候进行调整有 bug 。如果选中文字往下拖到底,则有一定机率高度会变正常。目前尚未找到更好的解决方案。

以上就是本博客的改造过程。将博客改造成全站 AMP 并不是很困难。一方面是因为基本上没有什么逻辑,另一方面以文章为主的站点非常符合 AMP 的定位。

如果你的站点逻辑非常多,或者并不是以文章、资讯为主的站点,可能还需要再考量一下是否有必要。

最后再附一张改造之后的 Google AMP 缓存访问的图:

Google AMP 缓存

hexo-generator-amp生成的样式顺眼多啦。