Skip to content

Latest commit

 

History

History
278 lines (207 loc) · 8.29 KB

README-CN.md

File metadata and controls

278 lines (207 loc) · 8.29 KB

Hami-Vuex 🍈 (hami melon flavored)

npm package build status code coverage

Translate to English

哈密瓜味的Vuex! State management for Vue.js

主要特点:

  • 基于 Vuex 构建,可与 Vuex 3 & 4 兼容和混合使用
  • 兼容 Vue 2 和 Vue 3,低学习成本,无迁移压力
  • 易于编写模块化的业务代码,Store 文件不再臃肿
  • 完全的 TypeScript 支持,代码提示很友好
  • 单元测试 Line Coverage: 100%

开始使用

npm install --save hami-vuex

创建 Hami Vuex 实例

// store/index.js
import { createHamiVuex } from 'hami-vuex'
export const hamiVuex = createHamiVuex({
    vuexStore: /* 按照 Vuex 文档创建的 Vuex Store 实例 */
})

也可以使用默认创建的空 Vuex Store 对象,Vue 2 + Vuex 3 的写法:

import Vue from 'vue'
import Vuex from 'vuex'
import { createHamiVuex } from 'hami-vuex'

Vue.use(Vuex)

export const hamiVuex = createHamiVuex({
    /* 可选:Vuex Store 的构造参数 */
})

以及 Vue 3 + Vuex 4 的写法:

import { createApp } from 'vue'
import { createHamiVuex } from 'hami-vuex'

export const hamiVuex = createHamiVuex({
    /* 可选:Vuex Store 的构造参数 */
})

export const app = createApp()
app.use(hamiVuex)

Hami Vuex 实例用于创建和管理 Hami Store,所有业务功能都通过 Hami Store 实现。

创建一个 Counter Store

// store/counter.js
import { hamiVuex } from '@/store/index'

// 实现原理:动态注册的 Vuex Module 对象
export const counterStore = hamiVuex.store({

    // 设置一个唯一名称,方便调试程序和显示错误信息
    $name: 'counter',

    // 定义状态,可以是简单的对象(会自动做深拷贝处理)
    $state: {
        count: 0,
    },

    // 定义状态,也可以是返回状态的函数
    $state() {
        return { count: 0 }
    }

    // 定义一个 getter,和 Vue computed 类似
    get double() {
        return this.count * 2
    },

    // 定义一个函数,等价于 Vuex action
    increment() {
        // $patch 是内置的 Vuex mutation,用于更新状态
        // 可以传递 K:V 形式的对象,进行浅拷贝赋值
        this.$patch({
            count: this.count + 1
        })
        // 对于复杂的操作,可以通过回调函数更新状态
        this.$patch(state => {
            state.count = this.count + 1
        })
    },

    // 定义一个异步函数,这也等价于 Vuex action
    async query() {
        let response = await http.get('/counter')
        this.$patch({
            count: response.count
        })
    }
})

我们不再需要定义 mutations,用内置的 $patch 就够了,所有的函数都是 actions 。

在 Vue 组件中使用 Counter Store

import { counterStore } from '@/store/counter'

export default {
    computed: {
        // 使用 state 中定义的属性
        count: () => counterStore.count,
        // 使用 getter 定义的属性
        double: () => counterStore.double
    },
    methods: {
        // 使用 action 方法
        increment: counterStore.increment,
    },
    async mounted() {
        // 直接调用 action 方法,以及访问属性(也可以 Store 之间互相调用)
        await counterStore.query()
        console.log(counterStore.count)
    },
    destroyed() {
        // 可以通过 $reset 重置状态
        counterStore.$reset()
    }
}

恭喜你,可以尽情享用哈密瓜味的 Vuex 了!

TypeScript支持

Hami-Vuex 完全支持 TypeScript,可以通过 TypeScript 配置,让类型推断更智能(代码提示更友好)。

// tsconfig.json
{
  "compilerOptions": {
    // 与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 这可以对 `this` 上的数据 property 进行更严格的推断
    "strict": true
  }
}

详细说明可参考:TypeScript 支持 - Vue.js

高级用法

如果不需要 Vue SSR 服务端渲染,则可以跳过以下高级用法(大部分情况都不需要)。

在上述基础用法中,Store 对象是 有状态的单例,在 Vue SSR 中使用这种对象,需要设置 runInNewContext 参数为 true,这会带来比较大的服务器性能开销。

高级用法可以避免「有状态的单例」,适合在 Vue 3 setup 方法中使用,也可以用于 Vue 选项式写法。

定义一个 Counter Store

import { defineHamiStore } from 'hami-vuex'

// 注意:这里是首字母大写的 CounterStore !
export const CounterStore = defineHamiStore({
    /* 这里参数和 hamiVuex.store() 一样 */
})

使用方法1: 绑定 Vuex Store 实例

const vuexStore = /* Vuex Store 实例 */
const counterStore = CounterStore.use(vuexStore)
await counterStore.query()
console.log(counterStore.count)

使用方法2: 在 Vue setup 方法中使用

export default {
    setup() {
        const counterStore = CounterStore.use()
        await counterStore.query()
        console.log(counterStore.count)
    },
}

使用方法3: 在 Vue computed 中使用

export default {
    computed: {
        // 效果类似于 Vuex mapActions, mapGetters
        counterStore: CounterStore.use,
        count: CounterStore.count,
        query: CounterStore.query,
    },
}

使用方法4: 在 Store 中互相使用

const otherStore = defineHamiStore({
    get counterStore(){
        return CounterStore.use(this)
    },
    get count(){
        return this.counterStore.count
    }
})

参考和鸣谢

特别感谢以上项目和资料给我带来了很多灵感,也希望 Hami Vuex 能给社区带来新的灵感!

设计思路

为什么要用「有状态的单例」?

有状态的单例使用更加简单方便,仅在 Vue SSR 方面性能不佳。对于大部分单页应用都不需要 Vue SSR,所以可以放心使用「有状态的单例」,在必要情况下也可以切换到高级用法。

为什么要把 getters 和 actions 写在同一层级?

由于 TypeScript 难以对 Vue 选项式接口做类型推断,写在同一层级可以避免这个问题。

具体原因可以参考以下资料:

为什么 $name 和 $state 要加 $ 前缀?

因为把所有字段写在同一层级之后,需要避免名称冲突,所以特殊字段都用 $ 前缀。

为什么不用 mutations 了?

用 mutations 的好处是在 devtools 调试更方便,坏处是代码稍微繁琐一些。还有一个问题是,把所有字段写在同一层级之后,难以区分 actions 和 mutations。经过权衡,决定不用 mutations 了。

为什么不用 Pinia ?

用过一段时间 Pinia,觉得 devtools 插件支持不太好,不稳定。另外 Pinia 的写法对 Vue 选项式用法不够友好,应该是为了支持 Vue SSR 导致的。

为什么基于 Vuex 实现?

我认为 Vuex 本身做的很好,改进一下接口设计就可以达到我要的效果,不需要重复造轮子。而且基于 Vuex 实现,可以最大程度兼容老代码,很容易做平滑迁移。

联系方式

技术问题建议通过 issues 提交,方便讨论和搜索查阅。

博客:Guyskk的博客 / 公众号:自宅创业