优秀的编程知识分享平台

网站首页 > 技术文章 正文

TypeScript+vue使用与迁移经验总结

nanyue 2024-07-20 23:55:55 技术文章 10 ℃


源宝导读:ERP平台的前端底层使用了Vue作为组件的基础架构,而使用了TypeScript语言进行组件的封装与开发。本文将简要介绍平台在使用TypeScript和Vue框架进行老功能重构时的经验总结。

一、背景

下面主要探讨是以下三个方面:

  • 目前项目中使用到的vue+ts的哪些特性,还有哪些特性值得去使用,不会涉及到太多的ts语法知识;
  • 老项目的迁移为ts,有哪些点需要改造;
  • 各抒己见,探讨下各位都有哪些心得和见解。

二、为什么要用typescript

TypeScript简单介绍:

  • 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。
  • TypeScript 是 JavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。

总结优势:

  • 静态类型检查: 类型校验,能够避免许多低级代码错误;
  • IDE 智能提示: 使用一个方法时,能清楚知道方法定义的参数和类型和返回值类型;使用一个对象时,只需要.就可以知道有哪些属性以及属性的类型;
  • 代码重构: 经过不停的需求迭代,代码重构避免不了,在重构时,如果前期有清晰和规范的接口定义、类定义等,对于重构帮助很大;
  • 规范性和可读性: 类似于强类型语言,有了合理的类型定义、接口定义等,对于代码实现的规范性和可读性都有很大提高,不然搜索整个项目这个方法在哪里调用、怎么定义等。

个人认为最有价值点:写代码前,会先构思功能需求的整体代码架构。

三、安装和起步

一般我们会面临两个情况:

  • 新项目创建;
  • 觉得ts不错,想将老项目切换为vue+ts。

3.1、新项目起步

  • 安装vue-cli3.0;
  • vue create vue-ts-hello-world;
  • 选择Manually select features,勾选typescript。其他配置根据项目情况勾选。

3.2、老项目切换为vue+ts

  • 安装ts依赖(或使用yarn);
    • yarn add vue-class-component vue-property-decorator;
    • yarn add ts-loader typescript tslint tslint-loader tslint-config-standard —dev。
  • 配置 webpack,添加ts-loader和tslint-loader;
  • 添加 tsconfig.json;















































// 这是平台目前用的tsconfig.json{  "compilerOptions": {    "target": "esnext",    "module": "esnext",    "strict": true,    "jsx": "preserve",    "importHelpers": true,    "moduleResolution": "node",    "experimentalDecorators": true,    "esModuleInterop": true,    "allowSyntheticDefaultImports": true,    "strictNullChecks": false,    "sourceMap": true,    "baseUrl": ".",    "types": [      "webpack-env",      "jest"    ],    "paths": {      "@/*": [        "src/*"      ],      // 别名追加      "components/*": [        "src/components/*"      ],    },    "lib": [ // 编译过程中需要引入的库文件的列表      "esnext",      "dom",      "dom.iterable",      "scripthost"    ]  },  "include": [    "src/**/*.ts",    "src/**/*.tsx",    "src/**/*.vue",    "tests/**/*.ts",    "tests/**/*.tsx"  ],  "exclude": [    "node_modules",    "ui-tests"  ]}

备注: ts 也可支持 es6 / es7,在 tsconfig.json中,添加对es6 / es7的支持。








 "lib": [  "dom",  "es5",  "es6",  "es7",  "es2015.promise"]
  • 添加 tslint.json 或者 prettierrc(可以视情况而定)。







// 目前平台使用的是.prettierrc.jsmodule.exports = {  "$schema": "http://json.schemastore.org/prettierrc",  "singleQuote": true,  "endOfLine": "auto",  "semi": false}
  • 让 ts 识别 .vue。




declare module "*.vue" {  import Vue from "vue";  export default Vue;}
    • 而在代码中导入 .vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 .ts 文件,不识别 *.vue 文件。
    • 添加vue-shim.d.ts,让vue文件给vue模块来编译。
  • 改造 .vue文件,将vue中script切换为<script lang="ts">;
  • 改造.js文件,修改为ts语法,定义类型等。

四、vue+ts常用的装饰器

这里主要用到了vue-property-decorator,这个是在vue-class-component基础上做了一层增强,新增了一些装饰器,使用更加便捷。这里只分享一些常用的,对于老项目改写vue文件很有用:

