Angular Prerender SEO實踐

來源:互聯網
上載者:User

標籤:

前置0

angular.js好用, 但是有一點不好的就是, 對於SEO不友好, 因為angular更適合於SPA單頁面應用. 這樣的話, 所有的html都是使用angular動態產生的. 因此搜尋引擎就沒有辦法對整個網站進行索引.

對於這個問題, 我看了一篇文章javascript SEO. 看了這篇文章後, 對於使用angular的SEO, 有了一個簡單的瞭解. 並且看到了線上已經在啟動並執行一個網站http://answers.gethuman.com/, 知道按照文章中說的是完全可以既對搜尋引擎友好, 同時又能完全發揮angular的優勢, 來構建一個單頁面應用的.

經過和部落格作者的郵件溝通, 瞭解了一些具體的細節, 同時我也想通過一個例子進行實驗一下. 所以自己進行了一番嘗試, 在嘗試的過程中, 自然遇到了一些問題. 經過一步步的尋找並解決, 現在對於angular單頁面應用的SEO問題有了一個大體的瞭解, 因此在這裡記錄一下.

過程1 - 實現後端Prerender

實現這個思路應該不是太難, 我的做法是, 在後端使用ejs進行渲染, 在前端就是angular本身的渲染了. 這樣雖然會存在兩套模板, 但是其實成本並不大, 經過後面的說明就能明白.

對於資料來源, 我的做法是, 在後端有一個資料擷取層, 一個API層. 在前端就是angular的擷取資料層.

  1. 後端的資料擷取層, 只負責擷取資料的邏輯部分, 輸出的是結構化的資料.
  2. 後端的API層, 對上面的資料擷取層, 進行json或者jsonp的封裝, 返回給前端.
  3. 前端angular的資料擷取, 通過2中的API層進行資料擷取.

渲染流程為:

  1. 後端ejs部分, 直接通過後端的資料擷取層, 拿到資料進行渲染.
  2. 前端的angular部分, 則通過後端的API層擷取資料, 進行前端渲染.

由於後端的API層, 只是對資料進行簡單的json或jsonp封裝, 因此, 前後端拿到的資料實際上是一樣的. 這樣就能保證, 前後端兩套模板的邏輯是一樣的, 只是ejs和angular模板文法的一些簡單差異, 比如迴圈, if判斷等等. 只需要拿其中一套模板, 然後將文法變成另外一種即可, 所以對於維護的成本, 個人感覺並不是太大.

過程2 - 前端angular的渲染問題

前端如果要使用angular進行資料繫結, 使用者互動等操作, 就需要讓angular接管頁面的全部或部分. 由於這裡我是完全使用angular + angular-uirouter, 因此這裡就是接管全部頁面了.

但是這裡有一個問題.

如果將後端渲染的內容填充在ui-view中, angular渲染頁面時需要的資料是在頁面載入完成後, 通過介面擷取的, 這個過程有等待, 但是angular在渲染之前就會把ui-view之間的內容全部清理掉, 就會造成剛進入頁面是正常的, 然後頁面突然空白一段時間(此時進行中資料擷取), 然後再次載入的問題.

如果將後端渲染的內容單獨放到頁面的一個部分中, 這部分內容是不受angular控制的. 同時, angular也會渲染一份相同的模板, 造成模板重複的問題.

所以為瞭解決這個問題, 我進行了一個小hack.

我把整個頁面的結構寫成這樣

<body ng-controller="topCtrl">    <div ui-view ng-hide="initLoad"></div>    <div ng-if="initLoad"><!-- 這裡是後端模板渲染的部分. -->    </div></body>

js部分寫成這樣

