nuxt缓存实践

13,739 阅读5分钟

nuxt是基于vuessr解决方案,可以是使用vue语法完成前后端的同构。

然而在与传统纯字符串拼接的ssr方案相比,性能就没那么好了,nuxt需要在服务端生成虚拟dom,然后再序列化出HTML字符串,我们常说nodejs的高性能指的是异步IO操作频繁的场景而非CPU操作密集的场景,毕竟nodejs是运行在单线程下的,在涉及到高并发的场景下,性能就会有所下降,可以考虑采用合理的缓存策略

nuxt的缓存可以分为组件级别缓存, API级别缓存以及页面级别缓存

组件级别的缓存

配置项nuxt.config.js的配置大概长这样子:

const LRU = require('lru-cache')
module.exports = {
  render: {
    bundleRenderer: {
      cache: LRU({
        max: 1000,                     // 最大的缓存个数
        maxAge: 1000 * 60 * 15        // 缓存15分钟
      })
    }
  }
}

并不是说配了该项就实现了组件级别的缓存,还需要在需做缓存的vue组件上增加name以及serverCacheKey字段,以确定缓存的唯一键值,比如:

export default {
  name: 'AppHeader',
  props: ['type'],
  serverCacheKey: props => props.type
}

上述组件会根据父组件传下来的type值去做缓存,键值是:AppHeader::${props.type},由此,新的请求到来时,只要父组件传下来的type属性之前处理过,就可以复用之前的渲染缓存结果,以增进性能

从该例子可以看出,如果该组件除了依赖父组件的type属性,还依赖于别的属性,serverCacheKey这里也要做出相应的改变,因此,如果组件依赖于很多的全局状态,或者,依赖的状态取值非常多,意味需要缓存会被频繁被设置而导致溢出,其实就没有多大意义了,在lru-cache的配置中,设置的最大缓存个数是1000,超出部分就会被清掉

其次,不应该缓存可能对渲染上下文产生副作用的子组件,比如,组件的createdbeforeCreated的钩子在服务端也会走,组件被缓存后就不会执行了,这些可能影响到渲染上下文的地方也要小心,更多内容请参考:组件级别缓存

一般来说,比较适合的场景是v-for大量数据的渲染,因为循环操作比较耗cpu

API级别的缓存

在服务端渲染的场景中,往往会将请求放在服务端去做,渲染完页面再返回给浏览器,而有些接口是可以去做缓存的,比如,不依赖登录态且不依赖过多参数的接口或者是单纯获取配置数据的接口等,接口的处理也是需要时间的,对接口的缓存可以加快每个请求的处理速度,更快地释放掉请求,从而增进性能

api的请求使用axiosaxios即可以在服务端使用也可是在浏览器使用,代码大概长这样子

import axios from 'axios'
import md5 from 'md5'
import LRU from 'lru-cache'

// 给api加3秒缓存
const CACHED = LRU({
  max: 1000,
  maxAge: 1000 * 3
})

function request (config) {
  let key
  // 服务端才加缓存,浏览器端就不管了
  if (config.cache && !process.browser) {
    const { params = {}, data = {} } = config
    key = md5(config.url + JSON.stringify(params) + JSON.stringify(data))
    if (CACHED.has(key)) {
      // 缓存命中
      return Promise.resolve(CACHED.get(key))
    }
  }
  return axios(config)
    .then(rsp => {
      if (config.cache && !process.browser) {
        // 返回结果前先设置缓存
        CACHED.set(key, rsp.data)
      }
      return rsp.data
    })
}

使用上跟平时使用axios还是一样的,就多加了个cache的属性标识是否需要在服务端做缓存

const api = {
  getGames: params => request({
    url: '/gameInfo/gatGames',
    params,
    cache: true
  })
}

页面级别的缓存

在不依赖于登录态以及过多参数的情况下,如果并发量很大,可以考虑使用页面级别的缓存, 在nuxt.config.js增加serverMiddleware属性

const nuxtPageCache = require('nuxt-page-cache')

module.exports = {
  serverMiddleware: [
    nuxtPageCache.cacheSeconds(1, req => {
      if (req.query && req.query.pageType) {
        return req.query.pageType
      }
      return false
    })
  ]
}

上面的例子根据链接后面的参数pageType去做缓存,如果链接后面有pageType参数,就做缓存,缓存时间为1秒,也就是说在1秒内相同的pageType请求,服务端只会执行一次完整的渲染

nuxt-page-cache参考了route-cache,写得比较简陋,你也可以重新封装下,nuxt最终返回数据是使用res.end(html, 'utf-8'),页面级别的缓存大概的思想如下:

const LRU = require('lru-cache')

let cacheStore = new LRU({
  max: 100,         // 设置最大的缓存个数
  maxAge: 200
})

module.exports.cacheSeconds = function (secondsTTL, cacheKey) {
  // 设置缓存的时间
  const ttl = secondsTTL * 1000
  return function (req, res, next) {
    // 获取缓存的key值
    let key = req.originalUrl
    if (typeof cacheKey === 'function') {
      key = cacheKey(req, res)
      if (!key) { return next() }
    } else if (typeof cacheKey === 'string') {
      key = cacheKey
    }

    // 如果缓存命中,直接返回
    const value = cacheStore.get(key)
    if (value) {
      return res.end(value, 'utf-8')
    }

    // 缓存原先的end方案
    res.original_end = res.end

    // 重写res.end方案,由此nuxt调用res.end实际上是调用该方法,
    res.end = function () {
      if (res.statusCode === 200) {
        // 设置缓存
        cacheStore.set(key, data, ttl)
      }
      // 最终返回结果
      res.original_end(data, 'utf-8')
    }
  }
}

如果缓存命中,直接将原先的计算结果返回,大大提供了性能

总结

在高并发的情况下可以考虑使用缓存,而缓存策略的使用需要视场景而定,这里不再赘述,还可以考虑使用pm2开启集群模式去管理我们的进程,从而满足更高的并发。