vuejs/vue2-ssr-docs

Document strategy for registering Vue-instance-scoped properties with Vue.plugin() safely without runInNewContext

thomasboyt opened this issue · 0 comments

Nuxt has a useful plugin injection system that allows you to register instance properties on Vue.prototype, while still maintaining isolation by having it reference an underlying property on this.$root. I'd like this pattern to be documented here.

Background

I'm adding authentication to my Vue+Vuex+SSR app. The details of this aren't worth getting into, but the short version is that my app has an "auth token" that needs to get passed on every API request. During server-side rendering, the server would read the auth token out of a session cookie, and then set it in the Vuex store. On the client-side, the auth token would also be read out of this cookie and set locally.

I used to make API requests only inside Vuex actions, which made using this token relatively easy: it was in the Vuex context, and I just passed the context to an apiRequest() helper that wrapped Vuex.

However, I found myself also wanting to be able to make requests inside my components without going through Axios, and I got annoyed that I had to get the token out of the store every time. I started looking into other patterns for this, and found nuxt's axios module. This adds an $axios property to your Vue instance and stores.

I was surprised to see this module has a setToken() method allowing you to set an authentication token to use with all of your requests. My first thought was that this must have a security flaw - if $axios is registered on Vue.prototype, and $axios.setToken() updates the token for all $axios requests, wouldn't this mean that non-isolated server-side-rendered Vue instances could mutate the access token out from under each other, as the Avoiding Stateful Singletons section of the doc warns again?

I started investigating how Nuxt's "plugins" are implemented, and was impressed by their workaround for this: their inject method, as documented here and implemented here, uses the following technique (simplified here):

function injectProperty(appConfig, key value) {
   // pass the property to the root Vue instance's options
  appConfig[key] = value;

  Vue.use(() => {
    if (!Vue.prototype.hasOwnProperty(key)) {
      // register a Vue.prototype[key] getter that retrieves the value from the root options 
      Object.defineProperty(Vue.prototype, key, {
        get() {
          return this.$root.$options[key];
        },
      });
    }
  });
}

function createApp() {
  const appConfig = {
    // ...
  };

  const axiosInstance = axios.create({
    baseURL: `${process.env.API_URL}/api/`,
  });

  injectProperty(appConfig, '$axios', axiosInstance);
 
  const app = new Vue(appConfig);
  // ...
}

I used this code for my $axios use case and it seems to work well at first glance. Of course, I did walk away from all this pondering whether I should just go ahead and port from my Webpack hodgepodge over to Nuxt, but at least I won't have to do it today.

Considering that Vue's general strategy for avoiding singletons (such as through singleton module imports) is "register it on the instance," having a pattern for safely registering to Vue.prototype seems worth documenting here in the SSR guide.