4.1、@Component

标识该vue文件是一个组件,并且可以引入其他组件。

非ts版本:







import MyComponent from '@/components/MyComponent'export default {    components: {        MyComponent    }}

ts版本:










import { Vue, Component } from 'vue-property-decorator'import MyComponent from '@/components/MyComponent'@Component({    components: {        MyComponent    }})export default class YourComponent extends Vue {}

备注:这里不管有没有引入其他组件,都必须要使用@Component,目的是为了注册这个组件。否则在其他组件各种莫名其妙的问题。比如:路由找不到组件,而且不会报错。

4.2、@Prop

非ts版本:





















export default {  props: {    propA: {      type: Number    },    propB: {      default: 'default value'    },    propC: {      type: [String, Boolean]    },    propD: {        type: Object,        default: () => {},        validator(val: object) {          return val.prop = '1'        }    }  }}

ts版本:






















import { Vue, Component, Prop } from 'vue-property-decorator'
@Componentexport default class YourComponent extends Vue {  @Prop(Number)  readonly propA: number | undefined
  @Prop({ default: 'default value' })  readonly propB!: string
  @Prop([String, Boolean])  readonly propC: string | boolean | undefined
  // 也可以一起  @Prop({type: Object, default: () => {},    validator(val: object) {      return val.prop = '1'    }  })  readonly propD!: object // 只是举例,一般会定义一个interface}

4.3、@Watch

非ts版本:



























export default {  watch: {    child: {        handler: 'onChildChanged',        immediate: false,        deep: false    },    person: [      {        handler: 'onPersonChanged1',        immediate: true,        deep: true      },      {        handler: 'onPersonChanged2',        immediate: false,        deep: false      }    ]  },  methods: {    onChildChanged(val, oldVal) {},    onPersonChanged1(val, oldVal) {},    onPersonChanged2(val, oldVal) {}  }}

ts版本:














import { Vue, Component, Watch } from 'vue-property-decorator'
@Componentexport default class YourComponent extends Vue {  @Watch('child')  onChildChanged(val: string, oldVal: string) {}
  @Watch('person', { immediate: true, deep: true })  onPersonChanged1(val: Person, oldVal: Person) {}
  @Watch('person')  onPersonChanged2(val: Person, oldVal: Person) {}}

4.4、@Provide和@Inject

场景:一般用于父级嵌套比较深的子孙vue组件,但是数据不是很方便传到深层级vue组件中,利用树型结构组件。

非ts版本:










// 父组件provide () {  return {    OptionGroup: this  }}
// 子孙组件inject: ['OptionGroup']

ts版本:

父组件:





@Provide()  getObj () {    return this  }

子孙组件:






@Inject() getObj!: any
get obj() {    return this.getObj()}

Privide的弊端:

  • 依赖注入它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难;
  • 同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。

建议:

一般不推荐过度使用。

