网站首页 > 技术文章 正文
文章来自公众号@奇舞精选,https://mp.weixin.qq.com/s/1h4LKyeobSACU3I8sIYAtw
第一章
概要
在本文中,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):
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[2]。
在SolidJS中,这是通过其存储和内置元素更显式地完成的。例如,Show元素将跟踪内部发生的变化,而不是虚拟DOM。
在Svelte中,会生成“响应式”代码。Svelte知道哪些事件会导致更改,并生成简单的代码,在事件和DOM更改之间划线。
在Lit中,响应式是使用元素属性完成的,本质上依赖于HTML自定义元素的内置响应式。
逻辑
当框架为数据绑定提供声明性接口,并实现响应式时,它还需要提供某种方式来表达一些传统上以命定方式编写的逻辑。逻辑的基本构建块是“if”和“for”,所有主要的框架都提供了这些构建块的一些表达式。
条件语句/流控制
除了绑定数字和字符串等基本数据外,每个框架都提供一个“条件”原语。在React中,它是这样的:
const [hasError, setHasError] = useState(false);
return hasError ? <label>Message</label> : null;
…
setHasError(true);
SolidJS提供了一个内置的条件组件Show[3]:
<Show when={state.error}>
<label>Message</label>
</Show>
Svelte提供了#if指令:
{#if state.error}
<label>Message</label>
{/if}
在Lit中,你可以在渲染函数中使用一个显式的三目运算操作:
render() {
return this.error ? html`<label>Message</label>`: null;
}
Lists
另一个常见的框架原语是列表处理。列表是ui的关键部分---联系人列表、通知列表等等——为了有效地工作,它们需要是响应式的,而不是在一个数据项发生变化时更新整个列表。
在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和索引来决定在项目更改时更新哪些元素。它比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约为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-error label.error {display: block; }
</style>
<label class="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 <label className="error">{errorMessage}</label>
当我们有一个稳定的DOM和稳定的树形式和表单元素时,我们可以做以下事情:
<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>
它的原始形式看起来相当冗长,但它也非常稳定、直接和高性能。
input表单
通常,当我们构建SPA时,我们会使用一些类似json的API来更新我们的服务器或我们使用的任何模型。
这是一个很熟悉的例子(为了便于阅读,是用Typescript写的):
interface Contact {
id: string;
name: string;
email: string;
subscriber: boolean;
}
function updateContact(contact: Contact) { … }
在框架代码中,通过选择输入元素并一块一块地构造对象来生成这个Contact对象是很常见的。正确使用表单,有一个简洁的替代方案:
<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输入和JavaScript函数之间无缝地转换值。
组合表单和响应式
通过结合表单的高性能选择器稳定性和CSS响应式,我们可以实现更复杂的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>
注意,在这个例子中没有使用类——我们从表单的数据中开发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:
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();
}
注意,这两个接口中的所有函数都是void,并且只接收普通对象。这是故意的。ChaCha构建起来就像一个有两个端口的通道来发送消息,这允许它在EventSource、HTML MessageChannel、service worker或任何其他协议中工作。
ChaChas的优点是易于测试:您发送动作并期待特定的调用返回给观察者。
列表项的HTML模板元素
HTML模板是存在于DOM中但不被显示的特殊元素。它们的目的是生成动态元素。
当我们使用模板元素时,我们可以避免所有创建元素并在JavaScript中填充它们的样板代码。
下面将使用模板将一个名字添加到列表中:
<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>
通过使用列表项的模板元素,我们可以在原始HTML中看到列表项——它不是用JSX或其他语言“呈现”的。你的HTML文件现在包含了应用程序的所有HTML -静态部分是渲染DOM的一部分,动态部分在模板中表示,准备在时机成熟时被克隆和追加到文档中。
把它放在一起:TodoMVC
TodoMVC[8]是一个TODO列表的应用规范,用于展示不同的框架。TodoMVC模板附带了现成的HTML和CSS,以帮助您专注于框架。
你可以在GitHub库中使用结果[9],完整的源代码[10]是可用的。
从规范派生的chacha
我们将从规范[11]开始,并使用它来构建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;
}
任务模型中的功能直接从规范和用户可以做的事情中派生出来(清除已完成的任务,将所有任务标记为已完成或活动,获得活动和已完成的计数)。
请注意,它遵循ChaCha的指导原则:
- 有两个界面,一个是动作界面,一个是观察界面。
- 所有参数类型都是原语或普通对象(很容易转换为JSON)。
- 所有的函数都返回void。
- TodoMVC的实现使用localStorage作为后端。该模型非常简单,与UI框架的讨论没有太大关系。当需要时,它将保存到localStorage,并在一些变化时(无论是由于用户操作的结果,还是当模型第一次从localStorage加载时)向观察者发出更改回调。
精益,面向表单的HTML
接下来,我将使用TodoMVC模板,并将其修改为面向表单的—表单的层次结构,输入和输出元素表示可以用JavaScript更改的数据。
我如何知道是否需要一个表单元素?根据经验,如果它绑定到模型中的数据,那么它应该是一个表单元素。
完整的HTML代码[12]是可用的,但这里是它的主要部分:
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<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">X</button>
</li>
</form>
</template>
</ul>
</main>
<footer>
<output form="main" name="activeCount">0</output>
<nav>
<a name="/" href="#/">All</a>
<a name="/active" href="#/active">Active</a>
<a name="/completed" href="#/completed">Completed</a>
</nav>
<input form="main" type="button" name="clearCompleted" value="Clear completed" />
</footer>
</section>
本HTML包括以下内容:
- 我们有一个主表单,其中包含所有全局输入和按钮,还有一个用于创建新任务的新表单。注意,我们使用form属性[13]将元素与表单关联起来,以避免元素在表单中嵌套。
- 模板元素表示一个列表项,它的根元素是另一个表单,表示与特定任务相关的交互式数据。当添加任务时,可以通过克隆模板的内容来重复这个表单。
- 隐藏输入表示没有直接显示的数据,但用于样式化和选择。注意这个DOM是如何简洁的。它的元素中没有分散的类。它包含了应用程序所需的所有元素,以合理的层次结构排列。由于隐藏的输入元素,您已经可以很好地了解文档稍后可能发生的更改。
这个HTML不知道它将如何被样式化,也不知道它将绑定到什么数据。让CSS和JavaScript为HTML工作,而不是让HTML为特定的样式机制工作。这将使更改设计变得更加容易。
最小化的Controller---javascript
现在我们在CSS中有了大部分的反应性,并且在模型中有了列表处理,剩下的就是Controller代码——将所有东西连接在一起的管道胶带。在这个小应用程序中,Controller JavaScript[14]大约有40行代码。
下面是一个版本,并对每个部分进行了解释:
import TaskListModel from './model.js';
const model = new TaskListModel(new class {
在上面的代码中,我们创建了一个新模型。
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);
}
当一个项目被添加到模型中时,我们会在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反应,并格式化显示计数的输出。
const updateFilter = () => 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 }
当过滤器链接是当前链接时,它会得到一个红色的边框:
.todoapp input[name="filter"][value=""] ~ footer a[href$="#/"],
nav a:target {
border-color: #CE4646;
}
注意,我们可以使用link元素的href作为部分属性选择器——不需要JavaScript检查当前的过滤器,并在适当的元素上设置一个选定的类。
我们还使用:target选择器,这使我们不必担心是否要添加过滤器。
标题输入的视图和编辑样式会根据其只读模式而改变:
.task input[name="title"]:read-only {
…
}
.task input[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
猜你喜欢
- 2024-11-17 看完这几道 JavaScript 面试题,让你与考官对答如流(上)
- 2024-11-17 JS 鼠标框选(页面选择)时返回对应的 HTML 或文本内容
- 2024-11-17 一文读懂 WebAssembly(WASM)中的字符串
- 2024-11-17 http连接泄露,httpClient到底要关闭哪些资源
- 2024-11-17 DefineProperty 和Proxy双向绑定演示,你还不知道么?
- 2024-11-17 必考知识点-JavaScript类型转换(讲原理)
- 2024-11-17 文件上传,排版是伤(上传文件出现乱码是怎么回事)
- 2024-11-17 分享一些有趣的,你从不使用的html属性
- 2024-11-17 惊艳到了,每个开发人员都必须要知道的6个HTML属性!
- 2024-11-17 (鸡汤文)这一次我终于搞懂了 JavaScript 定时器的 this 指向
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-