• 【Nuxt】03 Nuxt2 页面缓存、组件缓存、api缓存


    基于nuxt.js的服务端渲染项目可以通过缓存来优化的场景有以下几点:

    优化点参考文档及思路优化场景/条件特别说明检测方法
    1. 页面缓存vue官方文档页面内容不是用户特定(即对于相同的 URL,总是为所有用户渲染相同的内容)一般来说,一个页面在服务端做了持久化缓存,那么对应页面的存在的api缓存,组件缓存也就没有意义了,对于页面缓存与api缓存同时存在的情况下(有可能存在),api缓存的时间应该比页面缓存的时间小,这样是为了让api响应的内容保持最新1、代码本地测试:在asyncData中打印测试日志,页面缓存后,刷新页面后服务端不会输出测试日志;2、比较html页面加载的DOMContentLoaded时间,刷新页面可以看到缓存后的值比首次页面加载(未缓存)的值要小
    2. api缓存在axios请求与响应拦截器中去做接口响应内容不是用户特定(即对于相同的api接口URL,即总是为所有用户响应相同的内容)一般请求方式为GET的api请求比较首次请求与缓存后的api接口响应的时间
    3. 组件缓存nuxtjs官网文档 vue 官网文档不依赖与全局状态,对渲染上下文不产生副作用的子组件要缓存的组件name值必须唯一,serverCacheKey根据某个prop的值作为唯一key检测方法同页面缓存检测方法一致,这个可能几乎察觉不到
    4. asyncData函数优化Promise.all该函数中请求api接口数超过1个,多的甚至达到10,20多个,这种情况我们不能使用async await,请求完一个再接着请求下一个(同步请求接口);如果有10个接口需要请求,每个接口平均响应1s,那么至少需要10s才会响应html页面;如果使用Promise.all异步请求10个接口,那么最快接近1s响应html页面;asyncData函数会在服务端执行代码,因此一定要做好容错处理;另外如果该函数代码一直未执行完,那么页面首次响应将会被挂起,一直处于加载中对于页面首次加载,该函数执行耗时越短,页面响应时间就越短(页面加载越快)

    1、页面缓存功能模块实现

    我们在项目根目录中创建一个文件 ~/serverMiddleware/page-cache.js

    import LRUCache from 'lru-cache'
    
    const cache = new LRUCache({
     maxAge: 1000 * 60 * 2, // 有效期2分钟
     max: 1000 // 最大缓存数量
    })
    
    export default function(req, res, next) {
     // 本地开发环境不做页面缓存
     if (process.env.NODE_ENV !== 'development') {
     try {
      const cacheKey = req.url
      const cacheData = cache.get(cacheKey)
      if (cacheData) {
      return res.end(cacheData, 'utf8')
      }
      const originalEnd = res.end
      res.end = function(data) {
      cache.set(cacheKey, data)
      originalEnd.call(res, ...arguments)
      }
     } catch(error) {
      // console.log(`page-cache-middleware: ${error}`)
      next()
     }
     }
     next()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2、api缓存功能模块实现

    我们在项目根目录中分别创建两个文件 ~/plugins/axios/createCacheKey.js 与 ~/plugins/axios/cache.js ;特别坑的一点是nuxt.js开发环境cache.js插件代码在页面刷新,路由切换都相当于首次运行,因此你会发现缓存功能失效,只有在 process.env.NODE_ENV === ‘production’ 生产环境中测试有效

    // ~/plugins/axios/createCacheKey.js
    
    
    import md5 from 'md5'
    
    /**
     * 根据请求配置,是否是请求拦截器 创建缓存key
     * @param {Object} config
     * @param {Boolean} isRequest 
     */
    
    export default function createCacheKey(
     config = {},
     isRequest = false
    ) {
     const {
     url,
     data,
     params,
     method,
     baseURL,
     } = config || {}
    
     let commonUrl = url
    
     /**
     * request拦截器中config.url是未拼接baseURL的,response拦截器中response.config.url是拼接过baseURL的,
     * 为了保持统一,使用统一拼接baseURL的commonUrl;注意下面的if条件判断
     */
     if (isRequest && !commonUrl.match(baseURL) && !commonUrl.match(/^https?/)) {
     commonUrl = !!baseURL.match(/.+\/$/) ? `${baseURL.replace(/\/$/, '')}${url}` : `${baseURL}${url}`
     }
    
     // 根据请求指令,url,body体,参数生成规则
     const rule = `method=${method}-url=${commonUrl}-data=${JSON.stringify(data || {})}-params=${JSON.stringify(params || {})}`
    
     // md5加密
     return md5(rule)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    // ~/plugins/axios/cache.js
    import LRUCache from 'lru-cache'
    import axios from 'axios'
    import globalConfig from '../../global-config'
    import createCacheKey from './createCacheKey'
    
    const cache = new LRUCache({
     maxAge: 1000 * 60, // 有效期60秒,如果存在页面缓存,api缓存的时间应该比页面缓存的时间小,这样是为了让api响应的内容保持最新
     max: 1000 // 最大缓存数量
    })
    
    /**
     * matchCacheCondition 是否满足持久化缓存条件:服务端运行时 && 非本地开发环境 && api请求为get请求方式
     * @param {Object} config 请求配置
     */
    function matchCacheCondition(config = {}) {
     return process.server && process.env.NODE_ENV !== 'development' && config.method.toLowerCase() === 'get'
    }
    
    /**
     * 如果所有页面都启用了缓存,api缓存就没有必要了
     */
    export default function({ $axios, redirect }) {
     $axios.interceptors.request.use(config => {
     const { baseUrl } = globalConfig
     config.baseURL = baseUrl[process.env.environment] || baseUrl['other']
    
     // 不满足缓存条件直接return config
     if (!matchCacheCondition(config)) {
      return config
     }
    
     const cacheKey = createCacheKey(config, true)
     const cacheData = cache.get(cacheKey)
    
     if (cacheData) {
      const source = axios.CancelToken.source()
      config.cancelToken = source.token
      source.cancel({ cacheData, cacheKey, url: config.url })
      return config
     }
    
     return config
     })
    
     $axios.interceptors.response.use(response => {
     if (matchCacheCondition(response.config)) {
      cache.set(createCacheKey(response.config), response)
     }
     return response
     }, (error) => {
     if (axios.isCancel(error) && matchCacheCondition(response.config)) {
      // console.log(`当前页面组件asyncData或者fetch函数中被缓存的接口url为:${error.message.url}`)
      return Promise.resolve(error.message.cacheData)
     }
    
     // 服务端打印api接口请求错误日志
     if (process.server) {
      try {
      const {
       config: {
       url
       },
       message
      } = error || {}
      console.log(`请求url:${url},错误消息:${message}`)
      } catch(error) {
      // console.log(error)
      }
     }
    
     // 服务端,客户端统一reject错误对象,因此页面组件asyncData,fetch函数请求api接口一定要做catch处理
     return Promise.reject(error)
     })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    3、组件缓存

    vue官网文档原话:如果 renderer 在组件渲染过程中进行缓存命中,那么它将直接重新使用整个子树的缓存结果。这意味着在以下情况,你不应该缓存组件:

    • 它具有可能依赖于全局状态的子组件。
    • 它具有对渲染上下文产生副作用(side effect)的子组件。
    • 因此,应该小心使用组件缓存来解决性能瓶颈。在大多数情况下,你不应该也不需要缓存单一实例组件。适用于缓存的最常见类型的组件,是在大的 v-for 列表中重复出现的组件。由于这些组件通常由数据库集合(database collection)中的对象驱动,它们可以使用简单的缓存策略:使用其唯一 id,再加上最后更新的时间戳,来生成其缓存键(cache key):
    const LRU = require('lru-cache')
    module.exports = {
      render: {
        bundleRenderer: {
          cache: LRU({
            max: 1000, // 缓存队列长度
            maxAge: 1000 * 60 // 缓存1分钟
         })
       }
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    需要做缓存的 vue 组件, 需增加 name 以及 serverCacheKey 字段,以确定缓存的唯一键值。

    
    export default {
      name: 'zzZyHome',
      props: ['type'],
      serverCacheKey: props => props.type + '::' + props.item.last_updated
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4、页面组件asyncData函数优化

    举一个简单的例子进行优化

    {
     async asyncData({ $axios }) {
     // 1、增加catch处理,是为了让服务端,客户端运行时不报错,特别是防止服务端运行时不报错,不然页面就挂了
     // 2、catch函数返回一个resolve空字面量对象的Promise,表明dataPromise1的状态未来始终是resolved状态
     const dataPromise1 = $axios.get('/api/data1').catch(() => Promise.resolve({}))
    
     const dataPromise2 = $axios.get('/api/data2').catch(() => Promise.resolve({}))
     const dataPromise3 = $axios.get('/api/data3').catch(() => Promise.resolve({}))
     const dataPromise4 = $axios.get('/api/data4').catch(() => Promise.resolve({}))
     const dataPromise5 = $axios.get('/api/data5').catch(() => Promise.resolve({}))
     const dataPromise6 = $axios.get('/api/data6').catch(() => Promise.resolve({}))
     const dataPromise7 = $axios.get('/api/data7').catch(() => Promise.resolve({}))
     const dataPromise8 = $axios.get('/api/data8').catch(() => Promise.resolve({}))
    
     // 保证apiData有数据
     const apiData = await new Promise(resolve => {
      Promise.all([
      dataPromise1, dataPromise2, dataPromise3, dataPromise4,
      dataPromise5, dataPromise6, dataPromise7, dataPromise8,
      ])
      .then(dataGather => {
       resolve({
       data1: dataGather[0],
       data2: dataGather[1],
       data3: dataGather[2],
       data4: dataGather[3],
       data5: dataGather[4],
       data6: dataGather[5],
       data7: dataGather[6],
       data8: dataGather[7],
       })
      })
     })
    
     return apiData
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
  • 相关阅读:
    JavaScript【History 对象、Cookie 对象 、封装cookie操作、apply、call和bind函数、面向对象简介、new 命令】(十八)
    TypeScript基本信息总结
    shell/bash脚本命令的基本使用教程
    HTML <!DOCTYPE>标记
    win11 定时计划任务
    基于Java+SpringBoot+Mybaties-plus+Vue+elememt + uniapp 新闻资讯 的设计与实现
    如何写好一篇学术论文
    【springboot】8、静态资源访问
    前端面试,备考第 14 天 - 指向问题:this/call/apply/bind
    【寻找密码】python实现-附ChatGPT解析
  • 原文地址:https://blog.csdn.net/qq_38987146/article/details/133356576