Skip to content

Instantly share code, notes, and snippets.

@ms-fadaei
Last active November 11, 2022 17:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ms-fadaei/3d081887d60a61b1a09a11f676810af7 to your computer and use it in GitHub Desktop.
Save ms-fadaei/3d081887d60a61b1a09a11f676810af7 to your computer and use it in GitHub Desktop.
Nuxt 2 Ultimate Cache

Nuxt.js Ultimate Cache

There are 3 different ways to use cache in Nuxt:

  1. Axios (API) Level Cache
  2. Component/Page Level Cache
  3. Route Level Cache

Axios (API) Level Cache

With an adapter in the Axios, we can easily add API Level Cache.

This is the code as a Nuxt module

import Redis from "ioredis";

export default async function () {
  const client = new Redis("127.0.0.1:6379");

  this.nuxt.hook("vue-renderer:ssr:prepareContext", (ssrContext) => {
    ssrContext.$axiosCache = client;
  });
}

And in a server plugin:

import LRUCache from "lru-cache";
import buildURL from "axios/lib/helpers/buildURL";

export default function ({$axios, ssrContext}) {
  const defaults = $axios.defaults;

  defaults.adapter = cacheAdapterEnhancer(defaults.adapter, {
    enabledByDefault: false,
    cacheFlag: "useCache",
    defaultCache: ssrContext.$axiosCache,
  });
}

function isCacheLike(cache) {
  return !!(
    cache.set &&
    cache.get &&
    cache.del &&
    typeof cache.get === "function" &&
    typeof cache.set === "function" &&
    typeof cache.del === "function"
  );
}

function buildSortedURL(...args) {
  const builtURL = buildURL(...args);

  const [urlPath, queryString] = builtURL.split("?");

  if (queryString) {
    const paramsPair = queryString.split("&");
    return `${urlPath}?${paramsPair.sort().join("&")}`;
  }

  return builtURL;
}

const FIVE_MINUTES = 1000 * 60 * 5;
const CAPACITY = 100;

function cacheAdapterEnhancer(adapter, options = {}) {
  const {
    enabledByDefault = true,
    cacheFlag = "cache",
    defaultCache = new LRUCache({maxAge: FIVE_MINUTES, max: CAPACITY}),
  } = options;

  return async (config) => {
    const {url, method, params, paramsSerializer, forceUpdate} = config;
    const useCache = config[cacheFlag] ? config[cacheFlag] : enabledByDefault;

    if (method === "get" && useCache) {
      // if had provide a specified cache, then use it instead
      const cache = isCacheLike(useCache) ? useCache : defaultCache;

      // build the index according to the url and params
      const index = buildSortedURL(url, params, paramsSerializer);

      let responsePromise = await cache.get(index);

      if (!responsePromise || forceUpdate) {
        responsePromise = (async () => {
          try {
            return await adapter(config);
          } catch (reason) {
            cache.del(index);
            throw reason;
          }
        })();

        // put the promise for the non-transformed response into cache as a placeholder
        responsePromise.then(({data}) => cache.set(index, JSON.stringify(data)));

        return responsePromise;
      }

      /* istanbul ignore next */
      if (process.env.LOGGER_LEVEL === "info") {
        // eslint-disable-next-line no-console
        console.info(`[axios-extensions] request cached by cache adapter --> url: ${index}`);
      }

      return Promise.resolve({data: JSON.parse(responsePromise)});
    }

    return adapter(config);
  };
}

Component/Page Level Cache

With the Vue SSR package, we can simply use page/component cache.

This is the code as a Nuxt module

import Redis from "ioredis";

export default function nuxtComponentCache(options) {
  if (this.options.render.ssr === false) {
    // SSR Disabled
    return;
  }

  // Create empty bundleRenderer object if not defined
  if (
    typeof this.options.render.bundleRenderer !== "object" ||
    this.options.render.bundleRenderer === null
  ) {
    this.options.render.bundleRenderer = {};
  }

  // Disable if cache explicitly provided in project
  if (this.options.render.bundleRenderer.cache) {
    return;
  }

  const redis = new Redis("127.0.0.1:6379");

  this.options.render.bundleRenderer.cache = {
    get: (key, cb) => {
      redis.get(key).then(deserialize).then(cb);
    },
    has: (key, cb) => {
      redis.exists(key).then(cb);
    },
    set: (key, val) => {
      redis.set(key, serialize(val));
    },
  };
}

function serialize(result) {
  return JSON.stringify(result, (key, value) => {
    if (typeof value === "object" && value instanceof Set) {
      return {_t: "set", _v: [...value]};
    }

    if (typeof value === "function") {
      return {_t: "func", _v: value.toString()};
    }

    return value;
  });
}

function deserialize(jsonString) {
  return JSON.parse(jsonString, (key, value) => {
    if (value && value._v) {
      if (value._t === "set") {
        return new Set(value._v);
      }

      if (value._t === "func") {
        // eslint-disable-next-line no-new-func
        return new Function(`return ${value._v}`);
      }
    }

    return value;
  });
}

Route Level Cache

Thanks to the Nuxt modules and hooks, we can create route-level caching.

With a module, we can save routes response:

import Redis from "ioredis";

export default function () {
  const redis = new Redis("127.0.0.1:6379");

  this.nuxt.hook("render:routeDone", async (url, result) => {
    const hasKey = await redis.exists(url);
    if (!hasKey) {
      redis.set(url, result.html, "EX", 60);
    }
  });
}

And with a server middle we can serve the cached response:

import Redis from "ioredis";

export default async function (req, res, next) {
  const cacheable = isCacheable(req);

  if (cacheable) {
    const redis = new Redis("127.0.0.1:6379");
    const hit = await redis.get(req.url);
    if (hit) {
      redis.quit();
      res.setHeader("ch-cache-hit", "HIT");
      return res.end(hit);
    }
  }

  res.setHeader("ch-cache-hit", "BYPASS");
  next();
}

function isCacheable(req) {
  return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment