qiuhongbingo / blog

Writing something in Issues.
https://github.com/qiuhongbingo/blog/issues
3 stars 0 forks source link

SPA Prerender 实现社交平台 Opengraph #38

Open qiuhongbingo opened 4 years ago

qiuhongbingo commented 4 years ago

哈!你是不是也不知道什么是 Opengraph

开头我先介绍一下什么是 Opengraph,先看图

我在 Slack 分享了一篇微信公众号的文章,虽然我只是发送了文章链接,但 Slack 很聪明地帮我把文章标题/描述/封面图都读取出来了,同样的,大部分社交平台都是这么聪明,像 Facebook/Twitter/微博/微信等等都这么智能,这就是 Opengraph 的成果,那么这是怎么做到的

我立马就在 chrome 查看了这篇公众号文章的 HTML 源码,发现 head 里有几处 meta 标签,分别定义好了文章的基本信息,嗯,原来社交平台这么聪明就是因为这里啊

这里我也附上 Facebook 官方 Opengraph 文档 以及 调试工具

到这里,是不是觉得我这篇文章就大结局了,这也太水了吧,不就是在 HTML 里加几个 meta 标签就好了嘛,还有什么好讲的???

客官莫急莫急,容我解释一下

SPA 单页面应用的缺点浮出水面

这篇文章我能写到这里,正是因为单页应用在处理这种 meta 标签是要绕绕路子的,要是传统的网页开发或者 SSR,哪来这么多屁事

众所周知,SPA 在 SEO 方面差强人意,先看图,这是一个 SPA 的页面,内容很丰富

但是当我们查看源代码,啊欧,发现根本没有文章列表的相关内容

原因是这样,SPA 靠 JavaScript 来填充页面的内容,就是那个大家都很熟悉的叫 #app 的 div 小朋友,这个小朋友长什么样全听 JavaScript 的,而浏览器只知道有小朋友这个人,不知道长什么样。那么当搜索引擎或者社交平台来到这个页面,空空如也,自然也就不帮你做内容收录和展示了

vue-meta 来帮忙

如果你的应用的所有页面都可以通用一个 meta 介绍,那么可以直接在应用的 index.html 加上 meta 即可

但大部分情况是页面都有不同的 meta,就以上面的文章页面来说,每篇文章的 meta 都需要依靠 JavaScript 单独设置,而 vue-meta 这个包就是解决这个问题的好帮手,使用方法也是很简单

export default {
  metaInfo: {
    meta: [
      { property: 'og:type', content: 'article' },
      { property: 'og:title', content: 'xxx' },
      { property: 'og:description', content: 'xxx' },
      { property: 'og:image', content: 'https://xxx.png' }
    ]
  }
}

但这样还是不够,浏览器依旧不知道小朋友长什么样,莫急,我们继续往下走

vue-meta 喊 prerender-spa-plugin 一起来帮忙

既然 vue-meta 只能帮一点忙,剩下就全靠 Prerender 预渲染了。预渲染的原理简单说,就是在 npm run build 的阶段,让浏览器打开你的页面,看清小朋友长什么样,并把样貌画下来,这样搜索引擎和社交平台也就知道了小朋友的样貌

明白了原理,我们开始用上 Prerender,本文以 vue-cli 的项目举例

vue.config.js 需要做的

const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

module.exports = {
  configureWebpack: () => {
    if (process.env.NODE_ENV !== 'production') return
    return {
      plugins: [
        new PrerenderSPAPlugin({
          staticDir: path.join(__dirname, 'dist'),
          // 这里配好你需要预渲染的页面,对应 vue-router 的路径
          routes: [
            '/paper',
          ],
          renderer: new Renderer({
            // 这里定义 Prerender 插件开始画样貌的信号
            renderAfterDocumentEvent: 'render-event'
          })
        })
      ]
    }
  }
}

main.js

new Vue({
  router,
  store,
  render: (h) => h(App),
  // 这里就是触发 Prerender 插件开始画样貌
  mounted: () => document.dispatchEvent(new Event('render-event'))
}).$mount('#app')

到这里,代码就改完了,但最终打包上线我也遇到一些坑,顺便分享一下

预渲染的副作用

如果你的应用也是用 Docker 打包,那么大概率你的镜像体积会变大一些,打包速度也会变慢,受限于 Prerender 依赖 chrome 内核,大陆环境下不容易获取

这里我推荐分为基础镜像和业务镜像单独打包,基础镜像可以使用 buildkite/puppeteer:latest,有 chromium 环境和 yarn 环境

docker/base/Dockerfile

FROM buildkite/puppeteer:latest
WORKDIR /app

COPY package.json ./

RUN yarn install
RUN yarn cache clean

基于基础镜像再去构建业务镜像,对打包速度和体积都有好处

docker/beta/Dockerfile

# build stage
FROM xxx-base:latest as build-stage
WORKDIR /app
COPY . .
RUN npm run build-beta

# production stage
FROM nginx:alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY docker/beta/nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

最后祭出 Makefile

NAME = xxx
REGISTRY = aaa
BETA_REGISTRY = bbb

base:
    echo building ${NAME}-base:latest
    cp docker/base/Dockerfile .
    docker build -t ${BETA_REGISTRY}/${NAME}-base:latest .
    rm Dockerfile
    docker push ${BETA_REGISTRY}/${NAME}-base:latest

beta: base
    echo building ${NAME}:beta
    cp docker/beta/Dockerfile .
    docker build -t ${BETA_REGISTRY}/${NAME}:beta .
    rm Dockerfile
    docker push ${BETA_REGISTRY}/${NAME}:beta

prod: base
    echo building ${NAME}:master
    cp docker/prod/Dockerfile .
    docker build -t ${REGISTRY}/${NAME}:master .
    rm Dockerfile
    docker push ${REGISTRY}/${NAME}:master

文末不禁想唱歌~这里的前端路十八弯~