Base 模块是框架的底层基础库,主要有三大模块:prototype.ts 原型扩展、aj.ts 基础工具函数、xhr.ts Ajax 远程请求。下面我们分别探讨。
原型扩展
虽说在 Vue 框架下应远离 DOM 操作,但也不是说一丁点的也不用写,我们还是可以精简出几个较为常用的 DOM 方法,安排在对象原型(prototype)的扩展中。
源码在 https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/base/prototype.ts。
Css Selector 查询元素
原生的 document.querySelector()
已经很方便了,那么不妨更方便一点,敲少几个字(太常用了)。我们要做的,就是安排在 Element 原型封装查询方法,命名为 $()
。另外的 up()
方法是为了查询父级的元素。
使用例子:
var div = document.body.$('.foo'); // 返回符合 className=foo 的元素,单个元素
var divs = document.body.$('div', function(item: HTMLElement, index: number){...}); // 遍历所有的 div 元素,并返回该 div 集合
$()
参数 cssSelector:cssSelector
,必填,CSS 选择器;参数 fn : Function
,可选,当送入该参数的时候,表示使用 querySelectorAll()
来查询多个 DOM 元素,故 fn 是个遍历器函数,其参数列表如 item、index、array。
对子元素查找,也是使用 $() 方法,参数列表跟上个方法一样。例子:
div.$('a').onclick = fn;
div.$('a', a => {...});
原理如下。
Element.prototype.$ = function (cssSelector: cssSelector, fn?: (item: Element, index: number, array: []) => void): Element | NodeListOf<Element> | null {
if (typeof fn == 'function') {
let children = this.querySelectorAll(cssSelector);
if (children && fn)
// @ts-ignore
Array.prototype.forEach.call(children, fn);
return children;
} else
// @ts-ignore
return this.querySelector.apply(this, arguments);
}
Element.prototype.up = function (tagName: string, className: string): Element | null {
if (tagName && className)
throw '只能任选一种参数,不能同时传';
let el: Element = <Element>this.parentNode;
tagName = tagName && tagName.toUpperCase();
while (el) {
if (tagName && el.tagName == tagName)
return el;
if (className && el.className && ~el.className.indexOf(className))
return el;
el = <Element>el.parentNode;
}
return null;
}
Element 是 ts 内建的页面对象,还有其他浏览器对象……总之官方模拟了整个浏览器 API,我们调用即可。然后我们这里对 Element 的 prototype 进行扩展,增加较多的是变量类型的声明和方法的返回值。但是这样不能为 ts 提供方法声明的支持,若直接调用如 document.body.$()
还是会报错,怎么让 ts 编译器知道我扩展了原型方法呢?通过 interface
接口完成扩展,如下声明。
interface Element {
/**
* 查找子元素
*
* @param cssSelector CSS 选择器
* @param fn 可选,当送入该参数的时候,表示使用 querySelectorAll 来查询多个 dom 元素,故 fn 是个遍历器函数,其参数列表如 item、index、array
* @returns 目标元素,如果没有找到返回 null
*/
$(cssSelector: cssSelector, fn?: Function): Element | NodeListOf<Element> | null,
/**
* 查找父元素,支持 标签名称 或 样式名称,任选其一而不能同时传。
*
* @param tagName 标签名称
* @param className 样式名称
* @returns 父级元素,如果没有找到返回 null
*/
up(tagName: string, className?: string): Element | null
}
值得一提的是 cssSelector 类型。它表示 CSS 选择符,我们都知道它本质是个字符串,但因为太常用和常见,于是为其设立专门的类型,就是这个 cssSelector 类型。况且 ts 支持类型赋值的,直接一句搞定。
/**
* CSS 选择符
*/
declare type cssSelector = string;
编译器而言 cssSelector 就是 string,或者说加多了一个别称而已。但对于维护代码而言,又能够更清晰了一点,说明这个参数或者返回值是一个 CSS 选择符,而不是其他 string。
函数委托
Function.prototype.delegate()
就是函数的委托,可以预先制定函数的参数具有什么,返回的就是不立刻执行的函数。函数列表可以指定部分的,不指定的就用 null 传入。也可以指定函数的 this 指向(类似 fn.bind(xxx)
原生方法),在 这个函数 scope 字段上指定(js 的函数也是对象,而且可以任意对其添加对象,叫“晚绑定”)。
举个例子,声明一个简单的函数 function add(x, y) { return x + y;}
,假设已知 y 为 2,x 未知,可以先不管,委托一下函数 add2 = add.delegate(null, 2)
,我们看到第一个参数为 null,意思就是 x 先不指定,第二参数为 y = 2,那么这时候指定了参数,然后返回的也是一个 Function 类型的结果,用 add2 表示,但这时并不立刻对 add2 进行调用。接着我们执行 add2(1)
,这时指定了第一个参数,返回结果 3。
其实这个就是函数式编程中 Partial functions 概念,参考文章:1、2。
Function.prototype.delegate
的源码在https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/base/prototype.ts第 67 行,这里就不粘贴了,直接可以看看其声明的接口。
interface Function {
/**
* 函数的委托,可以预先制定函数的参数具有什么,返回的就是不立刻执行的函数。
* 函数列表可以指定部分的,不指定的就用 null 传入。
* 也可以指定函数的 this 指向,在 这个函数 scope 字段上指定。
*
* @return {Function} 不立刻执行的函数
*/
delegate(..._args: any): Function
}
其思路来自于笔者学习 Js 框架时所接触的 Yahoo! UI,非常好用,所以保留至今。
日期格式化
日期格式化。详见我以往博客文章:《JavaScript自定义日期格式化函数》,例子:
interface Date {
/**
* 格式化日期
*
* @param fmt 日期格式
*/
format(fmt: string): string
}
alert(new Date().format("yyyy-MM-dd hh:mm:ss"));
另外有个扩展就是 polyfill,解决了 HTMLCanvasElement.toBlob 的兼容性。
全局工具函数
工具函数不是很多,连注释才 100 多行,其实挺没意思、挺无聊的,你们看看就知道。
源码在 https://gitee.com/sp42_admin/ajaxjs/blob/master/aj-ts/src/base/aj.ts。
对象复制
有人说着也是继承(早期的 js 库),这个不争论了,有时候还是会调用的。
/**
* 复制 b 对象到 a 对象身上
*
* @param {Object} 目标对象
* @param {Object} 源对象
*/
export function apply(a: any, b: any, c?: any): any {
for (var i in b)
a[i] = b[i];
return c ? aj.apply(a, c) : a;
}
加载脚本
也没啥好说的,
/**
* 加载脚本
*
* @param url 脚本地址
* @param id 脚本元素 id
* @param cb 回调函数
*/
export function loadScript(url: string, id?: string, cb?: (ev: Event) => any): void {
var script: HTMLScriptElement = document.createElement("script");
script.src = url;
if (cb)
script.onload = cb;
if (id)
script.id = id;
document.getElementsByTagName("head")[0].appendChild(script);
}
并行和串行任务
参见: https://segmentfault.com/a/1190000013265925
/**
* 并行和串行任务
*
* @author https://segmentfault.com/a/1190000013265925
* @param arr
* @param finnaly
*/
export function parallel(arr: [], _finally: Function) {
let fn: Function, index: number = 0;
// @ts-ignore
let statusArr = Array(arr.length).fill().map(() => ({
isActive: false,
data: null
}));
let isFinished = function () {
return statusArr.every((item: any) => item.isActive === true);
}
let resolve = function (index: number): Function {
return function (data: any) {
statusArr[index].data = data;
statusArr[index].isActive = true;
let isFinish = isFinished();
if (isFinish) {
let datas = statusArr.map((item: any) => item.data);
_finally(datas);
}
};
};
// @ts-ignore
while ((fn = arr.shift())) {
fn(resolve(index));// 给 resolve 函数追加参数,可以使用 bind 函数实现,这里使用了柯里化
index++;
}
}
函数节流
参见 https://www.cnblogs.com/moqiutao/p/6875955.html
/**
* 函数节流
*
* @author https://www.cnblogs.com/moqiutao/p/6875955.html
* @param fn
* @param delay
* @param mustRunDelay
*/
export function throttle(fn: Function, delay: number, mustRunDelay: number): Function {
var timer: number, t_start: number;
return function () {
var t_curr = +new Date();
window.clearTimeout(timer);
if (!t_start)
t_start = t_curr;
if (t_curr - t_start >= mustRunDelay) {
// @ts-ignore
fn.apply(this, arguments);
t_start = t_curr;
} else {
var args = arguments;
// @ts-ignore
timer = window.setTimeout(() => fn.apply(this, args), delay);
}
};
};
小结
base 模块的 aj、prototype 就介绍到这里了,至于 XHR,我们下文再讲。