用 TypeScript 写一个轻量级的 UI 框架之四:base 模块

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 概念,参考文章:12

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,我们下文再讲。

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