用 TypeScript 写一个轻量级的 UI 框架之三:库描述文件与 namespace

TypeScript 倡导我们使用类型,顾名思义“类型的(Type)”、“脚本(Script)”。一般而言脚本语言追求轻便快捷,多数是无类型或弱类型的。然而在 ts 中要贯彻类型的思想,不仅是语言本身的一个飞跃,也是我们开发者自身要适应和理解必由之路。ts 在添加新特性的同时,仍向下兼容旧有的方式。最简单地说,你可以把旧的 js 代码一行不改,复制到 ts 文件中进行编译,编译器不会因为其中的错误中止编译(会提示或者警告,可忽略的)。并不是说我们忽略那些编译警告,不做 js 代码的升级,而是那样子允许了我们可以逐步逐步修改代码,渐进地升级到 ts,最终也是编译出来合法的 js 代码。这种宽容的做法对于初接触的入门者非常友好,不是拒人于千里之外,冷冰冰的警告或打叉,而是伴随有立马可以看得见的成就感。

向下兼容,笔者认为那是十分难得的,可以让使用者采取稳扎稳打的策略,小步快跑。君不见 Python 2.x 到 3.x 痛楚、Swift 1-6 任性的“激进”……宛如前后两门语言,开发者甘苦自知。

库描述文件

TypeScript 编译器比起 JavaScript 编译器有一大特色,就是要求语言的对象元素必须声明类型。如果对象或者函数没有声明过,那么会导致编译器检查失败(虽然只是警告,不妨碍后面的继续编译)。所以原则上先声明后使用,才是合规合法的 ts 程序。——这样问题来了,不是所有 js 代码都是 ts 程序,也不可能一个个重写 js 到 ts,那么一大批的老 js 库如何继续在 ts 中使用呢?

答案是通过 Typescript 的描述文件(以 d.ts 结尾的文件名,比如 xxx.d.ts,可放置项目文件夹内任何位置,一般在 lib 目录中)完成 js 所缺失的类型定义,里面是特殊的类型声明代码,额外地帮助编译器识别类型。

命名空间 namespace

在 ts 声明一个命名空间相当于在 js 中声明一个对象(var obj = {})。aj.d.ts(https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/lib/aj.d.ts) 是 aj-ts 的类型描述文件,其中声明 aj 为全局对象。

/**
 * AJAXJS UI 库占据 aj 一个全局变量
 */
declare namespace aj {
    declare var msg: TopMsg;

    /**
      * 顯示確定的對話框
      * 
      * @param {String} text        显示的文本
      * @param {Function} callback  回调函数,可选
      */
    declare var alert = (text: string, callback?: Function): void => { };

    /**
     * 顯示“是否”選擇的對話框
     * 
     * @param {String} text         显示的文本
     * @param {Function} callback   回调函数
     */
    declare var showConfirm = (text: string, callback?: Function, showSave?: boolean): void => { };

    // aj.admin
    namespace admin {

    }
}

最终编译的 js 相当于:

aj = {
	alert: function(){},
	showConfirm: function() {}
	admin: {}
};

有了声明之后,可以实现它了。在 msgbox.ts(https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/widget/modal/msgbox.ts) 中,尽管该文件的 namespace 属于 aj.widget(此时的 namespace 没有 declare),但是可以为 aj 对象分配成员(alert()showConfirm() 函数)。

/**
 * 消息框、弹窗、对话框组件
 */
namespace aj.widget {
    ……

    /**
     * 顯示確定的對話框
     * 
     * @param {String} text 显示的文本
     * @param {Function} callback 回调函数
     */
    aj.alert = (text: string, callback?: Function): void => {
        var alertObj = msgbox.show(text, {
            showYes: false,
            showNo: false,
            showOk: true,
            onOkClk(e: Event) { // 在 box 里面触发关闭,不能直接用 msgbox.close(e);
                alertObj.$el.classList.add('hide');
                callback && callback();
            }
        });
    }

    /**
     * 顯示“是否”選擇的對話框
     * 
     * @param {String} text         显示的文本
     * @param {Function} callback   回调函数
     */
    aj.showConfirm = (text: string, callback?: Function, showSave?: boolean): void => {
        var alertObj = msgbox.show(text, {
            showYes: true,
            showNo: true,
            showOk: false,
            showSave: showSave,
            onYesClk(e: Event) {
                alertObj.$el.classList.add('hide');
                callback && callback(alertObj.$el, e);
            },
            onNoClk() { // 在box里面触发关闭,不能直接用 msgbox.close(e);
                alertObj.$el.classList.add('hide');
            }
        });
    }

}

如果反过来,不先声明,而是直接在 aj 身上添加 alert(),——这是 ts 不允许的,所以必须在 d.ts 中先声明。如下图所示,xx 会直接报错。

在这里插入图片描述
但是,若通过 export 声明则是合法绑定,同样达到 aj.apply() 的效果。

全局变量

如果打算在 ts 暴露一个全局变量就可以用 namespace,例如下面用到库:

declare var Raphael: any;
declare var EXIF: any;

其他声明

还可声明接口等其他的类型。

/**
 * 实体,一般至少有 id 和 name 字段
 */
declare interface BaseObject {
    /**
     * 实体 id
     */
    id: number;

    /**
     * 实体名称
     */
    name: string;
}

没有 declare 的 namespace

一般的 ts 源码也会用到 namespace,就是一个分组的作用,和 Java 的包管理差别不大。Java 的是目录和包对应,ts 的则没这个约束,文件位置与命名空间没有必然联系。当然推荐开发者管理分组也是一个目录一个命名空间。除了部分类和静态的工具方法,UI 库大多为 Vue 的组件,经过 Vue.component() 方法来定义组件的,这是调用其静态方法,所以命名空间就无所谓了。

推荐理解 namespace 文章:12《如何编写一个d.ts文件》

实际上如果你用包管理机制(CommonJs/AMD/…),应该是可能用不上 namespace, 毕竟 export 就可以产生模块,,模块天然就有隔离分组作用。namespace 更像是兼容旧系统的老方法。

第三方库的描述文件

前面说过,旧 js 库依赖 d.ts 提供类型信息。typings 就是一个网络上的 d.ts 数据库。不过当前项目依赖的库就那么一两个,所以也不打算引入 typings。Vue 的 d.ts(https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/lib/vue.d.ts) 如下所示。

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: ""
    };
}

这样的话,编译器就不会报错了。

相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页