使用 Pinia 服务

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

Use service with Pinia

问题

I have a service, which handles the API communication (and should be the only file to do actual API calls). I have some issues to use this service in my store.

My current logic looks like this:
To swap the service out for better testability, the service is introduced to the Vue component via a prop. My issue is, that I could not find a good way to tell the store to use this service, so I created a composable useService to 'store' the service, which can then be imported and used in the store.

// component.vue
const props = defineProps<{
  service: ServiceInterface
}>()

// Get the composable and set the service
const actualService = useService()
actualService.setService(props.service)

// Use the store
const store = useStore()
// useService.ts
const service = ref<ServiceInterface>()

export const useService = () => {
  const setService = (newService: ServiceInterface) => {
    service.value = newService
  }

  return {
    service,
    setService
  }
}
// useStore.ts

// import composable
const { service } = useService()

export const useStore = defineStore('storeName', {
  actions: {
    async fetchData(id: string) {
      // Ensure service is set
      if (!service.value) {
        throw an Error('No service')
      }

      // Use service
      service.value.getData(id)
    }
  }
})

This setup does work, however I need to double-check in every method that the service is available and I feel like I overcomplicated the whole setup. Has somebody any idea to improve this flow?

I guess I am looking for something like this (not working):

export const useStore = (service: ServiceInterface) => {
  return defineStore('storeName', {
    actions: {
      async fetchData(id: string) {
        service.getData(id)
      }
    }
  })
}
英文:

I have a service, which handles the API communication (and should be the only file to do actual API calls). I have some issues to use this service in my store.

My current logic looks like this:
To swap the service out for better testability, the service is introduced to the Vue component via a prop. My issue is, that i could not find a good way to tell the store to use this service, so i created a composable useService to 'store' the service, which can then be imported and used in the store.

// component.vue
const props = defineProps&lt;{
  service: ServiceInterface
}&gt;()

// Get the composable and set the service
const actualService = useService()
actualService.setService(props.service)

// Use the store
const store = useStore()
// useService.ts
const service = ref&lt;ServiceInterface&gt;()

export const useService = () =&gt; {
  const setService = (newService: ServiceInterface) =&gt; {
    service.value = newService
  }

  return {
    service,
    setService
  }
}
// useStore.ts

// import composable
const { service } = useService()

export const useStore = defineStore(&#39;storeName&#39;, {
  actions: {
    async fetchData(id: string) {
      // Ensure service is set
      if (!service.value) {
        throw new Error(&#39;No service&#39;)
      }

      // Use service
      service.value.getData(id)
    }
  }
})

This setup does work, however i need to double check in every method that the service is available and i feel like i overcomplicated the whole setup. Has somebody any idea to improve this flow?

I guess i am looking for something like this (not working):

export const useStore = (service: ServiceInterface) =&gt; {
  return defineStore(&#39;storeName&#39;, {
    actions: {
      async fetchData(id: string) {
        service.getData(id)
      }
    }
  })
}

答案1

得分: 1

以下是翻译好的部分:

"I don't see any reason why you should make the service reactive. You want to be able to replace it with a mock in testing, but you don't need the ability to replace any of its methods on the fly once it has been instantiated (which is the only reason you'd want it to be reactive)"

"我不明白为什么要使服务响应式。您希望在测试中能够用模拟对象替换它,但您不需要在实例化后即时替换其任何方法的能力(这是您希望它成为响应式的唯一原因)。"

"I'd go with (service.ts):"

"我建议使用 (service.ts):"

"export const someCall = (...args) => {"
"export const someOtherCall = (...args) => {"
"import { someCall, someOtherCall } from '@/path/to/service'"
"// use them directly"

"或者:"

"import * as service from '@/path/to/service'"
"// use service.someCall(args)"

"注意:请将上面的 ...args 替换为每个调用所需的实际参数。"

"在测试中,您可以轻松替换 service.ts:"

"jest.mock('@/path/to/service')"

"或者仅模拟特定方法:"

"const mockSomeOtherCall = jest.fn()"
"jest.mock('@/path/to/service', () => {"
"...jest.requireActual('@/path/to/service'),"
"someOtherCall: mockSomeOtherCall"
"})"

"it('calls someOtherCall', () => {"
"mockSomeOtherCall.mockReset()"

"// 在此处挂载组件,然后:"

"expect(mockSomeOtherCall).not.toHaveBeenCalled()"

"// 触发应该调用 someOtherCall 的操作,然后:"

"expect(mockSomeOtherCall)"
".toHaveBeeCalledWith(expectedArgumentValues);"
"})"

"注意:在上面的代码中,'@/path/to/service' 是通用的。例如,如果您将 service.ts(或 .js)放在 src/services 内,路径实际上应该是 @/services/service。在测试套件中,通过使用 jest.mock('@/services/service')(或其更具体的变体之一),您实际上正在用模拟对象替换该模块的实际内容,同时运行该测试套件中的测试。"

英文:

I don't see any reason why you should make the service reactive. You want to be able to replace it with a mock in testing, but you don't need the ability to replace any of its methods on the fly once it has been instantiated (which is the only reason you'd want it to be reactive)

I'd go with (service.ts):

export const someCall = (...args) =&gt; {
  // make some call here, using optional args
},
export const someOtherCall = (...args) =&gt; {
  // make some other call here, using optional args
}

Anywhere else:

import { someCall, someOtherCall } from &#39;@/path/to/service&#39;
// use them directly

Or:

import * as service from &#39;@/path/to/service&#39;
// use service.someCall(args)

Note: replace ...args above with the actual arguments needed for each call.


You can replace service.ts in your tests with ease:

jest.mock(&#39;@/path/to/service&#39;)

Or mock a specific method only:

const mockSomeOtherCall = jest.fn()
jest.mock(&#39;@/path/to/service&#39;, () =&gt; {
  ...jest.requireActual(&#39;@/path/to/service&#39;),
  someOtherCall: mockSomeOtherCall
})

it(&#39;calls someOtherCall&#39;, () =&gt; {
  mockSomeOtherCall.mockReset()

  // mount component here, then:

  expect(mockSomeOtherCall).not.toHaveBeenCalled()

  // trigger something that should call `someOtherCall` then:

  expect(mockSomeOtherCall)
    .toHaveBeeCalledWith(expectedArgumentValues);
})

Note: in the above code @/path/to/service is generic. If, for example, you place service.ts (or .js) inside src/services, the path would actually be @/services/service. In a test suite, by using jest.mock(&#39;@/services/service&#39;) (or any of its more specific variants), you're effectively replacing the actual contents of that module with mocks, while running the tests in that test suite.

huangapple
  • 本文由 发表于 2023年3月1日 16:29:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/75601196.html
匿名

发表评论

匿名网友

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

确定