基于PrerenderSPAPlugin的SEO解决方案

497 阅读5分钟

需求背景

企业微信机器人设置自动回复链接会先检索链接的页面标题、图片、描述等,用来生成自动回复链接卡片(如图)

示例1.png

以上需求实际上可归结为SEO( 搜索引擎优化 )问题

什么是seo?

SEO(Search Engine Optimization):汉译为搜索引擎优化。是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。目的是让其在行业内占据领先地位,获得品牌收益。很大程度上是网站经营者的一种商业行为,将自己或自己公司的排名前移。 --百度百科

提出问题

前端三大框架Vue、React 、Angular的广泛应用,其单页面形式的普及对有SEO的需求极不友好,原因在于seo在爬虫时不会执行JS。在vue中我们通常使用Vue Router控制路由渲染对应的页面,所以搜索引擎只会收录到 index.html一个页面,且检索到的只能是初始状态一些固定内容,不能对对应的页面做TDK(title, keywords, description)不同的配置,每个页面的title和meta标签都是一样的。

解决方案

  1. 服务器端渲染(SSR)

    服务端渲染的模式下,当⽤户第⼀次请求页⾯时,由服务器把需要的组件或页⾯渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到⼿的,是可以直接渲染然后呈现给⽤户的 HTML 内容,不需要为了⽣成 DOM 内容⾃⼰再去跑⼀遍 JS 代码。使⽤服务端渲染的⽹站,页⾯上呈现的内容,我们在 html 源⽂件⾥也能找到。

    优点: ⾸屏渲染快、利于SEO

    缺点: 服务端压力较大、学习成本相对较高

  2. 客户端预渲染

    通过PrerenderSPAPlugin插件在构建(build)时简单地生成针对特定路由的静态 HTML 文件,配合vue-meta在所需页面内设置不同的标题、描述等信息。

    PrerenderSPAPlugin原理:( 在webpack打包结束并生成文件后(after-emit hook),会启动一个server模拟网站的运行,用puppeteer(google官方的headless 无头浏览器)访问指定的页面route,得到相应的html结构,并将结果输出到指定目录,过程类似于爬虫。 )

    优点: ⾸屏渲染快、利于SEO

    缺点: 方案尚未成熟可能会遇到一些无法预期的错误。

    需要更改路由模式,老旧项目可能会造成关联影响。

  3. 多页面打包

    通过在根目录设置多个html文件,打包生成多个不同title的index.html用来满足seo需求,后续再配合路由监听的动态更改title。

    优点: 利于SEO,部署简单,适合对seo需求页面少的项目

    缺点: 不能提升首屏渲染速度,同项目不同页面地址前缀不统一

综上,方案一因其缺点明显,本章不再讨论。

方案2:客户端预渲染

  1. 安装

    npm i prerender-spa-plugin
    
    npm i vue-meta
    
  2. router.js

    const router = new Router({
      mode: 'history',  //vue-router 路由模式改为history
      routes:[...]
    })
    
  3. main.js

    import Vue from 'vue'
    import VueMeta from 'vue-meta'
     
    Vue.use(VueMeta, {
      // optional pluginOptions
      refreshOnceOnNavigation: true
    })
    
    ...
    
    new Vue({
        store,
        router,
        render: h => h(App),
        mounted: () => document.dispatchEvent(new Event('render-event'))
      }).$mount('#app')
    }
    
    
  4. 有seo需求的页面

      export default {
        metaInfo: {
          title: 'MyPage',
          meta: [
          	{ charset: 'utf-8' },
            { name: 'description', content: 'page description' }
            {name: 'keyWords',content: 'My Example App'}
          ]
          link: [{                
            rel: 'asstes',
            href: 'https://assets-cdn.github.com/'
          }]
          htmlAttrs: {
            lang: 'en',
          }
        }
      }
      
    // 使用异步数据
    export default {
      data () {
        return {
          title: 'Foo Bar Baz'
        }
      },
      metaInfo () {
        return {
          title: this.title
        }
      }
    }
    
  5. webpack.config.js

    const path = require('path')
    const PrerenderSPAPlugin = require('prerender-spa-plugin')
    
    module.exports = {
      plugins: [
        ...
         new PrerenderSPAPlugin({
          // webpack输出的app预渲染的路径.
          staticDir: path.join(__dirname, '../dist'),
          // 需要预加载页面的路由.
           routes: ['/','/chat'],
           renderer: new  PrerenderSPAPlugin.PuppeteerRenderer({
                    inject: {
                      foo: 'bar'
                    },
    // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
             renderAfterDocumentEvent: 'render-event'
           }),
           navigationOptions: {
            timeout: 0,
          }
        }),
      ]
    }
    
  6. 打包结果

示例2.png

生成以需要预加载页面文件名命名的文件夹,其中的index.html 即预渲染生成的页面

方案2:避坑指南

  1. 在Linux (CentOS)系统下,打包 prerender-spa-plugin 插件时报错:

    [Prerenderer - PuppeteerRenderer] Unable to start Puppeteer Error: Failed to launch chrome! /server/jenkins/.jenkins/workspace/dlej-h5paas_sfapp_tj-dlej-h5_dev/node_modules/puppeteer/.local-chromium/linux-686378/chrome-linux/chrome: error while loading shared libraries: libXss.so.1: cannot open shared object file: No such file or directory

    解决:在Linux 系统下安装依赖包,命令如下:

    yum install libXScrnSaver atk java-atk-wrapper at-spi2-atk gtk3 libXt -y
    
  2. prerender-spa-plugin插件是需要依赖puppeteer的,即谷歌出品的无头浏览器插件,这个插件会下载最新版的chromium(大约300M)。如此大的插件会导致项目过于臃肿。

  3. TypeError: Cannot read property 'close' of undefined

    解决:node_modules中 @prerenderer/renderer-puppeteer/es6/renderer.js 140行 this._puppeteer.close() 更改为,源码修改方法见文章如何优雅的修改node_modules依赖源码

       setTimeout(() => {
         this._puppeteer.close()
       }, 500)
    
  4. 打包后页面js无响应

    解决:检查预渲染打包出来的html是不是少了id="app"的元素,如缺少需要在根组件添加id=‘app’,

    <div id='app'>
        <router-view />
    </div>
    
  5. 对于老旧项目,路由模式的切换可能会导致一些关联的产品地址失效。需提前评估该方案可行性。如只是个别页面有seo需求,建议采用方案3。

方案3:多页面打包

  1. 根目录下复制index.html,如下图indexQA.html,在复制出的html文件中更改title、description等信息

1653548474293.png

  1. webpack.config.js

      plugins:[
        new HtmlWebpackPlugin({
          filename: path.resolve(__dirname, '../dist/index.html'),
          template: 'index.html',
          inject: true,
          cache: true,
          ...
          }),
          
        // 额外的页面打包
        new HtmlWebpackPlugin({
          filename: path.resolve(__dirname, '../dist/chat/index.html'),
          template: 'indexQA.html',
          inject: true,
          cache: true,
          ...
          }), 
          ...
        ]
        
    
  2. 打包实际部署

    打包后的目录结构同方案2中的打包结果,html中内容即为根目录下indexQA.html打包后内容。

    此时除原项目地址外,在项目原地址后添加 /chat/打开的也是原项目,不同在于seo检索时两个地址检索到的页面信息不同。