import vue from vue_不使用装饰器在 Vue 中书写 TypeScript

v2-b66bad25efc24f08b9e218549ae371a1_1440w.jpg?source=172ae18b

为什么要用 TypeScript

一开始是坚定地 Javascript 派,但是后来接触了其他同事用ts写的的项目之后发现 VSCode 对 TypeScript 的支持是相当不错(可真有你的微软),类型提示非常好用,给开发带来了比较大的效率提升。

为什么不用装饰器

  • Class API 在 Vue 3 的 RFC 中被拒绝了
  • 同事没装这个包
  • 我个人也不是很喜欢装饰器
  • 因为他使用了class的写法,于是如果你是从以前的项目进行迁移的话,非常麻烦。 虽然写起来真的很舒服,新项目可以尝试一下,不过现在的话没有到推荐的程度。

那么现在 Vue 自己也有了 vue-cli,也有了官方的 TypeScript 支持之后想挑战一下如果不使用装饰器,Vue现在能够怎么和 TypeScript 进行结合。

以前的写法

两年前曾经在 Vue 2.4 的时候就写过一篇关于Vue+TypeScript 的体验文章,因为导出的只是对象,里边的 this 没法预测类型,只能作为 ComponentOptions 进行导出。

我们需要在导出的时候先写对象上的属性内容,再把导出对象加上as类型,让ts知道我们这个对象的this上有什么东西,也就是我们需要重复书写类型与值,写了两遍属性,有点繁琐。

嘛当然至少是能用的,就是写的code太多了

2.5的变化

https://medium.com/the-vue-point/upcoming-typescript-changes-in-vue-2-5-e9bd7e2ecf08

具体的变化点可以看上边,原理没有提及,可以去看PR源码以及如果搜一下Google的话也有很多源码解析

原理是利用工具类 ThisType<T>,在官网上是有详细例子进行解析的

简单拿 Vue 来做例子的话大概就是下面的样子

type DataDef<D> = D | (() => D);

type ComponentsOption<Data, Method> = {
  data?: DataDef<Data>;
  methods?: Method & ThisType<Data & Method>;  // Type of 'this' in methods is Data & Method
}

function Vue<Data, Method>(desc: ComponentsOption<Data, Method>): Data & Method {
  let data: Data;
  if (typeof desc.data == 'function')
    data = (desc.data as (() => Data))();
  else if (desc.data)
    data = desc.data;
  else data = {} as Data;

  let methods: object = desc.methods || {};
  return { ...data, ...methods } as Data & Method;
}

let obj = Vue({
  data() {
    return {
      x: 0,
      y: 0
    };
  },
  methods: {
    moveBy(dx: number, dy: number) {
      this.hello(); // Strongly typed this
      this.x += dx;  // Strongly typed this
      this.y += dy;  // Strongly typed this
    },
    hello() {
      console.log('Hello');
    }
  }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
obj.hello();

要注意的是,使用这个类型需要启用 noImplicitThis flag

具体使用

项目的依赖安装等

新项目可以用 vue-cli 直接创建并添加typescript,非常简单,创建项目的时候选择就可以了。

旧项目的话如果同样是用 vue-cli 创建的项目的话,升级 vue-cli 后使用 vue add typescript 也是非常简单就能添加了。

如果不是的话,需要手动升级 Vue 到 2.5+ 以及升级相关组件比如 vuex 、vue-router 等,再对 webpack 进行相关的配置。 建议参照 vue-cli 生成的 webpack 配置。

在 Vue 中的写法

实例属性

无论在 ts 文件中使用 Vue.component 进行定义还是在单文件组件(*.vue)中进行书写 TypeScript 都已经有了提示,但是在单文件组件中访问 $refs 等实例属性的话,需要在导出前使用 Vue.extend

export default {
  methods: {
    setMargin() {
      (this.$refs.svg as SVGElement).style.margin = '-1px';// Property '$refs' does not exist on type '{ methods: { setMargin(): void; }; }'.
    },
  }
}

这是为什么呢?

你可能会在碰到这个问题之前碰到另一个在构建时 *.vue 文件无法被识别的问题

1 ERROR 
• main.ts
[ts] Cannot find module './App'. (4 17)

又或者是并没有碰到这个问题,但你应该在项目的根目录下发现有一个叫做 shims-vue.d.ts 或者 shims-*.d.ts 这样的文件,这样的文件叫做 Declaration Files

打开这个文件后你应该能看到类似于

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

而 Vue 的默认导出有两个,一个是 interface Vue,一个是 const Vue: VueConstructor ,在这里因为导出的是一个对象,因此适用于 VueConstructor ,如果使用 Vue.extend 进行导出,则符合 VueConstructor 类型,但如果不使用 Vue.extend, 我并没有找到这部分的说明也没有追踪到相关源代码,而 Vetur 似乎仍然将他识别成了 VueConstructor ,但在编译时被识别成了 ComponentOptions ,因此没有办法通过编译。

Component 使用 as 类型断言(Type assertions)

Type assertions 是什么?

$refs 的使用

在上一个例子中你可能已经注意到

export default Vue.extend({
  methods: {
    setMargin() {
      (this.$refs.svg as SVGElement).style.margin = '-1px';
    },
  }
})

使用 $refs 的时候我对属性进行了类型断言,这是因为 $refs 是在运行时附加上的属性并且随时产生变化,因此并不能提前得到具体类型,因此它被定义成 readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };

于是不但可以转换成HTML元素,也可以根据需要转换成子组件的实例类型以用于调用子组件方法。

import Vue from "vue";
import Popup from "./Popup.vue";

export default Vue.extend({
  name: "YetAnotherPopup",
  methods: {
    open() {
      (this.$refs.popup as InstanceType<typeof Popup>).open();
    },
    close() {
      return (this.$refs.popup as InstanceType<typeof Popup>).close();
    },
  },
  components: {
    Popup
  }
});

这里用到的是 InstanceType ,因为导出的是 Vue 组件的构造器。

mixin 的使用

对于 mixin ,由于会有合并冲突,因此也并没有办法将方法与数据反映到被 mixin 的组件上。

同样的,当我们包括了mixin后,我们也可以使用类型断言

import { adIdMixin } from "@/mixins/adIdMixin";

export default Vue.extend({
  mixins: [adIdMixin],
  methods: {
    click() {
      (this as InstanceType<typeof adIdMixin>).sendTracking('purchase','click');
    },
});

Vuex store 的类型提示

Vuex 也加入了 TypeScript 的类型定义。

构造 vuex

首先导出 state

export const state = {
  loading: false,
  valueA: null as string | null,
  valueB: null as ({
    numberValue: number;
    stringValue: string;
  })[] | null
};

其次构造 Action (Mutation 大同小异)

import { ActionTree, Commit } from "vuex";
import * as API from "@/utils/api";

import { state } from "./state";
// 如果你是 namespaced store ,可以在这里导入 rootState 对其他 store 进行修改
import { rootState } from "@/store/types";

export default {
  // we didn't use rootState here
  getInfo({ commit, state, _rootState }) {
    state.loading = true;
    return API.getInfo()
      .then(res => commit('setState', res.data))
      .finally(() => {
        state.loading = false;
      });
  },

// 如果你是 namespaced store ,可以在这里导入 rootState 对其他 store 进行修改
// 如果不是,则再输入一次 `typeof state` ,因为此时 rootState == state
} as ActionTree<typeof state, typeof rootState>;
使用 typeof 与 类型断言让 TypeScript 自己推断类型,在初始化 state 时不用重复写两遍属性。

this.$store 使用 as 类型断言

在 Vue 中使用了 Vuex 后,this.$store 有了 State<any> 的定义,同样的因为这个属性是运行时动态添加变化,因此无法提前进行类型定义。

于是在代码中,我们可以使用 as 进行类型断言。

在这里,有三种方法。

方法一: 只对使用的 store 进行断言

import Vue from 'vue';
import { state as childState } from "@/store/childStore/state";

// import store from "@/store";

export default Vue.extend({
  computed: {
    isLoading() {
      return (this.$store.childState as typeof childState).loading;
    }
  },
});

方法二: 对使用的 store 进行整个 RootState 转换

import Vue from 'vue';
import rootStore from "@/store";

export default Vue.extend({
  computed: {
    isLoading() {
      return (this.$store as typeof rootStore).childState.loading;
      // or `as Store<typeof rootState>`
      // Store should be imported from vuex declartion
    }
  },
});

方法三: 直接导入store,不从 this.$store 上获取,无需书写任何类型断言

import Vue from 'vue';
import rootStore from "@/store";

export default Vue.extend({
  methods: {
    getLoading() {
      return rootStore.childState.loading;
    }
  },
});

mapStatemapGetters 问题

很遗憾,mapStatemapGetters 由于只通过字符串来对实例添加属性,因此他们只能告诉TypeScript在这个实例上有这个属性,并不能添加属性类型,会被认为成any。

import Vue from "vue";
import { mapState } from "vuex";

export default Vue.extend({
  computed: {
    ...mapState("childState", ["valueA"]),
  },
  methods: {
    getNumber() {
      this.valueA; // any
    }
  }
});

推荐只作为template映射进行使用

结语

这次探索了一下不用装饰器 ,VueTypeScript 到底可以结合到多少程度。 目前来看基本的类型推断都是够用,在项目中几乎不需要再使用装饰器以及 // @ts-ignore

TypeScript + VSCode 对开发效率的提升真的不是一点半点,自从尝试了 ts 后就对 ts 爱不释手了

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值