在组件外部使用 Pinia 引发无活动 Pinia 错误。

huangapple go评论73阅读模式
英文:

use pinia outside of component raises no active pinia error

问题

我正在使用 Vue.js 3 项目,使用 ViteSse 模板构建。我想在 setup 组件外部使用我的一个 Pinia 存储,名为 notificationStore,但是当我运行 dev 命令时,出现了错误 getActivePinia was called with no active Pinia. Did you forget to install pinia。这个错误的行为非常奇怪,因为如果我在 dev 服务器加载后添加 useNotificationStore,它就不会出现错误,而且能正常工作。但是如果 useNotificationStore 存在于我的 ts 文件中并且运行 dev 命令,就会出现错误。

我已经使用日志追踪了问题,并发现它在 createApp 被执行之前发生,而 createApp 初始化了 pinia,但是在那个时候没有激活的 pinia。我期望 main.ts 中的 createApp 在所有其他代码之前执行。在此提前感谢。

英文:

I'm working on a Vue.js 3 project build using ViteSse template, I wanna use one of my Pinia stores named notificationStore outside of the setup component but I get getActivePinia was called with no active Pinia. Did you forget to install pinia error when I run dev command It's behavior is so strange becasue it's not occured if I add useNotificationStore after dev server loaded and It woks well but when useNotificationStore exisit in my ts file and run dev command it raise error.

在组件外部使用 Pinia 引发无活动 Pinia 错误。

here is my setup:

main.ts

import { ViteSSG } from 'vite-ssg'
import { setupLayouts } from 'virtual:generated-layouts'
import { useAuth, useOidcStore } from 'vue3-oidc'
import type { UserModule } from './types'
import App from './app.vue'
import generatedRoutes from '~pages'
import './assets/styles/style.scss'
import '@/config/oidc'
const routes = setupLayouts(generatedRoutes)
const { autoAuthenticate } = useAuth()
const { state } = useOidcStore()
const isMocking = import.meta.env.VITE_API_MOCKING_ENABLED
if (isMocking === 'true') {
  const browser = await import('~/mocks/browser') as any
  browser.worker.start({ onUnhandledRequest: 'bypass' })
}

export const createApp = ViteSSG(
  App,
  { routes, base: import.meta.env.BASE_URL },
  async (ctx) => {
    Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
      .forEach(i => {
        i.install?.(ctx)
      })


    const DEFAULT_TITLE = 'Dashboard'
    ctx.router.beforeEach((to) => {
      if (!state.value.user) {
        autoAuthenticate()
      }
      let title = DEFAULT_TITLE
      if (to.meta.title)
        title = `${to.meta.title} - ${title}`

      document.title = title
    })
  },
)

notification.store.ts

import { acceptHMRUpdate, defineStore } from 'pinia'

interface NotificationState {
  messages: Array<Notification>
}
export interface Notification {
  type?: 'error' | 'info' | 'success' | 'warning'
  timeout?: number
  permanent?: boolean
  message: string
  hideAfterRouting?: boolean
  validationErrors?: Array<string>
  shown?: boolean
}

export const useNotificationsStore = defineStore('notification', {
  state: (): NotificationState => ({
    messages: [],
  }),
  actions: {
    addNotification(message: Notification) {
      if (isDuplicatedMessage(this.messages, message))
        return

      message.timeout = message.timeout || 3000
      message.shown = true
      this.messages.push(message)
    },
    remove(index: number) {
      this.messages.splice(index, 1)
    },
    clearAfterRoute() {
      const visibleNotification = this.messages.filter(
        item => item.hideAfterRouting === false,
      )
      this.messages = visibleNotification
    },
  },
})

function isDuplicatedMessage(messages: Notification[], message: Notification): boolean {
  if (!messages.length)
    return false
  return (messages[messages.length - 1].message === message.message)
}

if (import.meta.hot)
  import.meta.hot.accept(acceptHMRUpdate(useNotificationsStore, import.meta.hot))

http-client.ts

import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'
import { useNotificationsStore } from '~/store/notification.store'
const notification = useNotificationsStore()
const httpClient: AxiosInstance
  = axios.create({
    baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
    headers: {
      'Content-Type': 'application/json',
    },
  })

httpClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = tokenService.getLocalAccessToken()
    if (token && config.headers)
      config.headers.Authorization = `Bearer ${token}`

    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)
