“茴”字写法 —— Vue typescript 组件

孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!……回字有四样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见
我毫不热心,便又叹一口气,显出极惋惜的样子。

《“茴”字写法》系列文章主要总结常见的代码操作,给出多种实现方式,就像茴香豆的“茴”字有多种写法一样。

规则千万条,简洁第一条
本文链接:https://taskhub.work/article/75576861798703104


正文开始

Vue 组件有很多中写法,在3.0之后会更好的支持typescript,ts用过都知道,真香,无论是代码提示还是代码重构都非常方便,本人之前写过一个UI库大概4~5万行规模,全用ts,期间发现很多不合理的地方,对UI库进行重构,只花了2天时间。下面通过对比不同写法。

各个Vue 组件UI库实现方式:

UI 库 实现方式
muse-ui 完全不写 <template> 只使用 render 函数
iview 使用 .vue 文件,样式单独写
element 使用 .vue 文件,样式单独写
vant 使用 .vue 文件,样式单独写
ant-design-vue 使用 .jsx 文件,样式单独写
vux 使用带 <style> 的 .vue 文件,但在使用时必须用 vux-loader
cube-ui 使用带 <style> 的 .vue 文件,但有一些配置

在实际开发中用不用 *.vue 这样的单文件组件来开发呢?
网上有很多网友吐槽Vue的单文件组件模式写法,认为Vue的模板语法很鸡肋,各种不方便,不如全部jsx。决定这个问题的关键是解耦,包括功能解耦、模块解耦、甚至框架解耦。*.vue文件组织方式虽然多少和Vue相关,但是实际操作时发现要解耦重构也不是很大的问题,所以还OK。

三种组件写法对比

Object API 29 lines

import Vue, { PropOptions } from 'vue'

interface User {
  firstName: string
  lastName: number
}

export default Vue.extend({
  name: 'YourComponent',

  props: {
    user: {
      type: Object,
      required: true
    } as PropOptions<User>
  },

  data () {
    return {
      message: 'This is a message'
    }
  },

  computed: {
    fullName (): string {
      return `${this.user.firstName} ${this.user.lastName}`
    }
  }
})

Class API 17 lines

import { Vue, Component, Prop } from 'vue-property-decorator'

interface User {
  firstName: string
  lastName: number
}

@Component
export default class YourComponent extends Vue {
  @Prop({ type: Object, required: true }) readonly user!: User

  message: string = 'This is a message'

  get fullName (): string {
    return `${this.user.firstName} ${this.user.lastName}`
  }
}

Function API 25 lines

import Vue from 'vue'
import { computed, value } from 'vue-function-api'

interface User {
  firstName: string
  lastName: number
}

interface YourProps {
  user?: User
}

export default Vue.extend({
  name: 'YourComponent',

  setup ({ user }: YourProps) {
    const fullName = computed(() => `${user.firstName} ${user.lastName}`)
    const message = value('This is a message')

    return {
      fullName,
      message
    }
  }
})
写法 优点 缺点
Object API Vue 官方写法,方便Vue直接处理组件 1. 代码长、缩进多,组件复杂时难以理清逻辑,不好进行分割
2. 混入较多Vue的概念,新手学习成本高
Class API 相关概念可以用class的思路理解,可以更好地描述Vue的混入、data、computed,生命周期钩子等概念。Vue 3.0 将原生支持class写法 用到了修饰器语法特性,目前还在实验阶段(typescript可以使用helper函数解决兼容问题,问题不大)
Function API 无状态,更好的单元测试、并行化 函数式写法很容易写出回调地狱,导致代码可读性、可维护性差,目前纯粹function api 写法较少见

完成同样一件事,ts的class写法简洁得多,在工程较大时减少1/3左右的代码,可维护性大大提高。

typescript class 写法常见问题

  1. route 钩子无效问题
    使用class写法会发现部分Vue的钩子函数无法使用问题,可以通过注册钩子函数解决,如下:
    import Component from 'vue-class-component'
    
    // Register the router hooks with their names
    Component.registerHooks([
      'beforeRouteEnter',
      'beforeRouteLeave',
      'beforeRouteUpdate' // for vue-router 2.2+
    ])
    
  2. 与Vuex配合使用问题
    使用vuex-class 解决,如state映射
    import { Vue, Component } from 'vue-property-decorator';
    import { User } from '@/api/account';
    import { State } from 'vuex-class';
    
    
    @Component
    export default class TestPage extends Vue {
    
      @State(state => state.user, { namespace: 'account' })
      user!: User;
    
    }
    
  3. Vue 混入功能
    代码经常需要各种错误,包括用户输入错误、安全检测、后台错误、网络故障等。如果全部错误处理代码放进组件中,代码臃肿,阅读性差,可以将常见的错误处理逻辑提取出来,通过混入的方式插入组件中。class写法推荐使用vue-property-decorator 的 Mixins,参考附录

附录

附上模板代码,解决大多数Vue typescript 组件问题
Vue class 组件

import { Vue, Component, Prop, Watch, Model, Mixins } from 'vue-property-decorator';
import { State, Getter, Action, Mutation } from 'vuex-class';
import axios, { AxiosError } from 'axios';

interface Person {
  userId: string;
  nickname: string;
}

@Component
class CommonHandler extends Vue {
  onNetworkError(e: AxiosError) {
    console.log('on error');
  }
}

@Component
export default class Test extends Mixins(CommonHandler) {
  @Prop(Number) readonly propA: number | undefined

  @Prop({ default: 'default value' }) readonly propB!: string

  @Prop([String, Boolean]) readonly propC: string | boolean | undefined

  @Model('change', { type: Boolean }) readonly checked!: boolean
  
  message: string = 'hello world';

  get propBLen(): number {
    return this.propB.length;
  }

  @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) {}
 
  change() {
    console.log('on change');
  }

  created() {
    // 调用混入中的错误处理函数,简化代码
    axios.get('hello').catch(this.onNetworkError);
  }

  mounted() { console.log('mounted'); }
}

Vue 官方写法

import axios from 'axios';

const CommonHandler = {
  methods: {
    onNetworkError(e) {
      console.log('on error');
    }
  },
}

export default {
  mixins: [CommonHandler],
  
  props: {
    propA: {
      type: Number
    },
    propB: {
      default: 'default value'
    },
    propC: {
      type: [String, Boolean]
    }
  },

  model: {
    prop: 'checked',
    event: 'change'
  },

  data() {
    return {
      message: 'hello world',
    }
  },

  computed: {
    propBLen() {
      return this.propB.length;
    },
  },

  watch: {
    child: [
      {
        handler: 'onChildChanged',
        immediate: false,
        deep: false
      }
    ],
    person: [
      {
        handler: 'onPersonChanged1',
        immediate: true,
        deep: true
      },
      {
        handler: 'onPersonChanged2',
        immediate: false,
        deep: false
      }
    ]
  },

  methods: {
    change() {
      console.log('on change');
    },

    onChildChanged(val, oldVal) {},

    onPersonChanged1(val, oldVal) {},

    onPersonChanged2(val, oldVal) {}
  },

  // vue lifecycle hooks  
  created() {
    // 调用混入中的错误处理函数,简化代码
    axios.get('hello').catch(this.onNetworkError);
  },

  mounted() { console.log('mounted'); }
}

52行对比84行,同样功能减少38%的代码

最后打个广告~

image

TaskHub 是我们团队开发的一个 Markdown 加密网盘,支持常见的任务管理功能还有Markdown 文件编辑,所有功能与平台解耦,只使用Markdown的特性实现,更好的保障用户的数据安全,实乃团队协作之利器。欢迎大家使用~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342