  • provide 和 inject的绑定并不是可响应的,这是刻意为之的。但是,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的;
  • 如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像Vuex这样真正的状态管理方案了。

4.5、@Ref

非ts版本:











export default {  computed() {    anotherComponent () {        return this.$refs.anotherComponent    },    button () {        return this.$refs.aButton    }  }}

ts版本:
















import { Vue, Component, Ref } from 'vue-property-decorator'import AnotherComponent from '@/Components/another-component.vue'
@Componentexport default class YourComponent extends Vue {  @Ref() readonly anotherComponent!: AnotherComponent  @Ref('aButton') readonly button!: HTMLButtonElement
  // 我们目前是这样使用的  $refs!: {    popover: any    search: HcProjectSelectSearch    tree: HcProjectTree  }}

4.6、@Emit

用的很少,参数和时机不是很好控制。

非ts版本:


















export default {  methods: {    handleClick(e) {      this.$emit('click', e)    },    loadData() {      const promise = new Promise(resolve => {        setTimeout(() => {          resolve(20)        }, 0)      })      promise.then(value => {        this.$emit('load', value)      })    }  }}

ts版本:


















import { Vue, Component, Emit } from 'vue-property-decorator'
@Componentexport default class YourComponent extends Vue {  @Emit('click')  handleClick(e) {    // todo  }  @Emit()  promise() {    return new Promise(resolve => {      setTimeout(() => {        resolve(20)      }, 0)    })  }}

五、mixin改写

定义mixin:


















export const cusMixin = {  mounted() {    this.$refs = {}    // $0 instanceof HTMLElement    // this.$refs = {}    console.log('mixin mounted')  },
  beforeUpdate() {    this.$refs = {}    // console.log('global mounted')  },  updated() {    this.$refs = {}    // console.log('global mounted')  }}

引入mixin:


































import { Vue, Component } from 'vue-property-decorator'import cusMixin from '@/mixin'
@Component({  components: {},  mixins: [cusMixin]})export default class YourComponent extends Vue {}
// 或者尝试使用import { Component, Mixins, Vue } from 'vue-property-decorator';import { MyOtherMixin } from './MyOtherMixin';
@Componentexport class MyMixin extends Vue {   private created() {    console.log('what?');  }}
@Component// 继承多个mixin,使用数组 [MyMixin, MyOtherMixin] export default class App extends Mixins(MyMixin) {   private test = "test";  private laowang = 'laowang';
  created() {    console.log(this.test)    console.log(this.Kitchen)    console.log(this.Tv)  }
}

六、vue识别全局的方法和变量

  • vue-shim.d.ts文件中,增加如下代码:



















import Vue from 'vue'import VueRouter, { Route } from 'vue-router'import { Store } from 'vuex'// 声明全局方法declare module 'vue/types/vue' {  interface Vue {    // 内部变量    $router: VueRouter;    $route: Route;    $store: Store<any>;    // element-ui等组件    $Message: any    $Modal: any    // 自定义挂载到Vue.prototype上的变量    $api: any    $mock: any    $configs: any  }}

七、vuex的改写

关于store的改造,配置和结构和原来一样,具体编码设计没有特定套路,根据项目具体设计改写为ts的语法。

主要是关于ts在vue如何使用,目前主流的方案是vue-class-component + vuex-class,一般常用的mapGetters和mapActions改写:


yarn add vuex-class

非ts版本:















import { mapGetters, mapActions } from 'vuex'export default Vue.extend({  computed: {     ...mapGetters({       'name',       'age'     })  },  methods: {    ...mapActions([        'setNameAction'    ])  }})

ts版本:





















import { Vue, Component } from 'vue-property-decorator'import { Getter, Action } from 'vuex-class'import { Test } from '@/store'
export default class YourComponent extends Vue {  @Getter('name') name: string  @Getter('age') age: number  @Action('setNameAction') setNameAction: Function
  get innerName (): string {     return this.name  }  get innerAge (): number {     return this.age  }
  setName (name: string) {    this.setNameAction(products)  }}

备注:tsconfig.json需要调整下:










{  "compilerOptions": {    // 启用 vue-class-component 及 vuex-class 需要开启此选项    "experimentalDecorators": true,
    // 启用 vuex-class 需要开启此选项    "strictFunctionTypes": false  }}

八、vue render jsx语法改写

改写的原理还是和上面类似,都是借助目前流行的两个库,除了使用vue-property-decorator以外,还需要借助vue-tsx-support,vue-tsx-support是在Vue外面包装了一层,将prop、event等以泛型的方式加了一层ts接口定义传了进去,目的是为了防止ts的类检查报错。

  • 步骤:
    • 引入 yarn add vue-tsx-support --dev;
    • 导入ts声明,在main,ts中import "vue-tsx-support/enable-check";
    • 在vue.config.js中extensions添加.tsx。
  • 使用:






















import { Component, Prop } from "vue-property-decorator";import * as tsx from "vue-tsx-support";
interface YourComponentsProps {  name?: string  age?: number}
@Componentexport default class YourComponents extends tsx.Component<YourComponentsProps> {  @Prop() public name!: string;  @Prop() public age!: number;
  protected render() {    return (      <div>        <h1>姓名:{this.name}</h1>        <h1>年龄:{this.age}</h1>      </div>    );  }}

这里jsx改写为tsx大致简单了解下,如果大家有兴趣,以后可以一起学习探讨下。

九、思考

  • 关于老项目ts的改造,如何才能平滑过渡,不影响现有的功能。
  • 在vue中ts的实践,数据、视图、控制器分层设计的问题。

------ END ------


作者简介

罗同学: 研发工程师,目前负责ERP建模平台的设计与开发工作。


文章来源微信公众号:明源技术团队

最近发表
标签列表