# 预渲染

Vue 官方提供了 PrerenderSPAPlugin (opens new window) 插件来实现预渲染。使用方式如下:

// 首先,这是插件的入口
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

module.exports = {
  plugins: [
      new PrerenderSPAPlugin({
        // 必需 - webpack 打包输出路径
        staticDir: path.join(__dirname, 'dist'),
        
        // 可选 - 渲染后输出的目录位置,默认就是 staticDir 目录
        outputDir: path.join(__dirname, 'prerendered'),
        
        // 可选 - 启动的主页
        // 默认就是 打包输入路径里的 `index.html`
        indexPath: path.join(__dirname, 'dist', 'index.html'),
        
        // 必需 - 要预渲染哪些页面
        routes: [ '/', '/about', '/some/deep/nested/route' ],
        
        // 可选 - 允许在内容在最终写入到文件之前进行定义的修改,postProcess 提供一个参数 renderedRoute
        // renderedRoute 应该是与当前渲染内容相关的信息,通过修改它还修改最终输出的内容
        // renderedRoute 包含的属性:
        // {
        //   route: String, // Where the output file will end up (relative to outputDir)?
        //   originalRoute: String, // The route that was passed into the renderer, before redirects.
        //   html: String, // 当前路径下的 HTML
        //   outputPath: String // 当路由页面的输出路径
        // }
        postProcess (renderedRoute) {
          // 忽略任何重定向
          renderedRoute.route = renderedRoute.originalRoute
          // 删除空格. (Don't use this in production)
          renderedRoute.html = renderedRoute.html.split(/>[\s]+</gmi).join('><')
          // 如果目录名以.html文件扩展名结尾,则从输出路径中删除/index.html.
          // 例如: /dist/dir/special.html/index.html -> /dist/dir/special.html
          if (renderedRoute.route.endsWith('.html')) {
            renderedRoute.outputPath = path.join(__dirname, 'dist', renderedRoute.route)
          }
          return renderedRoute
        },
        
        // 可选 - Uses html-minifier (https://github.com/kangax/html-minifier)
        // 输出渲染结果的优化配置.
        // Option reference: https://github.com/kangax/html-minifier#options-quick-reference
        minify: {
          collapseBooleanAttributes: true,
          collapseWhitespace: true,
          decodeEntities: true,
          keepClosingSlash: true,
          sortAttributes: true
        },
        
        // Server configuration options.
        // server 属性在最终输出的纯静态的页面中是不起作用的,他的作用仅限于你项目中有数据请求是从接口哪里拿到的
        // 防止插件运行过程中因为数据拿不到导致报错,渲染失败情况
        server: {
          // Normally a free port is autodetected, but feel free to set this if needed.
          port: 8001,
          // proxy 接口代理,与vue的devServer相同
          proxy:{
            '/api': {
              target:'http://www.lanjz.com',
              changeOrigin:true, //是否跨域
            },
          }
        },
        
        // 实际使用的渲染器
        // Available renderers: https://github.com/Tribex/prerenderer/tree/master/renderers
        renderer: new Renderer({
          // 可选 - 要注入到全局环境的中变量名,这个变量名的值为下面 inject 配置内容
          injectProperty: '__PRERENDER_INJECTED',
          // 可选 - __PRERENDER_INJECTED 的值.
          inject: {
            foo: 'bar'
          },
          
          // 可选 - 默认为0,没有限制
          // 异步渲染路由.
          // 使用它来限制并行渲染的路由数量
          maxConcurrentRoutes: 4,
          
          // 可选 - 在特定的事件触发后再开始渲染
          // 如:当前配置表示执行 document.dispatchEvent(new Event('custom-render-trigger')) 事件后再显示内容
          renderAfterDocumentEvent: 'custom-render-trigger',
          
          // 可选 - 检测到特别元素时再呈现内容。 使用 `document.querySelector`
          renderAfterElementExists: 'my-app-element',
          
          // 可选 - 等待多久时间后再渲染
          // 不推荐使用
          renderAfterTime: 5000, // Wait 5 seconds.
          
          // Other puppeteer options.
          // (See here: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions)
          headless: false // Display the browser window when rendering. Useful for debugging.
        })
      })
  ]
}

# 工作方式

PrerenderSPAPlugin 插件另启动一个 webpack-dev-server 服务,将完整的项目运行在无头浏览器中,再使用 puppeteer 把对应路由的页面爬取下来

# 实战

添加 Webpack 配置

vue.config.js 为例:

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

module.exports = {
  configureWebpack: {
    output: {
      // 微应用的包名,这里与主应用中注册的微应用名称一致
      library: "VueMicroApp",
      // 将你的 library 暴露为所有的模块定义下都可运行的方式
      libraryTarget: "umd",
      // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
      jsonpFunction: `webpackJsonp_VueMicroApp`,
    },
    plugins: [
      new PrerenderSPAPlugin({
        staticDir: path.join(__dirname, './dist'),
        // 对应自己的路由文件,比如index有参数,就需要写成 /index/param1。
        routes: ['/vue/home'], // 因为该系统操作都是基于登录后的,所以只做登录页面的预渲染就行了
        renderer: new Renderer({
          headless: false,
          // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
          renderAfterDocumentEvent: 'render-event',
        }),
      })
    ]
  },
  
};

上面插件配置中,添加了配置项 enderAfterDocumentEvent: 'render-event',表示在页面触发 render-event 后再开始渲染。为了正确抓取到完整的页面应该在页面加载完再开始预渲染,所以需要在页面加载完成后再触发对应的事件

在应用中添加开始预渲染的事件

在入口 App.vuemounted 钩子中添加事件

<template>
  <router-view></router-view>
</template>
<script>
export default {
  mounted() {
    document.dispatchEvent(new Event('render-event'))
  }
}
</script>

正常的话重新打包就可以生预渲染页面

如果需要添加对应的 keywords, description 等信息,可以配置 vue-meta-info 配置使用

添加 vue-meta-info

yarn add vue-meta-info

import MetaInfo from 'vue-meta-info'

app.use(MetaInfo)

// 需要添加 meta 的组件添加钩子
export default {
  mounted() {
  },
  metaInfo: {
    title: 'My Example App', // set a title
    meta: [{                 // set meta
      name: 'keyWords',
      content: 'My Example App'
    }]
  }
}

# 预渲染和服务端渲染

预渲染我理解的话很像静态网站生成器的,所以如果页面基本静态且涉及的接口数据少的话挺合适的。

本质相当于为打包出的 html 页面在挂载元素中添加了 DOM 内容,这样渲染的时候就可以马上呈现出容,之后加载 JS 脚本后,挂载元素再被 vue 动态渲染的内容所代替

服务端渲染

服务端渲染时页面的内容渲染是在服务端进行的,客户端加载后不会重新加载 DOM 元素,这一点应该就是最主要的区别了

vue-cli+prerender-spa-plugin配置SEO (opens new window)