在 Nuxt.js 中将 Vuex 存储与服务器端同步

Synchronize Vuex store with server side in Nuxt.js

问题

Nuxt中间件以下

const inspectAuthentication: Middleware = async (): Promise<void> => {
  await AuthenticationService.getInstance().inspectAuthentication();
};

在每个页面 return 之前在服务器端执行 HTML 并且检查已通过用户身份验证。如果已经存在,则将 CurrentAuthenticatedUser 存储在 Vuex 模块中:

import {
  VuexModule,
  getModule as getVuexModule,
  Module as VuexModuleConfiguration,
  VuexAction,
  VuexMutation
} from "nuxt-property-decorator";


@VuexModuleConfiguration({
  name: "AuthenticationService",
  store,
  namespaced: true,
  stateFactory: true,
  dynamic: true
})
export default class AuthenticationService extends VuexModule {

  public static getInstance(): AuthenticationService {
    return getVuexModule(AuthenticationService);
  }


  private _currentAuthenticatedUser: CurrentAuthenticatedUser | null = null;

  public get currentAuthenticatedUser(): CurrentAuthenticatedUser | null {
    return this._currentAuthenticatedUser;
  }


  @VuexAction({ rawError: true })
  public async inspectAuthentication(): Promise<boolean> {

    // This condition is always falsy after page reloading
    if (this.isAuthenticationInspectionSuccessfullyComplete) {
      return isNotNull(this._currentAuthenticatedUser);
    }


    this.onAuthenticationInspectionStarted();

    // The is no local storage on server side; use @nuxtjs/universal-storage instead
    const accessToken: string | null = DependenciesInjector.universalStorageService.
        getItem(AuthenticationService.ACCESS_TOKEN_KEY_IN_LOCAL_STORAGE);

    if (isNull(accessToken)) {
      this.completeAuthenticationInspection();
      return false;
    }


    let currentAuthenticatedUser: CurrentAuthenticatedUser | null;

    try {

      currentAuthenticatedUser = await DependenciesInjector.gateways.authentication.getCurrentAuthenticatedUser(accessToken);

    } catch (error: unknown) {

      this.onAuthenticationInspectionFailed();
      // error wrapping / rethrowing
    }


    if (isNull(currentAuthenticatedUser)) {
      this.completeAuthenticationInspection();
      return false;
    }


    this.completeAuthenticationInspection(currentAuthenticatedUser);

    return true;
  }

  @VuexMutation
  private completeAuthenticationInspection(currentAuthenticatedUser?: CurrentAuthenticatedUser): void {

    if (isNotUndefined(currentAuthenticatedUser)) {
      this._currentAuthenticatedUser = currentAuthenticatedUser;
      DependenciesInjector.universalStorageService.setItem(
        AuthenticationService.ACCESS_TOKEN_KEY_IN_LOCAL_STORAGE, currentAuthenticatedUser.accessToken
      );
    }

    // ...
  }
}

上面的代码在服务器端工作正常,但是在客户端,如果要尝试获取 AuthenticationService.getInstance().currentAuthenticatedUser,它将是 null! 我希望 Nuxt.js 将包括 AuthenticationService 在内的 Vuex 存储与服务器端同步,但是,它没有。

目标

AuthenticationService必须与服务器端同步,所以如果用户已经通过身份验证,在客户端AuthenticationService.getInstance().currentAuthenticatedUser即使在页面重新加载后它也必须是非空的。

服务器端不需要同步整个Vuex store(比如负责浮动通知栏的模块只需要在客户端)但是如果没有开发选择性方法,至少同步整个Vuex现在商店就够了。

请不要向我推荐用于身份验证的库或 Nuxt 模块,例如 Nuxt Auth module,因为这里我们讨论的是 Vuex 存储与服务器的同步,而不是关于用于身份验证的最佳 Nuxt 模块。此外,客户端和服务器之间 vuex 存储的同步不仅可以用于身份验证。

更新

preserveState 解决方案尝试

不幸的是,

import { store } from "~/Store";
import { VuexModule, Module as VuexModuleConfiguration } from "nuxt-property-decorator";


@VuexModuleConfiguration({
  name: "AuthenticationService",
  store,
  namespaced: true,
  stateFactory: true,
  dynamic: true,
  preserveState: true /* New */
})
export default class AuthenticationService extends VuexModule {}

原因

Cannot read property '_currentAuthenticatedUser' of undefined

服务器端错误。

错误指的是

