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 文章:1、2《如何编写一个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: ""
};
}
这样的话,编译器就不会报错了。