httpClient.interceptors.response.use(
  (response) => {
    if (response.config.method !== 'get') {
      const successMsg = 'operation successfully completed'
      notification.addNotification({ type: 'success', message: successMsg })
    }
    return response
  },
  (error) => {
    if (error.message === 'Network Error') {
      notification.addNotification({ type: 'error', message: 'Network Error' })
      return Promise.reject(error)
    }
    let errorMessage = ''
    const validationErrors = getValidationErrors(error)
    errorMessage = getErrorMessage(error)
    notification.addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
  }
)

Vite.config.ts

// Plugins

// Utilities
import path from 'path'
import { URL, fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'

import vuetify from 'vite-plugin-vuetify'
import Vue from '@vitejs/plugin-vue'
import Pages from 'vite-plugin-pages'
import Layouts from 'vite-plugin-vue-layouts'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { VitePWA } from 'vite-plugin-pwa'
import VueI18n from '@intlify/vite-plugin-vue-i18n'
import Inspect from 'vite-plugin-inspect'

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    port: 9080,
    host: true,
    hmr: {
      host: 'localhost',
    },
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      '~': fileURLToPath(new URL('./src', import.meta.url)),
    },
    extensions: [
      '.js',
      '.json',
      '.jsx',
      '.mjs',
      '.ts',
      '.tsx',
      '.vue',
    ],
  },
  plugins: [
    Vue({
      include: [/\.vue$/],
      reactivityTransform: true,
    }),
    vuetify({
      autoImport: true,
    }),
    // https://github.com/hannoeru/vite-plugin-pages
    Pages({
      extensions: ['vue'],
      dirs: [
        { dir: 'src/router/views', baseRoute: '' },
      ],
      extendRoute: (route) => {
        if (route.path === '/')
          return { ...route, redirect: '/Dashboard' }

        return route
      },

    }),

    // https://github.com/JohnCampionJr/vite-plugin-vue-layouts
    Layouts({
      layoutsDirs: ['src/router/layouts'],
    }),

    // https://github.com/antfu/unplugin-auto-import
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        'vue-i18n',
        'vue/macros',
        '@vueuse/head',
        '@vueuse/core',
      ],
      dts: 'src/auto-imports.d.ts',
      dirs: [
        'src/composable',
        'src/store',
      ],
      vueTemplate: true,
    }),

    // https://github.com/antfu/unplugin-vue-components
    Components({
      extensions: ['vue'],
      include: [/\.vue$/, /\.vue\?vue/],
      dts: 'src/components.d.ts',
      deep: true,
    }),  

    // https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
    VueI18n({
      runtimeOnly: true,
      compositionOnly: true,
      include: [path.resolve(__dirname, 'locales/**')],
    }),

    // https://github.com/antfu/vite-plugin-inspect
    // Visit http://localhost:3333/__inspect/ to see the inspector
    Inspect(),
  ],

  // https://github.com/vitest-dev/vitest
  // test: {
  //   include: ['test/**/*.test.ts'],
  //   environment: 'jsdom',
  //   deps: {
  //     inline: ['@vue', '@vueuse', 'vue-demi'],
  //   },
  // },

  // https://github.com/antfu/vite-ssg
  ssgOptions: {
    script: 'sync',
    formatting: 'minify',
    // onFinished() { generateSitemap() },
  },

  ssr: {
    // TODO: workaround until they support native ESM
    noExternal: ['workbox-window', /vue-i18n/],
  },

  define: { 'process.env': {} },
})


I have traced issue with some logging and finded that it occured before createApp get executed that initate pinia and there was not active pinia on that time. I expected createApp in main.ts have been executed before all

thanks in advance

答案1

得分: 1

我通过仔细阅读文档成功解决了这个问题:

> 确保这总是被应用的最简单方法是将 useStore() 的调用延迟到在 pinia 安装后总是会运行的函数内部。

我通过将文件顶部的 useNotificationStore() 移除,并在需要 notificationStore 实例的地方使用 useNotificationStore().addNotification(...) 来更新 http-client。

import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'

const httpClient: AxiosInstance
  = axios.create({
    baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
    headers: {
      'Content-Type': 'application/json',
    },
  })

httpClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = tokenService.getLocalAccessToken()
    if (token && config.headers)
      config.headers.Authorization = `Bearer ${token}`

    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)
