close

作者:Noam Rosenthal

地址:https://www.smashingmagazine.com/2022/01/web-frameworks-guide-part1/

第一章概要

在本文中,Noam Rosenthal深入研究了一些跨框架通用的技術特性,並解釋了一些不同的框架如何實現它們以及它們的成本。

作者簡介

Noam Rosenthal是一名WEB平台顧問,WebKit & Chromium貢獻者、技術文章寫作者,也是一名經驗豐富的WEB開發者。他的工作重點是讓WEB開發和瀏覽器/標準開發二者之間聯繫的更加緊密。

背景

我最近對比較框架和普通的JavaScript非常感興趣。它開始於我在一些項目中使用React時遇到的一些挫折,以及我最近作為一個規範編輯對web標準有了更深入的了解。

我感興趣的是這些框架之間有什麼共同點和不同點,web平台作為一個精簡的替代方案應該提供什麼,以及它是否足夠。我的目標不是抨擊框架,而是了解其成本和收益,確定是否存在替代方案,並看看即使我們決定使用框架,我們是否可以從中學習。

在第一部分中,我將深入研究一些跨框架通用的技術特性,以及不同框架如何實現這些特性。我還將討論使用這些框架的成本。

框架

我選擇了4個框架來研究:React,它是當今的主流框架,還有3個新的競爭者聲稱他們的工作方式與React不同。

React "React讓我們可以輕鬆地創建交互ui。聲明式視圖使代碼更可預測,更容易調試。」

SolidJS 「Solid遵循着與React相同的理念……但是它有一個完全不同的實現,放棄了使用虛擬DOM。」

Svelte 「Svelte是一種全新的構建用戶界面的方法……當你構建應用時,它是一個編譯步驟。Svelte不是使用虛擬DOM差異等技術,而是編寫代碼,在應用狀態發生變化時,外科手術般地更新DOM。」

Lit 「在Web Components標準的基礎上,Lit添加了……響應式、聲明性模板和一些深思熟慮的特性。」

總結一下這些框架的不同點:

React通過聲明式視圖使ui的構建更加容易。
SolidJS遵循React的理念,但使用了一種不同的技術。
Svelte對ui使用編譯方法。
Lit使用現有的標準,並添加了一些輕量級特性。
框架解決了什麼問題

框架本身提到了聲明性、響應式和虛擬DOM。讓我們來探究一下這意味着什麼。

聲明式編程

聲明式編程是一種定義邏輯而不指定控制流的範例。我們描述的是結果需要是什麼,而不是我們需要採取什麼步驟才能達到目標。

在聲明式框架的早期,大約在2010年,DOM api要簡單和冗長得多,用命命式JavaScript編寫web應用程序需要大量的樣板代碼。這時,「模型-視圖-視圖模型」(MVVM)[1]的概念開始流行起來,當時具有開創性的Knockout和AngularJS框架提供了一個JavaScript聲明層來處理庫中的複雜性。

MVVM現在不是一個廣泛使用的術語,它在某種程度上是舊術語「數據綁定」的變體。

數據綁定

數據綁定是一種聲明性的方式,用來表示數據如何在模型和用戶界面之間同步。

所有流行的UI框架都提供了某種形式的數據綁定,它們的教程都從一個數據綁定示例開始。

下面是JSX中的數據綁定(SolidJS和React):

functionHelloWorld(){constname="SolidorReact";return(<div>Hello{name}!</div>)}

Lit中的數據綁定:

classHelloWorldextendsLitElement{@property()name='lit';render(){returnhtml`<p>Hello${this.name}!</p>`;}}

Svelte中的數據綁定:

<script>letname='world';</script><h1>Hello{name}!</h1>響應式

響應式是一種表達變更傳播的聲明性方式。

當我們有了一種聲明式表達數據綁定的方法時,我們需要一種有效的方法讓框架傳播更改。

React引擎會將渲染結果與之前的結果進行比較,並將差異應用到DOM本身。這種處理變更傳播的方法稱為虛擬DOM[2]。

在SolidJS中,這是通過其存儲和內置元素更顯式地完成的。例如,Show元素將跟蹤內部發生的變化,而不是虛擬DOM。

在Svelte中,會生成「響應式」代碼。Svelte知道哪些事件會導致更改,並生成簡單的代碼,在事件和DOM更改之間劃線。

