Web 框架能解決什么問題?
最近,我對對比框架和普通的 JavaScript 產生了濃厚的興趣。這始于我在一些自由職業項目中使用 React 時遇到的一些挫折,以及我最近作為規范編輯,對 Web 標準有了更多的認識。
我希望了解一下這些框架的共性和差異,Web 平臺作為一種更精簡的選擇,能提供什么,以及它是否足夠。我的目標并非要抨擊這些框架,而是要了解成本和效益,找出有沒有其他選擇,甚至當我們決定采用框架時,我們也能從中吸取教訓。
在本系列文章的第一部分中,我將深入探討一些框架的共性技術特性,并介紹幾種不同的框架是怎樣實現這些特性的。我還要看一下使用這些框架的成本。
框架
我選取四種架構進行研究。React 是當今的主流框架,還有三個較新的競爭者,它們聲稱自己的工作方式與 React 不同。
- React:“React 使創建交互式用戶界面變得不費力。聲明性視圖使你的代碼更可預測,更容易調試。”
- SolidJS:“Solid 遵循與 React 相同的理念……但它的實現方式完全不同,放棄了使用虛擬 DOM。”
- Svelte:“Svelte 是一種全新的構建用戶界面的方式……是一個在你構建應用時發生的編譯步驟。Svelte 不使用虛擬 DOM diffing 之類的技術,而是編寫代碼,當你的應用程序的狀態發生變化時,外科手術式地更新 DOM。”
- Lit:“在 Web Components 標準的基礎上,Lit 增加了……反應性、聲明性模板,以及一些深思熟慮的特性。”
總結一下這些框架對其差異化的說法:
- React 通過聲明式視圖使構建 UI 更容易。
- SolidJS 遵循 React 的理念,但是采用了另一種技術。
- Svelte 處理用戶界面采用了一種編譯時的方式。
- Lit 使用現有的標準,并增加了一些輕量級的特性。
框架能解決什么問題?
框架自身也提及了諸如聲明性、反應性和虛擬 DOM 等詞。讓我們深入了解它們的含義。
聲明性編程
聲明性編程是一種范式,在這種范式中,邏輯被定義,而沒有指定控制流。我們描述需要的結果是什么,而不是我們會采取什么步驟。
在 2010 年左右,聲明性框架的早期,DOM 的 API 更加簡單,更加冗長。而使用命令式的 JavaScript 編寫 Web 應用程序則需要大量的模板代碼。這時,“模型 - 視圖 - 視圖模型”(model-view-viewmodel,MVVM)的概念開始盛行,當時具有劃時代意義的 Knockout 和 AngularJS 框架,提供了一個 JavaScript 聲明層,在庫內處理這種復雜性。
今天,MVVM 并不是一個廣泛使用的術語,它在某種程度上是舊術語“數據綁定”的變種。
數據綁定
數據綁定是一種聲明性的方式,用來表示數據如何在模型和用戶界面之間同步。所有流行的 UI 框架都提供了某種形式的數據綁定,它們的教程都以數據綁定的例子開始。
以下是 JSX(SolidJS 和 React)中的數據綁定:
function HelloWorld() {
const name = "Solid or React";
return (
<div>Hello {name}!</div>
)
}
Lit 中的數據綁定:
class HelloWorld extends LitElement {
@property()
name = 'lit';
render() {
return html`<p>Hello ${this.name}!</p>`;
}
}
Svelte 中的數據綁定:
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
反應性
反應性是一種聲明性的方式來表達更改的傳播。
如果我們能夠用一種聲明的方式來表示數據綁定,那么我們就必須要有一個使框架能夠傳播更改的高效方法。
- React 引擎會把渲染的結果與之前的結果相比較,并將差異應用于 DOM 本身。這種處理更改傳播的方式,被稱為虛擬 DOM。
- 在 SolidJS 中,這是以其存儲和內置元素更明確地完成的。例如,Show 元素將跟蹤內部的變化,而不是虛擬 DOM。
- 在 Svelte 中,生成“active”代碼。Svelte 知道哪些事件會導致變化,它會生成直接的代碼,區分事件和 DOM 更改。
- 在 Lit 中,反應性是通過元素屬性來實現的,基本上是依賴 HTML 自定義元素的內置反應性。
邏輯
如果框架為數據綁定提供了聲明性的接口,并且能夠實現反應性,那么就必須提供一些方法來表達一些傳統意義上的邏輯,這些邏輯是以命令的方式寫的。邏輯的基本構件是 “if” 和 “for”,而所有的主流框架都提供了這些構件的一些表達。
(1) 條件句
除了綁定數字和字符串等基本數據外,每個框架都提供了一個“條件”原語。在 React 中,它看起來如下所示:
const [hasError, setHasError] = useState(false);
return hasError ? <label>Message</label> : null;
…
setHasError(true);
SolidJS 提供了內置的條件組件。
<Show when={state.error}>
<label>Message</label>
</Show>
Svelte 提供了 #if 指令:
{#if state.error}
<label>Message</label>
{/if}
在 Lit 中,你將在 render 函數中使用顯式三元運算:
render() {
return this.error ? html`<label>Message</label>`: null;
}
(2) 列表
另一個常見的框架基元是列表處理。列表是用戶界面的一個關鍵部分——如聯系人列表、通知等——要想高效工作,就必須有反應性,而不是在一個數據項發生變化時,對整個列表進行更新。
在 React 中,列表處理看起來像這樣:
contacts.map((contact, index) =>
<li key={index}>
{contact.name}
</li>)
React 使用特殊的 key 屬性來區分列表項,它確保整個列表不會在每次渲染時被替換。
在 SolidJS 中,使用了 for 和 index 內置元素:
<For each={state.contacts}>
{contact => <DIV>{contact.name}</DIV> }
</For>
在內部,SolidJS 將自身的存儲與 for 和 index 相結合,以確定在項目發生個更改時要更新哪些元素。它比 React 更清晰,使我們能夠避免虛擬 DOM 的復雜性。
Svelte 使用 each 指令,該指令根據其更新器被轉譯:
{#each contacts as contact}
<div>{contact.name}</div>
{/each}
Lit 提供了一個 repeat 函數,它的工作原理類似于 React 的基于鍵的列表映射:
repeat(contacts, contact => contact.id,
(contact, index) => html`<div>${contact.name}</div>`
組件模型
有一件事超出了本文的范圍,那就是不同框架中的組件模型,以及如何使用自定義 HTML 元素來處理它。
注意:這是一個很大的主題,我想在以后的文章里討論這個主題,因為這個主題會讓這篇文章變得太長。
成本
框架提供了聲明性的數據綁定、控制流原語(條件和列表),以及傳播更改的反應性機制。它們還提供了其他重要的東西,比如重用組件的方法,但這就是另一篇文章的主題了。
框架有用嗎?是的。它們帶給了我們所有這些方便的特性。但這是一個正確的問題嗎?使用框架需要付出一定的成本。讓我們來看一下這些成本。
包大小
在查看包大小時,我更愿意看到非 Gzip 的縮減大小。這個尺寸與 JavaScript 的 CPU 開銷有很大關系:
- ReactDOM 大約是 120 KB。
- SolidJS 大約是 18KB。
- Lit 大約是 16KB。
- Svelte 約為 2KB,但生成的代碼大小不同。
現在看來,在保持包大小上,現在的框架要優于 React。虛擬 DOM 要求使用很多 JavaScript。
構建
不知何故,我們習慣了“構建” Web 應用。如果不設置 Node.js 和 Webpack 這樣的捆綁器,不處理 Babel-TypeScript 啟動包中最近的一些配置更改,以及所有這些事情,就不可能啟動一個前端項目。
越是有表達力的框架,包大小就會變得更小,但構建工具和轉譯時間的負擔就越大。
Svelte 宣稱,虛擬 DOM 完全是一種開銷。我同意,但是可能像 Svelte 和 SolidJS 這樣的“構建”以及像 Lit 這樣的自定義客戶端模板引擎都只是單純的開銷嗎?
調試
在構建和轉譯過程中,需要付出的成本也是不同的。
我們在使用和調試 Web 應用程序時,所見到的代碼和我們所編寫的完全不一樣。我們現在依靠同樣品質的調試工具,逆向設計出一個站點,并把它和我們自己的代碼中的 bug 相關聯。
在 React 中,調用棧從來不是“你的”事情——React 會為你處理調度。這一特性在沒有 bug 的時候非常好用。但是,如果你試圖找出無限循環重現的原因,你將會陷入痛苦的境地。
在 Svelte 中,庫本身的包大小很小,但你要傳輸和調試一大堆神秘的生成代碼,這些代碼是 Svelte 對反應性的實現,根據你的應用需求定制。
Lit 并不需要進行大量的構建,但是要想有效地進行調試,你就必須熟悉其模板引擎。這也許是我對框架持懷疑態度的最大原因。
當你尋求自定義的聲明式解決方案時,你將面對更加困難的命令調試。本文中的示例采用了 TypeScript 來對 API 進行規范,但是該代碼本身并不需要轉譯。
升級
在本文中,我討論了四個框架,但是還有許多其他的框架,多得數不清(AngularJS、Ember.js 和 Vue.js,僅舉幾例)。你能指望框架、它的開發者、它的思想和它的生態系統在開發過程中為你工作?
除了修補自己的 bug 之外,還有一個更讓人沮喪的事情,就是必須為框架的錯誤找到變通方法。而且,還有一個更加令人沮喪的事情,那就是在沒有修改你的代碼的情況下,將框架升級為新的版本,會出現 bug。
誠然,瀏覽器中也有這樣的問題,但是這種問題一旦出現,就會影響到所有人,而且在大多數情況下,修復或者發布一個解決方案,都是迫在眉睫的。此外,本文提到的大部分模式都建立在成熟的 Web 平臺 API 之上,并不一定都需要采用尖端技術。
總結
我們對框架所要處理的核心問題有了更深刻的理解,并且著重于數據綁定、反應性、條件和列表。我們也對成本進行了討論。
在本系列的第二部分中,我們將會了解到,在沒有框架的情況下,我們是怎樣處理這些問題的,以及我們可以從中學習到什么。敬請關注!
原文鏈接:https://www.smashingmagazine.com/2022/01/web-frameworks-guide-part1/