nuxt-multi-cache
Version:
SSR route, component and data cache for Nuxt.js
191 lines (190 loc) • 5.49 kB
JavaScript
import {
defineComponent,
useSSRContext,
useSlots,
getCurrentInstance,
h
} from "vue";
import { useNuxtApp } from "#app";
import { encodeComponentCacheItem } from "../../helpers/cacheItem.js";
import { logger } from "../../helpers/logger.js";
import {
getExpiresValue,
getMultiCacheContext,
getCacheKeyWithPrefix
} from "./../../helpers/server.js";
import { getCacheKey, getCachedComponent, renderSlot } from "./helpers/index.js";
import { useRuntimeConfig } from "#imports";
export default defineComponent({
name: "RenderCacheable",
props: {
/**
* The tag to use for the wrapper. It's unfortunately not possible to
* implement this without a wrapper.
*/
tag: {
type: String,
default: "div"
},
/**
* Disable caching entirely for this component.
*/
noCache: {
type: Boolean,
default: false
},
/**
* The key to use for the cache entry. If left empty a key is automatically
* generated based on the props passed to the child.
* The key is automatically prefixed by the component name.
*/
cacheKey: {
type: String,
default: ""
},
/**
* Cache tags that can be later used for invalidation.
*/
cacheTags: {
type: Array,
default: () => []
},
/**
* Define a max age for the cached entry.
*/
maxAge: {
type: Number,
default: 0
},
/**
* Provide the async data keys used by the cached component.
*
* If provided the payload data will be cached alongside the component.
* If the component uses asyncData and the keys are not provided you will
* receive a hydration mismatch error in the client.
*/
asyncDataKeys: {
type: Array,
default: () => []
}
},
async setup(props) {
const slots = useSlots();
if (!slots.default) {
return () => "";
}
const defaultSlot = slots.default();
const first = defaultSlot[0];
const isServer = import.meta.server || import.meta.env.VITEST;
if (isServer && !props.noCache) {
const { debug } = useRuntimeConfig().multiCache || {};
const cacheKey = getCacheKey(props, first, debug);
if (!cacheKey) {
return () => h(props.tag, slots.default());
}
const currentInstance = getCurrentInstance();
const ssrContext = useSSRContext();
if (!ssrContext) {
if (debug) {
logger.warn("Failed to get SSR context.", props);
}
return () => h(props.tag, slots.default());
}
const getOrCreateCachedComponent = async () => {
if (!currentInstance?.parent) {
if (debug) {
logger.warn(
"Failed to get parent component in Cacheable component.",
props
);
}
return;
}
const nuxtApp = useNuxtApp();
const multiCache = getMultiCacheContext(ssrContext.event);
if (!multiCache?.component) {
return;
}
const fullCacheKey = getCacheKeyWithPrefix(cacheKey, ssrContext.event);
const cached = await getCachedComponent(
multiCache.component,
fullCacheKey
);
if (cached) {
const { data: data2, payload, expires } = cached;
if (expires) {
const now = Date.now() / 1e3;
if (now >= expires) {
return;
}
}
if (payload) {
Object.keys(payload).forEach((key) => {
nuxtApp.payload.data[key] = payload[key];
});
}
if (debug) {
logger.success("Returning cached component.", {
fullCacheKey,
payload: payload ? Object.keys(payload) : [],
expires,
props
});
}
return data2;
}
const data = await renderSlot(slots, currentInstance.parent);
try {
const cacheTags = props.cacheTags;
const payload = props.asyncDataKeys.reduce((acc, key) => {
acc[key] = nuxtApp.payload.data[key];
return acc;
}, {});
const expires = props.maxAge ? getExpiresValue(props.maxAge) : void 0;
multiCache.component.setItemRaw(
fullCacheKey,
encodeComponentCacheItem(data, payload, expires, cacheTags),
{ ttl: props.maxAge }
);
if (debug) {
logger.log("Stored component in cache.", {
file: currentInstance.type.__file,
fullCacheKey,
expires,
cacheTags
});
}
} catch (e) {
if (debug) {
logger.error("Failed to store component in cache.", {
fullCacheKey,
props
});
}
if (e instanceof Error) {
console.error(e.message);
}
}
return data;
};
try {
const cachedMarkup = await getOrCreateCachedComponent();
if (cachedMarkup) {
return () => h(props.tag, {
innerHTML: cachedMarkup
});
}
} catch (e) {
if (debug) {
logger.error("Failed to get component from cache.", {
props
});
}
if (e instanceof Error) {
console.error(e.message);
}
}
}
return () => h(props.tag, slots.default());
}
});