前端專案模組化的實踐3.1:使用 TypeScript 的收益
阿新 • • 發佈:2018-12-09
以下是關於前端專案模組化的實踐,包含以下內容:
- 使用 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<>
例項,只是內部元素由傳入的valueSelector
和keySelector
決定,該簽名使得鏈式呼叫成為可能;
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>
的內部實現
我們可以在安全地傳入引數和引用返回值,在程式碼編寫階段就能得到編譯器的語法、值入引數合法性檢查。