typescript vuex_vuex中的hooks在typescript下的设计思路

949cecff4fcdde5edfd5f91f9effffb2.png

前言

vue的新一版本vue3中采用了typescript对主项目vue vue-router进行了重写,同作为vue全家桶的状态管理插件vuex并没有被重写,只是在原基础上进行了优化

作为一个练手的小项目,最近用typescriptvuex进行了重写,重写项目可以点击查看,除了实现基本常用的功能外,添加了更适用于vue-nextsetup写法的hooks api,下面将hooks的设计思想分享给大家

为什么会有hooks api呢

当一个项目的模块module很多的时候,出于避免命名冲突考虑,我们会使用到命名空间namespaced选项

当命名空间复杂起来的时候,我们在读取状态值和修改状态值的方法上也会复杂

所以vuex3.x中提供了诸如mapState mapActions等方法来使写法更简单

但在vuex4.x的使用中,我们发现这几个api仅仅是对老版本进行了兼容,并没有对setup进行支持

cd1be94132c0525b1cd6a9b4ad73d44c.png

官方的demo也是直接在属性上进行了值的读取

所以我们能不能提供一个api让读取像mapState mapActions那样简单一点呢

答案肯定是可以的

Api

等同于 mapState mapGetters mapMutations mapActions

我们提供对应的四个 useState uesGetters useMutations useActions来进行读取

api对应的用法也是类似的

我们拿mapState来举列

这是官方提供的mapState的用法

import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

我们如果用hooks去写

const app = createApp({
  setup() {
    // state1.count = store.state.count
    const state1 = useState(['count']);

    // state2.countAlias = store.state.count
    const state2 = useState({
      // 换个属性名
      countAlias: 'count'
    });

    // 命名空间 state3.test = store.state.inner.count
    const state3 = useState('inner', ['count']);

    // 命名空间另外的写法 state4.test4 = store.state.inner.count
    const state4 = useState({
      test4: 'inner/count'
    });

    // 函数写法 state5.test5 = store.state.count
    const state5 = useState({
      test5: (state) => {
        return state.count;
      }
    });

    return () => h('div');
  }
});

可以看到hooks的写法和map的写法是类似的,而且都比上面贴的图里的demo要友好很多

其他几个hooks也和老版本的map api函数保持一致

我们再看几个其它hooks的最简单的写法

const store = createStore({
  modules: {
    inner: {
      namespaced: true,
      state: {
        test: 1
      },
      getters: {
        foo(state) {
          return state.test + 1;
        }
      },
      mutations: {
        change(state, num) {
          state.test = num;
        }
      },
      actions: {
        async delayChange({ commit }, num) {
          await new Promise((resolve) => {
            const timer = setTimeout(() => {
              clearTimeout(timer);
              resolve();
            }, 100);
          });
          commit('change', num);
        }
      }
    }
  }
});
const app = createApp({
  setup() {
    // state.test = store.state.inner.test
    const state = useState('inner', ['test']);
    // getters.foo = store.getters.inner.foo
    const getters = useGetters('inner', ['foo']);
    // mutations
    const { change } = useMutations({
      change: 'inner/change'
    });
    // actions
    const { delayChange } = useActions('inner', ['delayChange']);

    return () => h('div');
  }
});
app.use(store);
app.mount(document.createElement('div'));

即便你没用过hooks api但也能看得出vuex3.xmap api的影子

typescript设计

同样的我们通过useState这个hooks进行举例,其他的hooks与这个基本是类似的设计思想

基础的模型

function useState(a: any) {
  return a;
}

const state = useState({
  a: 'a'
});

以上便是一个最基础的hooks模型

b871035d9d363e709f12413569eee9f5.png

显然这个类型返回是不合适的,这里的state返回值是any,我们想要的肯定是在state上能够读取到a这个属性

用泛型解决any

ts中的泛型可以解决这个毛病

function useState<T>(state: T) {
  return state;
}

const state = useState({
  a: 'a'
});

c9350010dc840e6c69064bbecd9ad183.png

这么自动推导出来state.a就存在了

不过这里的写法仍然是有问题的

state.a推出来是字符串a的类型string并不是state.a的真实值,所以我们需要在优化一下

更准确的推导

我们在自动推导的情况下这里是没法拿到state.a的真实值的,所以我们需要将state.a返回值设置为any

