新聞中心
推出自己的框架?
普通的選擇
Web 平臺已經(jīng)提供了一個開箱即用的聲明性編程機(jī)制:HTML 和 CSS。這種機(jī)制是成熟的、經(jīng)過良好測試的、流行的、廣泛使用的,并且有文檔記錄。然而,它并沒有提供明確的數(shù)據(jù)綁定、條件渲染和列表同步的內(nèi)置概念,并且反應(yīng)性是一個細(xì)微的細(xì)節(jié),散布于多個平臺的特性之中。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、小程序定制開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了上城免費建站歡迎大家使用!
在瀏覽常見框架的文檔后,我就直接找到了第一部分中提及的特性。我在閱讀諸如 MDN 之類的 Web 平臺的文檔時,會發(fā)現(xiàn)很多工作方式都是雜亂無章的,沒有數(shù)據(jù)綁定,沒有列表同步,也沒有反應(yīng)性的結(jié)論性表述。我會試圖為在 Web 平臺上解決這些問題提供指導(dǎo),而不是用框架(也就是說,走普通路線)。
具有穩(wěn)定的 Dom 樹和級聯(lián)的反應(yīng)性
讓我們回到錯誤標(biāo)簽的示例上。在 ReactJS 和 SolidJS 中,我們會創(chuàng)建聲明性代碼,并將其轉(zhuǎn)化為命令性代碼,向 DOM 中加入標(biāo)簽或者刪除標(biāo)簽。在 Svelte 中,生成這些代碼。
但是,如果我們根本沒有這些代碼,而是用 CSS 來隱藏和顯示錯誤標(biāo)簽?zāi)兀?/p>
在這種情況下,反應(yīng)性是在瀏覽器中處理的:應(yīng)用的類變化會傳播到它的后代,直到瀏覽器的內(nèi)部機(jī)制決定是否渲染標(biāo)簽。
這種技術(shù)有幾個具有以下優(yōu)點:
- 包大小為零。
- 無構(gòu)建步驟。
- 變化傳播經(jīng)過優(yōu)化和良好的測試,在本地瀏覽器代碼中,避免了不必要的昂貴的 DOM 操作,如追加和刪除。
- 選擇器是穩(wěn)定的。在這種情況下,你可以指望標(biāo)簽元素的存在。你可以對它應(yīng)用動畫,而不必依賴復(fù)雜的結(jié)構(gòu),如“過渡組”。你可以在 JavaScript 中保持對它的引用。
- 如果標(biāo)簽被顯示或隱藏,你可以在開發(fā)工具的樣式面板中看到原因,它顯示了整個級聯(lián),即最終導(dǎo)致標(biāo)簽可見(或隱藏)的規(guī)則鏈。
即便你閱讀了本文,并且選擇繼續(xù)使用框架工作,但是要讓 DOM 保持穩(wěn)定,使用 CSS 的方式發(fā)生改變,這個想法還是很強大的。想想看,這對你來說有什么用處。
面向表單的“數(shù)據(jù)綁定”
在大量使用 JavaScript 的單頁應(yīng)用(single-page application,SPA)時代到來之前,表單是創(chuàng)建包含用戶輸入的 Web 應(yīng)用的主要方式。傳統(tǒng)上,用戶填寫表格并點擊“提交”按鈕,服務(wù)器端的代碼就會處理響應(yīng)。表單是數(shù)據(jù)綁定和互動性的多頁面應(yīng)用版本。難怪具有 input 和 output 基本名稱的 HTML 元素是表單元素。
表單 API 應(yīng)用范圍廣,歷史悠久,因此它具有一些潛在優(yōu)勢,可以幫助人們解決在傳統(tǒng)上認(rèn)為不能通過表單來處理的問題。
表單和表單元素作為穩(wěn)定的選擇器
表單可以通過名稱訪問(使用 document.forms),每個表單元素可以通過其名稱訪問(使用 form.elements)。此外,與一個元素相關(guān)的表單也可以被訪問(使用 form 屬性)。這不僅包括 input 元素,還包括其他表單元素,如 output、textarea 和 fieldset,這允許在一個樹中對元素進(jìn)行嵌套訪問。
在上一節(jié)的錯誤標(biāo)簽示例中,我們展示了如何反應(yīng)性地顯示和隱藏錯誤信息。這就是我們在 React 中更新錯誤信息文本的方法(在 SolidJS 中也類似):
const [errorMessage, setErrorMessage] = useState(null);
return
當(dāng)我們有一個穩(wěn)定的 DOM 和穩(wěn)定的樹形表單和表單元素時,我們可以做以下事情:
這個原始格式看起來相當(dāng)冗長,但是它也非常穩(wěn)定、直接和高效。
用于輸入的表單
通常,當(dāng)我們建立一個 SPA 時,我們有某種類似 JSON 的 API,我們用它來更新我們的服務(wù)器,或我們使用的任何模型。
這將是一個熟悉的示例(為了可讀性,用 Typescript 編寫):
interface Contact {
id: string;
name: string;
email: string;
subscriber: boolean;
}
function updateContact(contact: Contact) { … }在框架代碼中,通過選擇輸入元素并逐段構(gòu)建對象來生成這個聯(lián)系對象是很常見的。通過對表單的正確使用,有一個簡潔的替代方案。
通過使用隱藏的輸入和有用的 FormData 類,我們可以在 DOM 輸入和 JavaScript 函數(shù)之間無縫轉(zhuǎn)換數(shù)值。
結(jié)合表單和反應(yīng)性
通過結(jié)合表單的高性能選擇器穩(wěn)定性和 CSS 反應(yīng)性,我們可以實現(xiàn)更復(fù)雜的 UI 邏輯:
請注意,在這個示例中并沒有使用類:我們從表單的數(shù)據(jù)中開發(fā) DOM 的行為和風(fēng)格,而不是通過手動更改元素的類。
我不喜歡過度使用 CSS 類作為 JavaScript 選擇器。我認(rèn)為它們應(yīng)該被用來將風(fēng)格相似的元素組合在一起,而不是作為改變組件風(fēng)格的一種萬能機(jī)制。
表單的優(yōu)點
- 與級聯(lián)一樣,表單是內(nèi)置于 Web 平臺的,其大部分特性是穩(wěn)定的。這意味著更少的 JavaScript,更少的框架版本不匹配,而且沒有“構(gòu)建”。
- 默認(rèn)情況下,表單是可訪問的。如果你的應(yīng)用程序正確地使用表單,就不需要 ARIA 屬性、“輔助插件”和最后一分鐘的審核。表單適合于鍵盤導(dǎo)航、屏幕閱讀器和其他輔助技術(shù)。
- 表單帶有內(nèi)置的輸入驗證特性:通過 regex 模式進(jìn)行驗證,對 CSS 中無效和有效表單進(jìn)行反應(yīng)性驗證,處理必需表單和可選表單,等等。為了享受這些特性,你不需要看起來像表單的東西。
- 表單的 submit 事件是非常有用的。例如,它允許在沒有提交按鈕的情況下捕獲“Enter”鍵,并允許通過 submitter 屬性來區(qū)分多個提交按鈕(正如我們將在后面的 TODO 示例中看到的)。
- 默認(rèn)情況下,元素與它們所包含的表單相關(guān)聯(lián),但也可以使用 form 屬性與文檔中的任何其他表單相關(guān)聯(lián)。這使我們能夠在不對 DOM 樹產(chǎn)生依賴的情況下進(jìn)行表單關(guān)聯(lián)。
- 使用穩(wěn)定的選擇器有助于實現(xiàn) UI 測試自動化。我們可以使用嵌套的 API 作為一種穩(wěn)定的方式來鉤住 DOM,而不管它的布局和層次結(jié)構(gòu)如何。form > (fieldsets) > element 的層次結(jié)構(gòu)可以作為你的文檔的互動骨架。
CHACHA 和 HTML 模板
框架提供了它們自己表達(dá)可觀察列表的方式。現(xiàn)在很多開發(fā)者也依賴提供這種功能的非框架庫,如 MobX。
通用的可觀察列表的主要問題在于它們是通用的。這以性能為代價增加了便利性,而且還需要特殊的開發(fā)者工具來調(diào)試那些庫在后臺做的復(fù)雜動作。
使用這些庫并理解它們的作用是可以的,無論選擇什么樣的 UI 框架,它們都是有用的,但使用替代方案可能不會更復(fù)雜,而且可以避免一些在你試圖推出自己的模型時產(chǎn)生的陷阱。
變化通道(或 CHACHA)
CHACHA——也被稱為變化通道(Changes Channel)——是一個雙向流,其目的是通知意圖方向和觀察方向的變化。
- 在意圖方向上,UI 將用戶意圖的變化通知給模型。
- 在觀察方向上,模型將對模型所做的改變通知給 UI,而這些改變需要顯示給用戶。
這也許是一個有趣的名字,但它不是一個復(fù)雜或新穎的模式。雙向流在 Web 和軟件中隨處可見(例如,MessagePort)。在這種情況下,我們正在創(chuàng)建一個雙向流,它有一個特殊的目的:向 UI 報告實際的模型變化,并向模型報告意圖。
CHACHA 的接口通常可以從應(yīng)用的規(guī)范中導(dǎo)出,而不需要任何 UI 代碼。
例如,一個允許你添加和刪除聯(lián)系人并從服務(wù)器加載初始列表的應(yīng)用程序(帶有刷新選項)可以有一個 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();
}請注意,這兩個接口中的所有函數(shù)都是無效的,只接收普通對象。這是故意的。CHACHA 被構(gòu)建成一個通道,有兩個端口來發(fā)送消息,這使得它可以在 EventSource、HTML MessageChannel、服務(wù)工作者或任何其他協(xié)議中工作。
CHACHA 的好處是,它們很容易測試。你發(fā)送動作并期待對觀察者的特定調(diào)用作為回報。
列表項的 HTML 模板元素
HTML 模板是存在于 DOM 中的特殊元素,但不會被顯示。它們的目的是生成動態(tài)元素。
當(dāng)我們使用 template 元素時,我們可以避免在 JavaScript 中創(chuàng)建元素和填充它們的所有模板代碼。
下面將使用 template 為列表添加名稱:
通過使用列表項的 template 元素,我們可以在原始 HTML 中看到列表項——它不是用 JSX 或其他語言“渲染”的。你的 HTML 文件現(xiàn)在包含了應(yīng)用程序的所有 HTML——靜態(tài)部分是渲染的 DOM 的一部分,而動態(tài)部分在模板中表達(dá),準(zhǔn)備在時機(jī)成熟時被克隆并追加到文檔中。
集大成者:TodoMVC
TodoMVC 是一個 TODO 列表的應(yīng)用規(guī)范,用于展示不同的框架。TodoMVC 模板帶有現(xiàn)成的 HTML 和 CSS,幫助你專注于框架。
你可以在 GitHub 資源庫中使用這個結(jié)果,并且可以獲得完整的源代碼。
從規(guī)范派生的 CHACHA 開始
我們將從規(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ù)模型中的函數(shù)直接來自規(guī)范和用戶可以做的事情(清除已完成的任務(wù),將所有任務(wù)標(biāo)記為已完成或正在進(jìn)行,獲得正在進(jìn)行和已完成的計數(shù))。
請注意,它遵循 CHACHA 的準(zhǔn)則。
- 有兩個界面,一個是動作的,一個是觀察的。
- 所有的參數(shù)類型都是基元或普通對象(很容易翻譯成 JSON)。
- 所有的函數(shù)都返回 void。
TodoMVC 的實現(xiàn)使用 localStorage 作為后端。
該模型非常簡單,與關(guān)于 UI 框架的討論沒有多大關(guān)系。它在需要的時候保存到 localStorage,并在某些情況發(fā)生變化時向觀察者觸發(fā)回調(diào),這些變化可能是用戶操作的結(jié)果,也可能是模型第一次從 localStorage 加載的時候。
精簡的、面向表單的 HTML
接下來,我將采用 TodoMVC 模板,并將其修改為面向表單的模板:表單的層次結(jié)構(gòu),輸入和輸出元素代表可以用 JavaScript 改變的數(shù)據(jù)。
我怎么知道某個東西是否需要成為表單元素?作為一個經(jīng)驗法則,如果它與模型中的數(shù)據(jù)綁定,那么它就應(yīng)該是一個表單元素。
完整的 HTML 文件是可用的,但這里是其主要部分:
todos
此 HTML 包括以下內(nèi)容:
- 我們有一個 main 表單,其中有所有的全局輸入和按鈕,還有一個新的表單用于創(chuàng)建一個新任務(wù)。請注意,我們使用 form 屬性將元素與表單聯(lián)系起來,以避免表單中的元素嵌套。
- template 元素代表一個列表項,它的根元素是另一個表單,代表與特定任務(wù)相關(guān)的互動數(shù)據(jù)。當(dāng)任務(wù)被添加時,這個表單將通過克隆模板的內(nèi)容而被重復(fù)。
- 隱藏的輸入表示不直接顯示的數(shù)據(jù),但用于樣式設(shè)計和選擇。
注意這個 DOM 是如何簡潔的。它沒有在其元素中散布類。它包括應(yīng)用程序所需的所有元素,以合理的層次結(jié)構(gòu)排列。多虧了隱藏的輸入元素,你已經(jīng)可以很好地感覺到以后文檔中可能會有什么變化。
這個 HTML 不知道它將如何被樣式化,也不知道它到底與什么數(shù)據(jù)綁定。讓 CSS 和 JavaScript 為你的 HTML 工作,而不是讓你的 HTML 為某個特定的造型機(jī)制工作。這將使你在改變設(shè)計時變得更加容易。
最小控制器 JavaScrip
現(xiàn)在我們在 CSS 中已經(jīng)有了大部分的反應(yīng)性,在模型中也有了列表處理,剩下的就是控制器的代碼了,也就是把所有的東西固定在一起的“膠帶”。在這個小程序中,控制器的 JavaScript 大約是 40 行。
下面是一個版本,每個部分都有解釋:
import TaskListModel from './model.js';
const model = new TaskListModel(new class {
上面,我們創(chuàng)建了一個新模型。
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)一個項目被添加到模型中,我們在用 UI 中創(chuàng)建其相應(yīng)的列表項。
在上面的代碼段中,我們克隆了項目 template 的內(nèi)容,為一個特定的項目分配了事件監(jiān)聽器,并將新的項目添加到列表中。
注意,這個函數(shù),以及 onUpdate、onRemove 和 onCountChange,都是要從模型中調(diào)用的回調(diào)。
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)一個項目被更新時,我們設(shè)置它的 completed 和 title 值,然后 blur(退出編輯模式)。
onRemove(key) { document.forms[`task-${key}`].remove(); }當(dāng)從模型中移除一個項時,我們將從視圖中移除其對應(yī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)完成的或活動的項目數(shù)量發(fā)生變化時,我們設(shè)置適當(dāng)?shù)妮斎雭碛|發(fā) CSS 反應(yīng),并格式化顯示計數(shù)的輸出。
const updateFilter = () => filter.value = location.hash.substr(2);
window.addEventListener('hashchange', updateFilter);
window.addEventListener('load', updateFilter);
而我們從 hash 片段中更新過濾器(以及在啟動時)。我們在上面所做的只是設(shè)置一個表單元素的值:CSS 處理其余部分。
document.querySelector('.todoapp').addEventListener('submit', e => e.preventDefault(), {capture: true});在這里,我們確保當(dāng)表單被提交時我們不會重新加載頁面。這一行代碼把這個應(yīng)用程序變成了一個 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());而這就處理了主要的操作(創(chuàng)建、標(biāo)記所有、清除完成)。
與 CSS 的反應(yīng)性
完整的 CSS 文件可以供你查看。
CSS 處理了規(guī)范中的很多要求(做了一些有利于無障礙的修正)。我們來看看一些示例。
根據(jù)規(guī)范,“X”(destroy)按鈕只在懸停時顯示。我還添加了一個輔助位,使它在任務(wù)被聚焦時可見。
.task:not(:hover, :focus-within) button[name="destroy"] { opacity: 0 }當(dāng) filter 鏈接是當(dāng)前鏈接時,它會得到一個紅色邊框:
.todoapp input[name="filter"][value=""] ~ footer a[href$="#/"
網(wǎng)頁題目:一文了解 Web 框架的替代方案
本文URL:http://m.fisionsoft.com.cn/article/cdjedce.html


咨詢
建站咨詢
