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.
Nội dung chính
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ư secret
và hasError
:
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.$subscribe và store.$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ả store
và store.$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 DefineStoreOptions
và DefineSetupStoreOptions
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 PiniaPluginContext
và Plugin
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!