Vue3-Vuex-TypeScript 踩坑之旅

8,534 阅读5分钟

前言

vue3 已经来了,vuex 新版本也在 beta 中,在应用于项目前,先过过水,气的我差点把电脑砸了,为啥要砸,咱们开始正文吧...

安装

npm init vite-app vue3-vuex-ts
npm install

开始正文

1.将 js 文件改造成 ts 文件

  • main.js -> main.ts
  • 修改 vue 文件中的 script 类型为 lang = "ts"
  • 修改 index.html
<script type="module" src="/src/main.ts"></script>

2.创建 vue 的文件声明

  • ts 环境中没有 vue 模块声明

  • src 下创建 vue.d.ts 文件

declare module '*.vue' {
  import { FunctionalComponent, defineComponent } from 'vue';
  const component: ReturnType<typeof defineComponent> | FunctionalComponent;
  export default component;
}

接入 vuex

npm install vuex@next --save

installing,let's talk about of vuex

为什么 vuex2 能进行双向绑定?

let Vue;

class Store {
  constructor(options) {
    const { getters, state, mutations, actions } = options;
    this._mutations = mutations;
    this._actions = actions;
    if (getters) {
      this.handleGetters(getters);
    }
    this._vm = new Vue({
      data: {
        $$state: state,
      },
    });
    this.commit = this.commit.bind(this);
    this.dispatch = this.dispatch.bind(this);
  }

  get state() {
    return this._vm.data.$$state;
  }

  commit(type, payload) {
    const entry = this._mutations[type];
    if (entry) {
      entry(this.state, payload);
    }
  }

  dispatch(type, payload) {
    const entry = this._actions[type];
    if (entry) {
      entry(this, payload);
    }
  }

  handleGetters(getters) {
    this.getters = {};
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
      });
    });
  }
}

function install(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store;
      }
    },
  });
}

export default {
  Store,
  install,
};
  • vuex 强绑定了 Vue(将观测数据 new vue),因此 vux 无法作为状态工具被别的状态使用
  • 通过 Vue.mixin 的 beforeCreate 将 $store 挂在到 Vue 的原型链上 不吹不喷哈:Vue 不能回收,所有状态无法回收,所有 Vue 插件无法回收 每一次使用组件都走一遍原型链,在 vue 渲染过程中,beforeCreate 会被执行无数遍 😓

vuex3

  • 利用 reactive、provide 来进行双向绑定,这里就不细说了,指出几个关键点
store._state = reactive({
  data: state
})

install (app, injectKey) {
  app.provide(injectKey || storeKey, this)
  app.config.globalProperties.$store = this
}

准备蹲坑吧

1.main.ts

import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
import './index.css';

createApp(App).use(store).mount('#app');

2.创建 store 的相关东西

  • index.ts
import { createStore } from 'vuex';
import modules from './modules';

import { userState } from './modules/user';
import { detailState } from './modules/detail';

export interface State {
  user: userState;
  detail: detailState;
}

export default createStore <
  State >
  {
    modules,
  };
  • modules.ts
import user from './modules/user';
import detail from './modules/detail';

export default {
  user,
  detail,
};
  • modules/user.ts
export type userState = {
  isLogin: boolean,
};

const state: userState = {
  isLogin: true,
};

export default {
  namespaced: true,
  state,
  getters: {
    loginInfo: (state: userState): string => {
      return `${state.isLogin ? '已登陆' : '未登陆'}`;
    },
  },
  mutations: {
    setUserInfo(state: userState, payload: boolean): void {
      console.log('数据请求', payload);
      state.isLogin = payload;
    },
  },
  actions: {
    changeUserInfo({ commit }, payload: { data: boolean }): void {
      console.log('action执行成功');
      setTimeout(function () {
        commit('setUserInfo', false);
      }, 2000);
    },
  },
};
  • detail.ts 此处省略

  • HelloWorld.vue

<template>
  <div>
    <h1>{{ msg }}</h1>
    <button @click="count++">count is: {{ count }}</button>
    <div>{{ info1 }}</div>
    <div>{{ info2 }}</div>
    <button @click="logout">退出</button>
  </div>
</template>

<script lang="ts">
import { useStore } from 'vuex';
import { defineComponent, computed } from 'vue';
import { State } from '../store';

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  setup(props) {
    const { state, getters, dispatch, commit } = useStore<State>();
    console.log('🍊', state.user.isLogin); // 🍊 true
    console.log('🍎', state.detail.title); // 🍎 hello
    console.log('🚀', getters['user/loginInfo']); // 🚀 已登陆

    const info1 = computed(() => getters['user/loginInfo']);
    const info2 = computed(() => getters['detail/detailInfo']);

    const logout = () => commit('user/setUserInfo', false); // 数据请求 false
    // dispatch()
    return { info1, info2, logout };
  },
});
</script>

