Chào các bạn độc giả trên Cafedev! Trong bài viết này, chúng ta sẽ khám phá về việc sử dụng các plugin trong Pinia Vuejs. Điều này cho phép mở rộng hoàn toàn các store Pinia. Bạn sẽ khám phá các tính năng thú vị như thêm thuộc tính mới cho cửa hàng, thêm tùy chọn khi định nghĩa cửa hàng, và nhiều hơn nữa. Cùng nhau tìm hiểu và khai phá sức mạnh của việc sử dụng plugin trong Pinia Vuejs!

Các store Pinia có thể được mở rộng hoàn toàn nhờ vào một API cấp thấp. Dưới đây là danh sách các điều bạn có thể thực hiện:
– Thêm các thuộc tính mới vào các store
– Thêm các tùy chọn mới khi định nghĩa các store
– Thêm các phương thức mới vào các store
– Bọc các phương thức hiện có
– Chặn các hành động và kết quả của chúng
– Thực hiện các hiệu ứng phụ như Local Storage
– Áp dụng chỉ cho các store cụ thể

Các plugin được thêm vào phiên bản pinia với pinia.use(). Ví dụ đơn giản nhất là thêm một thuộc tính tĩnh vào tất cả các store bằng cách trả về một đối tượng:

import { createPinia } from 'pinia'

// add a property named `secret` to every store that is created
// after this plugin is installed this could be in a different file
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// give the plugin to pinia
pinia.use(SecretPiniaPlugin)

// in another file
const store = useStore()
store.secret // 'the cake is a lie'

Điều này hữu ích để thêm các đối tượng toàn cục như router, modal hoặc quản lý toast.

1. Giới Thiệu

Một plugin Pinia là một hàm mà tùy chọn trả về các thuộc tính để thêm vào một store. Nó nhận một đối số tùy chọn, một context:

export function myPiniaPlugin(context) {
  context.pinia // the pinia created with `createPinia()`
  context.app // the current app created with `createApp()` (Vue 3 only)
  context.store // the store the plugin is augmenting
  context.options // the options object defining the store passed to `defineStore()`
  // ...
}

Sau đó, hàm này được truyền vào pinia với pinia.use():

pinia.use(myPiniaPlugin)

Các plugin chỉ được áp dụng cho các store được tạo sau những plugin chính nó, và sau khi pinia được truyền vào ứng dụng, nếu không chúng sẽ không được áp dụng.

2. Mở Rộng Một Store

Bạn có thể thêm các thuộc tính vào mỗi store bằng cách đơn giản trả về một đối tượng chúng trong một plugin:

pinia.use(() => ({ hello: 'world' }))

Bạn cũng có thể đặt thuộc tính trực tiếp trên store nhưng nếu có thể hãy sử dụng phiên bản trả về để chúng có thể được theo dõi tự động bởi devtools:

pinia.use(({ store }) => {
  store.hello = 'world'
})

Bất kỳ thuộc tính nào được trả về bởi một plugin sẽ được tự động theo dõi bởi devtools, vì vậy để làm cho hello hiển thị trong devtools, đảm bảo thêm nó vào store._customProperties chỉ trong chế độ dev nếu bạn muốn gỡ lỗi nó trong devtools:

// from the example above
pinia.use(({ store }) => {
  store.hello = 'world'
  // make sure your bundler handle this. webpack and vite should do it by default
  if (process.env.NODE_ENV === 'development') {
    // add any keys you set on the store
    store._customProperties.add('hello')
  }
})

Lưu ý rằng mỗi store được bao bọc bởi reactive, tự động mở bỏ bất kỳ Ref nào (ref(), computed(), …) mà nó chứa:

const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // each store has its individual `hello` property
  store.hello = ref('secret')
  // it gets automatically unwrapped
  store.hello // 'secret'

  // all stores are sharing the value `shared` property
  store.shared = sharedRef
  store.shared // 'shared'
})

Đây là lý do tại sao bạn có thể truy cập tất cả các thuộc tính tính toán mà không cần .value và tại sao chúng là có phản ứng.

2.1 Thêm trạng thái mới

Nếu bạn muốn thêm các thuộc tính trạng thái mới vào một store hoặc các thuộc tính được dùng trong quá trình hydrat hóa, bạn sẽ cần phải thêm vào hai nơi:
– Trên store để bạn có thể truy cập vào nó bằng store.myState
– Trên store.$state để nó có thể được sử dụng trong devtools và, được serialize trong quá trình SSR.
Ngoài ra, bạn chắc chắn sẽ phải sử dụng một ref() (hoặc các API phản ứng khác) để chia sẻ giá trị qua các truy cập khác nhau:

import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // to correctly handle SSR, we need to make sure we are not overriding an
  // existing value
  if (!store.$state.hasOwnProperty('hasError')) {
    // hasError is defined within the plugin, so each store has their individual
    // state property
    const hasError = ref(false)
    // setting the variable on `$state`, allows it be serialized during SSR
    store.$state.hasError = hasError
  }
  // we need to transfer the ref from the state to the store, this way
  // both accesses: store.hasError and store.$state.hasError will work
  // and share the same variable
  // See https://vuejs.org/api/reactivity-utilities.html#toref
  store.hasError = toRef(store.$state, 'hasError')

  // in this case it's better not to return `hasError` since it
  // will be displayed in the `state` section in the devtools
  // anyway and if we return it, devtools will display it twice.
})

Lưu ý rằng các thay đổi hoặc thêm vào trạng thái mà xảy ra trong một plugin (bao gồm gọi store.$patch()) xảy ra trước khi store được hoạt động và do đó không kích hoạt bất kỳ đăng ký nào.
Cảnh báo Nếu bạn đang sử dụng Vue 2, Pinia sẽ chịu các lưu ý phản ứng tương tự như Vue. Bạn sẽ cần phải sử dụng Vue.set() (Vue 2.7) hoặc set() (từ @vue/composition-api cho Vue <2.7) khi tạo ra các thuộc tính trạng thái mới như secrethasError:

import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
  if (!store.$state.hasOwnProperty('secret')) {
    const secretRef = ref('secret')
    // If the data is meant to be used during SSR, you should
    // set it on the `$state` property so it is serialized and
    // picked up during hydration
    set(store.$state, 'secret', secretRef)
  }
  // set it directly on the store too so you can access it
  // both ways: `store.$state.secret` / `store.secret`
  set(store, 'secret', toRef(store.$state, 'secret'))
  store.secret // 'secret'
})

Thiết lập lại trạng thái được thêm vào trong các plugin

Theo mặc định, $reset() sẽ không thiết lập lại trạng thái được thêm bởi các plugin nhưng bạn có thể ghi đè nó để cũng thiết lập lại trạng thái bạn thêm vào:

import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // this is the same code as above for reference
  if (!store.$state.hasOwnProperty('hasError')) {
    const hasError = ref(false)
    store.$state.hasError = hasError
  }
  store.hasError = toRef(store.$state, 'hasError')

  // make sure to set the context (`this`) to the store
  const originalReset = store.$reset.bind(store)

  // override the $reset function
  return {
    $reset() {
      originalReset()
      store.hasError = false
    },
  }
})

3. Thêm các thuộc tính bên ngoài mới

Khi thêm các thuộc tính bên ngoài, các thể hiện lớp từ các thư viện khác, hoặc đơn giản là các đối tượng không phản ứng, bạn nên bao bọc đối tượng đó với markRaw() trước khi chuyển nó vào pinia. Dưới đây là một ví dụ thêm router vào mỗi store:

import { markRaw } from 'vue'
// adapt this based on where your router is
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

4. Gọi $subscribe bên trong các plugin

Bạn có thể sử dụng store.$subscribestore.$onAction trong các plugin:

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // react to store changes
  })
  store.$onAction(() => {
    // react to store actions
  })
})

5. Thêm các tùy chọn mới

Có thể tạo ra các tùy chọn mới khi định nghĩa stores để sau này sử dụng chúng từ các plugin. Ví dụ, bạn có thể tạo ra một tùy chọn debounce cho phép bạn debounce bất kỳ hành động nào:

defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // this will be read by a plugin later on
  debounce: {
    // debounce the action searchContacts by 300ms
    searchContacts: 300,
  },
})

Sau đó, plugin có thể đọc tùy chọn đó để bọc các hành động và thay thế các hành động gốc:

// use any debounce library
import debounce from 'lodash/debounce'

pinia.use(({ options, store }) => {
  if (options.debounce) {
    // we are overriding the actions with new ones
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

Lưu ý rằng các tùy chọn tùy chỉnh được truyền như đối số thứ 3 khi sử dụng cú pháp setup:

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // this will be read by a plugin later on
    debounce: {
      // debounce the action searchContacts by 300ms
      searchContacts: 300,
    },
  }
)

6. TypeScript

Mọi thứ được hiển thị ở trên đều có thể được thực hiện với hỗ trợ gõ, vì vậy bạn không bao giờ cần phải sử dụng any hoặc @ts-ignore.

6.1 Thêm plugins

Một plugin Pinia có thể được gõ dấu như sau:

import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  // ...
}