function useState<T>(
  state: T
): {
  [x in keyof T]: any;
} {
  return state;
}

const state = useState({
  a: 'a'
});

8e7484d928b84f0470399afc9870b97e.png

我们通过[x in keyof T]拿到对应的属性值,并把对应的属性返回值设置为any,这样一来就解决了上面的问题

手动指定泛型

自动推导能推的类型只能帮助我们拿到最后结果的属性

当我们明确知道state.a的属性值类型时,我们需要手动指定泛型T的值

const state = useState<{
  a: number    
}>({
  a: 'a'
});

我们希望上面读取state.a的结果是number类型

我们这里指定的泛型是函数结果的返回类型,并不是函数参数的类型,所以我们得改两个位置

function useState<T>(
  state: {
    [x in keyof T]: string;
  }
): {
  [x in keyof T]: T[x];
} {
  const obj = Object.create(null);
  obj.a = 1;
  return obj;
}

对于函数的参数类型,只有属性是与之对应的,属性值应该永远是字符串类型

对于函数的返回值类型,属性和属性值类型都是对应的

现在让我们再来看看手动指定泛型对应的结果

const state = useState<{
  a: number;
}>({
  a: 'a'
});

d6b79716f68d043e22d3337420fbe890.png

可以看到state.a被正确推导为number类型

同样的我们来看看自动推导的结果

const state = useState({
  a: 'a'
});

6e0e01a81279f3ff96f78efa4e1ddf35.png

可以看到结果被推导为了unknown

首先解释下为什么会是unknown

泛型的自动推导得是在这个泛型被函数参数用上时

比如下面这个列子

function foo<T>(a: T) {
  return a;
}

const res = foo({
  a: 1
});

6a8cb8efc53cb68f672be2de4029e331.png

这里的res就能自动推导为对应的预期结果

我们上面的函数参数可以看到我们只通过[x in keyof T]用上了对应的属性,值是被永久设置为了string,我们并没有在此处用上对应泛型的属性值T[x],所以推导出对应的便是unknown

解决自动推导下返回值为unknown的问题

这里需要用到extends去判断当前返回类型是不是unknown

如果是则表明是自动推导的那么返回值需要返回any

那么用 T[x] extends unknown可以做到吗?

答案是不可以

这里我们期待的是当T[x]unknown可以让这个式子为true

但实际上T[x]为任何值 这个式子都是true

unknown extends unknown

string extends unknown

any extends unknown

以上全为true

其实也好理解 unknown代表的就是不知道是啥类型 所以各个类型都算它的子集

所以这里得麻烦一点 我们得反其道行之 去取已知类型判断

type KnownType = string | number | boolean | undefined | null | object;

function useState<T>(
  state: {
    [x in keyof T]: string;
  }
): {
  [x in keyof T]: T[x] extends KnownType ? T[x] : any;
} {
  const obj = Object.create(null);
  obj.a = 1;
  return obj;
}

const state = useState({
  a: 'a'
});

这样当T[x]有返回值的时候先取返回值表示是手动指定的泛型,但是其他值的时候则表示是自动推导的类型

5798c876c2cca5ae92c7dd8a3ee68a4f.png

可以看到state.a在自动推导下返回值正确了

兼容数组写法

现在我们设计完了useState的一种对象形式,看上面api设计中我们知道它还有一种数组写法

function useState(state: string[]) {
  const obj = Object.create(null);
  obj.a = 1;
  return obj;
}

const t = useState(['a']);

现在这个t得到的是any,现在我们的目标是让t.a能够被自动推导出来

这个有方法吗

方法肯定是有的 不过很麻烦 说的是自动推导 其实麻烦程度感觉和手动赋值快接近了

同样的我们使用泛型来看看推导出来的数组是啥

function useState<T extends string[]>(state: T) {
  return state;
}
const state = useState(['a']);

a6165992c78d9d2ee288e6e784c21286.png

我们可以看到state中没有获取到准确的属性a只能获取个大范围string

这种泛型约束是没有办法去读取到准确的属性的,即便去读取索引项的值,也只能获取大范围

function useState<T extends string[]>(state: T): T[number] {
  return state[0];
}
const state = useState(['a']);

fa6584c17b730f900eff2053f3a1d003.png

可以看到state返回仍是string

所以我们也需要减小我们定义泛型的范围,将泛型定义在具体的每一个索引中

