前端项目模块化的实践3:使用 TypeScript 的收益

以下是关于前端项目模块化的实践,包含以下内容:

  1. 搭建 NPM 私有仓库管理源码及依赖
  2. 使用 Webpack 打包基础设施代码
  3. 使用 TypeScript 编写可靠类库

使用 Webpack 打包基础设施代码已经很大程度上解决了生产力,但日益复杂业务和逻辑仍然让前端陷入“动态一时爽、重构火葬场”的笑谈,TypeScript 为解决这个问题而来。

在本章节我们使用 TypeScript 完成一个类似 LINQ 中 Enumerable<T> 的实现,涉及代码编写与测试用例,仍然不深入关于 TypeScript 的讨论。

场景

假想需要实现这样一个功能:比如一组学生,我们希望按照根据班级分组,接着按年龄排序,最后按照名称排序。

这并不复杂,这些步骤是有先后的,每步操作得到的都是一组对象集合,分组和排序是常规数组操作,写几个循环就可以达到目标。

C# 的实现

void Main() {
    var students = new List<Student> {
        new Student { Classes =  "class-1", Name = "Rattz", Age = 11 },
        new Student { Classes =  "class-2", Name = "Rose", Age = 10 },
        new Student { Classes =  "class-1", Name = "Mike", Age = 11 }
    };
    
    var seq = students.GroupBy(x => x.Classes)
        .Select(g => new {
            Classes = g.Key,
            Members = g.OrderBy(x => x.Age)
                .ThenBy(x => x.Name)
                .Select(x => x)
        });
}

class Student {
    public String Classes { get; set; }
    public String Name { get; set; }
    public Int32 Age { get; set; }
}

得力于静态语言的类型保证及 LINQ 语法对内存对象的操作能力,一行代码就简单而有表现力地解决了问题,在 LINQPad 里组织如下:

ES6 很明了但代码略多

现在来看 JavaScript 的写法,使用 ES6 的 Map 对象极大地减化了代码。

let students = [
    {'classes': 'class-1', 'name': 'Rattz', 'age': 11},
    {'classes': 'class-2', 'name': 'Rose', 'age': 10},
    {'classes': 'class-1', 'name': 'Mike', 'age': 11},
];

let map = new Map();
for (let item of students) {
    let classes = map.get(item.classes);
    if (Object.is(classes, undefined)) {
        classes = [item];
        map.set(item.classes, classes);
    }
    else {
        classes.push(item);
    }
}

for (let [, value] of map.entries()) {
    value.sort((p1, p2) => {
        if (p1.age !== p2.age) {
            return p1.age - p2.age;
        }
        return p1.name.localeCompare(p2.name);
    });
}

let groups = [];
for (let [key, value] of map.entries()) {
    groups.push({
        classes: key,
        members: value,
    });
}

ES5 reduce 很强有力但很难一次编写正确

代码略多,包含了3个循环,如果不使用 Map 代码就要进行数组查找;如果想压缩循环,就使用 Array.prototype.reduce 函数,代码就很难懂了。

let students = [
    {'classes': 'class-1', 'name': 'Rattz', 'age': 11},
    {'classes': 'class-2', 'name': 'Rose', 'age': 10},
    {'classes': 'class-1', 'name': 'Mike', 'age': 11},
];

let groups = students.reduce((previous, current) => {
    let arr = previous.filter(x => x.key === current.classes);
    if (arr.length > 0) {
        arr[0].members.push(current);
    }
    else {
        previous.push({
            classes  : current.classes,
            members: [current],
        });
    }
    return previous;
}, []);

for(let g of groups) {
    g.members.sort((a, b) => a.name.localeCompare(b.name));
}

TypeScript 的使用

下文通过编写类库完成类似功能,我们充分使用生产力而先不考虑语言、版本问题,看看具体的调用部分

interface Student {
    classes: string,
    name: string
    age: number
}

let students: Student[] = [
    {'classes': 'class-1', 'name': 'Rattz', 'age': 11},
    {'classes': 'class-2', 'name': 'Rose', 'age': 10},
    {'classes': 'class-1', 'name': 'Mike', 'age': 11},
];

let groups = Enumerable.from(students)
    .groupBy(x => x.classes)
    .select(x => ({
        classes: x.key,
        members: Enumerable.from(<Student[]>x.items)
            .sortBy((x, y) => x.age - y.age)
            .thenSortBy((x, y) => x.name.localeCompare(y.name))
    }));

完整代码见于 Enumerable 行 100 左右。

如果 Enumerable 的实现是 JavaScript 版本,这部分代码可能充满了疑问,即便阅读源码也很难一下子理解实现者的用意

  • groupby 需要什么样的参数?
  • select 使用了 groupBy 返回值,它包含怎样的数据结构?
  • sortBy 但返回了什么?
  • thenSortBy 如何进行二次排序,又返回了什么?

TypeScript 能够解答上述问题问题,以下是函数签名。

  • groupBy:完整签名是 groupBy<K, U>(keySelector: (value: T, index: number) => K, valueSelector?: (value: T, index: number) => U): Enumerable<Group<K, T | U>>,虽然稍长但是阅读起来也就那回事
    • keySelector: 接受2个参数(分别是数组元素和索引)返回1个值,通常是对象的某个属性,
    • valueSelector: 和keySelector相似,表示得到新元素的方法,术语是“投影”
    • Enumerable<Group<K, T | U>>groupBy 的返回类型,表示仍然是 Enumerable<> 实例,只是内部元素由传入的 valueSelectorkeySelector 决定,该签名使得链式调用成为可能;
  • select: 完整签名是 select<U>(callback: (value: T, index: number) => U): Enumerable<U>,和 groupBy 类似,但更加简单
  • sortBy: 和 Array.prototype.sort 功能相似,但签名是 sortBy(compareFn: (a: T, b: T) => number): OrderedEnumerable<T>,返回 OrderedEnumerable<> 实例
  • thenSortBy:同 sortBy,依赖 OrderedEnumerable<T> 的内部实现

我们可以在安全地传入参数和引用返回值,在代码编写阶段就能得到编译器的语法、值入参数合法性检查。

posted @ 2018-09-11 11:24  Jusfr  阅读(430)  评论(0编辑  收藏  举报