关于 Vue + TypeScript 组件化的一点思考

19 篇文章 2 订阅
18 篇文章 0 订阅

前面说过,组件化是使用 Vue 引入之后带来的一大优点。未有 Vue 之前——那个年代——苦苦寻思没有一个好的组件化方案。在当初 JavaScript 连类都尚未健全的情况下,真是费煞了苦心,最简单的“对象”是有了,可那不能构建复杂的大型 UI 系统,颗粒度太低了。对象、组件的关系,一小一大分别自然很清楚。那么加多一个“类 Class”呢?你能理清楚这些名词的关系吗?面向对象与组件有什么区别与联系呢?

其实不清楚没啥关系。码多了,你自然明白:)。关于 JS Class,我写过不少博文(https://blog.csdn.net/zhangxin09/category_782926.html),——尽管现在已经没啥卵用了:)。

总之,没有一个恰当组件化方案,是不行的。即使你有了类方案,构建一个新实例(new XXX()),那也不能”妄称“为组件。君不见,组件里面可不止 JavaScript 啊,还有标签和样式,都不是一门语言呢。如何把它们三者既辩证又统一地结合一起,确实是重中之重。另一边厢,标准的 WebComponent 方案现在不知怎么样,反正现在一个前端框架,就有其自己的组件化方案。

Vue 于 TypeScript 下的问题

回到 Vue 的话题上。话说,Vue 的组件蔚然一股新风气,JavaScript 下很好,但到了 TypeScript 下面,加多了类型提示,长成这样子。

在这里插入图片描述

可见区别也不大。不过问题来了,this 对象指针不明确,各个函数“各为其主”,不能连结为一个组件身上。这体现为上图红色框的报错,还有 mouted() 里面访问的 this.positionFixed 属性,就算没有报错,但编译器只能当作是 any 类型,不能明确为组件已经声明过的 positionFixed : boolean 类型——我都声明过了却都还不能用,不能正确地提示类型和约束——那是不是太蠢太不合理了啊?

坦白说,尽管我们人类写的时候,晓得那是一个整体,一个对象、一个组件。但是 IDE、编译器可不那么想,它没有更多的讯息告诉它那是一个整体,一个对象、一个组件。那么该怎么办呢?我所知道的有两个对应的方法。

interface 声明

为了解决满屏报错不和谐的局面,我们可以通过 interface 补充完整的类型信息,供 TS 编译知道。interface 直译接口,在 OOP 中非常常见,地位仅次于 Class,不过吊诡的是,不同人、不同语言往往对 interface 概念理解出入甚大,抽象嘛~加了点玄幻的味道进去自然不好理解。不过总的来说,和 Java 的 interface 一样,TS 的接口不能写实现,编译后没有对应的 JS 代码,仅仅声明一个对象是什么,结构有什么(包括了那些字段或属性、方法)。TS 的接口可以表征一个普通的 JS 对象,又叫 Object、JSON、Map,反正叫什么都好,就是 var obj = {}; 或者 var obj = new Object(); 的意思。

下面就是补充 interface 信息的例子。

在这里插入图片描述

还是之前那个例子,我们添加点东西。代码虽有点长,不过我们只关注红色框里面的就好。首先声明了 CalendarInput 的接口,说明具体成员有哪些,这里仅仅列出了属性,其实还可以声明方法(只是这里没用到而已);然后在组件 watch 里面的函数、mounted() 函数都是声明了第一个“参数” this 及其类型为 CalendarInput

我们学习 JS 的时候,都知道 this 其实是一个“隐藏的参数”,总会传入。好了,现在把这个 this “显式”声明出来,不就搞定了函数“各为其主”的问题吗?总之,在函数出现的时候你都可以指定 this 这个类型如何,而不管这个函数在哪里的位置。

函数里面会访问到 Vue API 如 $el、$refs……没有在 TS 里面声明过它们是会报错的。对此解决的方法很简单,接口支持 extends 继承,新建一个叫 Vue 的类,包含了所有 API 成员,然后继承它就可以了。TS 神奇的是,接口可以继承 declare class——这是在 Java 不可能的。

declare class Vue {
    public $el: HTMLElement;

    public $props: any;

    public $refs: any;

    public BUS: any;

    public $parent: Vue;

    public $options: any;

    public $children: any[];

    public static options: any;

    constructor(cfg: any) {
    }

    public $watch(...any): void;
    
    public $set(...any): void;

    public $destroy() { }

    public $emit(e: string, ...obj: any) { }

    public static component(string, Object): void {
    }

    public static set(...any): void {
    }

    public static extend(...any): any {
    }
    
    public ajResources = { // 我自己扩展的,非 vue 官方 API
        imgPerfix: "",
        ctx: ""
    };
}

它以 *.dt.ts 库声明的形式保存。

这是我对应上述问题的第一个方案,解决是解决了,但略显不够优雅,所以它只能作为最后的手段,总是可以解决的。——什么,你看不出哪里不优雅?好吧,公布答案就是代码重复,interface 声明了一次,实现又要重复一次,——冗余了:),而且还有每个方法 this:XXX。有见及此,还是得寻求更好的解决方案:)

