詳細(xì)的對(duì)比現(xiàn)代前端框架到底解決了什么問題?
大家好,我是 桃翁,相信各位在 Web 開發(fā)的工作中已經(jīng)離不開框架了,不知道有多少同學(xué)還用原生 JS 寫代碼呢?你有認(rèn)真思考過框架究竟為我們解決了什么樣的問題嗎?脫離了這些框架,我們可以解決這些問題嗎?我們來看看今天的文章:
最近,我對(duì)將框架與原生的 JavaScript 進(jìn)行對(duì)比非常感興趣。我很想知道這些框架之間的共性和差異是什么,Web 平臺(tái)作為一個(gè)精簡(jiǎn)的替代方案應(yīng)該提供什么,以及它本身是否可以足夠滿足我們的需求。
我的目標(biāo)不是要抨擊這些框架,而是想要了解使用框架的成本和收益,確定是否存在某些替代方案,并看看即使我們決定使用框架,是不是可以從中學(xué)到一些什么。
首先,我們先深入研究一些跨框架通用的技術(shù)特性,以及不同框架如何實(shí)現(xiàn)這些特性。
框架
我選擇了四個(gè)框架來研究:當(dāng)今處于主導(dǎo)地位的框架 React ,以及其他三個(gè)聲稱與 React 工作方式不同的競(jìng)爭(zhēng)者。
React:“React以聲明式編寫 UI,可以讓你的代碼更加可靠,且方便調(diào)試?!?/li>SolidJS:“SolidJS遵循與React相同的理念…… 但是它有一個(gè)完全不同的實(shí)現(xiàn),它放棄了使用虛擬DOM。”Svelte:"Svelte是一種全新的構(gòu)建用戶界面的方法。傳統(tǒng)框架如 React 會(huì)在瀏覽器中需要做大量的工作,而Svelte將這些工作放到構(gòu)建應(yīng)用程序的編譯階段來處理?!?/li>Lit:“在Web Components標(biāo)準(zhǔn)之上構(gòu)建,額外增加了響應(yīng)式、聲明性模板等能力?!?/li>
簡(jiǎn)單總結(jié)一下這些框架的區(qū)別:
React使用聲明式視圖讓構(gòu)建 UI 變得更容易。SolidJS遵循 React 的理念,但使用了不同的技術(shù)。Svelte對(duì) UI 在編譯時(shí)做了大量處理。Lit使用現(xiàn)有標(biāo)準(zhǔn),并添加了一些輕量級(jí)功能。
框架為我們解決什么問題?
聲明式編程
聲明式編程是一種在不指定控制流的情況下定義邏輯的范例。我們描述的是結(jié)果需要是什么,而不是我們需要采取什么步驟。
在聲明式框架的早期,大約在 2010 年,DOM API 非常冗長(zhǎng),使用命令式 JavaScript 編寫 Web 應(yīng)用程序需要大量的樣板代碼。那時(shí) “model-view-viewmodel” (MVVM) 的概念開始流行起來,當(dāng)時(shí)開創(chuàng)性的 Knockout 和 AngularJS 框架提供了一個(gè) JavaScript 聲明層來處理庫內(nèi)部的復(fù)雜性。
數(shù)據(jù)綁定
數(shù)據(jù)綁定是一種聲明性的方式,它用來表示數(shù)據(jù)如何在模型和用戶界面之間同步。
所有流行的 UI 框架都提供了某種形式的數(shù)據(jù)綁定,它們的教程基本上都從一個(gè)數(shù)據(jù)綁定示例開始。
下面是 JSX 中的數(shù)據(jù)綁定(SolidJS 和 React):
function?HelloConardLi()?{
?const?name?=?"Solid?or?React;
?return?(
?????Hello?{name}!
?)
}
Lit 中的數(shù)據(jù)綁定:
class?HelloConardLi?extends?LitElement?{
?@property()
?name?=?'lit';
?render()?{
???return?html`<p>Hello?${this.name}!p>`;
?}
}
Svelte 中的數(shù)據(jù)綁定:
<script>
??let?name?=?'world';
script>
<h1>Hello?{name}!h1>
響應(yīng)式
響應(yīng)式是一種表達(dá)變化和傳遞的聲明性方式。
當(dāng)我們有了一種聲明式表達(dá)數(shù)據(jù)綁定的方法時(shí),我們需要一種有效的方法讓框架傳遞這個(gè)更改。
React 引擎會(huì)將渲染結(jié)果與之前的結(jié)果進(jìn)行比較,并將差異應(yīng)用到 DOM 本身。這種處理變更傳播的方法稱為虛擬 DOM。
在 SolidJS 中,這通過它的存儲(chǔ)和內(nèi)置元素更顯式地完成。例如,Show 元素將跟蹤內(nèi)部發(fā)生的變化,而不是虛擬 DOM。
在 Svelte 中,會(huì)生成“響應(yīng)式”代碼。Svelte 知道哪些事件會(huì)導(dǎo)致更改,并生成簡(jiǎn)單的代碼,在事件和 DOM 更改之間劃清界限。
在 Lit 中,響應(yīng)式是使用元素屬性完成的,本質(zhì)上依賴于 HTML 自定義元素的內(nèi)置響應(yīng)性。
邏輯
當(dāng)框架為數(shù)據(jù)綁定提供一個(gè)聲明式接口,并實(shí)現(xiàn)響應(yīng)式時(shí),它還需要提供某種方式來表達(dá)一些傳統(tǒng)上以命定方式編寫的邏輯。比如傳統(tǒng)的 “if” 和 “for” 語句,所有主要的框架都提供了這些邏輯的一些表達(dá)式。
條件
除了綁定數(shù)字和字符串等基本數(shù)據(jù)外,每個(gè)框架都提供一個(gè)“條件”原語。在 React 中,它是這樣的:
const?[hasError,?setHasError]?=?useState(false);??
return?hasError???<label>出錯(cuò)了!label>?:?null;
…
setHasError(true);
SolidJS 提供了一個(gè)內(nèi)置的條件組件 Show :
??<label>出錯(cuò)了!label>
</Show>
Svelte 提供了 #if 指令:
{#if?state.error}
??在 Lit 中,你可以在 render 函數(shù)中使用三元運(yùn)算:
render()?{
?return?this.error???html`<label>出錯(cuò)了!label>`:?null;
}
列表渲染
還有一個(gè)比較常見的就是列表處理,它是 UI 里非常的關(guān)鍵部分,為了有效地工作,它們需要是響應(yīng)式的,而不是在一個(gè)數(shù)據(jù)項(xiàng)發(fā)生變化時(shí)更新整個(gè)列表。
在 React 中,列表處理看起來像這樣:
contacts.map((contact,?index)?=>
?<li?key={index}>
???{contact.name}
?li>)
React 使用特殊的 key 屬性來區(qū)分列表中的每一項(xiàng),確保整個(gè)列表不會(huì)全部重新渲染。
在 SolidJS 中,使用 for 和 index 內(nèi)置元素:
<For?each={state.contacts}>
??{contact?=>?<DIV>{contact.name}DIV>?}
For>
在內(nèi)部,SolidJS 使用它自己的內(nèi)存與 for、index 決定狀態(tài)更改時(shí)需要改動(dòng)哪些元素。它比 React 更明確,而且避免了虛擬 DOM 的復(fù)雜性。
Svelte 使用 each 指令:
{#each?contacts?as?contact}
??{contact.name}</div>
{/each}
Lit 提供了一個(gè) repeat 函數(shù),工作方式類似于 React 的 key
repeat(contacts,?contact?=>?contact.id,
????(contact,?index)?=>?html`<div>${contact.name}div>`
框架帶來的成本
上面我們提到,框架提供聲名式的數(shù)據(jù)綁定、條件和列表渲染、以及傳遞更改的響應(yīng)式機(jī)制,另外還提供組件復(fù)用等能力。
這些能力雖然給我們帶來了方便,但也額外增加了很多成本。
捆綁依賴包的大小
在查看捆綁依賴包的大小時(shí),我習(xí)慣查看壓縮后非 Gzip 的大小。這是與 JavaScript 執(zhí)行的 CPU 成本最相關(guān)的大小。
ReactDOM 大約 120 KB。SolidJS 大約 18 KB。Lit 約為 16 KB。Svelte 大約 2 KB,但生成的代碼大小不同。
似乎最新推出的框架在保持包大小方面都比 React 做得更好。虛擬 DOM 需要大量的 JavaScript 代碼。
構(gòu)建
不知從何時(shí)開始,我們習(xí)慣了“構(gòu)建”我們的 Web 應(yīng)用程序。如果不設(shè)置 Node.js 和 Webpack 之類的打包器、處理 Babel-TypeScript 啟動(dòng)包中最近的一些配置更改等等,就不可能啟動(dòng)前端項(xiàng)目。
框架的表現(xiàn)力越強(qiáng),包體積越小,同時(shí)構(gòu)建工具和編譯時(shí)間的負(fù)擔(dān)就越大。
Svelte 聲稱虛擬 DOM 是純粹的開銷。我同意,但 “編譯”(如 Svelte 和 SolidJS)和自定義客戶端模板引擎(如 Lit)是不是也是一種不同類型的純開銷呢?
調(diào)試
我們?cè)谑褂没蛘{(diào)試 Web 應(yīng)用程序的時(shí)候,看到的代碼和我們編寫的代碼是完全不同的。為了方便調(diào)試,我們一般需要依靠一些特殊調(diào)試工具來對(duì)網(wǎng)站上的代碼進(jìn)行逆向,并將其與我們自己代碼中的錯(cuò)誤聯(lián)系起來。
在 React 中,調(diào)用堆棧永遠(yuǎn)不是你想象的那樣,因?yàn)樗械母露际?React 為你處理調(diào)度的。在沒發(fā)生 bug 的情況下,這樣挺好的。但是,比如你現(xiàn)在要嘗試找到一個(gè)無限循環(huán)重新渲染的 bug,是非常痛苦的。
在 Svelte 中,庫本身的包體積很小,但你需要發(fā)布和調(diào)試一大堆額外生成的代碼,這些代碼是用來實(shí)現(xiàn) Svelte 響應(yīng)式的,它們會(huì)據(jù)應(yīng)用的需要進(jìn)行定制。
使用 Lit 的話,它與構(gòu)建無關(guān),但如果想對(duì)它進(jìn)行調(diào)試,你就必須了解它的模板引擎。這可能是我對(duì)這個(gè)框架持懷疑態(tài)度的最大原因。
升級(jí)
在這篇文章中,我們介紹了4個(gè)框架,但還有很多框架 (AngularJS、Ember.js 和 Vue.js 等) 我們沒提到。在這些框架的發(fā)展過程中,你能指望它的開發(fā)者、它的思想和它的生態(tài)系統(tǒng)能持續(xù)為你服務(wù)嗎?
還有一件比修復(fù)自己的 bug 更麻煩的事,就是你需要持續(xù)考慮這些框架的 bug。另外你還要考慮是不是在沒有修改代碼的情況下,升級(jí)了一個(gè)框架的版本就引入一些新的 bug。
確實(shí),這樣的問題也存在于瀏覽器中,但是瀏覽器一旦有問題,每個(gè)人都跑不了。并且瀏覽器在大多數(shù)情況下,修復(fù)問題或發(fā)布解決方法都是非常迅速的。另外,本文中的大部分模式都基于成熟的 Web 平臺(tái) API,我們也并不是一直都要考慮升級(jí)。
自己實(shí)現(xiàn)一個(gè)框架?
在沒有框架的情況下進(jìn)行探索,似乎一個(gè)不可避免的結(jié)果就是實(shí)現(xiàn)一個(gè)自己的框架來進(jìn)行響應(yīng)式數(shù)據(jù)綁定。之前我也嘗試過,但是看到它的成本有多大后,我決定在這次探索中遵循下面的原則:
不使用框架,也不是自己封裝框架,而是想看看能不能直接使用 Web 原生的 API 實(shí)現(xiàn)。
原生選擇
Web 平臺(tái)已經(jīng)為我們提供了開箱即用的聲明式編程機(jī)制:HTML 和 CSS。它們已經(jīng)非常成熟、而且已經(jīng)經(jīng)過了非常廣泛的測(cè)試。
但是,它們沒有提供明確的數(shù)據(jù)綁定、條件渲染和列表渲染這樣的概念,并且也沒有跨平臺(tái)響應(yīng)式這樣微妙的功能。
下面我將嘗試整理一些關(guān)于如何在不借助框架的情況下,使用原生的 Web API 解決這些問題的指南。
使用 DOM 樹的響應(yīng)式
我們回到前面提到的錯(cuò)誤標(biāo)簽的示例。在 ReactJS 和 SolidJS 中,我們創(chuàng)建了可以轉(zhuǎn)換為命令式代碼的聲明式代碼,在 DOM 中添加或刪除這個(gè)標(biāo)簽。在 Svelte 中,會(huì)直接編譯生成這樣的代碼。
但是如果我們根本沒有這樣的代碼,而是直接使用 CSS 來隱藏和顯示錯(cuò)誤標(biāo)簽?zāi)兀?/p>
<style>
????label.error?{?display:?none;?}
????.app.has-error?label.error?{display:?block;?}
style>
<label?class="error">出錯(cuò)啦!label>
<script>
???app.classList.toggle('has-error',?true);
script>
在這種情況下,響應(yīng)是在瀏覽器中處理的 — 應(yīng)用程序的類更改會(huì)傳播到它的后代,直到瀏覽器中的內(nèi)部機(jī)制決定是否渲染標(biāo)簽。
這樣的技術(shù)有幾個(gè)優(yōu)點(diǎn):
- 捆綁依賴包的大小為零。
- 沒有構(gòu)建的步驟。
- 在本地瀏覽器代碼中,變更的傳播經(jīng)過了優(yōu)化和測(cè)試,并且避免了例如追加和刪除這樣不必要的
DOM 操作。 - 選擇器是穩(wěn)定的,在這個(gè)例子里你可以借助 label 元素的存在,在不借助
transition groups 這樣的復(fù)雜結(jié)構(gòu)的情況下實(shí)現(xiàn)動(dòng)畫,而且可以在 JavaScript 中保存對(duì)它的引用。 - 標(biāo)簽是顯示還是隱藏,你可以在開發(fā)人員工具的樣式面板中很清晰的看到原因。
先不說這篇文章的場(chǎng)景,就算你在使用框架的時(shí)候,考慮使用 CSS 保持 DOM 穩(wěn)定和更改狀態(tài)的想法也是非常不錯(cuò)的。
面向表單的“數(shù)據(jù)綁定”
在使用大量 JavaScript 的單頁應(yīng)用程序(SPA)時(shí)代之前,表單是創(chuàng)建包含用戶輸入的 Web 應(yīng)用程序的主要方式。
在以前的多頁應(yīng)用中,用戶將填寫表單并單擊 “Submit” 按鈕,然后服務(wù)端代碼會(huì)處理響應(yīng)。
由于表單 API 的廣泛使用和悠久的歷史,它也積累了一些隱藏的優(yōu)點(diǎn),使得它們也可以解決那些看起來解決不了的問題。
作為穩(wěn)定選擇器的表單和表單元素
表單可以通過名稱訪問( document.forms ),并且每個(gè)表單元素也都可以通過名稱訪問(form.elements)。另外,與元素相關(guān)聯(lián)的表單也是可以訪問的( form attribute )。這不僅包括 Input ,還包括其他表單元素,如 output、textarea 和 fieldset,它們?cè)试S嵌套訪問樹中的元素。
在前面的錯(cuò)誤標(biāo)簽示例中,我們展示了如何響應(yīng)式地顯示和隱藏錯(cuò)誤消息。下面就是我們?cè)?React 中更新錯(cuò)誤消息文本的方式(在 SolidJS 中也是一樣的):
const?[errorMessage,?setErrorMessage]?=?useState(null);
return?<label?className="error">{errorMessage}label>
當(dāng)我們擁有穩(wěn)定的 DOM 和穩(wěn)定的樹形表單元素時(shí),我們可以執(zhí)行下面的操作:
<form?name="contactForm">
??<fieldset?name="email">
?????<output?name="error">output>
??fieldset>
form>
<script>
??function?setErrorMessage(message)?{
??document.forms.contactForm.elements.email.elements.error.value?=?message;
??}
script>
這樣的原始代碼看起來非常冗長(zhǎng),但它也非常穩(wěn)定、直接且非常高效。
表單的 Input
通常,當(dāng)我們構(gòu)建一個(gè) SPA 項(xiàng)目時(shí),我們會(huì)使用某種類似 JSON 的 API 來更新我們的服務(wù)器或我們使用的任何模型。
下面是個(gè)簡(jiǎn)單的例子(一個(gè)聯(lián)系人類型、以及一個(gè)更新聯(lián)系人的方法):
interface?Contact?{
??id:?string;
??name:?string;
??email:?string;
??subscriber:?boolean;
}
function?updateContact(contact:?Contact)?{?…?}
在框架代碼中,通過選擇 Input 元素并逐個(gè)構(gòu)造對(duì)象來生成這個(gè) Contact 對(duì)象是很常見的操作。通過正確的使用表單,有個(gè)簡(jiǎn)潔的替代方案:
<form?name="contactForm">
??<input?name="id"?type="hidden"?value="136"?/>
??<input?name="email"?type="email"/>
??<input?name="name"?type="string"?/>
??<input?name="subscriber"?type="checkbox"?/>
form>
<script>
???updateContact(Object.fromEntries(
???????new?FormData(document.forms.contactForm));
script>
借助 FormData 類,我們可以在 DOM Input 和 JavaScript 函數(shù)之間無縫轉(zhuǎn)換這些數(shù)據(jù)。
組合表單和響應(yīng)式
通過組合表單的高性能選擇器穩(wěn)定性和 CSS 響應(yīng)性,我們可以實(shí)現(xiàn)更復(fù)雜的 UI 邏輯:
<form?name="contactForm">
??<input?name="showErrors"?type="checkbox"?hidden?/>
??<fieldset?name="names">
?????<input?name="name"?/>
?????<output?name="error">output>
??fieldset>
??<fieldset?name="emails">
?????<input?name="email"?/>
?????<output?name="error">output>
??fieldset>
form>
<script>
??function?setErrorMessage(section,?message)?{
??document.forms.contactForm.elements[section].elements.error.value?=?message;
??}
??function?setShowErrors(show)?{
??document.forms.contactForm.elements.showErrors.checked?=?show;
??}
script>
<style>
???input[name="showErrors"]:not(:checked)?~?*?output[name="error"]?{
??????display:?none;
???}
style>
注意,在這個(gè)例子中沒有使用 class — 我們從表單的數(shù)據(jù)中開發(fā) DOM 的行為和樣式,而不是去手動(dòng)更改元素類。
我不喜歡過度使用 CSS class 作為 JavaScript 選擇器。我認(rèn)為它們應(yīng)該用于將類似樣式的元素組合在一起,而不是作為一種改變組件樣式的萬能機(jī)制。
表單的優(yōu)點(diǎn)
- 表單是內(nèi)置在
Web 平臺(tái)中的原生 API,大部分功能都是穩(wěn)定的。這意味著更少的 JavaScript 代碼,更少的框架版本不匹配,并且沒有“構(gòu)建” 這樣的環(huán)節(jié)。 - 默認(rèn)情況下表單是可以訪問的,它同樣適用于鍵盤導(dǎo)航、屏幕閱讀器等其他輔助技術(shù)。
- 表單具有內(nèi)置的輸入驗(yàn)證功能:我們可以通過正則表達(dá)式模式進(jìn)行驗(yàn)證、借助 CSS 對(duì)無效和有效的表單、是否必選等進(jìn)行處理,而不需要進(jìn)行額外的開發(fā)。
- 表單的
submit 事件非常有用。例如,它允許在沒有提交按鈕的情況下捕獲 “Enter” 鍵,并允許通過 submitter 屬性區(qū)分多個(gè)提交按鈕(在后面的例子中我們會(huì)看到這個(gè))。 - 默認(rèn)情況下,元素與它們所包含的表單相關(guān)聯(lián)。這允許我們?cè)诓灰蕾?
DOM 樹的情況下處理表單關(guān)聯(lián)。 - 使用穩(wěn)定的選擇器會(huì)讓 UI 自動(dòng)化測(cè)試更簡(jiǎn)單:我們可以使用嵌套
API 作為一種穩(wěn)定的方式來和 DOM 掛鉤,而不用管它的布局和層次結(jié)構(gòu)是怎么樣的。form > (fieldsets) > element 這樣的層次結(jié)構(gòu)可以作為文檔的交互式骨架。
CHACHA
Changes Channel — 我們簡(jiǎn)稱為 CHACHA,代表一個(gè)雙向數(shù)據(jù)流,它可以通知 intent 方向和 observe 方向的變化,類似我們常說的雙向綁定。
- 在
intent 方向上,UI 會(huì)通知模型用戶打算進(jìn)行的更改。 - 在
observe 方向上,模型會(huì)通知 UI 對(duì)模型所做的更改以及需要向用戶顯示的更改。
這是個(gè)挺有趣的名字,但它并不是一個(gè)很復(fù)雜或者很新穎的模式。雙向數(shù)據(jù)流在 Web 或其他軟件中都很常見(例如MessagePort)
ChaCha 的界面通??梢詮?App 的規(guī)范中衍生出來,而無需任何 UI 代碼。
例如,一個(gè)應(yīng)用程序允許你添加和刪除聯(lián)系人,并從服務(wù)器加載初始列表(可以刷新),它可以有這樣一個(gè) ChaCha:
interface?Contact?{
??id:?string;
??name:?string;
??email:?string;
}
//?"Observe"?Direction
interface?ContactListModelObserver?{
??onAdd(contact:?Contact);
??onRemove(contact:?Contact);
??onUpdate(contact:?Contact);
}
//?"Intent"?Direction
interface?ContactListModel?{
??add(contact:?Contact);
??remove(contact:?Contact);
??reloadFromServer();??
}
注意,這兩個(gè)接口中的所有函數(shù)都是 void,并且只接收普通對(duì)象。這是故意這樣做的,ChaCha 構(gòu)建起來就像一個(gè)有兩個(gè)端口的通道來發(fā)送消息,這允許它在 EventSource、HTML MessageChannel、Service Worker 或任何其他協(xié)議中工作。
ChaChas 的優(yōu)點(diǎn)是它很方便測(cè)試:你可以發(fā)送動(dòng)作并期待特定的調(diào)用返回給觀察者。
使用HTML模板渲染列表項(xiàng)
HTML template 是存在于 DOM 中但不會(huì)顯示的特殊元素,它們的目的是生成動(dòng)態(tài)元素。
當(dāng)我們使用一個(gè) template 元素時(shí),我們可以避免在渲染或更新列表的時(shí)候頻繁操作DOM,下面是個(gè)例子:
<ul?id="names">
??<template>
???<li><label?class="name"?/>li>
??template>
ul>
<script>
??function?addName(name)?{
????const?list?=?document.querySelector('#names');
????const?item?=?list.querySelector('template').content.cloneNode(true).firstElementChild;
????item.querySelector('label').innerText?=?name;
????list.appendChild(item);
??}
script>
通過使用列表項(xiàng)的 template 元素,我們可以在原始 HTML 中看到這些列表項(xiàng) — 而不是用 JSX 或其他語言 “渲染” 出來的。你的 HTML 文件現(xiàn)在會(huì)包含應(yīng)用程序的所有 HTML — 靜態(tài)部分是渲染的 DOM 的一部分,而動(dòng)態(tài)部分在 template 中表示,在一定時(shí)機(jī)會(huì)被克隆并 append 到文檔中。
TodoMvc
TodoMVC 是一個(gè)用于展示不同框架的 TODO LIST 的應(yīng)用程序規(guī)范。TodoMVC 模板帶有現(xiàn)成的 HTML 和 CSS,可幫助你專注于框架。