angular.module(‘demo‘, [‘ngResource‘, ‘ui.router‘]).config([‘$stateProvider‘, function($stateProvider){    $stateProvider.state(‘state1‘, {        url: ‘/state1/:param1‘,        templateUrl: ‘tpl/template.html‘,        controller: ‘demoCtrl‘    })}])factory(‘Resource1‘, [‘$resource‘, function($resource){    return $resource(‘/api/:param1‘, {        param1: ‘@param1‘    });}]).controller(‘topCtrl‘, [‘$scope‘, ‘$rootScope‘, function($scope, $rootScope){    // initLoad確定第一次載入頁面時, angular不會把後端載入的頁面清掉.    // 當頁面載入後, 設定initLoad為false, 當下一次進行angualr操作時,    // 就可以自動將後端渲染的東西清理掉.    var initLoad = $scope.initLoad = true;    $scope.markInit = function(){        // 如果是首次載入, 此處只是將標記更新一下, 然後直接返回,        // 當下次再執行此方法時, 就需要使用angular渲染ui-view來替換後端渲染的模板        if(initLoad){            initLoad = false;            return;        }        // 當$scope.initLoad的值變為false後, angular就會自動把後端渲染的模板清理掉.        // 然後展示使用ui-view渲染的前端模板        $scope.initLoad = false;    };    $rootScope.$on(‘$stateChangeStart‘, function(){        $scope.markInit();    });}]).controller(‘demoCtrl‘, [‘$scope‘, ‘Resource1‘, ‘$stateParams‘, function($scope, Resource1, $stateParams){    Resource1.query({        param1: $stateParams.param1    }).$promise.then(function(data){        $scope.data = data;    })    // ...}])

實現思路是, 讓ui-view部分先隱藏起來, 只顯示後端渲染部分. 當前端進行了一些操作, 需要跳轉到ui-view的其它狀態時, 再把服務端渲染的html去掉.

重點部分是topCtrl中的initLoad這個東西. 我們先把這個變數設為true或false,來保證ui-view部分是隱藏或顯示.

在angular和uirouter初始化頁面的時候, $rootScope會觸發$stateChangeStart這個事件, 我們就利用這個事件來知道, 當前展示的頁面是否是從服務端渲染來的, 還是後來由angular渲染來的.

第一次觸發這個的時候, 是angular在進行首次渲染, 不應該把$scope.initLoad設為true, 所以我們只是把initLoad這個臨時變數設為false, $scope.initLoad仍然為true.

當下一次再觸發的時候, 首先檢查initLoad這個變數, 此時為false, 證明不是首次載入了, 所以需要將$scope.initLoad設為false. 一旦$scope.initLoad變成false後, ng-if就會起作用, 將後端渲染的模板清理掉, 同時, 將angular渲染的模板展示出來.

這樣, 過程2開頭說到的問題基本就解決了.

過程3 - 保證首次載入後, 使用者互動仍然可用.

過程2中只是做到後端渲染模板與前端渲染模板不衝突, 但是還無法解決一個問題. 如何保證在首次載入的後端模板不清理的情況下, 正確響應使用者的click dblclick這些操作呢? 這些部分可是不在ui-view的controller控制之下的.

解決辦法, 利用$scope的繼承特性.

整個代碼修改為下面這樣.

angular.module(‘demo‘, [‘ngResource‘, ‘ui.router‘]).config([‘$stateProvider‘, function($stateProvider){    $stateProvider.state(‘state1‘, {        url: ‘/state1‘,        templateUrl: ‘tpl/template.html‘,        controller: ‘demoCtrl‘    })}])factory(‘Resource1‘, [‘$resource‘, function($resource){    return $resource(‘/api/:param1‘, {        param1: ‘@param1‘    });}]).controller(‘topCtrl‘, [‘$scope‘, ‘$rootScope‘, function($scope, $rootScope){    // initLoad確定第一次載入頁面時, angular不會把後端載入的頁面清掉.    // 當頁面載入後, 設定initLoad為false, 當下一次進行angualr操作時,    // 就可以自動將後端渲染的東西清理掉.    var initLoad = $scope.initLoad = true;    $scope.markInit = function(){        // 如果是首次載入, 此處只是將標記更新一下, 然後直接返回,        // 當下次再執行此方法時, 就需要使用angular渲染ui-view來替換後端渲染的模板        if(initLoad){            initLoad = false;            return;        }        // 當$scope.initLoad的值變為false後, angular就會自動把後端渲染的模板清理掉.        // 然後展示使用ui-view渲染的前端模板        $scope.initLoad = false;    };    $scope.addMethod = function(evtName, func){        // 此處的this指向的是ui-view對應的controller中的$scope        this[evtName] = func;        $scope[evtName] = func;    };    $rootScope.$on(‘$stateChangeStart‘, function(){        $scope.markInit();    });}]).controller(‘demoCtrl‘, [‘$scope‘, ‘Resource1‘, ‘$stateParams‘, function($scope, Resource1, $stateParams){    $scope.addMethod(‘clickImg‘, function(){        alert(‘click img‘);    });    Resource1.query({        param1: $stateParams.param1    }).$promise.then(function(data){        $scope.data = data;    })    // ...}])

這樣, 假如, 後端渲染部分如下

<div ng-if="initLoad"><!-- 這裡是後端模板渲染的部分. -->    <img src=""  on-click="clickImg()"></div>

這樣修改之後, ui-view的controller添加一個方法後, 上層的topCtrl就能添加同樣的方法, 就能正確響應使用者的操作了.

只是, 這種修改方法有一個不好的地方. 如果我先寫一個前端模板, 然後轉換成ejs模板的文法, 就需要決定, 哪些angular文法需要轉換, 哪些angular文法需要保留, 以便能夠正確響應使用者操作.

當然, 為了能夠達到既使用angular, 又對SEO友好的最終目的, 這一切都不是問題.

過程4 - ngCloak

基本問題解決了, 那就寫一個頁面吧. 此時的頁面可以後端prerender, 首次進入頁面後, 也沒有頁面閃動現象, 還能夠正確響應使用者的一些操作, 看上去一切似乎都是perfect. 但是, 還是有很多問題.

頁面閃動, 這裡的頁面閃動, 是後續的操作中的頁面閃動, 從一個ui-view的state轉換到另一個state的時候, 就像前面說的, angular會把頁面的內容全部清理掉, 然後再進行渲染. 而不是, 等一切渲染就緒之後, 再把頁面上的內容清掉.

使用angular ui-view flicker關鍵詞進行搜尋後, 發現了使用ng-cloak進行解決的方法, 但是我實驗之後, 基本沒有效果. 因為, ng-cloak的本質是一個class類, 在渲染的過程中, 是display:none狀態, 當渲染完畢後,把這個class去掉.

看來, 這個東西, 並不能解決我說的問題, 既, 先清理頁面內容, 然後再進行渲染. 由於渲染過程, 需要到伺服器端擷取資料,所以這個過程中, 整個頁面就是白的.

過程5 - ui-router的resolve

又經過的一番搜尋, 搜尋到了ui-router中的一個東西, resolve, 通過文檔可以看到, 這個東西, 是為了保證, ui-view對應的controller初始化時, 所有依賴的東西都已經載入完畢.

文檔如下

You can use resolve to provide your controller with content or data that is custom to the state. resolve is an optional map of dependencies which should be injected into the controller.

If any of these dependencies are promises, they will be resolved and converted to a value before the controller is instantiated and the $routeChangeSuccess event is fired.

因此, 我把整個js代碼修改成這樣

angular.module(‘demo‘, [‘ngResource‘, ‘ui.router‘]).config([‘$stateProvider‘, function($stateProvider){    $stateProvider.state(‘state1‘, {        url: ‘/state1‘,        templateUrl: ‘tpl/template.html‘,        controller: ‘demoCtrl‘,        resolve: {            // 在這裡進行resource1Data的擷取工作            resource1Data: [‘Resource1‘, ‘$stateParams‘, function(Resource1, $stateParams){                return Resource1.query({                    param1: $stateParams.param1                }).$promise;            }]        }    })}])factory(‘Resource1‘, [‘$resource‘, function($resource){    return $resource(‘/api/:param1‘, {        param1: ‘@param1‘    });}]).controller(‘topCtrl‘, [‘$scope‘, ‘$rootScope‘, function($scope, $rootScope){    // initLoad確定第一次載入頁面時, angular不會把後端載入的頁面清掉.    // 當頁面載入後, 設定initLoad為false, 當下一次進行angualr操作時,    // 就可以自動將後端渲染的東西清理掉.    var initLoad = $scope.initLoad = true;    $scope.markInit = function(){        // 如果是首次載入, 此處只是將標記更新一下, 然後直接返回,        // 當下次再執行此方法時, 就需要使用angular渲染ui-view來替換後端渲染的模板        if(initLoad){            initLoad = false;            return;        }        // 當$scope.initLoad的值變為false後, angular就會自動把後端渲染的模板清理掉.        // 然後展示使用ui-view渲染的前端模板        $scope.initLoad = false;    };    $scope.addMethod = function(evtName, func){        this[evtName] = func;        $scope[evtName] = func;    };    $rootScope.$on(‘$stateChangeStart‘, function(){        $scope.markInit();    });}]).controller(‘demoCtrl‘, [‘$scope‘, ‘resource1Data‘, function($scope, resource1Data){    // 這是不再注入Resource1以及$stateParams, 而是直接注入resolve中定義的resource1Data    $scope.addMethod(‘clickImg‘, function(){        alert(‘click img‘);    });    $scope.data = resource1Data;    // ...}])

經過以上修改, 就能保證, 當頁面切換時, 會先去擷取ui-view對應的controller需要的所有注入項, 等所有的注入項都已經是resolve狀態時, 再進行controller的初始化工作. 這樣, 頁面閃動的問題就解決了.

過程6 - 完美方案

通過上面的resolve方案, 既然能夠解決後續頁面之間切換時的頁面閃動問題, 那是否可以解決頁面首次載入時的頁面閃動問題呢? 因為首頁載入的頁面冷卻也是由於resource去擷取資料造成的.

所以, 實驗一下, html代碼修改為下面這樣

<body>    <div ui-view>        <!-- 這裡是後端模板渲染的部分. -->    </div></body>

js代碼修改為如下

angular.module(‘demo‘, [‘ngResource‘, ‘ui.router‘]).config([‘$stateProvider‘, function($stateProvider){    $stateProvider.state(‘state1‘, {        url: ‘/state1‘,        templateUrl: ‘tpl/template.html‘,        controller: ‘demoCtrl‘,        resolve: {            // 在這裡進行resource1Data的擷取工作            resource1Data: [‘Resource1‘, ‘$stateParams‘, function(Resource1, $stateParams){                return Resource1.query({                    param1: $stateParams.param1                }).$promise;            }]        }    })}]).factory(‘Resource1‘, [‘$resource‘, function($resource){    return $resource(‘/api/:param1‘, {        param1: ‘@param1‘    });}]).controller(‘demoCtrl‘, [‘$scope‘, ‘resource1Data‘, function($scope, resource1Data){    $scope.clickImg = function(){        alert(‘click img‘);    }    $scope.data = resource1Data;    // ...}])

經過實驗, 首頁載入時的頁面閃動問題也可以解決. 通過上面的方法, 也不需要topCtrl, 因為頁面載入後, angular也會再次渲染, 但是這裡的渲染過程不會出現頁面閃動, 使用者幾乎察覺不到整個頁面由後端模板向前端模板的過渡過程. 對於後端模板正確響應使用者操作的hack, 同樣也能去除.

以上就是我為了實現angular prerender SEO進行的一些研究, 以及為了達到一些目標而進行的hack, 並且一步步探索, 並尋找更優方案的過程. 雖然有些地方寫起來看著挺簡單, 好像一筆帶過的樣子, 但是其中的思考確實不太容易.

  »   本文原創地址:http://ISay.me/2014/06/angular-prerender-seo-and-use-resolve-for-page-flicker.html

Angular Prerender SEO實踐

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.