@VuexAction({ rawError: true })
public async inspectAuthentication(): Promise<boolean> {
  if (this.isAuthenticationInspectionSuccessfullyComplete) {
    // HERE ⇩
    return isNotNull(this._currentAuthenticatedUser);
  }
}

我检查了 this 值。这是一个大对象;我将只留下值得注意的部分:

{                                                                                                                      
  store: Store {
    _committing: false,
    // === ✏ All actual action here
    _actions: [Object: null prototype] {
      'AuthenticationService/inspectAuthentication': [Array],
      'AuthenticationService/signIn': [Array],
      'AuthenticationService/applySignUp': [Array],
      // ...      

  // === ✏ Some mutations ...
  onAuthenticationInspectionStarted: [Function (anonymous)],
  completeAuthenticationInspection: [Function (anonymous)],
  // ...
  context: {
    dispatch: [Function (anonymous)],
    commit: [Function (anonymous)],
    getters: {
      currentAuthenticatedUser: [Getter],
      isAuthenticationInspectionSuccessfullyComplete: [Getter]
    },
    // === ✏ The state in undefined!
    state: undefined
  }
}

我想我需要告诉我如何初始化 vuex 存储。 动态模块的 working Nuxt methodology 是:

// store/index.ts
import Vue from "vue";
import Vuex, { Store } from "vuex";


Vue.use(Vuex);

export const store: Store<unknown> = new Vuex.Store<unknown>({});

nuxtServerInit 解决方案尝试

还有一个问题——如何在上面的store初始化方法中集成nuxtServerInit?我想,要回答这个问题,需要 Vuex 和 vuex-module-decorators。在下面store/index.ts中,nuxtServerInit甚至不会被调用:

import Vue from "vue";
import Vuex, { Store } from "vuex";


Vue.use(Vuex);

export const store: Store<unknown> = new Vuex.Store<unknown>({
  actions: {
    nuxtServerInit(blackbox: unknown): void {
      console.log("----------------");
      console.log(blackbox);
    }
  }
});

我把这个问题提取到 other question

这是使用 SSR 时面临的主要挑战之一。 因此,在从服务器接收到带有静态 HTML 的响应后,客户端会发生一个名为 Hydration 的过程。 (您可以阅读更多相关内容 Vue SSR guide

由于 Nuxt 的构建方式以及 SSR/Client 关系如何用于水合作用,可能会发生的情况是您的服务器呈现您的应用程序的快照,但在客户端安装之前异步数据不可用应用程序,导致它呈现不同的存储状态,破坏水合作用。

事实框架,如 Nuxt 和 Next(用于 React)为 Auth 和许多其他实现自己的组件,是为了处理正确水合作用的手动协调过程。

因此,深入了解如何在不使用 Nuxt 内置身份验证模块的情况下解决该问题,您可能需要注意以下几点:

  1. 那里有 serverPrefetch 方法,它将在服务器端调用,它会等到 promise 被解决,然后再发送给客户端进行渲染
  2. 除了组件渲染之外,还有服务器发送给客户端的上下文,可以使用 rendered 挂钩注入,当应用程序完成渲染时调用,所以在正确的时机发送你的将状态存储回客户端以在水化过程中重用它
  3. 在商店本身,如果您使用 registerModule,它支持属性 preserveState,负责保持服务器注入的状态。

有关如何使用这些部分的示例,您可以查看 this page

上的代码

最后,与您的用户身份验证挑战更相关的是,另一种选择是在存储操作中使用 nuxtServerInit 到 运行 此身份验证处理,因为它会在之后直接传递给客户端, 如 Nuxt docs.

所述

更新

same page 上,文档显示 nextServerInit 的第一个参数是 context,这意味着您可以从那里获得 store,例如。

还有一个重要的要点是,在你最初的问题中,你提到你不想要第三方库,但你已经在使用一个给 table,即nuxt-property-decorator。 因此,您不仅要处理与使用框架时一样复杂的 SSR,而且您不是使用纯 Vue,而是使用 Next,也不是使用纯 TS Nuxt,而是为商店的装饰器添加了另一种复杂性。

我为什么要提它?因为快速查看库问题,有 other people with the same issue 无法正确访问 this

来自同时使用 Nuxt (Vue) 和 Next (React) 的人的背景,我对你的建议是在尝试很多不同的东西之前尝试降低复杂性。 所以我会在没有这个 nuxt-property-decorator 的情况下测试 运行ning 你的应用程序,以检查它是否适用于开箱即用的商店实现,确保它不是在没有完全准备好支持 SSR 复杂度。