function uesState<A extends string, B extends string>(
  state: [A, B]
): Record<A | B, any> {
  const obj = Object.create(null);
  return obj;
}

const state = uesState(['a', 'b']);

我们不是用一整个泛型T去定义整个数组,而是将数组拆分,用了两个泛型A B去接受对应的数组索引项

这样推导出来的结果便是正确的

dee93d5f726d2e38daceec4e0063609a.png

但这里还要问题,上面指定的是两个索引,如果出现多个索引呢,此时就要用到typescript的另外一个特性重载

function useState<A extends string>(state: [A]): Record<A, any>;
function useState<A extends string, B extends string>(
  state: [A, B]
): Record<A | B, any>;
function useState(state: any) {
  return Object.create(null);
}

7ca6632c0e0aa140060e41dad4f2a09e.png

这样1个参数或者2个参数都可以被推导出来

如果真的要这么做,重载10次最多10个参数应该能满足绝大部分需求了,这也是前面提到过的为啥这么麻烦

如何使类型推导更简便

那么有方法可以稍显正常一点完成这个操作吗,其实也是有的,只不过我们需要改一下函数的参数传递方式

既然传递一个数组没法获取,那我们就一个一个参数传递

我们来看看这个函数

function useState<T extends string[]>(...args: T): T {
  return args;
}
const state = useState('a', 'b');

这里的泛型仍然指的是传入参数的数组值,但传入的方式已经改变了这样推导出来的类型便是实际的值

543e023947ca448f4f1c87f4c1517a81.png

可以看到推导出来的state['a','b'],此时对类型加上number,便可以得到a | b

function useState<T extends string[]>(...args: T): T[number] {
   let t: any;
   return t;
}
const state = useState('a', 'b');

0b9be49d80a2a4b52d5938e6db4d4139.png

然后我们再用Record包装一下

function useState<T extends string[]>(...args: T): Record<T[number], any> {
  let t: any;
  return t;
}
const state = useState('a', 'b');

可以看到返回的state已经有了ab这两个属性值

421a504badc714f5e57dffc5f18af1e9.png

这里还有一个namespace选项没有加上,因为参数是一个一个传的,所以我们在包一层函数用来独立接受namespace

以下就是完整的useState写法

function useState<T extends string[]>(...args: T) {
  return function (namespace: string): Record<T[number], any> {
    let t: any;
    return t;
  };
}
const state = useState('a', 'b', 'c')('inner');

bc0ed8023f14ec73f4fbfe45f3e1b62f.png

可以看到state可以正确的被自动推导

这种情况下虽然是被自动推导了,但和原本的api有冲突,且大部分时候也不应该去用自动推导,所以我们还是保持了原本的api模式,仍然是传入数组,而不是一个一个参数的传入

结合两种写法

这里又得用上typescript的重载,不过其实重载的次数不是两次,而是四次,因为还有一个命名空间的参数,这个属性可能存在也可能不存在

type KnownType = string | number | boolean | undefined | null | object;

type Dictionary<T = any> = Record<string, T>;

function useState<States = Dictionary>(states: string[]): States;
function useState<States = Dictionary>(
  namespaced: string,
  states: string[]
): States;
function useState<States>(
  states: {
    [x in keyof States]?: string;
  }
): {
  [x in keyof States]: States[x] extends KnownType ? States[x] : any;
};
function useState<States>(
  namespaced: string,
  states: {
    [x in keyof States]?: string;
  }
): {
  [x in keyof States]: States[x] extends KnownType ? States[x] : any;
};
function useState(namespaced?: unknown, states?: unknown) {
  let t: any;
  return t;
}

const state1 = useState({
  a: 'a'
});

const state2 = useState<{
  a: number;
}>({
  a: 'a'
});

const state3 = useState(['a']);

const state4 = useState<{
  a: number;
}>(['a']);

可以看到各个state的值都被正确推导了

04c70f84ae1678d5eeaef8f44792ece9.png

cda8990950fe860e5188bbc9f88726f3.png

bed8644b3e95acefcdd44894a5ddb266.png

3ac499afc8b363ea103f8cbda5d44d53.png

这里的ts设计只列举了useState这个api,其它api的思路几乎是类似的

结语

以上就是hooks的设计思路的探索,完整的重写项目可以点这里查看,如果有更好的推导方式或api设计也可以交流交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值