官方方案

显然,比较简单直接的方法就是采用类 Class,把组件写成类就行。而且现在很多人在 Vue + TS 环境下开发,此问题有足够的“能见度”,自然不会被官方缺席,官方文档就有介绍如何在 TS 环境下开发 Vue 的问题。话分两头说,先看 Vue 2.x 版本的,官方推荐的是 Class Component。写法就是继承 Class 那样子了,见截图如下。

在这里插入图片描述

除了官方这个方法,还有基于它更好的 vue-property-decorator,用法参见 https://blog.csdn.net/marker__/article/details/105784520

通过 TS 注解+装饰器(Decorator)是比较好的解决方案,但是笔者观察后还是放弃了,原因是笔者还不太熟练新玩意 npm、import 等等这些的:( 好吧,学不动了,只能拿“避免复杂”当作推搪的理由。

然后我把目光转向了新版 Vue 3。不知为何,Vue 团队好像不太感冒 Class 风格,推荐一种莫名其妙的组合式(Composition) API,于是我表示——变动太大,也学不动了。

无论是 vue 2 还是 3,官方方案终究难合我口味,——装饰器模式的思路是对的,采用注解的方法我也想到,不就是 Java 那样子的手段嘛~但是我不清楚 TS 里面能否对注解进行反射(Reflection)……好吧,我承认,又要自己动手搞一个“轮子”了。

“轮子”方案

首先还是我原来 Java 所熟悉的注解手段。咋一看,TS 语言层面支持注解,但我不理解编译中怎么处理注解,可否反射得到注解的信息?这是我的存疑,因为编译到 JS 后就类型消除了,注解也不存了,怎么可以得到注解的信息?调研一番,TS 官方并没有反射一说,而是装饰器(Decorators)来处理注解。一看,又是高大上的东西,而且好像还不是反射的意思。

于是只能放弃注解的方案。这个难题先放一放,让我们想想怎么由 Vue.component() 的写法转为 Class 风格的写法。实际上这个问题一点也不复杂,几行代码的一个函数就行。核心思想就是反射获取 Class 各成员然后组装成为 {xxx……},然后送入到 Vue.component() 第二个参数中去。Vue 组件允许有什么的成员,Class 就可以出现那些什么的成员,例如组件生存周期的函数 mounted(),类身上有这个方法,那么直接对应 {……mounted() {}……}。Class 本身就是一个 key/value 结构,妇孺皆懂,操作方便:),而这个 mounted 字眼是约定好的、固定的,在反射的循环中轻易获取:

for(var  i in obj) {
  // i 就是 Class 成员名词,字符串类型
}

那么再组装一下组件的配置参数,十分轻松。如果是组件方法,要求是在 methods : {xxx, yyy……} 下面的,那么也没关系,一切不是 Vue API 的方法(typeof obj[i] == 'function'且排除 Vue API 的)都视作是组件方法,扔到 methods : {xxx, yyy……} 即可。

