Angular Prerender SEO实践

前导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

时间: 2024-10-12 17:50:27

Angular Prerender SEO实践的相关文章

苏州土豆seo实践课程

注册注博客目的:册和操作,学习博客的基本操作技能,理解程序和模板的关系.模仿知名博客,设置栏目.发布文章.做锚文本.写标题等.每个内容页面,都有随机文章.相关文章这样的功能.理解二级域名.目标关键词.长尾关键词的含义.绝大部分SEO的操作和思路,都可以在上面实现,你可以优化一些简单的关键词,比如:男/女朋友的名字.这个博客,之后可以作为你的主站的外链资源. 原文地址:http://blog.51cto.com/13887413/2150121

基于angularJs的单页面应用seo优化及可抓取方案原理分析

公司使用angularJs(以下都是指ng1)框架做了互联网应用,之前没接触过seo,突然一天运营那边传来任务:要给网站做搜索引擎优化,需要研发支持.搜了下发现单页面应用做seo比较费劲,国内相关实践资料分享出来的也比较少,略懵,前后花了一番功夫总算完成了.在这里记录下来,做一个总结,也希望能够帮助在做类似工作的朋友少走一点弯路.还是建议需要seo的网站技术选型尽量不要使用angular react一类的单页面框架.如果你和我一样网站做完了发现需要seo,那么往下看吧.如果各位已有更优的方案欢迎

2015前端各大框架比较(angular,vue,react,ant)

前端流行框架大比拼 angular vue react ant-design angularjs angular是个MVVM的框架.针对的是MVVM这整个事.angular的最主要的场景就是单页应用,或是有大量数据绑定的情况. 特性 双向数据绑定 ioc依赖注入 指令 上面这几点用起来确实很爽,随便指定个区域,配一个controller,然后里面的东西就都在scrope里了,确实很方便 如果各位想看,参见 https://github.com/i5ting/ionic_ninja/blob/ma

从搜索引擎角度看SEO

前段时间google发布了官方seo指南.这个文档中包含了适用于google的最佳seo实践策略. 一切都要以用户体验为中心.google表面的立场是:seo要最终回归到用户体验.是的,用户体验,我们经常听到这个词,但恐怕没有多少人真正理解为什么google把用户体验适用于一切事情. google在搜索市场占据统治地位的原因是人们喜欢google提供的搜索结果,比如,最近发布的一项研究显示,google搜索结果页是第三大最可信的在线信息来源.如果google不再让人们对他们的搜索结果感到满意,也

罪恶的SEO优化

1. 基础概念开始 SEO,搜索引擎优化.概括来说就是针对分析搜索引擎的网站收录以及评价规律,来对网站的结构,内容以及其他因素作出一些合理调整,使得网站更容易被搜索引擎收录,并且能够尽量排在搜索引擎自然排名的前列. 在上段中提到了自然排名这个概念,是因为在搜索引擎中,还有着另外一个部分,在常规意义上,我们称之为"右侧广告".不过我们在查看时发现这个概念已经不够准确,因为百度的广告已经不只在右侧.如图: 而google在这里则体现了技术型公司和商业型公司的差别,google只有右侧的广告

论 Angular的混乱

关于angular,一直都有一种云里雾里的感觉,我想很多开发者尤其是搞过后端的程序员,对于angular中的scope controller service 都有很多特别的感触,那就是一个字 乱 关于angular的最佳实践,社区曾给出不少提议,然而我个人觉得,这些提议本身就指出了angular在一些概念上的模糊之处,JavaScript本身就是一个灵活性极大的语言,作为架构之上的框架,一点不清晰将导致很多显而易见的问题 1.scope 到底是什么 modal?state?viewModal?

Angular 快速学习笔记(1)

创建组件 ng generate component heroes {{ hero.name }} {{}}语法绑定数据 管道pipe 格式化数据 <h2>{{ hero.name | uppercase }} Details</h2> [(ngModel)] 双向绑定,form需要引入FormsModule AppModule 放置元数据(metadata) a. @NgModule 装饰器 imports导入外部模块 b. declarations 放置组件 @NgModule

成为优秀Angular开发者所需要学习的19件事

一款to-do app基本等同于前端开发的"Hello world".虽然涵盖了创建应用程序的CRUD方面,但它通常只涉及那些框架或库也能做到的皮毛而已. Angular看起来似乎总是在改变和更新 - 但实际上,还是有一些事情仍然保持不变.以下是关于Angular所需要学习的核心概念的概述,以便大家可以正确地利用JavaScript框架. 说到Angular,我们需要学习很多东西,很多人被困在初学者的圈子里,仅仅是因为不知道去哪里搜索或者应该搜索什么关键词.下面我们会说到的这个指南(也

seo的概念(下)

<seo的概念(上)>的链接:https://blog.51cto.com/14563906/2468072 正文: 11.相关关键词相关关键词:为 目标关键词.长尾关键词.热搜关键词 服务·概念1.搜索引擎针对用户搜索的关键词推荐的相关关键词2.在同类网站上,出现A关键词,往往会同时出现B关键词,那么A和B会被认为是相关的当你化某关键词时--不管是目标关键词还是长尾关键词,在该关键词所在的项面,出现一些这个关键词的相关关键词是必要有时候文章加上超链接,这样对seo优化比较好 ·举例说明:雪朗