到了结束的时候了,感觉自己萌萌哒 🐶

No.No.No,如果团队项目这么做,估计脑瓜子得被销放屁了...🤯

这样真的结束了么?

  • 看图说话

    哦哦,State 我包了一层,提示粗糙

    阿西巴,这什么鬼,any,此时内心已经崩溃,无力吐槽...

    dispath 提示让传 string 类型的 type,我上拿知道要传啥(commit 也这德行),那我还不如直接用 js 写方便呢

    前期做了如此多的铺垫,结果提示让传 string 类型的 type,我特么上哪知道要传啥,交给 ts 的活儿,反过来让我自己弄,之前的全白费?

    这就好比,去泡吧,前戏铺垫的挺足,结果出去后发现是蕾丝~ 😓

    该搞还是得搞,无语ing

开始爬坑

1.修复 State 问题,modules/user.ts

const state = {
  isLogin: true,
};

export type userState = typeof state;

export default {
  // ....
};

但这样也不行,我不能每个组件都引一次 State 吧,咱们直接到下一步

2.修复 getters 问题~

  • vue 和 react 一样通过 hooks 处理状态,那么咱也整个 hooks,把 useStore 封装起来,在里面修复问题

  • 需要提供 gettters 中 如'loginInfo' 的注释,dispatch、 commit 的注释

    • 遍历 modules,找到所有 getters、actions、mutations,并获取它们的 type
    • 将它们通过 hooks 传递出去
  • 修改 HelloWorld.vue

<script lang="ts">
// import { useStore } from 'vuex';
import { defineComponent, computed } from 'vue';
// import { State } from '../store';
import { useStoreHooks } from '../hooks';

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  data() {
    return {
      count: 0,
    };
  },
  setup(props) {
    // const { state, getters, dispatch, commit } = useStore<State>();
    const { state, getters, dispatch, commit } = useStoreHooks();
    console.log('🍊', state.user.isLogin); // 🍊 true
    console.log('🍎', state.detail.title); // 🍎 hello
    console.log('🚀', getters['user/loginInfo']); // 🚀 已登陆

    const info1 = computed(() => getters['user/loginInfo']);
    const info2 = computed(() => getters['detail/detailInfo']);

    const logout = () => commit('user/setUserInfo', false); // 数据请求 false
    // dispatch()
    return { info1, info2, logout };
  },
});
</script>
  • 创建 hooks/index.ts
import { useStore } from 'vuex';
import { State } from '../store';
// 这几个玩意的type
import { Getters, Dispatch, Commit } from './utils';

interface UseStoreHooks {
  state: State;
  getters: Getters;
  commit: Commit;
  dispatch: Dispatch;
}

const useStoreHooks = (): UseStoreHooks => {
  const store = useStore<State>();
  console.log(store);
  // return store;
  // 自定义的进行输出结果
  const { state, getters, dispatch, commit }: UseStoreHooks = store;
  return {
    state,
    getters,
    commit,
    dispatch,
  };
};

export { useStoreHooks };
export default useStoreHooks;
  • hooks/utils.ts
    • 写 type,这玩意可不是 js,每一段都需要从下往上看
    • 安装最新 typescript,yarn add typescript@next(目前)
    • vscode 切换 typescrit 版本
/* 拿到store的modules */
import modules from '../store/modules';

/* 获取到 getters 结构类型 */
// 匹配到 单个module 下的 getter,小技巧 infer 为某一项
type GetGetter<GetterType> = GetterType extends { getters: infer G } ? G : unknown;
// 获取 vuex 所有的 getters 模块
type GetGetters<GetterTypes> = {
  [K in keyof GetterTypes]: GetGetter<GetterTypes[K]>;
};
type ModuleGetters = GetGetters<typeof modules>;

// --------------------

/* 获取到 mutations 结构类型 */
// 配到 单个 module 下的 mutations
type GetMutation<MutationType> = MutationType extends { mutations: infer M } ? M : unknown;
// 获取 vuex 所有的 mutations 模块
type GetMutations<MutationTypes> = {
  [K in keyof MutationTypes]: GetMutation<MutationTypes[K]>;
};
type ModuleMutations = GetMutations<typeof modules>;

// --------------------