Github:https://github.com/tastejs/todomvc
從規(guī)范派生的 CHACHA 開始
我們將基于 TodoMVC 的規(guī)范來構(gòu)建 ChaCha 接口:
interface?Task?{
???title:?string;
???completed:?boolean;
}
interface?TaskModelObserver?{
???onAdd(key:?number,?value:?Task);
???onUpdate(key:?number,?value:?Task);
???onRemove(key:?number);
???onCountChange(count:?{active:?number,?completed:?number});
}
interface?TaskModel?{
???constructor(observer:?TaskModelObserver);
???createTask(task:?Task):?void;
???updateTask(key:?number,?task:?Task):?void;
???deleteTask(key:?number):?void;
???clearCompleted():?void;
???markAll(completed:?boolean):?void;
}
任務(wù)模型中的功能就來自于規(guī)范中描述的用戶可以做什么樣的事情(清除已完成的任務(wù),將所有任務(wù)標(biāo)記為已完成或未完成,獲取未完成和已完成的任務(wù)數(shù)量)。
請(qǐng)注意,它遵循 ChaCha 的原則:
- 有兩個(gè)接口,一個(gè)用于代理,一個(gè)用于觀察。
- 所有參數(shù)類型都是原始類型或普通對(duì)象(很容易轉(zhuǎn)換為 JSON)。
- 所有函數(shù)都返回 void。
我們用 localStorage(https://github.com/noamr/todomvc-app-template/blob/main/js/model.js) 來模擬一下后端。
這個(gè) Model 非常簡(jiǎn)單,與這次我們UI框架的討論沒有太大關(guān)系。當(dāng)需要用到時(shí),它將保存到 localStorage,并在一些變化時(shí)向觀察者觸發(fā)更改的回調(diào)。
精簡(jiǎn)的、面向表單的 HTML
接下來,我們將使用 TodoMVC 模板,并將它修改為基于表單的實(shí)現(xiàn) — 表單的層次結(jié)構(gòu),輸入和輸出元素表示可以用 JavaScript 更改的數(shù)據(jù)。
我怎么知道某些東西是否需要成為一個(gè)表單元素?根據(jù)經(jīng)驗(yàn)來看,如果它綁定到模型中的數(shù)據(jù),那么它應(yīng)該是一個(gè)表單元素。
下面是 HTML 的主要部分:
<section?class="todoapp">
???<header?class="header">
???????<h1>todosh1>
???????<form?name="newTask">
???????????<input?name="title"?type="text"?placeholder="What?needs?to?be?done?"?autofocus>
???????form>
???header>
???<main>
???????<form?id="main">form>
???????<input?type="hidden"?name="filter"?form="main"?/>
???????<input?type="hidden"?name="completedCount"?form="main"?/>
???????<input?type="hidden"?name="totalCount"?form="main"?/>
???????<input?name="toggleAll"?type="checkbox"?form="main"?/>
???????<ul?class="todo-list">
???????????<template>
???????????????<form?class="task">
???????????????????<li>
???????????????????????<input?name="completed"?type="checkbox"?checked>
???????????????????????<input?name="title"?readonly?/>
???????????????????????<input?type="submit"?hidden?name="save"?/>
???????????????????????<button?name="destroy">Xbutton>
???????????????????li>
???????????????form>
???????????template>
???????ul>
???main>
???<footer>
???????<output?form="main"?name="activeCount">0output>
???????<nav>
???????????<a?name="/"?href="#/">Alla>
???????????<a?name="/active"?href="#/active">Activea>
???????????<a?name="/completed"?href="#/completed">Completeda>
???????nav>
???????<input?form="main"?type="button"?name="clearCompleted"?value="Clear?completed"?/>
???footer>
section>
這個(gè) HTML 包括下面的內(nèi)容:
- 我們有一個(gè)
main 表單,其中包含所有全局輸入和按鈕,還有一個(gè)用于創(chuàng)建新任務(wù)的新表單。注意,我們使用 form 屬性將元素與表單關(guān)聯(lián)起來,以避免將元素嵌套在表單中。 template 元素表示一個(gè)列表項(xiàng),它的根元素是另一個(gè)表單,表示與特定任務(wù)相關(guān)的交互式數(shù)據(jù)。當(dāng)添加任務(wù)時(shí),可以通過克隆模板的內(nèi)容來重復(fù)渲染這個(gè)表單。- 隱藏的
Input 表示沒有直接顯示的數(shù)據(jù),它們可能用于樣式和選擇。
這個(gè) DOM 是非常簡(jiǎn)潔的,它的元素中沒有分散的類。它包含了應(yīng)用程序所需的所有元素,以合理的層次結(jié)構(gòu)排列。由于隱藏的 Input 元素,你已經(jīng)可以很好地了解文檔稍后可能發(fā)生的更改。
這個(gè) HTML 不知道它將被設(shè)置什么樣的樣式,也不知道它將綁定到什么數(shù)據(jù)。讓 CSS 和 JavaScript 為 HTML 工作,而不是讓 HTML 為特定的樣式機(jī)制工作。這將使更改設(shè)計(jì)變得更加容易。
簡(jiǎn)單的 JavaScript 控制器
現(xiàn)在我們?cè)?CSS 中擁有了大部分的響應(yīng)式,并且我們?cè)谀P椭袚碛辛肆斜硖幚淼墓δ?,剩下的就是控制器代碼了,在這個(gè)小應(yīng)用程序中,控制器 JavaScript 大約有 40 行。
import?TaskListModel?from?'./model.js';
const?model?=?new?TaskListModel(new?class?{
上面,我們創(chuàng)建了一個(gè)新模型。
onAdd(key,?value)?{
???const?newItem?=?document.querySelector('.todo-list?template').content.cloneNode(true).firstElementChild;
???newItem.name?=?`task-${key}`;
???const?save?=?()?=>?model.updateTask(key,??Object.fromEntries(new?FormData(newItem)));
???newItem.elements.completed.addEventListener('change',?save);
???newItem.addEventListener('submit',?save);
???newItem.elements.title.addEventListener('dblclick',?({target})?=>?target.removeAttribute('readonly'));
???newItem.elements.title.addEventListener('blur',?({target})?=>?target.setAttribute('readonly',?''));
???newItem.elements.destroy.addEventListener('click',?()?=>?model.deleteTask(key));
???this.onUpdate(key,?value,?newItem);
???document.querySelector('.todo-list').appendChild(newItem);
}
當(dāng)一個(gè) item 被添加到 Model 中時(shí),我們會(huì)在 UI 中創(chuàng)建相應(yīng)的 item 項(xiàng)目。
在上面,我們克隆了 item 的內(nèi)容,template 為特定的 item 分配了事件監(jiān)聽器,并將新 item 添加到列表中。
請(qǐng)注意,這個(gè)函數(shù),連同 onUpdate、onRemove 和 onCountChange,都是從 Model 中調(diào)用的回調(diào)函數(shù)。
onUpdate(key,?{title,?completed},?form?=?document.forms[`task-${key}`])?{
???form.elements.completed.checked?=?!!completed;
???form.elements.title.value?=?title;
???form.elements.title.blur();
}
當(dāng)一個(gè)項(xiàng)目被更新時(shí),我們?cè)O(shè)置它的 completed 和 title 值,然后 blur(退出編輯模式)。
onRemove(key)?{?document.forms[`task-${key}`].remove();?}
當(dāng)從 Model 中刪除一個(gè) item,我們會(huì)從視圖中刪除其對(duì)應(yīng)的列表項(xiàng)。
onCountChange({active,?completed})?{
???document.forms.main.elements.completedCount.value?=?completed;
???document.forms.main.elements.toggleAll.checked?=?active?===?0;
???document.forms.main.elements.totalCount.value?=?active?+?completed;
???document.forms.main.elements.activeCount.innerHTML?=?`${active}?item${active?===?1???''?:?'s'}?left`;
}
在上面的代碼中,當(dāng)完成或未完成事項(xiàng)的數(shù)量發(fā)生變化時(shí),我們?cè)O(shè)置適當(dāng)?shù)妮斎雭碛|發(fā) CSS 的響應(yīng),并格式化顯示計(jì)數(shù)的輸出。
const?updateFilter?=?()?=>?filter.value?=?location.hash.substr(2);
window.addEventListener('hashchange',?updateFilter);
window.addEventListener('load',?updateFilter);
然后我們從 hash fragment (以及在啟動(dòng)時(shí))更新過濾器。上面我們所做的一切只是設(shè)置一個(gè)表單元素的值 — 其余的由 CSS 處理。
document.querySelector('.todoapp').addEventListener('submit',?e?=>?e.preventDefault(),?{capture:?true});
這里,我們確保表單提交時(shí)不會(huì)重新加載頁面。就是這幾行代碼把這個(gè)應(yīng)用變成了 SPA 應(yīng)用。
document.forms.newTask.addEventListener('submit',?({target:?{elements:?{title}}})?=>???
????model.createTask({title:?title.value}));
document.forms.main.elements.toggleAll.addEventListener('change',?({target:?{checked}})=>
????model.markAll(checked));
document.forms.main.elements.clearCompleted.addEventListener('click',?()?=>
????model.clearCompleted());
這里處理主要操作(創(chuàng)建、標(biāo)記、清除)。
CSS 的響應(yīng)式
CSS 處理了規(guī)范中的很多要求,我們看幾個(gè)例子:
根據(jù)規(guī)范,“X”(destroy) 按鈕只會(huì)在鼠標(biāo)懸停時(shí)顯示。我還添加了一個(gè)可訪問性位,讓它在任務(wù)集中時(shí)可見:
.task:not(:hover,?:focus-within)?button[name="destroy"]?{?opacity:?0?}
當(dāng) filter 是當(dāng)前鏈接時(shí),會(huì)出現(xiàn)紅色邊框:
.todoapp?input[name="filter"][value=""]?~?footer?a[href$="#/"],
nav?a:target?{
???border-color:?#CE4646;
}
注意,我們可以使用 link 元素的 href 作為部分屬性選擇器 — 而不需要 JavaScript 檢查當(dāng)前的過濾器,并在適當(dāng)?shù)脑厣显O(shè)置一個(gè)選定的類。
我們還使用 :target 選擇器,這使我們不必?fù)?dān)心是否要添加過濾器。
標(biāo)題輸入的視圖和編輯樣式會(huì)根據(jù)其只讀模式而變化:
.task?input[name="title"]:read-only?{
…
}
.task?input[name="title"]:not(:read-only)?{
…
}
過濾操作(即僅顯示未完成和已完成的任務(wù))是使用選擇器完成的:
input[name="filter"][value="active"]?~?*?.task
??????:is(input[name="completed"]:checked,?input[name="completed"]:checked?~?*),
input[name="filter"][value="completed"]?~?*?.task
?????:is(input[name="completed"]:not(:checked),?input[name="completed"]:not(:checked)?~?*)?{
???display:?none;
}
上面的代碼可能看起來有點(diǎn)冗長(zhǎng),使用 CSS 預(yù)處理器(如 Sass)可能可讀性會(huì)更好。如果功能讓這些樣式代碼變得越來越復(fù)雜,那么使用數(shù)據(jù)模型去實(shí)現(xiàn)會(huì)更好一點(diǎn)。
總結(jié)
我相信框架為了實(shí)現(xiàn)復(fù)雜的任務(wù)提供了非常方便的方法,并且它們具有超越技術(shù)本身的好處,比如讓一組開發(fā)人員遵循特定的風(fēng)格和模式。Web 平臺(tái)提供了許多選擇,采用一個(gè)框架可以讓每個(gè)人至少部分地在其中一些選擇上達(dá)成一致。這是有價(jià)值的。另外,聲明式編程的優(yōu)雅也有值得說明的地方,而組件化的主要特性并不是這篇文章討論的內(nèi)容。
但是請(qǐng)記住,存在替代模式,通常成本更低,并不是說需要的開發(fā)經(jīng)驗(yàn)就越少。讓自己對(duì)這些模式時(shí)刻感到好奇,后續(xù)我們?cè)僮黾夹g(shù)選型時(shí)也會(huì)更加簡(jiǎn)單。
原生實(shí)現(xiàn)的簡(jiǎn)單回顧:
- 保持 DOM 樹穩(wěn)定,它會(huì)讓后續(xù)開發(fā)更簡(jiǎn)單。
- 盡可能依靠
CSS 而不是 JavaScript 來實(shí)現(xiàn)響應(yīng)式。 - 使用表單元素作為表示交互式數(shù)據(jù)的主要方式。
- 使用
HTML template 元素而不是 JavaScript 生成的模板。 - 使用雙向數(shù)據(jù)流作為模型的接口。
本文譯自:https://www.smashingmagazine.com/2022/02/web-frameworks-guide-part2/
本文中的完整示例代碼:https://github.com/noamr/todomvc-app-template/
怎么樣,這個(gè)的原生實(shí)現(xiàn)的 TodoList 你覺的怎么樣?有解決框架給我們解決的問題嗎?在實(shí)際開發(fā)里面,你會(huì)怎么選呢?
“如果你看完文章之后有任何想法,歡迎在留言區(qū)交流,如果你覺得文章幫助到了你,歡迎關(guān)注加三連(點(diǎn)贊、在看、分享),你對(duì)筆者的每次支持,都是筆者前進(jìn)的動(dòng)力。
”
“如果你想加入前端交流群,或者想與筆者進(jìn)行其他交流,可以加我個(gè)人微信:1076629390
”
