前段時間對公司的社區h5網站,進行改版(整站重寫)。老版本的網站是在一套古老的php框架下開發的,包含不少模板文件,大部分頁面都是後端模板渲染,前端開發時要與後端溝通模板邏輯的編寫,先後端耦合嚴重,很是不利於開發。爲了實現先後端分離,減輕服務端的渲染壓力,咱們決定使用目前流行Vue框架,進行前端頁面組件化開發,使用前端路由,後端只提供數據接口和必要的模板變量渲染。
但這樣一來,網站的SEO就成爲不得不考慮的重要問題之一,本文就是對咱們實際開發中SEO解決方案的一個總結,介紹爲何要作SEO,客戶端渲染應用的SEO解決方案,以及咱們採用的方案。javascript
對於通常的功能性h5單頁應用,由於其入口或使用場景的緣由,使其對SEO並不敏感,例如微信下的滴滴打車。但對於社區類應用,經過搜索引擎搜索對應的帖子是基本的需求。所以在進行前期的技術方案調研時,咱們首先考慮的是如何作網頁的SEO。
對於服務端渲染的頁面,因爲頁面的HTML結構直接由後端吐出,自然對搜索引擎支持良好,考慮更多的是如何讓網站搜索排名更靠前。而對於頁面由前端渲染,HTML結構是js動態生成的網站,因爲搜索引擎目前並不支持js渲染內容的抓取,因此如何給搜索引擎爬蟲提供收錄的內容,成爲要考慮的首要問題。php
常見的單頁應用中,頁面的切換是經過URL中的哈希(#)來實現的,hash值得變化並不會發起瀏覽器請求,經過監聽hashChnage事件,來實現前端的路由切換。對於這種應用中,搜索引擎很難抓取不一樣頁面的內容,並且頁面的渲染大多也是ajax異步獲取數據後進行渲染,更加不利於SEO。爲此,Google提供了一套針對這種類型的網站開發者的SEO解決方案。
方案規定:html
網站提交sitemap給Google;前端
Google發現URL裏有#!符號,例如example.com/#!/detail/1,因而Google開始抓取example.com/?_escaped_fragment_=/detail/1;vue
_escaped_fragment_這個參數是Google指定的命名,若是開發者但願把網站內容提交給Google,就必須經過這個參數生成靜態頁面。
這種方案本質上是爲搜索引擎提供單獨頁面,以供爬蟲收錄。java
目前流行的前端路由庫,大可能是使用了HTML5 History API,經過這種方式,使得前端hash跳轉一樣可以很好的記錄歷史,兼容瀏覽器的前進後退按鈕,提供良好的用戶體驗。同時也都提供history模式,例如vue-router:ios
const router = new VueRouter({ mode: 'history', routes: routes });
這種模式下,加上服務端的配合,可以使前端路由更加接近後端路由,提供更加友好的url,
例如: http://domain.com/user/tom 等價於 非history模式下的http://domain.com/#/user/tomajax
至於如何設置服務端,能夠參看vue router教程history-mode;vue-router
由於網頁的的地址發生了變化,瀏覽器會發起請求,但因爲服務端設置,其實訪問的仍是同一個資源。這種模式下,其實SEO就可使用咱們下面介紹的方案。vuex
這個方案是阮一峯的一篇文章如何讓搜索引擎抓取AJAX內容?裏提到的,也是咱們最終採用的方案。
這個方案的主要思想是:
利用History api 實現前端路由跳轉
經過服務端配置,支持不帶#號的URL(這個可酌情考慮,是否有必要)
經過服務端將頁面主要內容渲染近<noscript>標籤,供搜索爬蟲抓取
這種模式下,不只使頁面更好的被搜索引擎收錄,同時使網站在禁用javascript的時候,也可以瀏覽基本的帖子內容。
咱們使用了第二種方案,來作網站的SEO。
後端提供了一套機制來將頁面的主要內容渲染進模板,供搜索引擎收錄。首次渲染以後,若是是用戶正常訪問頁面,後續的翻頁實際上是ajax請求接口,獲取數據後渲染進頁面。若是是爬蟲或者禁用js的狀況下,頁面經過noscript提供收錄內容和渲染頁面。
先來看咱們列表頁的結構:
<body> <div id="app"></div> <noscript> <!--板塊列表--> <div class="item"> <?php if (isset($data_seo['forums'])): ?> <?php foreach ($data_seo['forums'] as $key => $value): ?> <div title="<?=$value['group']?>" class="item"> <h1 title="<?=$value['group']?>"><?=$value['group']?></h1> <div> <?php foreach ($value['list'] as $_k => $_v): ?> <a title="魅族社區板塊<?=$_v['name']?>" href="<?=$_v['url']?>"><?=$_v['name']?></a> <?php endforeach ?> </div> </div> <?php endforeach ?> <?php endif ?> </div> <!--熱門推薦列表--> <?php if (isset($data_seo['list'])): ?> <div> <?php foreach ((array)$data_seo['list'] as $key => $value): ?> <a href="<?=$value['url']?>" title="<?=$value['subject']?>" target="_blank" class="item"> <h1><?=$value['subject']?></h1> <div class="info"> <div class="author"> <span title="做者"><?=$value['author']?></span> <img src="<?=$value['avatar']?>" title="<?=$value['author']?>的頭像" alt="<?=$value['author']?>" /> </div> <div class="view"> <span title="回覆數"><?=$value['replies']?></span> <span title="瀏覽數"><?=$value['views']?></span> </div> </div> <!--圖片搜索--> <div class="image"> <img src="<?=$value['pic']?>" title="<?=$value['subject']?>" alt="<?=$value['subject']?>" /> </div> </a> <?php endforeach ?> </div> <?php endif ?> <?=isset($data_pager) ? $data_pager : ''?> </noscript> <!-- built files will be auto injected --> </body>
在禁用js(爬蟲訪問時),獲得的dom結構以下圖
這樣瀏覽器即便禁用了js,依然可以顯示出網站的關鍵內容,而頁面上的網址也是爬蟲繼續收錄的入口。
其實,上面的方案在首屏渲染的時候,已經包含了頁面所需的數據,而這些數據是能夠被js渲染頁面時所利用的,將首屏數據渲染進js變量,就能夠減小首屏渲染的http請求。
例如,咱們將首屏的列表數據,渲染進全局變量,對應的地址: https://domain/forum-22-1.htm...
<script type="text/javascript"> var data_index_list = <?=isset($data_index_list) ? $data_index_list : 0?>; var data_current_page = <?=isset($data_current_page) ? $data_current_page : 0?>; </script>
而後在vuex獲取列表數據時,咱們就能夠判斷,若是當前頁面前端路由的頁面和後端的當前頁面是同一個,就直接在data_thread_list 取數據:
[actions.FETCH_FORUM_LIST]({commit, state}, params) { commit(actions.FETCH_FORUM_LIST_PENDING); if (window.data_current_page === params.page) { // 若是當前前端路由的頁面和後端的當前頁面是同一個,就直接在data_thread_list 取數據 let forumlistData = window.data_thread_list.data; commit(actions.FETCH_FORUM_LIST_SUCCESS, forumlistData); return; } axios.get() // ajax請求獲取頁面數據。 }
這樣一來,當頁面首次渲染時,咱們就不須要發起任何ajax請求: