Coder Social home page Coder Social logo

pinia-guide's Introduction

vuex4现状

Vue.js于2020-09-18发布3.0版本。Vuex于2020-03-15发布4.0-alpha.1版本,至2021-06-17发布4.0.2版本,Vuex已经有好几个月没有发新版了。我从2020年下旬开始在公司中尝试使用一整套Vue3生态 + TypeScript开发项目,算是比较早的使用者了,简单说下我的使用感受。

Vue3本身加其它几个核心库用起来很丝滑。如果不使用TypeScript的话,vuex4也没啥大毛病,但是一旦用上TypeScript,就难受起来了。Vuex4的TypeScript类型体验我只想用一个字来形容:拉胯,拉的一。究其原因,Vuex4源码仍然是使用js开发的,只是向外暴露了types类型声明,而且类型声明还是阉割的,仅仅在types/index.d.ts中我就搜索到了22个any。这就导致如果你想享受Vuex4的类型提示的话,你必须自己额外写非常非常冗长的模板代码来补全这被阉割的类型。

我知道你可能看过You Might Not Need Vuex with Vue 3这篇文章,然后说Vue3不需要Vuex。的确,简单场景下,这没问题。但是这也意味着你无法享受devtools扩展带来的调试的便利。另外,你无法在setup函数外使用你的响应式状态,试想一个场景:当你想在axios的请求拦截器中获取你响应式状态的用户token,是不是就抓瞎了?

实际上Vuex官方也清楚现在存在的问题,所以2021年3月2日kiaking就提交了一个PR:Vuex5的提案。不知道为什么,现在vue的rfcs列表里我找不到这个rfc了,但是通过上面PR链接里的commit记录我们仍然可以看到这条rfc的具体内容,感兴趣的可以看一下。

Pinia

说了那么多,那么在Vuex5发布之前,我们要怎么愉快地组织我们的状态管理呢?试试Pinia吧。Pinia本来是为了测试Vuex5提案而出现的,它的作者是Vue.js核心团队的成员,现在它不但支持Vue3也支持Vue2的options API,不过我只用在Vue3中,所以今天不讲options API的用法。虽然它现在的API和Vuex比较像,但是和Vuex相比,它还是又很多明显的不同之处的:

  • mutations很啰嗦所以移除了,现在只有actions,一开始加入mutations这个概念也只是为了在devtools中明确记录状态变化。
  • Pinia源码使用TypeScript开发,不需要像现在Vuex一样写一堆模板代码来支持类型推断。
  • Vuex有很多魔法字符串,比如$store.dispatch('user/login'),这些魔法字符串都是天生TypeScript不友好的。在Pinia里都变成了函数调用,类型推断友好:store.login()
  • Vuex一般是应用初始化的时候就在main.js里把store一次性注册好了,Pinia是你什么时候调用某个store才会在那个时刻创建。
  • 不再需要命名空间和模块嵌套了,比如以前Vuex里你划分了userapp这两个模块,通过人为约定的字符串来划分命名空间。现在只需要为这两个模块创建两个独立的文件,分别导出各自的store变量,这已经达到命名空间的目的了。

基本用法

关于Pinia的用法,我不会把官网翻译一遍,把所有知识点都罗列一遍,那实在没有意义,我只会把基本用法说一下,把容易忽略的点和容易踩坑的点指出来。为此我创建了一个仓库,并使用不同的分支来管理代码,感兴趣的可以查看。

首先要注册Pinia实例,并将实例挂载到app上:

// store/index.ts
import { createPinia } from 'pinia'
export const pinia = createPinia()
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { pinia } from './store'

createApp(App).use(pinia).mount('#app')

pinia实例挂载后,就是定义你的一个个store了,为了更好的组织代码,你可以一个store对应一个文件。

Pinia定义store有两种风格的写法,第一种风格的写法官网只粗略的一带而过,如果你不仔细看,可能都注意不到还能这么写:

composition API风格写法

该风格的代码可以在我composition-api分支下找到源码

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)

  // getters
  const double = computed(() => dount.value * 2)

  // actions
  function increment() {
    count.value++
  }

  return { count, double, increment }
})

在组件里使用:

import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()

    counter.count++
    counter.double // 2
    // with autocompletion ✨
    counter.$patch({ count: counter.count + 1 })
    // or using an action instead
    counter.increment()
  },
}

这种方式就和我们平时写composition API没啥区别,不过官方只花了很少的篇幅介绍了这种写法,而且这种写法貌似无法配置Pinia的plugin,所以后面主要说第二种用法。

常用的写法

该风格的代码可以在我vuex-style分支下找到源码

import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    }
  },
})

这种风格的写法比较贴近于Vuex的写法。

插件

Pinia还提供了插件API,插件API能做很多事,但是回想一下在使用Vuex时你最常用的插件是不是就是vuex-persist?没错,Pinia也提供了这样的能力。

首先需要定义一个plugin,它是一个函数:

// /store/plugins/piniaPersist.ts
export function piniaPersist(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()`
  // ...
}

然后需要注册这个plugin:

import { createPinia } from 'pinia'
import piniaPersist from './plugins/piniaPersist'

export const pinia = createPinia()
pinia.use(piniaPersist)

plugin的内容我直接用了pinia-plugin-persist这个库,不过这个库还处于开发阶段,我使用的时候发现它依赖的Pinia版本还处在0.x,与我当时使用的2.x版本不兼容,所以修改了部分代码放在本地来维护,内容如下:

import { watch } from 'vue'
import { PiniaPluginContext } from 'pinia'
import type { StateTree, _GettersTree } from 'pinia'

export interface PersistStrategy {
  key?: string
  storage?: Storage
  paths?: string[]
}

export interface PersistOptions {
  enabled: true
  strategies?: PersistStrategy[]
}

type Store = PiniaPluginContext['store']
type PartialState = Partial<Store['$state']>

declare module 'pinia' {
  export interface DefineStoreOptions<
    Id extends string,
    S extends StateTree,
    G extends _GettersTree<S>,
    A
  > {
    persist?: PersistOptions
  }
}

const updateStorage = (strategy: PersistStrategy, store: Store) => {
  const storage = strategy.storage || sessionStorage
  const storeKey = strategy.key || store.$id

  if (strategy.paths) {
    const partialState = strategy.paths.reduce((finalObj, key) => {
      finalObj[key] = store.$state[key]
      return finalObj
    }, {} as PartialState)

    storage.setItem(storeKey, JSON.stringify(partialState))
  } else {
    storage.setItem(storeKey, JSON.stringify(store.$state))
  }
}

export default ({ options, store }: PiniaPluginContext) => {
  if (options.persist?.enabled) {
    const defaultStrat: PersistStrategy[] = [
      {
        key: store.$id,
        storage: sessionStorage,
      },
    ]

    const strategies = options.persist?.strategies?.length
      ? options.persist?.strategies
      : defaultStrat

    strategies.forEach((strategy) => {
      const storage = strategy.storage || sessionStorage
      const storeKey = strategy.key || store.$id
      const storageResult = storage.getItem(storeKey)

      if (storageResult) {
        store.$patch(JSON.parse(storageResult))
        updateStorage(strategy, store)
      }
    })

    watch(
      () => store.$state,
      () => {
        strategies.forEach((strategy) => {
          updateStorage(strategy, store)
        })
      },
      {
        deep: true,
        immediate: true,
      }
    )
  }
}

用法是在定义store的时候添加一个persist属性:

import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
  persist: {
    enabled: true,
    strategies: [
      {
        storage: localStorage,
        key: 'PINIA_COUNTER',
      },
    ],
  },
})

持久化部分的源码可以在我plugin分支下找到源码。

pinia-guide's People

Contributors

nikolausliu avatar

Stargazers

 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.