在Lit中,響應式是使用元素屬性完成的,本質上依賴於HTML自定義元素的內置響應式。

邏輯

當框架為數據綁定提供聲明性接口,並實現響應式時,它還需要提供某種方式來表達一些傳統上以命定方式編寫的邏輯。邏輯的基本構建塊是「if」和「for」,所有主要的框架都提供了這些構建塊的一些表達式。

條件語句/流控制

除了綁定數字和字符串等基本數據外,每個框架都提供一個「條件」原語。在React中,它是這樣的:

const[hasError,setHasError]=useState(false);returnhasError?<label>Message</label>:null;…setHasError(true);

SolidJS提供了一個內置的條件組件Show[3]:

<Showwhen={state.error}><label>Message</label></Show>

Svelte提供了#if指令:

{#ifstate.error}<label>Message</label>{/if}

在Lit中,你可以在渲染函數中使用一個顯式的三目運算操作:

render(){returnthis.error?html`<label>Message</label>`:null;}Lists

另一個常見的框架原語是列表處理。列表是ui的關鍵部分---聯繫人列表、通知列表等等——為了有效地工作,它們需要是響應式的,而不是在一個數據項發生變化時更新整個列表。

在React中,列表處理是這樣的:

contacts.map((contact,index)=><likey={index}>{contact.name}</li>)

React使用特殊的key屬性來區分列表項,並確保整個列表不會在每次渲染時被替換。

在SolidJS中,for和index是作為內置元素被使用的:

<Foreach={state.contacts}>{contact=><DIV>{contact.name}</DIV>}</For>

在內部,SolidJS使用自己的存儲庫和for和索引來決定在項目更改時更新哪些元素。它比React更顯式,允許我們避免虛擬DOM的複雜性。

Svelte使用了each指令,根據它的更新器進行編譯:

{#eachcontactsascontact}<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約為16 KB。
Svelte大約是2 KB,但是生成的代碼大小各不相同。今天的框架似乎比React做得更好,能夠保持包的體積較小。虛擬DOM需要大量的JavaScript。
構建

不知怎麼的,我們習慣了「構建」我們的網絡應用。要啟動一個前端項目,必須先建立Node.js和Webpack這樣的打包工具,處理Babel-TypeScript的一些配置等等。

框架的包大小越小,表達能力越強,構建工具和翻譯時間的負擔就越大。

Svelte聲稱虛擬DOM是純粹的開銷[4]。這一點我同意,但也許「構建」(如使用Svelte和SolidJS)和定製客戶端模板引擎(如使用Lit)也是純粹的開銷,是一種不同的表現形式?

調試

構建和編譯帶來了一定的開銷和成本。

當我們使用或調試web應用程序時,我們看到的代碼與我們寫的完全不同。我們現在依賴於不同質量的特殊調試工具來逆向工程網站上發生的事情,並將其與我們自己代碼中的錯誤聯繫起來。

在React中,調用棧從來不是「你的」——React為你處理調度。在沒有bug的情況下,這種方法非常有效。但是嘗試着去識別無限循環重新呈現的原因,你將會經歷一個痛苦的世界。

在Svelte中,庫本身的包大小很小,但你需要發布和調試一大堆神秘的生成代碼,這是Svelte的響應式實現,根據應用的需要定製。

使用Lit,它與構建無關,但要有效地調試它,您必須理解它的模板引擎。這可能是我對框架持懷疑態度的最大原因。

當您尋找自定義聲明式解決方案時,您最終會遇到更痛苦的命令式調試。本文檔中的示例使用Typescript作為API規範,但代碼本身不需要編譯。

升級

在本文檔中,我介紹了4個框架,但還有很多框架(AngularJS、Ember.js和Vue.js等[5])。在它的發展過程中,你能指望這個框架、它的開發者、它的人氣和它的生態系統為你服務嗎?

有一件事比修復自己的漏洞更令人沮喪,那就是必須為框架漏洞找到變通方法。還有一件事比框架bug更令人沮喪,那就是當你沒有修改代碼就將框架升級到一個新版本時出現的bug。

確實,這個問題也存在於瀏覽器中,但是當它發生時,它會發生在每個人身上,並且在大多數情況下,修復或發布的解決方案是迫在眉睫的。此外,本文檔中的大多數模式都是基於成熟的web平台api;沒有必要總是去流血的邊緣。

小結

我們深入了解了框架試圖解決的核心問題,以及它們如何解決這些問題,重點關注數據綁定、響應式、條件和列表。我們也看了成本。

在後面的部分,我們將了解如何在根本不使用框架的情況下解決這些問題,以及我們可以從中學到什麼。請繼續關注!

特別感謝以下每個人的勘校:Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal和Louis Lazaris。

第二章

在第二部分中,Noam提出了一些如何直接使用web平台作為框架提供的一些解決方案的替代方案的模式。

在前面的第一章節中,我們從框架試圖解決的核心問題的角度出發,研究了使用框架的不同好處和成本,重點關注聲明式編程、數據綁定、響應式、列表和條件。現在,我們將看到是否可以從網絡平台本身出現一個替代方案。

推出自己的框架?

在沒有框架的情況下進行探索,似乎不可避免的結果是使用自己的框架來進行響應式數據綁定。在之前嘗試過這種方法,並看到它的代價有多大後,我決定在這次探索中遵循一條指導原則;我並不是要推出我自己的框架,而是想看看我能否以一種讓框架變得不那麼必要的方式直接使用web平台。如果您考慮使用自己的框架,請注意有一組成本沒有在本文中討論。

普通的選擇

web平台已經提供了一種開箱即用的聲明式編程機制:HTML和CSS。這種機制是成熟的、經過良好測試的、流行的、廣泛使用的和有文檔記載的。但是,它沒有提供明確的內置概念,如數據綁定、條件呈現和列表同步,而響應式是跨多個平台特性的一個微妙細節。

當我瀏覽流行框架的文檔時,我可以直接找到第1部分中描述的特性。當我閱讀web平台文檔時(例如,在MDN[6]上),我發現了許多令人困惑的如何做事的模式,沒有數據綁定、列表同步或響應式的結論性表示。我將嘗試繪製一些在web平台上解決這些問題的指導方針,而不需要框架(換句話說,通過普通的方式)。

穩定的DOM樹和級聯

讓我們回到錯誤標籤的例子。在ReactJS和SolidJS中,我們創建的聲明式代碼轉換為命令式代碼,將標籤添加到DOM或刪除它。在Svelte中,生成該代碼。

但是如果我們根本沒有這些代碼,而是使用CSS來隱藏和顯示錯誤標籤呢?

<style>label.error{display:none;}.app.has-errorlabel.error{display:block;}</style><labelclass="error">Message</label><script>app.classList.toggle('has-error',true);</script>

在這種情況下,響應式在瀏覽器中處理——應用程序對類的更改傳播到它的後代,直到瀏覽器中的內部機制決定是否呈現標籤。

這種技術有幾個優點:

bundle大小為0。
沒有任何構建步驟。
在本地瀏覽器代碼中,更改傳播經過了優化和測試,並避免了不必要的昂貴DOM操作,如追加和刪除。
選擇器是穩定的。在本例中,您可以依賴label元素的存在。你可以在不依賴「轉換組」等複雜構造的情況下對其應用動畫。您可以在JavaScript中保存對它的引用。
如果標籤顯示或隱藏,您可以在開發人員工具的樣式面板中看到原因,它向您顯示整個級聯,最終在標籤中的規則鏈是可見的(或隱藏的)。即使您閱讀了這篇文章並選擇繼續使用框架,使用CSS保持DOM穩定和狀態變化的想法也是非常強大的。考慮一下這對你可能有用的地方。
面向表單的「數據綁定」

在使用大量javascript的單頁應用程序(spa)時代之前,表單是創建包含用戶輸入的web應用程序的主要方式。傳統上,用戶將填寫表單並單擊「Submit」按鈕,然後服務器端代碼將處理響應。表單是數據綁定和交互性的多頁應用程序版本。毫無疑問,具有輸入和輸出基本名稱的HTML元素是表單元素。

由於表單api的廣泛使用和悠久的歷史,它積累了一些隱藏的優點,使得它們可以用於那些傳統上認為由表單解決不了的問題。

作為穩定選擇器的表單和表單元素

表單可以通過名稱訪問(使用document.forms "document.forms"),每個表單元素都可以通過名稱訪問(使用form.elements)。此外,可以訪問與元素相關聯的表單(使用form attributes[7])。這不僅包括input元素,還包括其他表單元素,如output、textarea和fieldset,這允許嵌套訪問樹中的元素。

在上一節的錯誤標籤示例中,我們展示了如何響應式地顯示和隱藏錯誤消息。這是我們在React中更新錯誤消息文本的方法(在SolidJS中也是如此):

const[errorMessage,setErrorMessage]=useState(null);return<labelclassName="error">{errorMessage}</label>

當我們有一個穩定的DOM和穩定的樹形式和表單元素時,我們可以做以下事情:

<formname="contactForm"><fieldsetname="email"><outputname="error"></output></fieldset></form><script>functionsetErrorMessage(message){document.forms.contactForm.elements.email.elements.error.value=message;}</script>

它的原始形式看起來相當冗長,但它也非常穩定、直接和高性能。

input表單

通常,當我們構建SPA時,我們會使用一些類似json的API來更新我們的服務器或我們使用的任何模型。

這是一個很熟悉的例子(為了便於閱讀,是用Typescript寫的):

interfaceContact{id:string;name:string;email:string;subscriber:boolean;}functionupdateContact(contact:Contact){…}

在框架代碼中,通過選擇輸入元素並一塊一塊地構造對象來生成這個Contact對象是很常見的。正確使用表單,有一個簡潔的替代方案:

<formname="contactForm"><inputname="id"type="hidden"value="136"/><inputname="email"type="email"/><inputname="name"type="string"/><inputname="subscriber"type="checkbox"/></form><script>updateContact(Object.fromEntries(newFormData(document.forms.contactForm));</script>

通過使用隱藏的輸入和有用的FormData類,我們可以在DOM輸入和JavaScript函數之間無縫地轉換值。

組合表單和響應式

通過結合表單的高性能選擇器穩定性和CSS響應式,我們可以實現更複雜的UI邏輯:

<formname="contactForm"><inputname="showErrors"type="checkbox"hidden/><fieldsetname="names"><inputname="name"/><outputname="error"></output></fieldset><fieldsetname="emails"><inputname="email"/><outputname="error"></output></fieldset></form><script>functionsetErrorMessage(section,message){document.forms.contactForm.elements[section].elements.error.value=message;}functionsetShowErrors(show){document.forms.contactForm.elements.showErrors.checked=show;}</script><style>input[name="showErrors"]:not(:checked)~*output[name="error"]{display:none;}</style>

注意,在這個例子中沒有使用類——我們從表單的數據中開發DOM的行為和樣式,而不是手工更改元素類。

我不喜歡過度使用CSS類作為JavaScript選擇器。我認為它們應該用於將類似樣式的元素組合在一起,而不是作為一種改變組件樣式的萬能機制。

表單的優點
與級聯一樣,表單是構建在web平台上的,而且它們的大部分特性都是穩定的。這意味着更少的JavaScript,更少的框架版本不匹配,沒有「構建」。
默認情況下,表單是可訪問的。如果您的應用程序正確地使用表單,那麼對ARIA屬性、「可訪問性插件」和最後審計的需求就會小得多。表單本身可以用於鍵盤導航、屏幕閱讀器和其他輔助技術。
表單具有內置的輸入驗證功能:通過正則表達式驗證,在CSS中對無效和有效表單的反應,處理必需的和可選的表單,等等。您不需要一些看起來像表單的東西來享受這些特性。
表單的提交事件非常有用。例如,它允許在沒有提交按鈕的情況下捕獲「Enter」鍵,並允許通過提交者屬性區分多個提交按鈕(我們將在稍後的TODO示例中看到)。
默認情況下,元素與它們所包含的表單相關聯,但可以使用form屬性與文檔中的任何其他表單相關聯。這允許我們在不依賴DOM樹的情況下處理表單關聯。
使用穩定的選擇器有助於UI測試自動化:我們可以使用嵌套的API作為一種穩定的方式來鈎子到DOM,而不管它的布局和層次結構。form>fieldset>element層次結構可以作為文檔的交互框架。
Chacha和HTML模板

框架提供了自己的表達可觀察列表的方式。如今,許多開發人員也依賴於提供這類特性的非框架庫,比如MobX。

通用目的可觀察列表的主要問題是它們是通用的。這在降低性能的同時增加了便利性,而且還需要特殊的開發工具來調試這些庫在後台執行的複雜操作。

使用這些庫並理解它們的作用是可以的,而且不管UI框架的選擇如何,它們都是有用的,但是使用替代方法可能不會更複雜,而且它可能會防止在嘗試運行自己的模型時發生的一些陷阱。

變化通道(或CHACHA)

ChaCha—也被稱為變更通道—是一個雙向流,其目的是通知意圖方向和觀察方向的變更。

在意圖方向中,UI通知模型用戶想要進行的更改。
在觀察方向上,模型通知UI對模型所做的更改,以及需要向用戶顯示的更改。這也許是個有趣的名字,但它並不是一個複雜或新穎的模式。雙向流在網絡和軟件中隨處可見(例如MessagePort)。在這種情況下,我們創建了一個雙向流,它有一個特殊的目的:向UI報告實際的模型更改和向模型報告意圖。

ChaCha的接口通常可以從應用的規範中派生出來,而不需要任何UI代碼。

例如,一個應用程序允許你添加和刪除聯繫人,並從服務器加載初始列表(帶有刷新選項),它可以有這樣一個ChaCha:

interfaceContact{id:string;name:string;email:string;}//"Observe"DirectioninterfaceContactListModelObserver{onAdd(contact:Contact);onRemove(contact:Contact);onUpdate(contact:Contact);}//"Intent"DirectioninterfaceContactListModel{add(contact:Contact);remove(contact:Contact);reloadFromServer();}

注意,這兩個接口中的所有函數都是void,並且只接收普通對象。這是故意的。ChaCha構建起來就像一個有兩個端口的通道來發送消息,這允許它在EventSource、HTML MessageChannel、service worker或任何其他協議中工作。

ChaChas的優點是易於測試:您發送動作並期待特定的調用返回給觀察者。

列表項的HTML模板元素

HTML模板是存在於DOM中但不被顯示的特殊元素。它們的目的是生成動態元素。

當我們使用模板元素時,我們可以避免所有創建元素並在JavaScript中填充它們的樣板代碼。

下面將使用模板將一個名字添加到列表中:

<ulid="names"><template><li><labelclass="name"/></li></template></ul><script>functionaddName(name){constlist=document.querySelector('#names');constitem=list.querySelector('template').content.cloneNode(true).firstElementChild;item.querySelector('label').innerText=name;list.appendChild(item);}</script>

通過使用列表項的模板元素,我們可以在原始HTML中看到列表項——它不是用JSX或其他語言「呈現」的。你的HTML文件現在包含了應用程序的所有HTML -靜態部分是渲染DOM的一部分,動態部分在模板中表示,準備在時機成熟時被克隆和追加到文檔中。

把它放在一起:TodoMVC

TodoMVC[8]是一個TODO列表的應用規範,用於展示不同的框架。TodoMVC模板附帶了現成的HTML和CSS,以幫助您專注於框架。

你可以在GitHub庫中使用結果[9],完整的源代碼[10]是可用的。

從規範派生的chacha

我們將從規範[11]開始,並使用它來構建ChaCha接口:

interfaceTask{title:string;completed:boolean;}interfaceTaskModelObserver{onAdd(key:number,value:Task);onUpdate(key:number,value:Task);onRemove(key:number);onCountChange(count:{active:number,completed:number});}interfaceTaskModel{constructor(observer:TaskModelObserver);createTask(task:Task):void;updateTask(key:number,task:Task):void;deleteTask(key:number):void;clearCompleted():void;markAll(completed:boolean):void;}

任務模型中的功能直接從規範和用戶可以做的事情中派生出來(清除已完成的任務,將所有任務標記為已完成或活動,獲得活動和已完成的計數)。

請注意,它遵循ChaCha的指導原則:

有兩個界面,一個是動作界面,一個是觀察界面。
所有參數類型都是原語或普通對象(很容易轉換為JSON)。
所有的函數都返回void。
TodoMVC的實現使用localStorage作為後端。該模型非常簡單,與UI框架的討論沒有太大關係。當需要時,它將保存到localStorage,並在一些變化時(無論是由於用戶操作的結果,還是當模型第一次從localStorage加載時)向觀察者發出更改回調。
精益,面向表單的HTML

接下來,我將使用TodoMVC模板,並將其修改為面向表單的—表單的層次結構,輸入和輸出元素表示可以用JavaScript更改的數據。

我如何知道是否需要一個表單元素?根據經驗,如果它綁定到模型中的數據,那麼它應該是一個表單元素。

完整的HTML代碼[12]是可用的,但這裡是它的主要部分:

<sectionclass="todoapp"><headerclass="header"><h1>todos</h1><formname="newTask"><inputname="title"type="text"placeholder="Whatneedstobedone?"autofocus></form></header><main><formid="main"></form><inputtype="hidden"name="filter"form="main"/><inputtype="hidden"name="completedCount"form="main"/><inputtype="hidden"name="totalCount"form="main"/><inputname="toggleAll"type="checkbox"form="main"/><ulclass="todo-list"><template><formclass="task"><li><inputname="completed"type="checkbox"checked><inputname="title"readonly/><inputtype="submit"hiddenname="save"/><buttonname="destroy">X</button></li></form></template></ul></main><footer><outputform="main"name="activeCount">0</output><nav><aname="/"href="#/">All</a><aname="/active"href="#/active">Active</a><aname="/completed"href="#/completed">Completed</a></nav><inputform="main"type="button"name="clearCompleted"value="Clearcompleted"/></footer></section>

本HTML包括以下內容:

我們有一個主表單,其中包含所有全局輸入和按鈕,還有一個用於創建新任務的新表單。注意,我們使用form屬性[13]將元素與表單關聯起來,以避免元素在表單中嵌套。
模板元素表示一個列表項,它的根元素是另一個表單,表示與特定任務相關的交互式數據。當添加任務時,可以通過克隆模板的內容來重複這個表單。
隱藏輸入表示沒有直接顯示的數據,但用於樣式化和選擇。注意這個DOM是如何簡潔的。它的元素中沒有分散的類。它包含了應用程序所需的所有元素,以合理的層次結構排列。由於隱藏的輸入元素,您已經可以很好地了解文檔稍後可能發生的更改。

這個HTML不知道它將如何被樣式化,也不知道它將綁定到什麼數據。讓CSS和JavaScript為HTML工作,而不是讓HTML為特定的樣式機制工作。這將使更改設計變得更加容易。

最小化的Controller---javascript

現在我們在CSS中有了大部分的反應性,並且在模型中有了列表處理,剩下的就是Controller代碼——將所有東西連接在一起的管道膠帶。在這個小應用程序中,Controller JavaScript[14]大約有40行代碼。

下面是一個版本,並對每個部分進行了解釋:

importTaskListModelfrom'./model.js';constmodel=newTaskListModel(newclass{

在上面的代碼中,我們創建了一個新模型。

onAdd(key,value){constnewItem=document.querySelector('.todo-listtemplate').content.cloneNode(true).firstElementChild;newItem.name=`task-${key}`;constsave=()=>model.updateTask(key,Object.fromEntries(newFormData(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);}

當一個項目被添加到模型中時,我們會在UI中創建相應的列表項目。

在上面,我們克隆了條目模板的內容,為特定的條目分配了事件監聽器,並將新條目添加到列表中。

請注意,這個函數,連同onUpdate、onRemove和onCountChange,都是從模型[15]中調用的回調函數。

onUpdate(key,{title,completed},form=document.forms[`task-${key}`]){form.elements.completed.checked=!!completed;form.elements.title.value=title;form.elements.title.blur();}

當一個項目被更新時,我們設置它的complete和title值,然後失去焦點(退出編輯模式)。

onRemove(key){document.forms[`task-${key}`].remove();}

當一個項目從模型中移除時,我們從視圖中移除它對應的列表項目。

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=`<strong>${active}</strong>item${active===1?'':'s'}left`;}

在上面的代碼中,當完成或活動項目的數量發生變化時,我們設置適當的輸入來觸發CSS反應,並格式化顯示計數的輸出。

constupdateFilter=()=>filter.value=location.hash.substr(2);window.addEventListener('hashchange',updateFilter);window.addEventListener('load',updateFilter);

然後我們從哈希片段(以及在啟動時)更新過濾器。上面我們所做的一切只是設置一個表單元素的值——CSS處理其餘的事情。

document.querySelector('.todoapp').addEventListener('submit',e=>e.preventDefault(),{capture:true});

這裡,我們確保表單提交時不會重新加載頁面。就是這條線把這個應用變成了SPA中心。

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());

這將處理主要操作(創建、標記全部、清除完成)。

使用CSS進行響應式

您可以查看完整的CSS代碼[16]。

CSS處理規範中的很多要求(為了便於訪問,還做了一些修改)。讓我們看一些例子。

根據規範,「X」(摧毀)按鈕只在懸停時顯示。我還添加了一個可訪問性位,使其在任務集中時可見:

.task:not(:hover,:focus-within)button[name="destroy"]{opacity:0}

當過濾器鏈接是當前鏈接時,它會得到一個紅色的邊框:

.todoappinput[name="filter"][value=""]~footera[href$="#/"],nava:target{border-color:#CE4646;}

注意,我們可以使用link元素的href作為部分屬性選擇器——不需要JavaScript檢查當前的過濾器,並在適當的元素上設置一個選定的類。

我們還使用:target選擇器,這使我們不必擔心是否要添加過濾器。

標題輸入的視圖和編輯樣式會根據其只讀模式而改變:

.taskinput[name="title"]:read-only{…}.taskinput[name="title"]:not(:read-only){…}

篩選(即只顯示活動的和已完成的任務)是通過選擇器來完成的:

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;}

上面的代碼可能看起來有點冗長,使用CSS預處理器(如Sass)可能更容易閱讀。但是它所做的事情很簡單:如果過濾器是活動的,完成的複選框被選中,或者反之亦然,那麼我們隱藏複選框和它的兄弟元素。

我選擇在CSS中實現這個簡單的過濾器,以顯示它能走多遠,但如果它開始變得複雜,那麼它將完全有意義的移動到模型中。

結論和要點

我相信框架為實現複雜的任務提供了方便的方法,而且除了技術方面的好處,比如讓一組開發人員遵循特定的風格和模式。web平台提供了許多選擇,採用框架可以讓每個人至少部分地在某些選擇上站在同一頁上。這是有價值的。另外,聲明式編程的優雅也有值得說明的地方,而組件化的主要特性並不是本文討論的內容。

但是請記住,存在替代模式,通常成本更低,並不總是需要更少的開發人員經驗。允許自己對這些模式感到好奇,即使您決定在使用框架時從中挑選。

模式回顧
保持DOM樹穩定。它開始了讓事情變得簡單的連鎖反應。
在可能的情況下,依靠CSS而不是JavaScript來實現響應式。
使用表單元素作為表示交互式數據的主要方式。
使用HTML模板元素而不是javascript生成的模板。
使用雙向的數據流作為模型的接口。

再次感謝各位同儕的文章勘校工作:Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal, Louis Lazaris。

參考資料
[1]

'模型-視圖-視圖模型'(MVVM): https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel

[2]

虛擬DOM: https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom

[3]

內置的條件組件Show: https://www.solidjs.com/docs/latest/api#%3Cshow%3E

[4]

虛擬DOM是純粹的開銷: https://svelte.dev/blog/virtual-dom-is-pure-overhead

[5]

基於javascript的WEB框架對比: https://en.wikipedia.org/wiki/Comparison_of_JavaScript-based_web_frameworks

[6]

MDN: https://developer.mozilla.org/zh-CN/

[7]

form attributes: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement

[8]

TodoMVC: https://todomvc.com/

[9]

the result: https://noamr.github.io/todomvc-app-template/index.html

[10]

full source code: https://github.com/noamr/todomvc-app-template

[11]

app-spec: https://github.com/tastejs/todomvc/blob/master/app-spec.md

[12]

todomvc-app-template: https://github.com/noamr/todomvc-app-template/blob/main/index.html

[13]

form attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-form

[14]

controller js: https://github.com/noamr/todomvc-app-template/blob/main/js/app.js

[15]

model.js: https://github.com/noamr/todomvc-app-template/blob/main/js/model.js

[16]

app.css: https://github.com/noamr/todomvc-app-template/blob/main/css/app.css

-END-

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()