1. 程式人生 > >前端專案模組化的實踐3.1:使用 TypeScript 的收益

前端專案模組化的實踐3.1:使用 TypeScript 的收益

以下是關於前端專案模組化的實踐,包含以下內容:

  • 使用 Mocha/Jest 進行單元測試 [實現中]

使用 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 的實現是 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> 的內部實現

我們可以在安全地傳入引數和引用返回值,在程式碼編寫階段就能得到編譯器的語法、值入引數合法性檢查。