Vue API 就那么十来个,所以转换不麻烦,逐个对应即可。比较棘手的是 vue 组件属性有两种:data 和 props,如果加上实例字段(不参与数据驱动的),就有三种属性。TS Class 只有一种属性,怎么区分呢?所以前面才会说用注解来解决。但既然注解不可行,又只能另辟蹊径了。——实际上说出来也很简单,甚至有点返璞归真的感觉。

仔细想想,组件的 data 和 props 的写法有显著区别的,props 要么 xxx: String | Number | Boolean 声明了类型,要么 xxx: {type: String, default : "abc"},特征足够完备,我们在反射时候即可判断为 props。至于 data,也好办,我们只需要判断是 props,那么剩下的就是 data。

一开始的思路还是那样,可我很快就遇到问题,实际情况没那么简单,主要以下两点:

  • props 要么 String | Number | Boolean,要么 {type: String, default : "abc"},虽然是属性挂在类身上,但访问 this.xxx 时报错,类型不对。正确方式是 xxx:string = "";
  • data 最终为 function 类型,这个没问题,换作类属性写成 xxx:string = ""; 也可以,this.xxx 能正确访问。问题是有时在 data(){……} 函数中可以作一些初始化的工作,如下图。那么此时就必须强制写成函数类型了,

在这里插入图片描述

类属性还是要写出。其实什么值无所谓,只要类型正确就可以了。

所以有点美中不足,既要写原先的 vue 写法,又要写类属性的写法,多少会造成重复,特别在声明 props 的时候。

然而还有一个遗憾,或者说比较尴尬的地方就是,除了写好类,还要调用一个逻辑“登记组件”:(。这是无法避免的(至少当前是)。仅仅声明的一个类,其实不会使用它的(new 实例化),因为使用组件是在标签中是使用 <aj-xxx> 的,所以你只能在手动调用一次逻辑执行这个类,就是转化为 vue 组件了。

new XXX().register();

这个 方法是固定的,因为我们组件类都继承于 VueComponent 类,他是一个抽象类包含了 register() 方法。详细 VueComponent 类源码如下。

/**
 * 为让 Vue 组件使用 Class 风格,通过一个类似语法糖的转换器
 * 这是实验性质的
 */
export abstract class VueComponent {
    /**
     * 组件名字,必填
     */
    abstract name: string;

    $el: HTMLElement = document.body;

    public $props: any;

    props: { [key: string]: any } = {};

    public BUS: any;

    public $parent: any;

    public $options: any;

    public $destroy() { }

    public $emit(e: string, ...obj: any) { }

    /**
     * 转换为 ClassAPI
     */
    register(instanceFields?: string[]): void {
        let cfg: { [key: string]: any } = {
            props: {},
            methods: {}
        };

        let dataFields: { [key: string]: any } = {};

        for (var i in this) {
            if (i == 'constructor' || i == 'name' || i == 'register' || i == '$destroy' || i == "$emit" || i == "$options")
                continue;

            let value: any = this[i];

            if (isVueCfg(i))
                cfg[i] = value;
            else if (isSimplePropsField(value))
                cfg.props[i] = value;
            else if (typeof value == 'function')
                cfg.methods[i] = value;
            else if (isPropsField(i, this.props))
                cfg.props[i] = this.props[i];
            else // data fiels
                dataFields[i] = value;
        }

        // 注意如果 类有了 data(){},那么 data 属性将会失效(仅作提示用),改读取 data() {} 的
        if (!cfg.data)
            cfg.data = () => {
                return dataFields;
            }

        console.log(cfg)

        Vue.component(this.name, cfg);
    }
}

可见,代码层面几行就搞定了,并不复杂。笔者这里尝试用文字去介绍反而比较多。不忘初心,为的就是简单办好事,所以也舍弃了一些高大全的考量。大家若认可,不妨将就用下:)。

最后补充一下,如果这个 vue 组件比较简单,那么按照原来的写法也是挺好的,不一定全都为类。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值