需求背景
企业微信机器人设置自动回复链接会先检索链接的页面标题、图片、描述等,用来生成自动回复链接卡片(如图)
以上需求实际上可归结为SEO( 搜索引擎优化 )问题
什么是seo?
SEO(Search Engine Optimization):汉译为搜索引擎优化。是一种方式:利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。目的是让其在行业内占据领先地位,获得品牌收益。很大程度上是网站经营者的一种商业行为,将自己或自己公司的排名前移。 --百度百科
提出问题
前端三大框架Vue、React 、Angular的广泛应用,其单页面形式的普及对有SEO的需求极不友好,原因在于seo在爬虫时不会执行JS。在vue中我们通常使用Vue Router控制路由渲染对应的页面,所以搜索引擎只会收录到 index.html一个页面,且检索到的只能是初始状态一些固定内容,不能对对应的页面做TDK(title, keywords, description)不同的配置,每个页面的title和meta标签都是一样的。
解决方案
-
服务器端渲染(SSR)
服务端渲染的模式下,当⽤户第⼀次请求页⾯时,由服务器把需要的组件或页⾯渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到⼿的,是可以直接渲染然后呈现给⽤户的 HTML 内容,不需要为了⽣成 DOM 内容⾃⼰再去跑⼀遍 JS 代码。使⽤服务端渲染的⽹站,页⾯上呈现的内容,我们在 html 源⽂件⾥也能找到。
优点: ⾸屏渲染快、利于SEO
缺点: 服务端压力较大、学习成本相对较高
-
客户端预渲染
通过PrerenderSPAPlugin插件在构建(build)时简单地生成针对特定路由的静态 HTML 文件,配合vue-meta在所需页面内设置不同的标题、描述等信息。
PrerenderSPAPlugin原理:( 在webpack打包结束并生成文件后(after-emit hook),会启动一个server模拟网站的运行,用puppeteer(google官方的headless 无头浏览器)访问指定的页面route,得到相应的html结构,并将结果输出到指定目录,过程类似于爬虫。 )
优点: ⾸屏渲染快、利于SEO
缺点: 方案尚未成熟可能会遇到一些无法预期的错误。
需要更改路由模式,老旧项目可能会造成关联影响。
-
多页面打包
通过在根目录设置多个html文件,打包生成多个不同title的index.html用来满足seo需求,后续再配合路由监听的动态更改title。
优点: 利于SEO,部署简单,适合对seo需求页面少的项目
缺点: 不能提升首屏渲染速度,同项目不同页面地址前缀不统一
综上,方案一因其缺点明显,本章不再讨论。
方案2:客户端预渲染
-
安装
npm i prerender-spa-pluginnpm i vue-meta -
router.js
const router = new Router({ mode: 'history', //vue-router 路由模式改为history routes:[...] }) -
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') } -
有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 } } } -
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, } }), ] } -
打包结果
生成以需要预加载页面文件名命名的文件夹,其中的index.html 即预渲染生成的页面
方案2:避坑指南
-
在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 -
prerender-spa-plugin插件是需要依赖puppeteer的,即谷歌出品的无头浏览器插件,这个插件会下载最新版的chromium(大约300M)。如此大的插件会导致项目过于臃肿。
-
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) -
打包后页面js无响应
解决:检查预渲染打包出来的html是不是少了id="app"的元素,如缺少需要在根组件添加id=‘app’,
<div id='app'> <router-view /> </div> -
对于老旧项目,路由模式的切换可能会导致一些关联的产品地址失效。需提前评估该方案可行性。如只是个别页面有seo需求,建议采用方案3。
方案3:多页面打包
-
根目录下复制index.html,如下图indexQA.html,在复制出的html文件中更改title、description等信息
-
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中的打包结果,html中内容即为根目录下indexQA.html打包后内容。
此时除原项目地址外,在项目原地址后添加
/chat/打开的也是原项目,不同在于seo检索时两个地址检索到的页面信息不同。