輕鬆遷移博客到AMP

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 的,這便是經常被說到的,Google 對 AMP 頁面提供了高速緩存。按照 Google 的說法,這種狀態下,頁面的邏輯仍然會運行,此時頁面既是在 Google 那裡的,也是在網站作者的控制下的。

這是插件默認模板的 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="cdn.ampproject.org/v0.j"></script>:照做
  • 在<head>中通過<link rel="canonical" href="$SOME_URL">指向非 AMP 版本的頁面,如果只有 AMP 版本則指向自身:照做,指向自身

link(rel="canonical",href=url_for(page.path))

  • 在<head>中包含<meta name="viewport" content="width_=device-width,minimum-scale=1,initial-scale=1">:無需改動
  • 包含一段 AMP 必須有的代碼:照做,注意這裡不要對代碼進行格式化,按原樣一行複製下來就好,否則會驗證不通過

做完回頭看一下,其實改動並不是很多。然後再次刷新瀏覽器,就能看到驗證信息了:

在圖上,我們可以看到大概有這樣幾個問題:

  • 使用link[rel-stylesheet]載入了一個 CDN 域名上的字體
  • script 標籤不允許(3次)
  • img的src屬性缺失
  • img不允許出現,可能需要amp-img

對這些問題一一進行解釋和解決。

2. 樣式表

第一個問題,它說我載入了一個 CDN 域名上的字體,這是怎麼回事呢?同樣,首先查看文檔,發現文檔上說,只允許載入以下幾個域名的自定義字體:

  • https://fast.fonts.net
  • https://fonts.googleapis.com
  • https://maxcdn.bootstrapcdn.com

但下面也說,在自定義 CSS 中,可以使用@font-face來引用字體,這種方式不受域名限制。

我的博客主題確實用到了自定義字體,而且確實是在我自己的 CSS 中定義的,而我的字體託管域名並不是上面那幾個域名,所以肯定是無法載入了。於是我將字體的定義從CSS中移到了<head>中:

<style>n@font-face {n font-family: sourcesanspro;n src: url(//toobug.s.f2er.info/font/sourcesanspro.woff2) format(woff2),n url(//toobug.s.f2er.info/font/sourcesanspro.woff) format(woff);n font-weight: normal;n font-style: normal;n}n</style>n

滿心希望地刷新了一下,結果發現這個錯誤依然存在。在花了三十分鐘百思不得其解之後,我終於想到了去翻一下 AMP 關於樣式的說明,文檔中明確說:「AMP pages can』t include external stylesheets, with the exception of custom fonts」,即 AMP 頁面中不允許載入外部樣式,除了自定義字體。

再明確一下,AMP 中不允許使用 CSS 樣式表載入樣式。唯一可以用 CSS 來載入的只有字體,而用來載入字體的 CSS 必須是上面的三個網址之一。

那麼解決方式就簡單了,使用<style amp-custom>將樣式文件內聯進來即可,具體到 jade 模板中,只要include一下就好:

style(amp-custom)n include ../../source/css/apollo.cssn

3. 腳本

AMP 頁面中不允許以我們熟悉的方式引入腳本,或者也可以先簡單理解為不允許使用腳本。而我的頁面上使用了 3 個腳本:

  1. 用於將 HTTP 訪問跳轉到 HTTPS 的腳本
  2. 用於圖片 lazyload 的腳本
  3. Google Analytics 統計腳本

第 1 個有一定歷史原因,因為最早將博客託管在 Gitlab.com 上,不支持自動 HTTPS 跳轉,所以只能使用腳本。現在使用了自己的伺服器,可以直接使用301跳轉,並支持 HSTS,所以這個腳本直接去掉即可。

第 2 個是自己寫的一個簡單的圖片 lazyload 的腳本,即構建時將img的src屬性換成data-src,然後在圖片滾動到當前視野中時再載入。因為 AMP 並不支持img,且amp-img有 lazyload 的特性,所以直接去掉。(關於圖片的問題下文詳述。)

第 3 個,GA 統計的腳本,AMP 有官方的組件可以支持,通過查看文檔,只要先引入amp-analytics組件腳本,然後將 GA 的代碼替換掉就可以解決:

<!-- 在head區 AMP 腳本之前引入 -->n<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>nn<!-- 將統計腳本替換成如下代碼 -->n<amp-analytics type="googleanalytics" id="UA-XXXXXXXX">n<script type="application/json">n {n "vars": {n "account": "UA-XXXXXXXX"n },n "triggers": {n "trackPageview": {n "on": "visible",n "request": "pageview"n }n }n }n</script>n</amp-analytics>n

至此,腳本的問題解決了。

4. 圖片

AMP 有一個很大的特點,就是強調頁面的靜態布局。

舉個例子,當瀏覽器載入一張圖片時,如果圖片沒有被顯式指定寬高,此時圖片的佔位大小是不確定的,因此瀏覽器會先對後面的內容進行排版,等圖片載入完之後再回來重新計算圖片占的位置,此時就會造成頁面布局的變化。

而 AMP 強調頁面布局應該是確定的,因此不允許像上面這樣的頁面布局變化存在。針對圖片,AMP 中需要使用<amp-img>元素來替代<img>,並且強制要求一定要指定圖片的布局方式和寬高。

首先第一張要處理的圖是博客頂部的 Logo,直接在模板中將它修改成<amp-img>:

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

這裡我使用了layout="fixed",表示這張圖片的大小是固定的。

接下來要處理的文章正文中的圖片,我利用了 Hexo 主題的勾子(Script)特性。按官方文檔的說法,只要在與source同級的目錄創建一個scripts目錄,裡面的腳本就會被執行。但是我是將scripts目錄放到了主題的根目錄下,同樣會被執行。

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

這段代碼註冊了一個勾子,在渲染 HTML 之後執行,所以可以對 HTML 中的<img>進行替換。

因為<amp-img>要求必須指定寬高,因此使用了image-size這個 npm 模塊來獲取圖片寬高。最後將<img>替換成<amp-img>即可。因為是文章正文中的圖片,layout指定了responsive,這樣就可以在不同寬度下自適應。最後我在替換的時候順手加了上 CDN 的前綴。

至此,首頁就已經改造完成了,刷新一下,看到錯誤信息已經沒有了。

5. 評論框

進入文章詳情頁,仍然會有一個使用了腳本的提示,這是因為引入了 disqus 評論組件。

var disqus_shortname = #{theme.disqus};nvar disqus_identifier = #{page.path};nvar disqus_title = #{page.title};nvar disqus_url = #{config.url}/#{page.path};n(function() {nvar dsq = document.createElement(script); dsq.type = text/javascript; dsq.async = true;n dsq.src = // + disqus_shortname + .disqus.com/embed.js;n (document.getElementsByTagName(head)[0] || document.getElementsByTagName(body)[0]).appendChild(dsq);n})();n

首先動用搜索,看看 disqus 官方是否向 GA 那樣有提供官方組件。結論是……沒有。但是,找到一篇官方的 AMP 頁面下使用指南。

事實上,這篇指南寫得並不是十分清楚,並且照做的話會有一些錯誤信息,也不能正常顯示和發布評論。在嘗試好幾次並做了反覆修改之後,才終於弄懂它的含義。它的大致原理是使用amp-iframe組件,將評論放在一個獨立的 iframe 中去。這是利用了 AMP 的規則,雖然 AMP 頁面不允許有腳本存在,但是可以通過amp-iframe來包含頁面,將腳本放到這個單獨的頁面中即可。

第一步,我們要準備一下這個被嵌入的頁面,它將包含主要的 disqus 相關的代碼:

<!-- 用於顯示評論框和評論列表的容器 -->n<div id="disqus_thread"></div>n<script>n// 監聽disqus組件傳遞的消息n// 其中有一個是`resize`,是disqus用於告訴當前頁面n// 「我載入完了,我的尺寸是XXX」nwindow.addEventListener(message, receiveMessage, false);nfunction receiveMessage(event)n{n if (event.data) {n var msg;n try {n msg = JSON.parse(event.data);n } catch (err) {n // Do nothingn }n if (!msg)n return false;nn if (msg.name === resize) {n // 向 AMP 頁面發送消息,要求重新設置 amp-iframe的高度n window.parent.postMessage({n sentinel: amp,n type: embed-size,n height: msg.data.heightn }, *);n }n }n}n</script>n<script>n// 用於解析url參數nfunction getQueryVariable(variable) {n var query = window.location.search.substring(1);n var vars = query.split("&");n for (var i=0;i<vars.length;i++) {n var pair = vars[i].split("=");n if(pair[0] == variable){return pair[1];}n }n return(false);n}n// 通過url參數獲取當前 AMP 頁面對應的文章相關參數n// 並賦值給page變數,稍後disqus將讀取page變數nvar disqus_config = function () {n this.page.title = decodeURIComponent(getQueryVariable("title"));n this.page.url = decodeURIComponent(getQueryVariable("url"));n this.page.identifier = decodeURIComponent(getQueryVariable("identifier"));n};nn// 引入disqus腳本n(function() { // DONT EDIT BELOW THIS LINEn var d = document, s = d.createElement(script);nn s.src = //toobug.disqus.com/embed.js;nn s.setAttribute(data-timestamp, +new Date());n (d.head || d.body).appendChild(s);n })();n</script>n

我已經在代碼中標上了注釋,這裡面有幾個關鍵點需要理解:

  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 中傳入文章的相關參數:

<amp-iframen width_="600"n height="140"n layout="responsive"n sandbox="allow-scripts allow-same-origin allow-modals allow-popups allow-forms"n resizablen src="https://toobug.s.f2er.info/amp/disqus/toobug.html?title=#{page.title}&url=#{config.url}/#{page.path}&identifier=#{page.path}">n <divn overflown tabindex=0n role=buttonn aria-label="Disqus Comments"n >Disqus Comments</div>n</amp-iframe>n

值得注意的點:

  1. layout寫responsive以便自適應寬度
  2. sandbox需要寫明許可權,否則可能導致評論無法操作
  3. 要有resizable屬性,這樣會接受 iframe 中頁面的消息,重新計算高度
  4. 要有div[overflow]子元素,否則會有報錯「Overflow element must be defined for resizable frames」

這樣就完成了 disqus 評論的改造。

圖:amp-iframe載入中

圖:disqus載入中

圖:disqus評論正常顯示

回顧一下完整的原理:

  • 使用amp-iframe來包含評論的邏輯
  • 接受 disqus 的消息,如果是高度變更了,那麼向 AMP 頁面發送一個消息,要求重新計算高度
  • 通過amp-iframe的src參數來指定disqus相關的參數

改造完之後發現一個疑似 AMP 的 bug:在移動端 Chrome 上訪問時,評論框顯示不完整,也就是說,高度調整並沒有完成。按照文檔中的說法,說這個高度調整不一定是立即完成,AMP 會判斷需要調整的時候再做調整。估計是這個判斷什麼時候進行調整有 bug 。如果選中文字往下拖到底,則有一定機率高度會變正常。目前尚未找到更好的解決方案。

以上就是本博客的改造過程。將博客改造成全站 AMP 並不是很困難。一方面是因為基本上沒有什麼邏輯,另一方面以文章為主的站點非常符合 AMP 的定位。

如果你的站點邏輯非常多,或者並不是以文章、資訊為主的站點,可能還需要再考量一下是否有必要。

最後再附一張改造之後的 Google AMP 緩存訪問的圖:

比hexo-generator-amp生成的樣式順眼多啦。

P.S. 發布這篇文章的時候發現沒有「AMP」這個標籤,可見這門技術好冷門……

打個廣告:歡迎關注我的公眾號「一分菜地」

推薦閱讀:

Google+ 會對 Google 搜索結果有哪些影響?
Google Year In Search 2017 視頻回顧了哪些年度事件?

TAG:前端开发 | 前端性能优化 | Google搜索 |