精华内容
下载资源
问答
  • 使用零配置将prerender-spa-plugin添加到您的Vue应用程序。 寻找共同维护者:我将继续维护该项目,但是我仍然希望在某些问题上获得帮助,并且随着它越来越受欢迎,通常可以帮助我保持该插件的运行。 如果您认为...
  • 基于Alpine Linux的轻量级Prerender容器,带有Node和Headless Chrome。 渲染5.8.2 Chrome86.0.4240.111 节点14.16.0 要求 码头工人 用法 拉并运行图像: docker pull tvanro/prerender-alpine:6.3.0 docker run...
  • 主要介绍了vue-cli单页面预渲染seo-prerender-spa-plugin操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • 要使用prerender-varnish,请在您的Varnish配置目录(通常是/etc/varnish )中将prerender.vcl和prerender_backend.vcl符号链接起来,并将它们添加到您的主要Varnish配置文件中: include " prerender.vcl " ; 更新...
  • Prerender SPA插件 灵活的,与框架无关的静态站点生成,用于使用Webpack构建的站点和SPA。 关于prerender-spa-plugin :backhand_index_pointing_right: 这是基于puppeteer的prerender-spa-plugin的稳定的3.x版本。...
  • 预加载器 Webpack的无痛通用预渲染。 与一起很好地工作。 :face_with_monocle: 什么是预渲染? ... 预渲染就像服务器端渲染一样,只是在构建时完成以生成静态文件。... prerender-loader在构建期间在We
  • Nginx的非官方prerender.io nginx.conf 有关用法示例,请参见nginx.conf。 在prerender.nginx.conf中更新您的prerender.io令牌,如果您不在另一个负载均衡器或代理后面,则将$ http_x_forwarded_proto与$ scheme...
  • vue-prerender实现了三种使用无头chrome渲染Vue.js页面的策略。 安装 $ npm install --save-dev vue-prerender 或者 $ yarn add --dev vue-prerender 用法 将vue-prerender集成到项目中的主要方法有两种: 创建一...
  • Laravel开发-laravel-prerender Laravel中间件,用于为SEO动态呈现JavaScript呈现的页面
  • prerender-test 是一个在 Google App Engine 环境下允许预呈现站点测试的应用程序。 标签:prerender
  • prerender.io cloudfront示例中间件 这是在通过Cloudfront服务的S3中集成prerender.io和SPA的示例。 指示 将prerender-cloudfront.yaml作为新堆栈上传到Cloudformation,这将为您设置示例。 询问时输入您的prerender...
  • 呈现 生成用于spa预渲染的文件。... " prerender " : " node ./node_modules/@websanova/js-prerender/build/prerender " , " sitemap " : " node ./node_modules/@websanova/js-prerender/build/sitemap " ,
  • prerender.js prerender.js可在任何浏览器上快速加载页面。 项目特色 在用户导航到该页面之前,预渲染将加载页面的所有资产。按照惯例,预渲染 ,然后在用户单击显示该隐藏的选项卡。 prerender.js是一种预加载...
  • 练习1.1 文章的代码 更多 项目设置 yarn install 编译和热重装以进行开发 yarn serve 编译并最小化生产 yarn build 整理和修复文件 yarn lint 自定义配置 请参阅。
  • vue单页面seo(prerender-spa-plugin,vue-meta-info)
  • prerender-mysql-cache 用于预渲染HTMLMySQL存储。 自动创建表“页面”并存储处理后HTML。 #安装: npm install prerender-mysql-cache 编辑server.js: process.env.PRERENDER_MYSQL_CACHE_MYSQL_URL = ...
  • vue预渲染之prerender-spa-plugin解析(二)

    千次阅读 2018-12-06 17:17:02
    前面我们有介绍了什么是预渲染、使用场景、然后简单的介绍了预渲染的集成过程,感兴趣的童鞋可以去看一下vue预渲染之prerender-spa-plugin解析(一),这一节我们重点来研究一下prerender-spa-plugin的源码. 附上...

    前面我们有介绍了什么是预渲染、使用场景、然后简单的介绍了预渲染的集成过程,感兴趣的童鞋可以去看一下vue预渲染之prerender-spa-plugin解析(一),这一节我们重点来研究一下prerender-spa-plugin的源码.

    附上prerender-spa-plugin的github地址: https://github.com/chrisvfritz/prerender-spa-plugin

    我们直接去github拖一份源码:

    在这里插入图片描述

    然后我们用的时候:
    webpack.prod.conf.js:

    webpackConfig.plugins.push(new PrerenderSPAPlugin({
      staticDir: path.join(config.build.assetsRoot),
      routes: ['/'],
      renderer: new Renderer({
        headless: false,
        renderAfterDocumentEvent: 'render-event'
      })
    }))
    

    可以看到,我们在webpack构建结束之前插入了一个叫PrerenderSPAPlugin的插件,PrerenderSPAPlugin是什么呢?

    const PrerenderSPAPlugin = require('../prerender')
    const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
    
    
    const path = require('path')
    const Prerenderer = require('./prerenderer')
    const PuppeteerRenderer = require('./renderer-puppeteer')
    const {minify} = require('html-minifier')
    
    function PrerenderSPAPlugin(...args) {
      const rendererOptions = {} // Primarily for backwards-compatibility.
    
      this._options = {}
    
      // Normal args object.
      if (args.length === 1) {
        this._options = args[0] || {}
    
        // Backwards-compatibility with v2
      } else {
        console.warn("[prerender-spa-plugin] You appear to be using the v2 argument-based configuration options. It's recommended that you migrate to the clearer object-based configuration system.\nCheck the documentation for more information.")
        let staticDir, routes
    
        args.forEach(arg => {
          if (typeof arg === 'string') staticDir = arg
          else if (Array.isArray(arg)) routes = arg
          else if (typeof arg === 'object') this._options = arg
        })
    
        staticDir ? this._options.staticDir = staticDir : null
        routes ? this._options.routes = routes : null
      }
    
      // Backwards compatiblity with v2.
      if (this._options.captureAfterDocumentEvent) {
        console.warn('[prerender-spa-plugin] captureAfterDocumentEvent has been renamed to renderAfterDocumentEvent and should be moved to the renderer options.')
        rendererOptions.renderAfterDocumentEvent = this._options.captureAfterDocumentEvent
      }
    
      if (this._options.captureAfterElementExists) {
        console.warn('[prerender-spa-plugin] captureAfterElementExists has been renamed to renderAfterElementExists and should be moved to the renderer options.')
        rendererOptions.renderAfterElementExists = this._options.captureAfterElementExists
      }
    
      if (this._options.captureAfterTime) {
        console.warn('[prerender-spa-plugin] captureAfterTime has been renamed to renderAfterTime and should be moved to the renderer options.')
        rendererOptions.renderAfterTime = this._options.captureAfterTime
      }
    
      this._options.server = this._options.server || {}
      this._options.renderer = this._options.renderer || new PuppeteerRenderer(Object.assign({}, {headless: true}, rendererOptions))
    
      if (this._options.postProcessHtml) {
        console.warn('[prerender-spa-plugin] postProcessHtml should be migrated to postProcess! Consult the documentation for more information.')
      }
    }
    
    PrerenderSPAPlugin.prototype.apply = function (compiler) {
      const compilerFS = compiler.outputFileSystem
    
      // From https://github.com/ahmadnassri/mkdirp-promise/blob/master/lib/index.js
      const mkdirp = function (dir, opts) {
        return new Promise((resolve, reject) => {
          compilerFS.mkdirp(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))
        })
      }
    
      const afterEmit = (compilation, done) => {
        const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          // Backwards-compatibility with v2 (postprocessHTML should be migrated to postProcess)
          .then(renderedRoutes => this._options.postProcessHtml
            ? renderedRoutes.map(renderedRoute => {
              const processed = this._options.postProcessHtml(renderedRoute)
              if (typeof processed === 'string') renderedRoute.html = processed
              else renderedRoute = processed
    
              return renderedRoute
            })
            : renderedRoutes
          )
          // Run postProcess hooks.
          .then(renderedRoutes => this._options.postProcess
            ? Promise.all(renderedRoutes.map(renderedRoute => this._options.postProcess(renderedRoute)))
            : renderedRoutes
          )
          // Check to ensure postProcess hooks returned the renderedRoute object properly.
          .then(renderedRoutes => {
            const isValid = renderedRoutes.every(r => typeof r === 'object')
            if (!isValid) {
              throw new Error('[prerender-spa-plugin] Rendered routes are empty, did you forget to return the `context` object in postProcess?')
            }
    
            return renderedRoutes
          })
          // Minify html files if specified in config.
          .then(renderedRoutes => {
            if (!this._options.minify) return renderedRoutes
    
            renderedRoutes.forEach(route => {
              route.html = minify(route.html, this._options.minify)
            })
    
            return renderedRoutes
          })
          // Calculate outputPath if it hasn't been set already.
          .then(renderedRoutes => {
            renderedRoutes.forEach(rendered => {
              if (!rendered.outputPath) {
                // rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route, 'index.html')
                rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route)
              }
            })
    
            return renderedRoutes
          })
          // Create dirs and write prerendered files.
          .then(processedRoutes => {
            const promises = Promise.all(processedRoutes.map(processedRoute => {
              return mkdirp(path.dirname(processedRoute.outputPath))
                .then(() => {
                  return new Promise((resolve, reject) => {
                    compilerFS.writeFile(processedRoute.outputPath, processedRoute.html.trim(), err => {
                      if (err) reject(`[prerender-spa-plugin] Unable to write rendered route to file "${processedRoute.outputPath}" \n ${err}.`)
                      else resolve()
                    })
                  })
                })
                .catch(err => {
                  if (typeof err === 'string') {
                    err = `[prerender-spa-plugin] Unable to create directory ${path.dirname(processedRoute.outputPath)} for route ${processedRoute.route}. \n ${err}`
                  }
    
                  throw err
                })
            }))
    
            return promises
          })
          .then(r => {
            PrerendererInstance.destroy()
            done()
          })
          .catch(err => {
            PrerendererInstance.destroy()
            const msg = '[prerender-spa-plugin] Unable to prerender all routes!'
            console.error(msg)
            compilation.errors.push(new Error(msg))
            done()
          })
      }
    
      if (compiler.hooks) {
        const plugin = {name: 'PrerenderSPAPlugin'}
        compiler.hooks.afterEmit.tapAsync(plugin, afterEmit)
      } else {
        compiler.plugin('after-emit', afterEmit)
      }
    }
    
    PrerenderSPAPlugin.PuppeteerRenderer = PuppeteerRenderer
    
    module.exports = PrerenderSPAPlugin
    
    

    代码有点多,不要慌,我们看重点,作为webpack的插件我们都知道,webpack会回调插件的apply方法,然后把编译器对象传递给插件:

    PrerenderSPAPlugin.prototype.apply = function (compiler) {
      const compilerFS = compiler.outputFileSystem
    
      // From https://github.com/ahmadnassri/mkdirp-promise/blob/master/lib/index.js
      const mkdirp = function (dir, opts) {
        return new Promise((resolve, reject) => {
          compilerFS.mkdirp(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))
        })
      }
    
      const afterEmit = (compilation, done) => {
        const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          // Backwards-compatibility with v2 (postprocessHTML should be migrated to postProcess)
          .then(renderedRoutes => this._options.postProcessHtml
            ? renderedRoutes.map(renderedRoute => {
              const processed = this._options.postProcessHtml(renderedRoute)
              if (typeof processed === 'string') renderedRoute.html = processed
              else renderedRoute = processed
    
              return renderedRoute
            })
            : renderedRoutes
          )
          // Run postProcess hooks.
          .then(renderedRoutes => this._options.postProcess
            ? Promise.all(renderedRoutes.map(renderedRoute => this._options.postProcess(renderedRoute)))
            : renderedRoutes
          )
          // Check to ensure postProcess hooks returned the renderedRoute object properly.
          .then(renderedRoutes => {
            const isValid = renderedRoutes.every(r => typeof r === 'object')
            if (!isValid) {
              throw new Error('[prerender-spa-plugin] Rendered routes are empty, did you forget to return the `context` object in postProcess?')
            }
    
            return renderedRoutes
          })
          // Minify html files if specified in config.
          .then(renderedRoutes => {
            if (!this._options.minify) return renderedRoutes
    
            renderedRoutes.forEach(route => {
              route.html = minify(route.html, this._options.minify)
            })
    
            return renderedRoutes
          })
          // Calculate outputPath if it hasn't been set already.
          .then(renderedRoutes => {
            renderedRoutes.forEach(rendered => {
              if (!rendered.outputPath) {
                // rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route, 'index.html')
                rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route)
              }
            })
    
            return renderedRoutes
          })
          // Create dirs and write prerendered files.
          .then(processedRoutes => {
            const promises = Promise.all(processedRoutes.map(processedRoute => {
              return mkdirp(path.dirname(processedRoute.outputPath))
                .then(() => {
                  return new Promise((resolve, reject) => {
                    compilerFS.writeFile(processedRoute.outputPath, processedRoute.html.trim(), err => {
                      if (err) reject(`[prerender-spa-plugin] Unable to write rendered route to file "${processedRoute.outputPath}" \n ${err}.`)
                      else resolve()
                    })
                  })
                })
                .catch(err => {
                  if (typeof err === 'string') {
                    err = `[prerender-spa-plugin] Unable to create directory ${path.dirname(processedRoute.outputPath)} for route ${processedRoute.route}. \n ${err}`
                  }
    
                  throw err
                })
            }))
    
            return promises
          })
          .then(r => {
            PrerendererInstance.destroy()
            done()
          })
          .catch(err => {
            PrerendererInstance.destroy()
            const msg = '[prerender-spa-plugin] Unable to prerender all routes!'
            console.error(msg)
            compilation.errors.push(new Error(msg))
            done()
          })
      }
    
      if (compiler.hooks) {
        const plugin = {name: 'PrerenderSPAPlugin'}
        compiler.hooks.afterEmit.tapAsync(plugin, afterEmit)
      } else {
        compiler.plugin('after-emit', afterEmit)
      }
    }
    

    prerender-spa-plugin做的第一件事就是监听webpack的构建过程:

    if (compiler.hooks) {
        const plugin = {name: 'PrerenderSPAPlugin'}
        compiler.hooks.afterEmit.tapAsync(plugin, afterEmit)
      } else {
        compiler.plugin('after-emit', afterEmit)
      }
    

    当webpack构建完毕后就会发送after-emit事件,然后执行插件的afterEmit方法,我们找到afterEmit方法:

    const afterEmit = (compilation, done) => {
        const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          // Backwards-compatibility with v2 (postprocessHTML should be migrated to postProcess)
          .then(renderedRoutes => this._options.postProcessHtml
            ? renderedRoutes.map(renderedRoute => {
              const processed = this._options.postProcessHtml(renderedRoute)
              if (typeof processed === 'string') renderedRoute.html = processed
              else renderedRoute = processed
    
              return renderedRoute
            })
            : renderedRoutes
          )
          // Run postProcess hooks.
          .then(renderedRoutes => this._options.postProcess
            ? Promise.all(renderedRoutes.map(renderedRoute => this._options.postProcess(renderedRoute)))
            : renderedRoutes
          )
          // Check to ensure postProcess hooks returned the renderedRoute object properly.
          .then(renderedRoutes => {
            const isValid = renderedRoutes.every(r => typeof r === 'object')
            if (!isValid) {
              throw new Error('[prerender-spa-plugin] Rendered routes are empty, did you forget to return the `context` object in postProcess?')
            }
    
            return renderedRoutes
          })
          // Minify html files if specified in config.
          .then(renderedRoutes => {
            if (!this._options.minify) return renderedRoutes
    
            renderedRoutes.forEach(route => {
              route.html = minify(route.html, this._options.minify)
            })
    
            return renderedRoutes
          })
          // Calculate outputPath if it hasn't been set already.
          .then(renderedRoutes => {
            renderedRoutes.forEach(rendered => {
              if (!rendered.outputPath) {
                // rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route, 'index.html')
                rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route)
              }
            })
    
            return renderedRoutes
          })
          // Create dirs and write prerendered files.
          .then(processedRoutes => {
            const promises = Promise.all(processedRoutes.map(processedRoute => {
              return mkdirp(path.dirname(processedRoute.outputPath))
                .then(() => {
                  return new Promise((resolve, reject) => {
                    compilerFS.writeFile(processedRoute.outputPath, processedRoute.html.trim(), err => {
                      if (err) reject(`[prerender-spa-plugin] Unable to write rendered route to file "${processedRoute.outputPath}" \n ${err}.`)
                      else resolve()
                    })
                  })
                })
                .catch(err => {
                  if (typeof err === 'string') {
                    err = `[prerender-spa-plugin] Unable to create directory ${path.dirname(processedRoute.outputPath)} for route ${processedRoute.route}. \n ${err}`
                  }
    
                  throw err
                })
            }))
    
            return promises
          })
          .then(r => {
            PrerendererInstance.destroy()
            done()
          })
          .catch(err => {
            PrerendererInstance.destroy()
            const msg = '[prerender-spa-plugin] Unable to prerender all routes!'
            console.error(msg)
            compilation.errors.push(new Error(msg))
            done()
          })
      }
    

    又是一长串代码,不要慌,我们继续往下走~

    我们看到创建了一个Prerenderer对象,然后就是执行的Prerenderer对象的一些方法,所以我们找到Prerenderer定义:

     const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          .....
    

    prerenderer.js

    const Server = require('./server')
    const PortFinder = require('portfinder')
    
    const PACKAGE_NAME = '[Prerenderer]'
    
    const OPTION_SCHEMA = {
      staticDir: {
        type: String,
        required: true
      },
      indexPath: {
        type: String,
        required: false
      }
    }
    
    function validateOptionsSchema (schema, options, parent) {
      var errors = []
    
      Object.keys(schema).forEach(key => {
        // Required options
        if (schema[key].required && !options[key]) {
          errors.push(`"${parent || ''}${key}" option is required!`)
          return
        // Options with default values or potential children.
        } else if (!options[key] && (schema[key].default || schema[key].children)) {
          options[key] = schema[key].default != null ? schema[key].default : {}
          // Non-required empty options.
        } else if (!options[key]) return
    
        // Array-type options
        if (Array.isArray(schema[key].type) && schema[key].type.indexOf(options[key].constructor) === -1) {
          console.log(schema[key].type.indexOf(options[key].constructor))
          errors.push(`"${parent || ''}${key}" option must be a ${schema[key].type.map(t => t.name).join(' or ')}!`)
          // Single-type options.
        } else if (!Array.isArray(schema[key].type) && options[key].constructor !== schema[key].type) {
          errors.push(`"${parent || ''}${key}" option must be a ${schema[key].type.name}!`)
          return
        }
    
        if (schema[key].children) {
          errors.push(...validateOptionsSchema(schema[key].children, options[key], key))
          return
        }
      })
    
      errors.forEach(function (error) {
        console.error(`${PACKAGE_NAME} ${error}`)
      })
    
      return errors
    }
    
    class Prerenderer {
      constructor (options) {
        this._options = options || {}
    
        this._server = new Server(this)
        this._renderer = options.renderer
    
        if (this._renderer && this._renderer.preServer) this._renderer.preServer(this)
    
        if (!this._options) throw new Error(`${PACKAGE_NAME} Options must be defined!`)
    
        if (!this._options.renderer) {
          throw new Error(`${PACKAGE_NAME} No renderer was passed to prerenderer.
    If you are not sure wihch renderer to use, see the documentation at https://github.com/tribex/prerenderer.`)
        }
    
        if (!this._options.server) this._options.server = {}
    
        const optionValidationErrors = validateOptionsSchema(OPTION_SCHEMA, this._options)
    
        if (optionValidationErrors.length !== 0) throw new Error(`${PACKAGE_NAME} Options are invalid. Unable to prerender!`)
      }
    
      async initialize () {
        // Initialization is separate from construction because science? (Ideally to initialize the server and renderer separately.)
        this._options.server.port = this._options.server.port || await PortFinder.getPortPromise() || 13010
        await this._server.initialize()
        await this._renderer.initialize()
    
        return Promise.resolve()
      }
    
      destroy () {
        this._renderer.destroy()
        this._server.destroy()
      }
    
      getServer () {
        return this._server
      }
    
      getRenderer () {
        return this._renderer
      }
    
      getOptions () {
        return this._options
      }
    
      modifyServer (server, stage) {
        if (this._renderer.modifyServer) this._renderer.modifyServer(this, server, stage)
      }
    
      renderRoutes (routes) {
        return this._renderer.renderRoutes(routes, this)
        // Handle non-ASCII or invalid URL characters in routes by normalizing them back to unicode.
        // Some browser environments may change unicode or special characters in routes to percent encodings.
        // We need to convert them back for saving in the filesystem.
        .then(renderedRoutes => {
          renderedRoutes.forEach(rendered => {
            rendered.route = decodeURIComponent(rendered.route)
          })
    
          return renderedRoutes
        })
      }
    }
    
    module.exports = Prerenderer
    
    

    好在代码不是很多哈,我们看一下构造函数:

    constructor (options) {
        this._options = options || {}
        //创建一个node服务器管理类
        this._server = new Server(this)
        this._renderer = options.renderer
    
        if (this._renderer && this._renderer.preServer) this._renderer.preServer(this)
    
        if (!this._options) throw new Error(`${PACKAGE_NAME} Options must be defined!`)
    
        if (!this._options.renderer) {
          throw new Error(`${PACKAGE_NAME} No renderer was passed to prerenderer.
    If you are not sure wihch renderer to use, see the documentation at https://github.com/tribex/prerenderer.`)
        }
    
        if (!this._options.server) this._options.server = {}
    
        const optionValidationErrors = validateOptionsSchema(OPTION_SCHEMA, this._options)
    
        if (optionValidationErrors.length !== 0) throw new Error(`${PACKAGE_NAME} Options are invalid. Unable to prerender!`)
      }
    

    忽略一些初始化变量的赋值,最重要的就是创建一个Server(node服务器):

     this._server = new Server(this)
    

    然后我们再次回到之前的入口文件:

    const afterEmit = (compilation, done) => {
        const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
    

    执行了Prerenderer的initialize方法,所以我们找到Prerenderer的initialize方法:

    async initialize () {
        // Initialization is separate from construction because science? (Ideally to initialize the server and renderer separately.)
        this._options.server.port = this._options.server.port || await PortFinder.getPortPromise() || 13010
        await this._server.initialize()
        await this._renderer.initialize()
    
        return Promise.resolve()
      }
    

    分别直接了this._server跟this._renderer的initialize方法,我们一个一个看哈~

    首先看一下:

     constructor (options) {
        this._options = options || {}
        //创建一个node服务器管理类
        this._server = new Server(this)
        ....
       }
    
     await this._server.initialize()
    

    Server是什么呢? 它的initialize方法又干了什么呢?

    server.js:

    const express = require('express')
    const proxy = require('http-proxy-middleware')
    const path = require('path')
    
    class Server {
      constructor (Prerenderer) {
        this._prerenderer = Prerenderer
        this._options = Prerenderer.getOptions()
        this._expressServer = express()
        this._nativeServer = null
      }
    
      initialize () {
        const server = this._expressServer
    
        this._prerenderer.modifyServer(this, 'pre-static')
    
        server.get('*.*', express.static(this._options.staticDir, {
          dotfiles: 'allow'
        }))
    
        this._prerenderer.modifyServer(this, 'post-static')
    
        this._prerenderer.modifyServer(this, 'pre-fallback')
    
        if (this._options.server && this._options.server.proxy) {
          for (let proxyPath of Object.keys(this._options.server.proxy)) {
            server.use(proxyPath, proxy(this._options.server.proxy[proxyPath]))
          }
        }
    
        server.get('*', (req, res) => {
          res.sendFile(this._options.indexPath ? this._options.indexPath : path.join(this._options.staticDir, req.path))
        })
    
        this._prerenderer.modifyServer(this, 'post-fallback')
    
        return new Promise((resolve, reject) => {
          this._nativeServer = server.listen(this._options.server.port, () => {
            resolve()
          })
        })
      }
    
      destroy () {
        this._nativeServer.close()
      }
    }
    
    module.exports = Server
    
    

    很简单,就是利用express创建了一个node服务器,然后监听url返回对应的资源~

       server.get('*', (req, res) => {
          res.sendFile(this._options.indexPath ? this._options.indexPath : path.join(this._options.staticDir, req.path))
        })
    
    

    那么监听的又是哪里的文件呢? 我继续贴一下上一节中的流程图:
    在这里插入图片描述

    监听的就是我们直接用webpack打包出来没有经过预渲染的资源文件.

    server类我们算是看完了,然后我们继续分析:

    await this._renderer.initialize()
    

    this._renderer又是什么呢? this._renderer就是我们在webpack.prod.conf.js文件中传递进去的PuppeteerRenderer对象:

    .....
    const PrerenderSPAPlugin = require('../prerender')
    const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
    .....
    webpackConfig.plugins.push(new PrerenderSPAPlugin({
      staticDir: path.join(config.build.assetsRoot),
      routes: ['/'],
      renderer: new Renderer({
        headless: false,
        renderAfterDocumentEvent: 'render-event'
      })
    }))
    module.exports = webpackConfig
    
    
    new Renderer({
        headless: false,
        renderAfterDocumentEvent: 'render-event'
      })
    

    我们找到Renderer的源码~

    renderer-puppeteer/es6/renderer.js:

    const promiseLimit = require('promise-limit')
    const puppeteer = require('puppeteer')
    
    const waitForRender = function (options) {
      options = options || {}
    
      return new Promise((resolve, reject) => {
        // Render when an event fires on the document.
        if (options.renderAfterDocumentEvent) {
          if (window['__PRERENDER_STATUS'] && window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED) resolve()
          document.addEventListener(options.renderAfterDocumentEvent, () => resolve())
    
          // Render after a certain number of milliseconds.
        } else if (options.renderAfterTime) {
          setTimeout(() => resolve(), options.renderAfterTime)
    
          // Default: Render immediately after page content loads.
        } else {
          resolve()
        }
      })
    }
    
    class PuppeteerRenderer {
      constructor(rendererOptions) {
        this._puppeteer = null
        this._rendererOptions = rendererOptions || {}
    
        if (this._rendererOptions.maxConcurrentRoutes == null) this._rendererOptions.maxConcurrentRoutes = 0
    
        if (this._rendererOptions.inject && !this._rendererOptions.injectProperty) {
          this._rendererOptions.injectProperty = '__PRERENDER_INJECTED'
        }
      }
    
      async initialize() {
        try {
          // Workaround for Linux SUID Sandbox issues.
          if (process.platform === 'linux') {
            if (!this._rendererOptions.args) this._rendererOptions.args = []
    
            if (this._rendererOptions.args.indexOf('--no-sandbox') === -1) {
              this._rendererOptions.args.push('--no-sandbox')
              this._rendererOptions.args.push('--disable-setuid-sandbox')
            }
          }
    
          this._puppeteer = await puppeteer.launch(this._rendererOptions)
        } catch (e) {
          console.error(e)
          console.error('[Prerenderer - PuppeteerRenderer] Unable to start Puppeteer')
          // Re-throw the error so it can be handled further up the chain. Good idea or not?
          throw e
        }
    
        return this._puppeteer
      }
    
      async handleRequestInterception(page, baseURL) {
        await page.setRequestInterception(true)
    
        page.on('request', req => {
          // Skip third party requests if needed.
          if (this._rendererOptions.skipThirdPartyRequests) {
            if (!req.url().startsWith(baseURL)) {
              req.abort()
              return
            }
          }
    
          req.continue()
        })
      }
    
      async renderRoutes(routes, Prerenderer) {
        const rootOptions = Prerenderer.getOptions()
        const options = this._rendererOptions
    
        const limiter = promiseLimit(this._rendererOptions.maxConcurrentRoutes)
    
        const pagePromises = Promise.all(
          routes.map(
            (route, index) => limiter(
              async () => {
                const page = await this._puppeteer.newPage()
    
                if (options.consoleHandler) {
                  page.on('console', message => options.consoleHandler(route, message))
                }
    
                if (options.inject) {
                  await page.evaluateOnNewDocument(`(function () { window['${options.injectProperty}'] = ${JSON.stringify(options.inject)}; })();`)
                }
    
                const baseURL = `http://localhost:${rootOptions.server.port}`
    
                // Allow setting viewport widths and such.
                if (options.viewport) await page.setViewport(options.viewport)
    
                await this.handleRequestInterception(page, baseURL)
    
                // Hack just in-case the document event fires before our main listener is added.
                if (options.renderAfterDocumentEvent) {
                  page.evaluateOnNewDocument(function (options) {
                    window['__PRERENDER_STATUS'] = {}
                    document.addEventListener(options.renderAfterDocumentEvent, () => {
                      window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED = true
                    })
                  }, this._rendererOptions)
                }
    
                const navigationOptions = (options.navigationOptions) ? Object.assign({waituntil: 'networkidle0'}, options.navigationOptions) : {waituntil: 'networkidle0'};
                await page.goto(`${baseURL}${route}`, navigationOptions);
    
                // Wait for some specific element exists
                const {renderAfterElementExists} = this._rendererOptions
                if (renderAfterElementExists && typeof renderAfterElementExists === 'string') {
                  await page.waitForSelector(renderAfterElementExists)
                }
                // Once this completes, it's safe to capture the page contents.
                await page.evaluate(waitForRender, this._rendererOptions)
    
                const result = {
                  originalRoute: route,
                  route: await page.evaluate('window.location.pathname'),
                  html: await page.content()
                }
    
                await page.close()
                return result
              }
            )
          )
        )
    
        return pagePromises
      }
    
      destroy() {
        this._puppeteer.close()
      }
    }
    
    module.exports = PuppeteerRenderer
    
    

    代码有点多,我们直接看重点方法initialize:

    async initialize() {
        try {
          // Workaround for Linux SUID Sandbox issues.
          if (process.platform === 'linux') {
            if (!this._rendererOptions.args) this._rendererOptions.args = []
    
            if (this._rendererOptions.args.indexOf('--no-sandbox') === -1) {
              this._rendererOptions.args.push('--no-sandbox')
              this._rendererOptions.args.push('--disable-setuid-sandbox')
            }
          }
    
          this._puppeteer = await puppeteer.launch(this._rendererOptions)
        } catch (e) {
          console.error(e)
          console.error('[Prerenderer - PuppeteerRenderer] Unable to start Puppeteer')
          // Re-throw the error so it can be handled further up the chain. Good idea or not?
          throw e
        }
    
        return this._puppeteer
      }
    

    可以看到,最主要的就是执行了一行:

    this._puppeteer = await puppeteer.launch(this._rendererOptions)
    

    puppeteer是啥呢?

    Puppeteer(中文翻译”木偶”) 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome .

    简单来说就是可以操作你本地的谷歌浏览器~~感兴趣的小伙伴可以自己去搜一搜哈,Puppeteer工具还是很强大的.

    好啦,我们继续回到我们的入口文件:

      PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
    

    可以看到,执行完initialize方法后(启动了一个server,并且初始化了Puppeteer工具),执行了:

    return PrerendererInstance.renderRoutes(this._options.routes || [])
    
    async renderRoutes(routes, Prerenderer) {
        const rootOptions = Prerenderer.getOptions()
        const options = this._rendererOptions
    
        const limiter = promiseLimit(this._rendererOptions.maxConcurrentRoutes)
    
        const pagePromises = Promise.all(
          routes.map(
            (route, index) => limiter(
              async () => {
                const page = await this._puppeteer.newPage()
    
                if (options.consoleHandler) {
                  page.on('console', message => options.consoleHandler(route, message))
                }
    
                if (options.inject) {
                  await page.evaluateOnNewDocument(`(function () { window['${options.injectProperty}'] = ${JSON.stringify(options.inject)}; })();`)
                }
    
                const baseURL = `http://localhost:${rootOptions.server.port}`
    
                // Allow setting viewport widths and such.
                if (options.viewport) await page.setViewport(options.viewport)
    
                await this.handleRequestInterception(page, baseURL)
    
                // Hack just in-case the document event fires before our main listener is added.
                if (options.renderAfterDocumentEvent) {
                  page.evaluateOnNewDocument(function (options) {
                    window['__PRERENDER_STATUS'] = {}
                    document.addEventListener(options.renderAfterDocumentEvent, () => {
                      window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED = true
                    })
                  }, this._rendererOptions)
                }
    
                const navigationOptions = (options.navigationOptions) ? Object.assign({waituntil: 'networkidle0'}, options.navigationOptions) : {waituntil: 'networkidle0'};
                await page.goto(`${baseURL}${route}`, navigationOptions);
    
                // Wait for some specific element exists
                const {renderAfterElementExists} = this._rendererOptions
                if (renderAfterElementExists && typeof renderAfterElementExists === 'string') {
                  await page.waitForSelector(renderAfterElementExists)
                }
                // Once this completes, it's safe to capture the page contents.
                await page.evaluate(waitForRender, this._rendererOptions)
    
                const result = {
                  originalRoute: route,
                  route: await page.evaluate('window.location.pathname'),
                  html: await page.content()
                }
    
                await page.close()
                return result
              }
            )
          )
        )
    
        return pagePromises
      }
    

    又是一长串注释,小伙伴不要被吓到哈,首先是遍历我们在webpack.prod.conf.js配置文件中传入的routes数组:

    webpackConfig.plugins.push(new PrerenderSPAPlugin({
      staticDir: path.join(config.build.assetsRoot),
      routes: ['/'],
      renderer: new Renderer({
        headless: false,
        renderAfterDocumentEvent: 'render-event'
      })
    }))
    

    可以看到,我们在webpack的配置文件中传入了 routes: [’/’]:

    async renderRoutes(routes, Prerenderer) {
        const rootOptions = Prerenderer.getOptions()
        const options = this._rendererOptions
    
        const limiter = promiseLimit(this._rendererOptions.maxConcurrentRoutes)
    
        const pagePromises = Promise.all(
          routes.map(
            (route, index) => limiter(
              async () => {
                //开启一个新页面
                const page = await this._puppeteer.newPage()
                //是否需要console回调
                if (options.consoleHandler) {
                  page.on('console', message => options.consoleHandler(route, message))
                }
                //是否需要给window对象中注入内容
                if (options.inject) {
                  await page.evaluateOnNewDocument(`(function () { window['${options.injectProperty}'] = ${JSON.stringify(options.inject)}; })();`)
                }
                //获取需要预渲染页面的地址
                const baseURL = `http://localhost:${rootOptions.server.port}`
    
                // 设置打开后窗体的大小(可以指定屏幕的大小{width: 375,height:1440})
                if (options.viewport) await page.setViewport(options.viewport)
                //拦截页面中的请求
                await this.handleRequestInterception(page, baseURL)
    
                // Hack just in-case the document event fires before our main listener is added.
                //监听预加载结束的通知
                if (options.renderAfterDocumentEvent) {
                  page.evaluateOnNewDocument(function (options) {
                    window['__PRERENDER_STATUS'] = {}
                    document.addEventListener(options.renderAfterDocumentEvent, () => {
                      window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED = true
                    })
                  }, this._rendererOptions)
                }
                
                const navigationOptions = (options.navigationOptions) ? Object.assign({waituntil: 'networkidle0'}, options.navigationOptions) : {waituntil: 'networkidle0'};
                //打开指定页面
                await page.goto(`${baseURL}${route}`, navigationOptions);
    
                // Wait for some specific element exists
                const {renderAfterElementExists} = this._rendererOptions
                if (renderAfterElementExists && typeof renderAfterElementExists === 'string') {
                  await page.waitForSelector(renderAfterElementExists)
                }
                // 等待预加载结束
                await page.evaluate(waitForRender, this._rendererOptions)
                
                const result = {
                  originalRoute: route,
                  route: await page.evaluate('window.location.pathname'),
                  html: await page.content()
                }
                
                await page.close()
                //返回加载过后的结果
                return result
              }
            )
          )
        )
    
        return pagePromises
      }
    

    遍历完所有页面,并打开所有页面,然后拿到所有页面的返回结果(静态html内容),主要流程算是已经走完了.

    我们回到入口文件:

     PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          // Backwards-compatibility with v2 (postprocessHTML should be migrated to postProcess)
          .then(renderedRoutes => this._options.postProcessHtml
            ? renderedRoutes.map(renderedRoute => {
              const processed = this._options.postProcessHtml(renderedRoute)
              if (typeof processed === 'string') renderedRoute.html = processed
              else renderedRoute = processed
    
              return renderedRoute
            })
            : renderedRoutes
          )
          // Run postProcess hooks.
          .then(renderedRoutes => this._options.postProcess
            ? Promise.all(renderedRoutes.map(renderedRoute => this._options.postProcess(renderedRoute)))
            : renderedRoutes
          )
          // Check to ensure postProcess hooks returned the renderedRoute object properly.
          .then(renderedRoutes => {
            const isValid = renderedRoutes.every(r => typeof r === 'object')
            if (!isValid) {
              throw new Error('[prerender-spa-plugin] Rendered routes are empty, did you forget to return the `context` object in postProcess?')
            }
    
            return renderedRoutes
          })
          // Minify html files if specified in config.
          .then(renderedRoutes => {
            if (!this._options.minify) return renderedRoutes
    
            renderedRoutes.forEach(route => {
              route.html = minify(route.html, this._options.minify)
            })
    
            return renderedRoutes
          })
          // Calculate outputPath if it hasn't been set already.
          .then(renderedRoutes => {
            renderedRoutes.forEach(rendered => {
              if (!rendered.outputPath) {
                // rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route, 'index.html')
                rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route)
              }
            })
    
            return renderedRoutes
          })
          // Create dirs and write prerendered files.
          .then(processedRoutes => {
            const promises = Promise.all(processedRoutes.map(processedRoute => {
              return mkdirp(path.dirname(processedRoute.outputPath))
                .then(() => {
                  return new Promise((resolve, reject) => {
                    compilerFS.writeFile(processedRoute.outputPath, processedRoute.html.trim(), err => {
                      if (err) reject(`[prerender-spa-plugin] Unable to write rendered route to file "${processedRoute.outputPath}" \n ${err}.`)
                      else resolve()
                    })
                  })
                })
                .catch(err => {
                  if (typeof err === 'string') {
                    err = `[prerender-spa-plugin] Unable to create directory ${path.dirname(processedRoute.outputPath)} for route ${processedRoute.route}. \n ${err}`
                  }
    
                  throw err
                })
            }))
    
            return promises
          })
          .then(r => {
            PrerendererInstance.destroy()
            done()
          })
          .catch(err => {
            PrerendererInstance.destroy()
            const msg = '[prerender-spa-plugin] Unable to prerender all routes!'
            console.error(msg)
            compilation.errors.push(new Error(msg))
            done()
          })
    

    可以看到我们已经跑完了:

    PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
    

    接着就是对结果的处理工作了,包括文件压缩、重组文件等等…

    PrerenderSPAPlugin.prototype.apply = function (compiler) {
      const compilerFS = compiler.outputFileSystem
    
      // From https://github.com/ahmadnassri/mkdirp-promise/blob/master/lib/index.js
      const mkdirp = function (dir, opts) {
        return new Promise((resolve, reject) => {
          compilerFS.mkdirp(dir, opts, (err, made) => err === null ? resolve(made) : reject(err))
        })
      }
    
      const afterEmit = (compilation, done) => {
        const PrerendererInstance = new Prerenderer(this._options)
    
        PrerendererInstance.initialize()
          .then(() => {
            return PrerendererInstance.renderRoutes(this._options.routes || [])
          })
          // 可以传递postProcessHtml参数对返回的html做处理
          .then(renderedRoutes => this._options.postProcessHtml
            ? renderedRoutes.map(renderedRoute => {
              const processed = this._options.postProcessHtml(renderedRoute)
              if (typeof processed === 'string') renderedRoute.html = processed
              else renderedRoute = processed
    
              return renderedRoute
            })
            : renderedRoutes
          )
          //可以传递postProcess函数进一步对结果进行处理
          .then(renderedRoutes => this._options.postProcess
            ? Promise.all(renderedRoutes.map(renderedRoute => this._options.postProcess(renderedRoute)))
            : renderedRoutes
          )
          //验证返回的结果
          .then(renderedRoutes => {
            const isValid = renderedRoutes.every(r => typeof r === 'object')
            if (!isValid) {
              throw new Error('[prerender-spa-plugin] Rendered routes are empty, did you forget to return the `context` object in postProcess?')
            }
    
            return renderedRoutes
          })
          // 是否需要压缩打包过后的html文件
          .then(renderedRoutes => {
            if (!this._options.minify) return renderedRoutes
    
            renderedRoutes.forEach(route => {
              route.html = minify(route.html, this._options.minify)
            })
    
            return renderedRoutes
          })
          // 文件重组
          .then(renderedRoutes => {
            renderedRoutes.forEach(rendered => {
              if (!rendered.outputPath) {
                // rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route, 'index.html')
                rendered.outputPath = path.join(this._options.outputDir || this._options.staticDir, rendered.route)
              }
            })
    
            return renderedRoutes
          })
          // 重组并输出
          .then(processedRoutes => {
            const promises = Promise.all(processedRoutes.map(processedRoute => {
              return mkdirp(path.dirname(processedRoute.outputPath))
                .then(() => {
                  return new Promise((resolve, reject) => {
                    compilerFS.writeFile(processedRoute.outputPath, processedRoute.html.trim(), err => {
                      if (err) reject(`[prerender-spa-plugin] Unable to write rendered route to file "${processedRoute.outputPath}" \n ${err}.`)
                      else resolve()
                    })
                  })
                })
                .catch(err => {
                  if (typeof err === 'string') {
                    err = `[prerender-spa-plugin] Unable to create directory ${path.dirname(processedRoute.outputPath)} for route ${processedRoute.route}. \n ${err}`
                  }
    
                  throw err
                })
            }))
    
            return promises
          })
          //预加载结束
          .then(r => {
            PrerendererInstance.destroy()
            done()
          })
          .catch(err => {
            PrerendererInstance.destroy()
            const msg = '[prerender-spa-plugin] Unable to prerender all routes!'
            console.error(msg)
            compilation.errors.push(new Error(msg))
            done()
          })
      }
    

    好啦,预加载的全部内容都结束了~~
    总结一下:
    1、利用puppeteer预加载页面,页面的加载出来样式就是某一种设备的效果,页面兼容性会有问题.
    2、绕了一圈,感觉这是在模仿jsp页面呀(哈哈哈)
    3、比如页面有一个轮播图,加载出来的页面是没法滚动的,因为代码都在js文件中.

    做一些静态的、简单的比如“公司介绍页面”这样的用用预渲染还不错,其它页面就算了,意义不大,你们觉得呢? 哈哈~ 欢迎一起交流~

    这节先到这里了,下一节我们照着老套路从头到尾的撸一遍源码.
    先到这里啦,欢迎志同道合的小伙伴入群,一起交流一起学习~~ 加油骚年!!
    qq群链接:
    在这里插入图片描述

    展开全文
  • $ php composer.phar require zfr/zfr-prerender:3. * 文献资料 这个怎么运作 检查以确保我们应该显示预渲染的页面 检查请求是否来自搜寻器(代理字符串或通过检测escaped_fragment查询参数) 检查以确保我们没有...
  • #prerender-header-forwarder Prerender ( ) 插件,使您能够指定一组标头,以便在原始请求中出现时将其转发到 phantomJs。 ##如何使用 在您的本地预渲染项目中运行: $ npm install prerender-header-...
  • 插件引入和配置 首先,我们需要引入一个预渲染插件,执行命令: mnpm i prerender-spa-plugin -D 这个命令除了安装插件本身以外,依赖了puppeteer,然后puppeteer又依赖落地chromium,所以最后我们其实是需要在依赖...

    背景

    因为之前的网站是使用Vue开发的,这种前端JavaScript渲染的开发模式,对于搜索引擎来说非常的不友好,没有办法抓取到有效的信息。因此为了进行SEO,我们需要对页面进行一些预渲染。

    预渲染比较适合静态或者变化不大的页面,能够通过部署前的一次静态渲染,将页面上大部分内容都渲染出来。这样搜索引擎在爬取的时候,就能够爬到相关的内容信息。

    现状

    目前商企通官网情况列举如下:

    • 技术栈使用的是Vue,脚手架使用的是vue-cli,使用JavaScript前端渲染方案(这个方案对技术栈没有要求,兼容所有方案)
    • 发布工具使用的是公司的工具,打包过程中,HTML资源传递到A域名下,CSS、JS、Image等资源传递到B域名下。

    目标

    希望能够通过预渲染,让页面在初次访问没有执行JavaScript时,就能够携带足够的信息,即将JavaScript渲染的内容提前渲染到HTML中。

    发布期望不做过多的修改。

    方案

    我们本次方案主要采用的是prerender-spa-plugin这个webpack的插件来实现的。

    它的主要原理是启动浏览器,渲染完成后抓取HTML,然后再替换掉原有HTML。

    我们需要实现预渲染,那么我们需要完成以下几件事情:

    1. 插件引入和配置。
    2. 本地验证。
    3. 改造打包构建流程。
    4. 线上验证。

    下面,我们一个一个来说下,我们如何做这个事情的。

    插件引入和配置

    首先,我们需要引入一个预渲染插件,执行命令:

    mnpm i prerender-spa-plugin -D

    这个命令除了安装插件本身以外,依赖了puppeteer,然后puppeteer又依赖落地chromium,所以最后我们其实是需要在依赖中安装一个chromium。

    安装完成后,我们就可以在webpack的配置文件中增加对应的配置了。

    如果大家使用的也是vue-cli,那么我们需要增加的配置是在vue.config.js中,如果是直接修改webpack的配置,那么方法也是类似。

    下面我们以vue.config.js的修改为例:

    const PrerenderSPAPlugin = require('prerender-spa-plugin');
    
    module.exports = {
      ...,
      configureWebpack: {
        ...,
        chainWebpack: config => {
          config.plugin('prerender').use(PrerenderSPAPlugin, [
            {
              staticDir: path.join(__dirname, 'build'),
              routes: [
                '/',
                '/product',
                '/case',
                '/about',
                '/register',
              ],
              renderer: new Renderer({
                headless: true,
                executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
                // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
                renderAfterDocumentEvent: 'render-event',
              }),
            },
          ]);
        }
      }
    }

    因为我们在项目中使用了webpack-chain,所以我们的语法是上面类似链式调用的方法。如果大家直接修改的话,就是采用vue的原来的修改配置的方式。

    下面我简单的给大家介绍下,上面的一些配置的含义:

    • staticDir:这个指的是输出预渲染文件的目录。
    • routes:这个指的是需要预渲染的路由。这里需要注意的是,vue的hash路由策略是没有办法进行预渲染的,所以如果要进行预渲染,需要改成history路由,然后预渲染后会变成多个HTML文件,每个文件都带全量路由功能,只是默认路由不一样而已。
    • renderer:这个是可以传入的puppeteer的配置,我说下我用过的这几个配置:

        - headless:是否使用headless模式渲染,建议选择true。

        - executablePath:指定chromium的路径(也可以是chrome)。这个配置在talos中是需要指定的,talos中的chrome地址默认是/usr/bin/google-chrome。

        - renderAfterDocumentEvent:这个的意思是在哪个事件触发后,进行预渲染的抓取。这个事件是需要在代码中自己使用dispatchEvent来触发的,这样自己可以控制预渲染的时机。

    一般我们都是在最外层的组件的mounted钩子中触发,如果大家有其他需求也可以自己指定。

    开发完成后,我们可以在本地构建一次,看看是否能够生成符合我们预期的代码。

    vue.config.js指定publicPath导致预渲染失败问题

    如果大家和我这个项目一样,在vue.config.js中传入publicPath指定第三方CDN域名,会将CSS、JavaScript、Image等资源传递到不同的域名上,类似配置如下:

    module.exports = {
      ...,
      publicPath: `//awp-assets.cdn.net/${projectPath}`,
      ...,
    };

    如果没有预渲染,这种方案会在打包完成后分别上传至不同的CDN域名,在线上访问是没有问题的。

    但是在本地,这个时候CSS和JS资源还没有上传到CDN中,浏览器无法加载对应的资源进行页面的渲染,这样的话会导致本地预渲染失败。

    为了解决这个问题,有两个解决思路。

    1. 【推荐】调整打包的策略,将非HTML资源也上传至同一个CDN域名下,这样的话,我们就可以使用相对路径来访问这些资源,不需要传递新域名给publicPath,这样我们在本地构建的时候就可以访问到这些值。这个是个比较靠谱合理的方法,比较推荐。
    2. (如果上面那个方法实在无法实现,那么可以考虑这个方案)在预渲染之前,资源是在本地可以通过相对路径访问到的,这个时候使用替换的方式把HTML中的资源文件地址替换掉,然后预渲染完成后再替换回来。这个方法比较hack,但是经过实际验证确实是可以生效。具体的做法是自己写一个简单的webpack插件。

        首先,我们需要安装一个新的NPM包,用来对文件中的内容进行替换(自己写正则也可以,不过用这个会方便一些),具体命令如下:

    mnpm i replace-in-file

        安装后,我们需要增加两个webpack的插件,分别作用在afterEmit和done这两个钩子节点上。如果想要了解为什么是这两个钩子节点,那么你可以阅读下webpack插件的开发章节。

    const replace = require('replace-in-file');
    
    let publicPath = `//awp-assets.cdn.net/${projectPath}`;
    
    // 第1个替换插件,主要是将原先打包过程中带有CDN域名的路径替换成相对路径
    function ReplacePathInHTMLPlugin1(cb) {
      this.apply = compiler => {
        if (compiler.hooks && compiler.hooks.afterEmit) {
          compiler.hooks.afterEmit.tap('replace-url', cb);
        }
      };
    }
    
    function replacePluginCallback1() {
      replace({
        files: path.join(__dirname, '/build/**/*.html'),
        from: new RegExp(
          publicPath.replace(/([./])/g, (match, p1) => {
            return `\\${p1}`;
          }),
          'g'
        ),
        to: '',
      })
        .then(results => {
          console.log('replace HTML static resources success', results);
        })
        .catch(e => {
          console.log('replace HTML static resources fail', e);
        });
    }
    
    // 第2个替换插件,主要是将预渲染后的HTML文件中的相对路径替换成带有CDN域名的路径
    function ReplacePathInHTMLPlugin2(cb) {
      this.apply = compiler => {
        if (compiler.hooks && compiler.hooks.done) {
          compiler.hooks.done.tap('replace-url', cb);
        }
      };
    }
    
    function replacePluginCallback2() {
      replace({
        files: path.join(__dirname, '/build/**/*.html'),
        from: [/href="\/css/g, /href="\/js/g, /src="\/js/g, /href="\/favicon.ico"/g],
        to: [
          `href="${publicPath}/css`,
          `href="${publicPath}/js`,
          `src="${publicPath}/js`,
          `href="${publicPath}/favicon.ico"`,
        ],
      })
        .then(results => {
          console.log('replace HTML static resources success', results);
        })
        .catch(e => {
          console.log('replace HTML static resources fail', e);
        });
    }

    上述代码就是我们需要增加的两个webpack的替换插件和对应的回调函数,接下来我们看下在webpack中怎么配置。

    module.exports = {
      publicPath,
      outputDir,
      crossorigin: 'anonymous',
      chainWebpack: config => {
        config.plugin('replaceInHTML').use(new ReplacePathInHTMLPlugin1(replacePluginCallback));
        config.plugin('prerender').use(PrerenderSPAPlugin, [
          {
            staticDir: path.join(__dirname, 'build'),
            // 我们应该只会使用根路径,因为是hash路由,所以其他页面预渲染没有意义,因此不进行预渲染
            routes: ['/'],
            renderer: new Renderer({
              headless: true,
              executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
              // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
              renderAfterDocumentEvent: 'render-event',
            }),
          },
        ]);
        config.plugin('replaceInHTML2').use(new ReplacePathInHTMLPlugin2(replacePluginCallback2));
      }

    我们第一个替换插件,需要在预渲染插件前执行,在预渲染插件执行前,将HTML中的资源的地址替换成本地的相对路径;第二个则需要在替换后执行,这样将预渲染后端资源中的相对路径,再替换成CDN地址。

    通过这两个插件,我们就可以完成在预渲染前替换掉路径完成预渲染,然后在预渲染后再完成替换保证线上可用。

    本地验证

    通过上面的方式,我们应该已经得到了一个预渲染完成的HTML,接下来我们就是要验证下这个HTML是否符合预期了。

    比较简单的验证方式,可以直接访问那个HTML文件,或者启动一个HTTP静态资源服务来验证。

    验证的话,你可以使用curl来进行请求,这种情况下JavaScript不会执行,你可以看到HTML的源文件是什么。

    如果你想开发小程序或者了解更多小程序的内容,可以通过专业开发公司,来帮助你实现开发需求:厦门在乎科技-专注厦门小程序定制开发、APP开发、网站开发、H5小游戏开发

    展开全文
  • 预渲染插件prerender-spa-plugin使用总结

    千次阅读 2019-09-26 19:59:04
    最近两周刚刚接手工程化的工作,领导希望能够集成一个可以预渲染html的插件来解决webpack生成的空白模板问题。 通过调研发现webpack有一个prerender-spa-plugin的插件完全可...

    最近两周刚刚接手工程化的工作,领导希望能够集成一个可以预渲染html的插件来解决webpack生成的空白模板问题。

    通过调研发现webpack有一个prerender-spa-plugin的插件完全可以满足需求,故将这个插件集成到我们内部的构建流程中。整个过程还是有很多坑的,主要我对node、webpack这些还是小白的阶段,所以犯了很多错误,特意记录一下。

    为什么要使用prerender-spa-plugin

    主要原因有两点:

    1、SEO:SPA应用的SEO比较差,爬虫爬到的页面结构大多只有入口页,然而入口页又几乎没有什么内容,因此搜索排名很低。

    2、首屏体验:由webpack生成的入口页基本上只有一个主js,本身页面没有内容,因此在这个js加载完成之前,用户看到的都是一片空白。为了解决这个问题,一般是把规定好的静态页给后台,用户请求直接返回的是静态页,这样在主js加载之前用户还是能看到一些东西的。但是这样也会有问题,比如我们想做一个Loading的组件放到首屏,就非常麻烦,同时也是又一个和后台沟通的成本(其实就是大佬不想总是依赖后台-_-)。所以如果我们自己能直接生成体验良好的首屏页面,那肯定是极好的。

    这就是prerender-spa-plugin的作用,它能够直接生成拥有静态结构的html,可以尽情地往首屏里面放入你想展现的东西。

    安装

    执行npm i prerender-spa-plugin即可,但是如果不能够翻墙的话就会遇到一个坑。

    prerender-spa-plugin插件是需要依赖puppeteer的,也就是谷歌出品的无头浏览器插件,这个插件会下载最新版的chromium(大约200M ),所以如果不能翻墙,下载的时候就报错了。

    如图中所示,可以通过设置参数跳过该步骤,但是如果省略了,调用的时候还会报错。

    网上大多搜到的解决办法都是针对使用puppeteer本身的,通过指定本机路径再启动,但是prerender-spa-plugin在puppeteer外层,因此并不适用。这个问题处理的过程比较长,我放到下面再描述,这里先讲能够翻墙的情况下之后的使用。

    webpack中配置

    使用的主要的目的就是取代build后生成的html,因此需要修改的就是webpack.prod.conf.js这个文件,一般在这文件中如果使用脚手架工具会有html-webpack-plugin的插件,如果有就不要动了,因为prereder的插件是需要这个插件生成的html作为模板的,只需要在html-webpack-plugin的配置后加上新的配置即可。

    const path = require('path')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const baseWebpackConfig = require('./webpack.base.conf')
    const PrerenderSPAPlugin = require('prerender-spa-plugin')
    const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
     
    const webpackConfig = merge(baseWebpackConfig, {
        plugins: [
            // vue-cli生成的配置中就已有这个了,不要动
            new HtmlWebpackPlugin({
                filename: config.build.index,
                template: 'index.html',
                inject: true,
                minify: {
                    removeComments: true,
                    collapseWhitespace: true,
                    removeAttributeQuotes: true
                },
                chunksSortMode: 'dependency'
            }),
            
            // 在vue-cli生成的文件的基础上,只有下面这个才是我们要配置的
            new PrerenderSPAPlugin({
                // 生成文件的路径,也可以与webpakc打包的一致。
                // 下面这句话非常重要!!!
                // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
                staticDir: path.join(__dirname, '../dist'),
                
                // 对应自己的路由文件,比如index有参数,就需要写成 /index/param1。
                routes: ['/', '/index', '/skin', '/slimming', '/exercise', '/alPay', '/wxPay'],
                
                // 这个很重要,如果没有配置这段,也不会进行预编译
                renderer: new Renderer({
                    inject: {
                      foo: 'bar'
                    },
                    headless: false,
                    // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
                    renderAfterDocumentEvent: 'render-event'
                })
            })
        ]
    })
    
    

    到此为止,如果只是个人使用的话,就全部OK了,之后在需要预渲染的页面的js中触发docurender-event事件(比如vue中就在main.js的mounted函数中加上dispatchEvent),执行npm run build 就会生成预渲染的html,至于具体的配置项,可以去github上的prerender-spa-plugin查看 https://github.com/chrisvfritz/prerender-spa-plugin

    踩坑

    首先我们的项目是要集成到所有开发人员都要是使用的工程化工具中,因此就需要考虑刚才安装时候的两个问题:
    1、假设所有的开发人员都不能翻墙,要怎么下载这个插件?
    2、200M大小的chromium是不是太大?如果每个项目都要装这么大的插件很显然会太臃肿。

    针对这两个问题,我们最终讨论的方案如下:
    1、将prerender-spa-plugin变成我们自己的组件(简称myPlugin)
    2、将myPlugin中的puppeteer依赖指向全局的myPUppeteer(本来是指向它自己的puppeteer)
    3、将prerender-spa-plugin中依赖的puppeteer组件也变成我们自己的(用于解除强依赖,简称myPuppeteer)
    4、我个人将不同系统的最新版chromium下载到公司的ftp中
    5、修改myPuppeteer组件里下载chromium的地址,指向ftp
    6、在myPlugin中添加错误提示,当require不到puppeteer时提示开发者全局安装myPuppeteer

    这样一通操作的最终效果:我们的开发人员只需要在第一次使用该插件时,全局安装一次myPuppeteer,并且不会有网络问题。

    当然每个项目还是要安装myPlugin的

    下面按照这个顺序说一下要怎么搞。

    实现步骤

    步骤一/二

    首先要找到哪里依赖了puppeteer

    1、prerender-spa-plugin里:

    2、@prerender/renderer-puppeteer里:

    所以我们需要在@prerender/renderer-puppeteer里替换掉依赖puppeteer的这句话

    try{
        puppeteer = require('prerender-browser')
    }catch(err){
        console.log("You are using prerender-html-plugin to generate html but no puppeteer installed");
        console.log("If you don't want this function, you can set prerender to false in marauder.config");
        console.log("If you want this function, you can run:1、npm install -g prerender-browser  2、npm link prerender-browser");
    }
    

    prerender-browser就是上文中的myPuppeteer组件。

    try catch是为了能够在使用过程中可以在控制台输出报错语句。

    这里可以看到我提示大家执行两句node命令

    1、npm install -g prerender-browser

    2、npm link prerender-browser

    这里涉及到一个全局包的引用问题:
    当我全局安装并且代码引用之后,我发现还是会报没有定义的错误。经过查询后发现require会从node_modules中查询所需的包,直到最顶层的全局变量。如果全局安装但并没有生效,就是因为环境变量中没有配置全局的node_modules路径。

    所以在网上很多方法都是要去设置环境变量,但还有另一种简单的方法就是直接执行node link <package_name> 这个命令。npm link命令可以将一个任意位置的npm包链接到全局执行环境,这样就不会有引用无法找到的问题了。

    步骤三/四/五

    在puppeteer的BrowserFetcher.js文件中可以找到DEFAULT_DOWNLOAD_HOST和downloadURLs这两个变量,将这两个变量替换为公司内部可以访问的资源地址即可。

    const DEFAULT_DOWNLOAD_HOST = 'http://www.fanqiangma.com';
    const downloadURLs = {
      linux: '%s/chromium-browser-snapshots/Linux/chrome-linux.zip',
      mac: '%s/chromium-browser-snapshots/Mac/chrome-mac.zip',
      win32: '%s/chromium-browser-snapshots/Win32/chrome-win32.zip',
      win64: '%s/chromium-browser-snapshots/Win64/chrome-win32.zip',
    };
    

    这里还有两个坑:

    1、在BrowserFetcher.js代码的下载地址上原本会拼上版本号,比如:

    const url = util.format(downloadURLs[this._platform], this._downloadHost,revision);
    
    

    revision就代表版本号,这个版本号是从puppeteer的package.json中读取的,这个参数即用来拼接下载的url,同时还用来指定chromium的执行地址。这里我们只需要修改下载地址,所以我将代码中需要拼接版本号的url都去掉了revision这个参数,否则会提示无法下载。

    2、即便安装时能够下载了,但在运行时,会报无法关闭puppeteer实例的错误。看代码发现,puppeteer在启动的时候(launch函数)还需要指定到chromium的安装地址,这里还需要上面的revision参数,修改Puppeteer.js中的启动函数

      static launch(options) {
        options.executablePath = this.executablePath('revision');
        return Launcher.launch(options);
      }
    

    这里的代码比较迷,我也没有太看懂,为什么只是修改了下载地址却没有自动指定启动目录,有兴趣的大佬可以探究一下。

    TIPS:

    提取和发布组件的时候遇到两个小问题:
    1、将组件拆分成自己的组件之后,有一些依赖就需要咱们手动添加到package.json中了,这个大家根据提示一一加上就好。
    2、在npm发布组件的时候遇到了一个非常白痴的错误,首先淘宝源是不能够发布组件的,必选换成npm源,但是我当时在网上搜到的大多数npm源都是http://www.npmjs.org。这个源安装依赖是不会有问题的,但是如果想要发布必须是 https://registry.npmjs.org,否则就会报错。这个真的是坑死我了,我一开始都没有怀疑过npm源的正确性,而且windos使用npm发包还真的就有issue。。。。。

    结尾

    到此,所有的工作就全部完成了,经过试验,我们组的小伙伴都可以愉快的生成静态页了。

    这是我第一次写文章记录工作中的技术,也是第一次用markdown,可能写的不太好,啰嗦的地方或者没有提到的点还希望见谅,如果有问题欢迎指出。

    参考文章:
    1、 puppeteer 安装失败的解决办法
    2、 vue项目做seo(prerender-spa-plugin预渲染)

    展开全文
  • 使用Prerender.io进行网站预渲染

    千次阅读 2019-01-29 22:53:50
    文章目录前言目标运行流程图安装中间件安装Prerender服务安装Chrome启动Prerender.io服务测试If you use html5 push state (recommended):If you use the hashbang (#!):通过curl命令测试 前言 使用Angular,Vue,...

    前言

    使用Angular,Vue,React进行单页网站开发,用户浏览时浏览器动态解析JS,呈现出最终的页面,用户体验比较好,网站性能也提高不少。

    但网络爬虫并不会动态解析Js,访问所有URL得到的只会是项目入口文件中的代码,不能得到具体的内容,也就无法做网站SEO。

    使用Prerender.io做网站预渲染,可以将网站页面渲染之后再返回给网络爬虫,间接完成网页的解析。
    Prerender相较于其他的解决方案,配置相对要简单一些,不用修改项目源码,代码零侵入,是一个不错的解决方案。

    目标

    搭建基于Centos 7 和 Nginx 环境的Prerender渲染服务,完成Angular项目中网页的预渲染

    运行流程图

    运行流程

    安装中间件

    1. 首先注册登录 Prerender.io,并且获得个人token
      token
    2. 根据开发文档,配置对应的中间件,如Nginx,Apache等。
    3. 配置Nginx中间件,参考配置如下:
    server {
        listen 80;
        server_name example.com;
     
        root   /path/to/your/root;
        index  index.html;
    
        location / {
            try_files $uri @prerender;
        }
     
        location @prerender {
            # 将 YOUR_TOKEN替换为你的个人token
            proxy_set_header X-Prerender-Token YOUR_TOKEN;
            
            set $prerender 0;
            if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator") {
                set $prerender 1;
            }
            if ($args ~ "_escaped_fragment_") {
                set $prerender 1;
            }
            if ($http_user_agent ~ "Prerender") {
                set $prerender 0;
            }
            if ($uri ~* "\.(js|css|xml|less|png|jpg|jpeg|gif|pdf|doc|txt|ico|rss|zip|mp3|rar|exe|wmv|doc|avi|ppt|mpg|mpeg|tif|wav|mov|psd|ai|xls|mp4|m4a|swf|dat|dmg|iso|flv|m4v|torrent|ttf|woff|svg|eot)") {
                set $prerender 0;
            }
            
            #resolve using Google's DNS server to force DNS resolution and prevent caching of IPs
            resolver 8.8.8.8;
     
            if ($prerender = 1) {
                
                # 后续将service.prerender.io替换为自己的prerender服务,如127.0.0.1:3000
                set $prerender "service.prerender.io";
                rewrite .* /$scheme://$host$request_uri? break;
                proxy_pass http://$prerender;
            }
            if ($prerender = 0) {
                rewrite .* /index.html break;
            }
        }
    }
    

    参考配置:https://gist.github.com/thoop/8165802
    5. 检测nginx配置,并重启nginx

    nginx -t
    service nginx restart
    
    1. 中间件安装完成

    安装Prerender服务

    1. 在服务器上安装Node环境
    2. 下载Prerender服务
    git clone https://github.com/prerender/prerender.git
    

    若没有安装git服务,可手动从Github下载再上传到/usr文件夹下,再解压到当前目录下
    3. 安装npm依赖

    cd /usr/prerender
    
    # Phantomjs 官方的下载地址会超时,此处重新指定其下载地址为淘宝镜像
    export PHANTOMJS_CDNURL=https://npm.taobao.org/mirrors/phantomjs
    
    npm install
    

    文件结构如下:
    文件结构
    4. 运行server.js

    # 启动Server.js, 默认监听3000端口
    node server.js
    

    此时,如果预先没有安装过Chrome,则会启动失败

    提示启动Chrome失败,未检测到Chrome,此时安装Chrome就好了

    为什么要安装Chrome呢,因为Prerender并不负责真正的网页解析,Prerender只负责解析前后的处理,实际是由Chrome负责网页的解析。

    安装Chrome

    1. 配置yum源
      因为国内无法访问Google,所以需要自己配置yum源,在目录 /etc/yum.repos.d/ 下新建google-chrome.repo文件
    cd /ect/yum.repos.d/
    
    touch google-chrome.repo
    
    
    1. 写入内容
    vi google-chrome.repo
    
    [google-chrome]
    name=google-chrome
    baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
    enabled=1
    gpgcheck=1
    gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub
    
    1. 安装运行
    # 国内推荐
    yum -y install google-chrome-stable --nogpgcheck
    
    1. 安装路径
      安装成功后,Chrome的安装路径应该是
      /opt/google/chrome

    默认情况下,root用户不能直接运行chrome,所以可以新建另一个用户如other来运行

    cd /opt/google/chrome
    
    su other
    
    ./chrome
    
    1. Chrome安装完成

    启动Prerender.io服务

    1. 已other用户再次运行server.js
    su other
    
    cd /usr/prerender
    
    node ./server.js
    

    此时应该是可以成功启动的,并且可以看到该服务监听3000端口

    启动结果:
    启动结果
    2. 修改nginx配置

    if ($prerender = 1) {
                
                # 修改如下:
                # set $prerender "service.prerender.io";
                set $prerender "127.0.0.1:3000";
                rewrite .* /$scheme://$host$request_uri? break;
                proxy_pass http://$prerender;
            }
    
    1. 保存重启Nginx
    2. 再次启动Prerender服务
    nohup node ./server.js &
    

    其中nohup命令是将该服务加入守护进程,避免ssh对话窗口关闭导致服务关闭,参考Linux设置Jar后台运行

    1. 如果开启了防火墙,需要将3000端口加入防火墙
    firewall-cmd —zone=public —add-port=3000/tcp —permanent
    
    # 重启防火墙
    firewall-cmd —reload
    
    1. 至此,Prerender服务已经安装并启动成功
    2. 查看端口
      端口结果
      Node,Google-Chrome,Nginx服务都应在后台运行

    测试

    If you use html5 push state (recommended):
    Just add this meta tag to the <head> of your pages
    
    <meta name="fragment" content="!"> 
    
    If your URLs look like this:
    http://www.example.com/user/1 
    
    Then access your URLs like this:
    http://www.example.com/user/1?_escaped_fragment_=
    
    If you use the hashbang (#!):
    If your URLs look like this:
    http://www.example.com/#!/user/1 
    
    Then access your URLs like this:
    http://www.example.com/?_escaped_fragment_=/user/1
    
    通过curl命令测试
    curl http://www.example.com/user/1?_escaped_fragment_=
    

    在配置prerender服务前,以上返回的只是index.html的内容, 如果配置成功则会返回解析后的内容。

    展开全文
  • Prerender.io

    2020-11-03 10:13:36
    近些年来,越来越多的JavaScript框架(即AngularJS,BackboneJS,ReactJS)变得越来越流行。许多公司和开发人员使用这些JavaScript框架开发应用程序。这些框架有很多的优势: 前端和后端独立开发 ...
  • 使用Prerender Rails Embedded可以避免在 rails 环境中安装新的 node.js 服务器,因为 rails 将启动一个phantomjs二进制文件来呈现 js 页面。 只是为了尝试技术和低流量情况,这个插件并不意味着支持高流量负载。 ...
  • 如果您使用了CLI的所有默认设置,angular-prerender将能够自己获取所有必要的信息,并且只需在命令行中调用即可执行。 npx angular-prerender 也可以跳过明确安装的角度压角器。 以下是一个完整的示例,它将生成...
  • 使用prerender-spa-plugin做预渲染,但是每次都是跑了一会,然后报错 2.解决 TimeoutError: Navigation Timeout exceeded: 30000ms exceeded 找源码,node_modules/prerender-spa-plugin/es6/index.js,143...
  • Prerender.io非常棒,它允许无头浏览器呈现您的页面。 使用此中间件,您可以拦截来自搜寻器的请求,并将其路由到外部Prerender服务,以检索所请求页面的静态HTML。 _escaped_fragment_遵守Google的_escaped_...
  • vue 项目预渲染(prerender-spa-plugin)

    千次阅读 2019-06-03 14:53:56
    服务器端渲染 vs 预渲染 (SSR vs Prerendering) 如果你调研服务器端渲染 (SSR) 只是用来...3、prerender-spa-plugin 是基于 puppeteer 如果发现少了这个,手动安装一下(npm install puppeteer --save-dev)  
  • Vue.js 2.0 + vue-router + prerender-spa-plugin 3.x Prerender SPA示例 这里使用的是prerender-spa-plugin稳定3.x版本,基于puppeteer 。如果你使用的2.x版本,则是基于PhantomJS 。 重要提示:如果您在npm ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 8,173
精华内容 3,269
关键字:

prerender