这个文章的主要目的是通过 redis 缓存 nuxt2 中服务端渲染的页面。从而优化加载速度以及减轻服务端的压力。
Nuxt
是什么
Nuxt.js
是一个基于 Vue.js
的开源框架,旨在为开发者提供一个简单的方式来构建高性能的 Vue
应用。它提供了许多功能,使得开发服务器端渲染(SSR
)、静态站点生成(SSG
)和单页面应用(SPA
)变得更加容易。nuxt2
官网、nuxt3
的地址
Redis
是什么
Redis
是一个开源的内存数据结构存储系统,广泛用于数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合和有序集合。它具有高性能的特性,可以处理每秒数百万个请求,具有极高的读写性能。通常会被我们使用在缓存的存储中,提高应用程序的响应速度。Redis
的官网
前期准备
首先我们本地开发的时候,本地要先安转 redis
。下载后,终端运行 redis-server
既可开启 Redis
服务。开启后终端中就能看到这个图标。
# 开启 redis-server
redis-server
# 打开 redis cli
redis-cli
Nuxt 通过 Redis 添加缓存
开始
首先我们要有一个 nuxt
项目,这里我新建一个项目,如果原先有项目,也可以直接在原项目中进行。通过官方文档,建立一个新的项目。
# npm
npm init nuxt-app nuxt2
然后就是下载依赖,选择所需框架。最后 cd nuxt2
,执行 npm run dev
跑起来。
下载 Node Redis
npm install redis@3
这里我们下载 redis
npm 包,我们这里用的nuxt2
,所以这里指定版本下载的是 redis@3
。redis@3
的文档可以看这个(https://www.npmjs.com/package/redis/v/3.1.2)。
在根目录下创建一个 cache
目录文件,用来存储 redis
相关配置。
// cache > index.js
const redis = require('redis')
const client = redis.createClient(6379, '127.0.0.1')
client.on('error', function(error) {
console.error('redis error:', error)
})
// 封装 get、set
function getRedis(key, callback) {
client.get(key, callback)
}
function setRedis(key, value, timeout = 60*60) {
client.set(key, value, redis.print)
// 设置超时时间
client.expire(key, timeout)
}
export {
getRedis,
setRedis
}
通过 redis
的文档可以看到,get [key]
的时候会传进一个回调函数,用来获取错误信息或对应的 value
。
redis@3
目前不支持原生 Promise
,这一个功能会在 v4
版本中推出。但是文档中给出了包装成 promise
的方法,利用的是 util.promisify
方法。具体的文档位置
// 文档中的 promise 包装例子
const { promisify } = require('util')
const getAsync = promisify(client.get).bind(client)
getAsync.then(console.log).catch(console.error)
在进行下一步之前,我们可以将 redis
的相关配置提取出来,可以在 cache
中创建一个 _config.js
文件,用来存储配置相关。而 redis.createClient 方法的相关配置,可以查看文档位置
// cache > _config.js
let redisConfig = {
post: 6379,
host: '127.0.0.1'
}
// production 判断,redisConfig = { ... }
module.exports = {
redisConfig
}
index
中修改
const { redisConfig } = require('./_config')
// 修改部分
// - const client = redis.createClient(6379, '127.0.0.1')
const client = redis.createClient(redisConfig.post, redisConfig.host)
写中间件
redis
相关封装完毕后,我们就可以来写中间件了。首先在根目录创建 middleware
文件夹,用来存储所有的中间件。在 nuxt
中,中间件有 middleware
和 serverMiddleware
。他们是两个完全不同的概念。
middleware
middleware
一般用于 页面 与 路由 之间执行的函数,可以用来处理用户请求前的逻辑。一般用来进行 身份验证、用户权限判断、重定向用户、数据预处理等 。适用于 客户端 与 服务端。
serverMiddleware
serverMiddleware
则是用于服务器端运行的中间件,用来处理 HTTP 请求。所以他用来 处理 API 请求、处理文件上传、或者是其他自定义的服务器逻辑。他运行在服务器接收到请求时执行。
两者的写法
// middleware
export default function ({ store, redirect }) {
// ...
}
// serverMiddleware
export default function (req, res, next) {
// ...
next()
}
cache 中间件
我们来写 cache 中间件,在 middleware 文件夹中创建一个 cache.js 文件。
这里为了区别 serverMiddleware 和 middleware。可以在 middleware 中创建 server,然后在 server 中创建 cache.js。也有在根目录中创建 server > middleware > [servermiddleware].js 的。都是可以的。
// middleware > cache.js
import { getRedis, setRedis } from '../cache'
export default function (req, res, next) {
const reg = /.(js|css|png|json)$/g
const useCache = !reg.test(req.url)
if (useCache) {
const redisKey = `PN:PAGE:${req.url}`
getRedis(redisKey, function(err, val) {
if (err) next()
else cacheMain(redisKey, val, req, res, next)
})
} else {
next()
}
}
function cacheMain(key, val, req, res, next) {
if (val) {
// 有缓存
res.end(val, 'utf-8')
} else {
// 没有缓存,存缓存,并且返回
res.origin_end = res.end
res.end = (data) => {
if (res.statusCode === 200) {
// 写入
setRedis(key, data)
}
}
res.origin_end(data, 'utf-8')
}
next()
}
然后我们在 nuxt.config.js
中配置 serverMiddleware
// nuxt.config.js
export default {
// ... 其他配置
serverMiddleware: [
'~/middleware/cache.js'
]
}
验证
这时我们在浏览器访问 localhost:3000
,然后在终端 redis-cli
中查看 keys *
,可以看到已经存在相关数据。get [key]
,即可得到我们的页面数据。
keys *
get [key]
这里我们也可以创建多个页面进行验证,在 pages
目录下创建一个 m.vue
文件,浏览器再访问 location:3000/m
<!-- pages > m.vue -->
<template>
<div>
<p style="color: blue;">移动端页面</p>
</div>
</template>
这时我们可以看到 keys *
中有两条数据。
超时检测,这个可以调小 timeout
,比如设置成 60s 来进行测试。
至此,我们已经完成了对页面的缓存。
缓存优化
存储内容
目前我们存储在 redis 中的页面数据是整个 html 文件。当前我初始化的项目的页面内容比较少,但是如果是一个正式上线的项目,页面渲染的内容就不一定是简单的内容展示了。所以这里我们来对缓存内容进行优化,缓存前进行压缩,读取后进行解压处理。
数据压缩和解压的库这里我选择用 zlib
,他对比与 pako
,更适用于服务器端。
# 安装 zlib
npm i zlib
下载完后再 cache
目录下创建 zlib.js
文件,首先我们先写一段代码测试下 zlib
的压缩和解压功能。
const zlib = require('zlib')
const str = 'hello, zlib'
let compressData
zlib.deflate(Buffer.from(str), (err, end) => {
console.log(err, end.toString('base64'), 'deflate')
compressData = end.toString('base64')
})
setTimeout(() => {
zlib.inflate(Buffer.from(compressData, 'base64'), (err, data) => {
console.log(err, data.toString(), 'inflate')
})
}, 1000)
测试可以看出加压和解压的功能是可行的,接下来就是对加压和解压功能做封装处理。
// cache > zlib.js
const zlib = require('zlib')
/**
* 压缩
* @param {*} string
*/
function compress(string) {
return new Promise((resolve, reject) => {
const buffer = Buffer.from(string)
zlib.deflate(buffer, (err, compressData) => {
if (err) { reject(err) }
else {
resolve(compressData.toString('base64'))
}
})
})
}
/**
* 解压
* @param {*} compressData
*/
function decompress(compressData) {
return new Promise((resolve, reject) => {
const buffer = Buffer.from(compressData, 'base64')
zlib.inflate(buffer, (err, decompressData) => {
if (err) { reject(err) }
else {
resolve(decompressData.toString())
}
})
})
}
export {
compress,
decompress
}
同时 middleware 中的 cache.js 要引入并使用相关方法。
// middleware > cache.js
import { compress, decompress } from "../cache/zlib"
function cacheMain(val, key, req, res, next) {
if (val) {
// 有缓存,直接获取缓存数据
decompress(val).then(decompressRes => {
res.end(decompressRes, 'utf-8')
}).catch(err => {
console.error(err)
})
} else {
// 没有缓存,通过 res.end 获取页面数据
res.original_end = res.end
res.end = (data) => {
if (res.statusCode === 200) {
// 修改位置
setRedisCache(key, data)
}
res.original_end(data, 'utf-8')
}
}
function setRedisCache(key, data) {
// setRedis(key, data)
compress(data).then(res => {
setRedis(key, res)
}).catch(() => {
// 压缩失败
return false
})
}
配置完上面的数据,我们重新运行可以项目是可以正常运行的,页面也没有异常。我们的验证重心要放在 redis 中存储的数据。在进行验证之前,我们先查看一下当前存储的页面数据大小。通过 memory usage [key]
查看。我本地的 /
页的大小是:
随后我们进行数据压缩存储,通过 get [key]
可以看出,redis
中存储的数据已经变成了 base64
格式。同时我们查看数据大小也已经发生了改变。
缓存时间
完成页面内容压缩后,我们再从缓存时间来找一下优化点。目前项目中的页面都是统一缓存时间。但是页面被访问的次数并不一定是一致的。有时我们就想,如果在页面缓存过期时间内,页面被多次访问,或到达一定的访问次数,页面缓存过期时间就跟着叠加。这个功能实现起来不难,主要是我们要去记录这个访问次数以及如何去记录这个访问次数。
目前我们 redis 中记录的内容是压缩后的 page 内容。我们可以修改这个存储内容,将存储信息变成一个对象,里面存储着访问次数,page 内容等。
const needCacheData = {
content: pageData,
accessCount: 1
}
然后存储的时候压缩 JSON.stringify(needCacheData)
,取值的时候获取 needCacheData.content,访问次数则是 accessCount。
// middleware > cache.js
function cacheMain(val, key, req, res, next) {
if (val) {
decompress(val).then(decompressRes => {
// 修改位置,这里的 decompressRes 不是 pageContent 了,而是 { accessCount, content }
const cacheData = JSON.parse(decompressRes)
if (cacheData.accessCount === 1) {
// 更新缓存
cacheData.accessCount++
setRedisCache(key, cacheData)
}
res.end(cacheData.content)
})
} else {
// ... 其他代码
// 修改部分
res.end = (data) => {
// 写入缓存前初始化缓存数据
const cacheData = {
accessCount: 1,
content: data
}
setRedisCache(key, cacheData)
}
}
}
// setRedisCache 第二个参数改成 obj
function setRedisCache(key, obj) {
// 初始化
let expire = 60 * 60
if (data.accessCount !== 1) {
expire = 24 * 60 * 60
}
compress(JSON.stringify(data)).then(res => {
setRedis(key, res, expire)
}).catch(err => {
// ...
})
}
这里的 expire
超时可以按照自己的需求设置。例如 accessCount
次数到达多少次后再加长时间等。至于验证的话,第一次访问的时候,可以通过 TTL [key]
查看当前时间,二次访问后再看过期时间。