6.2 Thêm các thuộc tính mới của store

Khi thêm các thuộc tính mới vào stores, bạn cũng nên mở rộng giao diện PiniaCustomProperties.

import 'pinia'
import type { Router } from 'vue-router'

declare module 'pinia' {
  export interface PiniaCustomProperties {
    // by using a setter we can allow both strings and refs
    set hello(value: string | Ref<string>)
    get hello(): string

    // you can define simpler values too
    simpleNumber: number

    // type the router added by the plugin above (#adding-new-external-properties)
    router: Router
  }
}

Sau đó, nó có thể được viết và đọc một cách an toàn:

pinia.use(({ store }) => {
  store.hello = 'Hola'
  store.hello = ref('Hola')

  store.simpleNumber = Math.random()
  // @ts-expect-error: we haven't typed this correctly
  store.simpleNumber = ref(Math.random())
})

PiniaCustomProperties là một loại generic cho phép bạn tham chiếu các thuộc tính của một store. Hãy tưởng tượng ví dụ sau khi chúng ta sao chép các tùy chọn ban đầu như $options (điều này chỉ hoạt động cho các stores tùy chọn):

pinia.use(({ options }) => ({ $options: options }))

Chúng ta có thể thêm cho điều này một cách đúng đắn bằng cách sử dụng 4 loại generic của PiniaCustomProperties:

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties<Id, S, G, A> {
    $options: {
      id: Id
      state?: () => S
      getters?: G
      actions?: A
    }
  }
}

Mẹo Khi mở rộng các loại trong generics, chúng phải được đặt tên chính xác như trong mã nguồn. Id không thể được đặt tên là id hoặc I, và S không thể được đặt tên là State. Dưới đây là ý nghĩa của mỗi chữ cái:
– S: State
– G: Getters
– A: Actions
– SS: Setup Store / Store

6.3 Thêm trạng thái mới

Khi thêm các thuộc tính trạng thái mới (cho cả storestore.$state), bạn cần thêm kiểu vào PiniaCustomStateProperties thay vì. Khác biệt so với PiniaCustomProperties, nó chỉ nhận generic State:

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomStateProperties<S> {
    hello: string
  }
}

6.4 Thêm tùy chọn tạo mới

Khi tạo các tùy chọn mới cho defineStore(), bạn nên mở rộng DefineStoreOptionsBase. Khác biệt so với PiniaCustomProperties, nó chỉ hiển thị hai generic: State và loại Store, cho phép bạn giới hạn những gì có thể được định nghĩa. Ví dụ, bạn có thể sử dụng tên của các hành động:

import 'pinia'

declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    // allow defining a number of ms for any of the actions
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

Mẹo Cũng có một loại StoreGetters để trích xuất các getters từ một loại Store. Bạn cũng có thể mở rộng các tùy chọn của setup stores hoặc option stores chỉ bằng cách mở rộng các loại DefineStoreOptionsDefineSetupStoreOptions tương ứng.

7. Nuxt.js

Khi sử dụng pinia cùng với Nuxt, bạn sẽ phải tạo ra một Nuxt plugin trước tiên. Điều này sẽ cho bạn truy cập vào thể hiện pinia:

{14-16}
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'

function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // Note this has to be typed if you are using TS
  return { creationTime: new Date() }
}

export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
})

Thông tin
Ví dụ trên đang sử dụng TypeScript, bạn phải loại bỏ các chú thích kiểu PiniaPluginContextPlugin cũng như các import của chúng nếu bạn đang sử dụng một tệp .js.

7.1 Nuxt.js 2

Nếu bạn đang sử dụng Nuxt.js 2, các loại sẽ khác nhau một chút:

{3,15-17}
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'

function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // Note this has to be typed if you are using TS
  return { creationTime: new Date() }
}

const myPlugin: Plugin = ({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
}

export default myPlugin

Đó là một cuộc hành trình thú vị qua việc sử dụng các plugin trong Pinia Vuejs! Chúng ta đã tìm hiểu về cách mở rộng store Pinia thông qua việc thêm mới các tính năng và tùy chọn. Hy vọng rằng thông qua bài viết này, bạn đã có cái nhìn tổng quan và hiểu biết sâu hơn về sức mạnh của việc sử dụng các plugin trong Vuejs. Hãy tiếp tục khám phá và áp dụng những kiến thức này vào dự án của bạn trên Cafedev!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!