<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          詳細(xì)的對(duì)比現(xiàn)代前端框架到底解決了什么問題?

          共 14299字,需瀏覽 29分鐘

           ·

          2022-03-07 10:12

          大家好,我是 桃翁,相信各位在 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)性的 KnockoutAngularJS 框架提供了一個(gè) JavaScript 聲明層來處理庫內(nèi)部的復(fù)雜性。

          數(shù)據(jù)綁定

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

          所有流行的 UI 框架都提供了某種形式的數(shù)據(jù)綁定,它們的教程基本上都從一個(gè)數(shù)據(jù)綁定示例開始。

          下面是 JSX 中的數(shù)據(jù)綁定(SolidJSReact):

          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 中,使用 forindex 內(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ù),工作方式類似于 Reactkey

          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.jsWebpack 之類的打包器、處理 Babel-TypeScript 啟動(dòng)包中最近的一些配置更改等等,就不可能啟動(dòng)前端項(xiàng)目。

          框架的表現(xiàn)力越強(qiáng),包體積越小,同時(shí)構(gòu)建工具和編譯時(shí)間的負(fù)擔(dān)就越大。

          Svelte 聲稱虛擬 DOM 是純粹的開銷。我同意,但 “編譯”(如 SvelteSolidJS)和自定義客戶端模板引擎(如 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.jsVue.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ī)制:HTMLCSS。它們已經(jīng)非常成熟、而且已經(jīng)經(jīng)過了非常廣泛的測(cè)試。

          但是,它們沒有提供明確的數(shù)據(jù)綁定、條件渲染和列表渲染這樣的概念,并且也沒有跨平臺(tái)響應(yīng)式這樣微妙的功能。

          下面我將嘗試整理一些關(guān)于如何在不借助框架的情況下,使用原生的 Web API 解決這些問題的指南。

          使用 DOM 樹的響應(yīng)式

          我們回到前面提到的錯(cuò)誤標(biāo)簽的示例。在 ReactJSSolidJS 中,我們創(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、textareafieldset,它們?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ì)使用某種類似 JSONAPI 來更新我們的服務(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 InputJavaScript 函數(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)成的 HTMLCSS,可幫助你專注于框架。

          1893caa6f924ab816f08484399b1d4f4.webp

          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ù)。讓 CSSJavaScriptHTML 工作,而不是讓 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è)置它的 completedtitle 值,然后 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

          瀏覽 110
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  韩国毛片一级 | 手机免费在线观看AV网站 | 青青草99 | 77777色婷婷 | 国女团被爆操视频 |