/* 获取到 actions 结构类型 */
// 配到 单个 module 下的 action
type GetAction<ActionType> = ActionType extends { actions: infer A } ? A : unknown;
// 获取 vuex 所有的 actions 模块
type GetActions<ActionTypes> = {
  [K in keyof ActionTypes]: GetAction<ActionTypes[K]>;
};
type ModuleActions = GetActions<typeof modules>;

// --------------------

/* Getter/Commit/Dispatch 智能提示处理 */
// gettters[模块名/方法]、commit[模块名/方法]、dispatch[模块名/方法]
// ts4.1 以上支持 模板字符串语法,需要安装最新的 yarn typescript(目前yarn add typescript@next)
// 传入的是 keyof 有可能是symbol | number,所以 P & string 取其中的string
type AddPrefix<P, K> = `${P & string}/${K & string}`;

// 调换一下顺序:user: "user/loginInfo" => "user/loginInfo": user
type GetSpliceKey<Module, Key> = AddPrefix<Key, keyof Module>
/**
 * { 'user/loginInfo': () => {} }
 */
// type GetSpliceKeys<Modules> = {
//   [K in keyof Modules]: GetSpliceKey<Modules[K], K>
// }
// type xx = GetSpliceKeys<ModuleGetters>

type GetSpliceKeys<Modules> = {
  [K in keyof Modules]: GetSpliceKey<Modules[K], K>
}[keyof Modules]
// type xx = GetSpliceKeys<ModuleGetters>

type GetFunc<T, A, B> = T[A & keyof T][B & keyof T[A & keyof T]];
type GetSpliceObj<T> = {
  // K extends `${infer A}/${infer B}`   相当于  user/loginInfo  A=>user B=>loginInfo
  [K in GetSpliceKeys<T>]:K extends `${infer A}/${infer B}` ? GetFunc<T, A, B> : unknown
}

// --------------------

/* Getters/Mutations/Actons 拼接好 xxx/xxx 的格式  */
type GenGetters = GetSpliceObj<ModuleGetters>;
type Getters = {
  [K in keyof GenGetters]:ReturnType<GenGetters[K]>
}

// --------------------

type GenMutations = GetSpliceObj<ModuleMutations>;
type Mutations = {
  [K in keyof GenMutations]:ReturnType<GenMutations[K]>
}

// --------------------

type GenActions = GetSpliceObj<ModuleActions>;
type Actions = {
  [K in keyof GenActions]:ReturnType<GenActions[K]>
}

// --------------------

// commit 获取 payload 参数类型
type MutationsPayload = {
  // Parameters 获取函数参数
  [K in keyof GenMutations]:Parameters<GenMutations[K]>[1]
}

interface GetCommit<T> {
  // 可选参数类型,会自动加上undefined
  <K extends keyof T>(mutation: K, payload?: T[K]): void;
}

type Commit = GetCommit<MutationsPayload>;

// --------------------

// dispatch 获取 payload 参数类型
type ActionPayload = {
  // Parameters 获取函数参数
  [K in keyof GenActions]:Parameters<GenActions[K]>[1]
}
interface GetDispatch<T> {
  // 可选参数类型,会自动加上undefined
  <K extends keyof T>(action: K, payload?: T[K]): Promise<unknown>;
}

type Dispatch = GetDispatch<ActionPayload>;

// --------------------

export {
  Getters, Mutations, Actions, Dispatch, Commit
};
  • 来看一下结果 hooks,里面已经拿到 type 了

我特么心态崩了,我在 hooks 里面明明可以拿到 getters,在外面又拿不到了 ~~

如梦初醒,仿如隔世

❤️ 千疮百孔

  • ts 中可以取到,vue 拿不到,呵呵~

  • 好吧,把 script 部分拿出来,总可以了吧,耐心已经耗尽...

    1.把 HelloWorld.vue 中的 ts 部分全部放到 HelloWorld.ts

    2.修改HelloWorld.vue

<template>
  <div>
    <h1>{{ msg }}</h1>
    <button @click="count++">count is: {{ count }}</button>
    <div>{{ info1 }}</div>
    <div>{{ info2 }}</div>
    <button @click="logout">退出</button>
  </div>
</template>

<script lang="ts" src="./HelloWorld.ts"></script>
  • 去 HelloWorld.ts 去验证一下,特么终于成功了

getters

commit

dispatch

本文代码

尾声

抬眼看窗外,天已亮了,烟已抽完。不喜不悲,厌倦了...

真心学不动了:手拿打火机,哪里不会点哪里,妈妈再也不用担心我的学习了...

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B