本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
在上一篇的最後,我們提到了前端 MV* 框架的快速發展。 在這系列文的最後,我們把時間軸拉回現代,來談談 Web Component 的崛起並漸漸成為前端開發的主流意識。
早在多年以前,人們就試圖將網頁中各區塊以區塊結構的方式做拆分。
早期常見的做法會將網頁內的區塊以 frame
頁框的形式來切分,最後再透過另一個 frameset
網頁將這些頁框組合起來。 但這種方法不但執行效能不好,當搜尋引擎來爬取時,也只會得到一個空白的 frameset
頁,現在已經被廢棄。
後來在動態網頁 (指後端語言) 時期,開始引進了模板引擎 (template engine) 的概念,同樣將網頁結構在原始碼階段做拆分,然後在渲染的時候把它們組合起來一起輸出,在 client 端來看就像是獨立的一個網頁。
這也是現今主流的做法。
但我們在前一篇文章曾提過,當 client 端的效能越來越好,瀏覽器提供的功能越來越多,以往只能在後端做的事情漸漸移到前端來處理,前端再次面臨同樣的問題。 此時前端的模組化 (這裡指的是頁面元素的模組,功能的模組化是另一個議題) 再度被提出來討論,於是 Web Component 的概念因應而生。
Web Component 其實不是只有一個標準而已,而是同時包含了好幾個部分,最主要的有 Shadow DOM、Custom Element 以及 HTML Imports。 最主要想解決的問題其實就是剛剛前面所提到的,將網頁的元件做拆分單位,然後透過自訂的標籤使用,達到最高效率地重複利用。
前面的系列文我們曾經提到過,前一個世代的「關注點分離」著重在將「結構」、「樣式」與「行為」,也就是 HTML、CSS、JavaScript 語言的切分。 然而,這個規則在 Web Components 出現之後,似乎打破了過去人們的對「關注點分離」的想法。
過去我們將 HTML、CSS、JavaScript 各自拆分管理,然而現在改由元件思維來做分類時,又會發現一個有趣的現象。 以前拆分的東西,怎麼現在好像又合在一塊了。
而現今主流的前端框架,如 React、Angular 或是 Vue 的 components ,雖然實作方式各有不同,但概念上是相通的。
像 React 的 styled-components:
import styled from 'styled-components';
const Button = styled.button`
border-radius: 3px;
padding: 0.25em 1em;
margin: 0 1em;
background: transparent;
color: palevioletred;
border: 2px solid palevioletred;
`
render(
<div>
<Button>Normal Button</Button>
</div>
)
Angular4 的 Component decorator:
@Component({
selector: 'hero-app',
template: `
<h1>Tour of Heroes</h1>
<hero-app-main [hero]=hero></hero-app-main>`,
styles: ['h1 { font-weight: normal; }']
})
export class HeroAppComponent {
/* ... */
}
Vue Component file 的 Style 標籤:
/* HTML */
<template>
<h1 @click="clickHandler">HELLO</h1>
</template>
/* script */
<script>
module.exports = {
methods:{
clickHandler(){
alert('hi');
}
}
}
</script>
/* scoped CSS */
<style scoped>
h1 {
color: red;
font-size: 46px;
}
</style>
你會發現,過去我們很習慣的拆分方式,到現在有著很大的變化。 過去我剛入行的時候,前輩大力倡導 HTML / CSS / JavaScript 三者負責的領域要切得越乾淨越好,簡單來說,inline-style 與 inline-script 都是禁止的。 一旦 inline-style 與 inline-script 寫得越多,程式碼就會像義大利麵般攪在一起難以維護。
而自從 component-based 的前端工具出現後,過去的這些原則漸漸被打破。 以元件作為開發思維後,要強調的是:「該放在一起的東西就放在一起」。 為什麼會有這樣的變化,我認為可以從 CSS 的管理講起。
這麼多年來,CSS 的管理一直都是開發者的夢靨。
很大的原因在於 CSS 的程式化與 JavaScript 相比其實是相對困難的, 尤其在於 JS 至少還有它的 function 或 ES6 的 block scope 可以切分,而 CSS 在這點是相對弱勢的,所有的 CSS 樣式都是 global scoped,也就是根本沒有作用範圍的限定,只要寫了一次,整個頁面都會生效。 當頁面上的模組過多,管理起來更是難以維護。
所以,早期在程式面會有所謂的 LESS, SASS, Stylus 等這樣的工具,藉由 preprocessor (預處理器) 來做編譯,可以做到繼承、重用、複寫等功能。 另一方面,除了上述說的透過工具的預先編譯外,也有另一派提倡的是,由 CSS 的命名/架構學來完成 CSS 模組的管理與複用。
像是 OOCSS、SMACSS、BEM 等,這些都不是什麼新技術,只是一種撰寫 CSS 的設計模式以及類別命名方式的定義。
不明白這些是什麼的朋友,可以參考我幾年前的投影片: 漫談 CSS 架構方法 - 以 OOCSS, SMACSS, BEM 為例,或者是 will 保哥寫的這篇: CSS 筆記、建議與指導方針總整理。
然而大家都知道,規範這種違反自然人性的東西,只要有人不遵守,那就完全沒有意義。 所以後來出現了比較符合人性的 CSS Modules,透過工具來處理先前那些人工命名規範要做的事,也就是說,由 Webpack 做了本來 BEM 要做的事情。 過去 BEM 是人工手動做,而 Webpack 是交給工具自動化做,用類似 JavaScript module 的方式來處理 CSS Scope。
於是就進展到我們前面所說的,Web Components 以及 CSS in JS 的概念,以 Component 為單位來切分 CSS 的作用範圍。 [註1]
既然談到了現代主流的前端框架,這些工具還有另一個很重要的特性,就是資料變動的偵測與更新。
早期像是 jQuery 這類型前端工具庫,它們的特點就是以 DOM Node 為基礎。 想要對網頁某個元素操作,就透過 DOM Selector 來取得,拿了就直接用。 這樣的開發方式雖然直覺容易上手,但當架構開始擴大、複雜後,程式碼混雜不易維護。
另外一種我最早是從 AngularJS (v1) 開始接觸的,稱作「宣告式渲染」。這種的特性是以操作物件模型為基礎的開發模式,關注點放在資料 (Model) 與狀態的處理。由於狀態與 DOM 的映射關係,所以我們可以透過操作狀態來更新網頁的內容。 主流的 MVVM 框架/工具庫大多採用這種模式。
既然現代主流 MVVM 框架大多以操作物件狀態為基礎,那麼資料變動的偵測與更新機制就顯得相對重要。
如果有用過 RxJS 的朋友應該會知道,它將取得資料的策略分為「Pull」以及「Push」兩種類型。
舉例來說,我們現在有個算式,變數 a
的值是 3,而變數 b
的值是 a
的五倍。
let a = 3;
let b = a * 5;
console.log(b); // 15
假設當 a
的值更新了,但 b
的數值並沒有變動。
a = 10;
console.log(b); // 15
所以還要再執行一次 b = a * 5
:
a = 10;
b = a * 5;
console.log(b); // 50
每次 a
修改時,都要手動去更新 b
實在太累了。
所以我們是不是可以寫成一個事件,當 a
更新的時候, b
的內容就重新計算:
onAChanged(() => {
b = a * 5;
})
以網頁的寫法,就像這樣:
<span class="cell b1"></span>
// 當狀態更新之後, b 的值就去找 state.a * 5
onStateChanged(() => {
document
.querySelector(".cell.b1")
.textContent = state.a * 5;
})
換成 MVVM Template 的語法,就像這樣:
<span class="cell b1">
{{ state.a * 10 }}
</span>
onStateChanged(() => {
view = render(state);
})
注意到了嗎? React 帶給開發者的新觀念,「 view = render(state)
」,當 state 被更新了之後, view 就會去做對應的變動,而中間的 render function 則是負責處理更新的邏輯。
那麼問題來了,什麼時候該去觸發這個 onStateChanged
事件呢?
以 React 來說,我們可以透過 setState
來更新內部資料:
onStateChanged(() => {
view = render(state);
})
setState({ a: 10 });
然後是 Angular 1:
$scope.$watch(() => {
view = render($scope);
})
$scope.a = 10;
// auto-called in event handlers
$scope.$apply();
Angular 2:
ngOnChanges() {
view = render(state);
})
state.a = 10;
// auto-called if in a zone
Lifecycle.tick();
不知道大家有沒有發現一個共通點?
當我們 Component 的狀態被更新了之後,不管是 React 或是 Angular 就會向那個 Component 發送更新的訊號,然後去觸發後面的子元件一併更新。
像這種方式,我們把它稱作為「Pull」(拉取)。
系統不會知道元件的狀態何時被更新,而狀態發生變化的時候,我們必須要執行某個程序,讓系統去做後續的更新。
以 react 來說,就是 setState()
,而 Angular 1 & 2 則是透過 $scope.$apply();
(dirty checking) 與 Lifecycle.tick();
。
那麼 Push 的更新策略則剛好相反,像 Vue 所採用的 Object.defineProperty
方法,它會針對狀態內的屬性去定義 get
與 set
方法。 換言之,元件本身不用在乎狀態什麼時候被更新,因為當狀態更新時,它會去「主動」呼叫先前定義好的 get
與 set
方法,然後再去處理後續的邏輯計算。
當然不管是哪一種更新策略都會有它的代價。
像 Push 雖然主動觸發更新,但這也代表著元件的每個屬性都必須要綁定 observer 或 watcher。 在執行時來說,也會是一種無形的成本。
理解背後的原理對在技術選擇以及後續的優化來說,會有一定的好處在。
最後來做個總結,讓我們回到這系列開始的問題。
「每 18 至 24 個月,前端都會難一倍」
前端工具不斷地推陳出新,反正 18 個月之後你又會想學新的了,要是覺得跟不上,安心放生也無所謂。
過去大家覺得 jQuery 好棒棒,現在甚至有人會說「哈哈哈,怎麼還在用 jQuery」這樣貴今賤古 ; 又或者手上拿著錘子就把看到的東西當釘子一樣:「人家只是寫 HTML, CSS 跟 Vanilla JS 搞不起來環境你就叫人家用 vue-cli 建立一個完整的 Vue 開發環境」
我認為這都不是面對技術應有的健康態度。
所謂「過時」的技術或工具並不是說現在就不能用,而是隨著時間過去,開發者的想法提升了、技術規格的標準進步了,我們有更好的做法,過去的舊技術適合的場景變少了,於是慢慢退場。 前一世代的工具解決了前一世代的問題,然後不管是思維也好,技術也好,都在繼續向前推進,過去的工具也許在未來某天被納入變成標準也說不定。
另一方面來說,如果 jQuery 就可以滿足你,那麼何苦非得用 MVVM 框架來整自己?
當你對工具理解得越多,你就會發現框架、工具都是因應問題而生,在不考慮問題的情況下去尋找答案都是毫無意義的。
沒有最完美的解決方案,只有相對適合的解決方案。
太赞了吧!官网虽然很详细,也有讲框架的原理,但是总感觉只认识表面的字罢了。看了这一篇之后,醍醐灌顶。很友好。