httpClient.interceptors.response.use(
  (response) => {
    if (response.config.method !== 'get') {
      const successMsg = '操作成功完成'
      useNotificationsStore().addNotification({ type: 'success', message: successMsg })
    }
    return response
  },
  (error) => {
    if (error.message === 'Network Error') {
       useNotificationsStore().addNotification({ type: 'error', message: '网络错误' })
      return Promise.reject(error)
    }
    let errorMessage = ''
    const validationErrors = getValidationErrors(error)
    errorMessage = getErrorMessage(error)
    useNotificationsStore().addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
  }
)
英文:

I managed to solve the problem by reading documentation carefully:

> The easiest way to ensure this is always applied is to defer calls of useStore() by placing them inside functions that will always run after pinia is installed.

I just update http-client by removing useNotificationStore() from the top of the file and where I need notificaitonStore instance use useNotificationStore().addNotification(...)

import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import tokenService from './token.service'

const httpClient: AxiosInstance
  = axios.create({
    baseURL: `${import.meta.env.VITE_APP_API_URL}/`,
    headers: {
      'Content-Type': 'application/json',
    },
  })

httpClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = tokenService.getLocalAccessToken()
    if (token && config.headers)
      config.headers.Authorization = `Bearer ${token}`

    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)
httpClient.interceptors.response.use(
  (response) => {
    if (response.config.method !== 'get') {
      const successMsg = 'operation successfully completed'
       useNotificationsStore().addNotification({ type: 'success', message: successMsg })
    }
    return response
  },
  (error) => {
    if (error.message === 'Network Error') {
       useNotificationsStore().addNotification({ type: 'error', message: 'Network Error' })
      return Promise.reject(error)
    }
    let errorMessage = ''
    const validationErrors = getValidationErrors(error)
    errorMessage = getErrorMessage(error)
    useNotificationsStore().addNotification({ type: 'error', message: errorMessage, validationErrors, hideAfterRouting: false })
  }
)

答案2

得分: 0

这是因为您尚未将Pinia与应用程序注册

```ts
export const createApp = ViteSSG(
  App,
  { routes, base: import.meta.env.BASE_URL },
  async (ctx) => {
    Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
      .forEach(i => {
        i.install?.(ctx)
      })

// 安装Pinia 
    const pinia = createPinia()
    ctx.app.use(pinia)

    if (import.meta.env.SSR)
      ctx.initialState.pinia = pinia.state.value
    else
      pinia.state.value = ctx.initialState.pinia || {}
// ============

    const DEFAULT_TITLE = 'Dashboard'
    ctx.router.beforeEach((to) => {
// ====== 使用store ====
      const { state } = pinia.useStore()

      if (!store.ready)
        // 执行(用户实现的)store动作以填充store的状态
        store.initialize()
// ===========
      if (!state.value.user) {
        autoAuthenticate()
      }
      let title = DEFAULT_TITLE
      if (to.meta.title)
        title = `${to.meta.title} - ${title}`

      document.title = title
    })
  },
)
英文:

this happens because you have not registered pinia with the app

export const createApp = ViteSSG(
  App,
  { routes, base: import.meta.env.BASE_URL },
  async (ctx) =&gt; {
    Object.values(import.meta.glob&lt;{ install: UserModule }&gt;(&#39;./modules/*.ts&#39;, { eager: true }))
      .forEach(i =&gt; {
        i.install?.(ctx)
      })

// install pinia 
    const pinia = createPinia()
    ctx.app.use(pinia)

    if (import.meta.env.SSR)
      ctx.initialState.pinia = pinia.state.value
    else
      pinia.state.value = ctx.initialState.pinia || {}
// ============

    const DEFAULT_TITLE = &#39;Dashboard&#39;
    ctx.router.beforeEach((to) =&gt; {
// ====== use store ====
      const { state } = useOidcStore()

      if (!store.ready)
        // perform the (user-implemented) store action to fill the store&#39;s state
        store.initialize()
// ===========
      if (!state.value.user) {
        autoAuthenticate()
      }
      let title = DEFAULT_TITLE
      if (to.meta.title)
        title = `${to.meta.title} - ${title}`

      document.title = title
    })
  },
)

> Please remove useOidcStore() on top level

huangapple
  • 本文由 发表于 2023年3月9日 18:57:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/75683651.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定