VIANILA-TODO:VIRENAL WEB开发可行技术的案例研究

2020-10-27 19:13:42

TeuxDeux在纯HTML、CSS和JavaScript中的克隆(没有构建步骤)。它是全动画的,以60fps流畅运行,总传输大小为44KB(未缩小)。

更重要的是,它是一个案例研究,表明就可维护性而言,普通web开发是可行的,在用户体验方面也是值得的(在这种情况下,加载速度提高了100%,带宽减少了90%)。

这里并没有发明定制的框架。相反,案例研究的目的是发现最小的可行模式,这些模式都是真正的香草。结果是可维护的,尽管冗长且有相当多的重复(ES6可能会减轻大部分重复的影响)。

如果说有什么不同的话,那就是案例研究验证了构建步骤和框架的价值,但也证明了标准Web技术可以得到有效利用,只有少数几个关键领域的普通方法明显逊色(特别是在浏览器测试中)。

我认为,在研究实用的、可伸缩的方法来构建无第三方依赖的Web应用程序方面投入的太少。

仅描述如何创建DOM节点或如何在没有框架的情况下切换类是不够的。写一篇文章说您不需要库X,然后继续描述如何运行您自己未经测试的低级版本X,这也是相当有害的。

缺少的是仅使用标准Web技术构建的复杂Web应用程序的完整示例,尽可能多地涵盖开发过程的各个方面。

这一案例研究试图填补这一空白,至少是一点点,并启发该领域的进一步研究。

为了这项研究,我选择构建一个功能相当的TeuxDeux克隆。用户界面面临着有趣的挑战,特别是在与动画结合时的性能拖放。

用户界面可以说很小(这对案例研究很好),但是足够大,需要考虑其体系结构。

为了产生有效的普通解决方案,并且因为约束激发创造力,我想出了一套在整个过程中要遵循的规则:

(2)这些通常最终会变成一个自定义的微框架,从而质疑您为什么不首先使用一个已建立并经过测试的库/框架。

由此产生的产品在功能、性能和设计方面应该与原始产品相当或更好。

本节将逐步介绍最终的实现,重点介绍在此过程中发现的技术和问题。我们鼓励您在本节旁边查看源代码。

由于排除了构建步骤,代码库是围绕纯HTML、CSS和JS文件组织的。HTML和CSS主要遵循rscss(由Rico Sta设计。Cruz),它产生了直观的、面向组件的结构。

样式表有点冗长,我在这里错过了SCSS或更少,我认为其中一个是大型项目的必备内容。

ES6模块被排除在外,因此所有JavaScript都位于全局命名空间(VT)之下。这在任何地方都行得通,但也有一些缺点。无法静态分析,可能会错过代码完成。

多边形填充是直接从polyful.ioI';获取的。我已经设置了noodule脚本属性,因此仅为较旧的浏览器提取多边形填充。

基本代码质量(代码样式、线条)由Pretier、Styelint和ESLint指导。我已将ESLint解析器设置为ES5,以确保只允许ES5代码。

请注意,我已经完全退出了Web组件。我无法清楚地表达我对它们的不喜欢之处,但在整个研究过程中,我从未错过过它们。

基本结构附带一些样板,例如引用HTML中的所有单个样式表和脚本;可能足以证明一个简单的构建步骤是合理的。

其他方面,理解起来都很简单和琐碎(字面上只有一堆HTML、CSS和JS文件)。

我发现组合使用函数、查询选择器和DOM事件就足以构建一个可伸缩、可维护的代码库,尽管如我们稍后将看到的那样需要进行一些权衡。

从概念上讲,提出的体系结构松散地将CSS选择器映射到挂载(即称为)onceper匹配元素的JS函数。这就产生了一个简单的心理模型,并与DOM和样式产生了协同作用:

Mount函数将DOM元素作为(唯一)参数,它们的职责是设置初始状态、事件侦听器,并为目标元素提供行为和呈现。

//安全初始化命名空间窗口。MYAPP=窗口。MYAPP||{};//定义装载函数//松散映射到";.hello-world";MYAPP。HelloWorld=function(El){//定义初始状态var state={title:';Hello,World!';,description:';A Example Vanilla Component';,Counter:0,};//设置刚性基础HTML//没有ES6模板文字:(EL.。InnerHTML=[';<;h1class=";title";>;<;/h1>;';,';<;p class=";description";>;<;/p>;';,';<;div class=";my-counter";>;<;/div>;';,]。Join(';\n';);//挂载子组件el。QuerySelectorAll(.my-Counter';)。For Each(MYAPP.。MyCounter);//附加事件监听器el。AddEventListener(';ModifyCounter';,function(E){update({count:stat.。计数器+e。Detail});});//公开公共接口//使用小写函数名el。Helloworld={update:update,};//初始更新update();//定义幂等更新函数update(Next){//update state//可选优化,例如,如果状态没有更改对象,则退出。Assign(state,next);//更新自己的HTML el。QuerySelector(';.title';)。InnerText=状态。头衔;头衔。QuerySelector(';.description';)。InnerText=状态。描述;//将数据传递给子组件e1。QuerySelector(';.my-counter';)。我的计数器。更新({值:状态。计数器,});}};//定义另一个组件//松散映射到";.my-counter";MYAPP。MyCounter=function(El){//定义初始状态var state={value:0,};//设置刚性基础HTML//没有ES6模板文字:(el.。InnerHTML=[';<;p&>;';,';<;SPAN class=";Value&34;>;<;/SPAN&>;';,';<;按钮class=";increment";>;Increment<;/button>;';,';<;按钮class=";decrement";>;Decrement<;/button>;';,';<;/p>;';,]。Join(';\n&39;);//附加事件侦听器el。QuerySelector(';.increment';)。AddEventListener(';单击';,function(){//调度操作//使用.Detail传输数据EL。DispatchEvent(new CustomEvent(';modfyCounter';,{Detail:1,气泡:true,}));});el。QuerySelector(';.deducment';)。AddEventListener(';单击';,function(){//调度操作//使用.Detail传输数据EL。DispatchEvent(new CustomEvent(';modfyCounter';,{Detail:-1,泡泡:true,}));});//公开公共接口//使用小写函数名el。MyCounter={update:update,};//定义幂等更新函数update(Next){Object.。分配(州,下一个);分配(州,下一个)。QuerySelector(';.value';)。InnerText=状态。Value;}};//挂载HelloWorld组件//文档中的任何<;div class=";hello-world";>;<;/div>;将被挂载文档。QuerySelectorAll(.hello-world';)。For Each(MYAPP.。你好世界);

它附带了相当多的样板,但也有一些有用的属性,我们将在下面几节中看到。

请注意,mount函数的任何部分都是完全可选的。例如,mount函数不必设置任何基本HTML,而可以只设置事件侦听器来启用某些行为。

还要注意,一个元素可以使用多个挂载函数挂载,例如,待办事项使用VT.TodoItem和VT.AppDraggable挂载。

与反应组件相比,挂载函数提供了有趣的灵活性,因为组件和行为可以使用相同的习惯用法实现,并且可以任意组合。

数据通过它们的公共接口从父组件向下流到子组件(通常是更新函数)。

操作通过自定义DOM事件向上流动(冒泡向上),通常会导致一些父组件状态更改,而这些更改又会通过更新函数向下传播。

数据存储被分解成一个单独的行为(VT.TodoStore),它只接收和分派事件,并封装所有数据逻辑。

使用标准API侦听和分派事件稍微有点冗长,这当然证明引入助手是合理的。我不需要像jQuery那样的事件委派来进行本研究,但我相信这是一个很有用的概念,使用标准API很难简明扼要地做到这一点。

应该避免使用.innerHTML幼稚地重新呈现整个组件,因为这可能会影响性能,并且可能会破坏浏览器几十年来一直在优化的重要功能,如输入状态、焦点、文本选择等。

如3.2.1.所示,渲染因此被分成一些刚性的基本HTML和一个幂等的、完整的更新函数,该函数只进行必要的更改。

幂等是这里的关键,即可以随时调用更新函数,并且应该始终正确地呈现组件。

完整性同样重要,也就是说,无论是什么触发了更新,更新函数都应该呈现整个组件。

实际上,这意味着几乎所有的DOM操作都在更新函数中完成,这极大地提高了代码库的健壮性和可读性。

如上所述,与JSX相比,这种方法相当冗长和丑陋,例如。然而,它的性能非常好,可以通过检查数据更改、缓存选择器等来进一步优化。它也很容易理解。

不出所料,这项研究最困难的部分是高效地呈现不同数量的动态组件。下面是一个来自实施的注释示例,概述了协调算法:

/*全局VT*/Window。VT=窗口。|{};vt.。TodoList=函数(El){var state={Items:[],};el.。InnerHTML=';<;div class=";Items";>;<;/div>;';;函数UPDATE(NEXT){对象。ASSIGN(STATE,NEXT);var CONTAINER=el。QuerySelector(';.Items';);//将当前子项标记为要删除var陈旧=new set(CONTAINER.。Child);//按data-key映射当前子项var Child drenByKey=new Map();废弃。ForEach(function(Child){ChildrenByKey。设置(子项。GetAttribute(';data-key';),Child);});//从data var Children=state构建新的子元素列表。物品。Map(function(Item){//Find Existing Child by Data-Key var Child=ChildrenByKey。获取(项。Id);如果(Child){//如果有Child,则将其作废。DELETE(CHILD);}ELSE{//否则,创建新的子级CHILD=DOCUMENT。CreateElement(';div&39;);子元素。ClassList。添加(';TODO-Item';);//设置数据键子项。SetAttribute(';data-key';,Item。Id);//挂载组件VT。TodoItem(Child);}//更新子级。图腾。Update({Item:Item});返回子项;});//删除作废的子项。ForEach(函数(子级){容器。RemoveChild(Child);});//(重新)插入新的子代子代列表。ForEach(function(Child,index){if(Child!==容器。子[索引]){容器。在前面插入(子项,容器。儿童[索引]);}});}el。TodoList={update:update,};};

它非常冗长,而且有很多机会引入错误。与JSX中的简单循环相比,这似乎是疯狂的。它的性能相当好,因为它只做最少的工作,但在其他方面是混乱的;绝对是实用函数或库的候选者。

最初使用库的性价比会高得多,但是,一旦我开始引入动画,定制的实现就得到了回报,因为两者都必须密切协调。我可以想象,当使用第三方代码时,这将是一个困难的问题。

拖放实现(再次)基于DOM事件,并与其余体系结构很好地集成。它显然是研究中最复杂的部分,但除了挂载行为和添加事件处理程序外,我能够在不更改现有代码的情况下实现它。

我怀疑拖放实现在触控设备上有一些微妙的问题,因为我还没有对它们进行广泛的测试。使用一个库来识别手势可能更明智,并将降低测试浏览器和设备的成本。

对于最终的产品,我希望大多数用户交互都有流畅的动画效果,这是一个横切的关注点,它是使用Paul Lewis设计的翻转技术实现的。

在不进行大型重构的情况下实现翻转动画是本案例研究的最大挑战,特别是与拖放结合使用时。经过几天的工作,我能够单独实现算法,并在应用程序的根级别将其与其他关注点协调起来。在这种情况下,addEventListener的use Capture模式被证明非常有用。

此外,大多数交互都以每秒60帧的速度流畅地设置动画。特别是,当元素重新排序时,拖放可以提供适当的视觉反馈。

当我几周前开始做案例研究时,后者比最初的应用程序有了改进。与此同时,TeuxDeuxTeam发布了一个更新,提供了更好的拖放体验。干得好!

一个值得注意的缺失功能是对Markdown的支持。从头开始实现Markdown是不明智的;这是使用外部库的有效候选,因为它与剩余的代码库完全正交。

原始TeuxDeux应用程序的新加载大约传输435 KB,并在大约1000毫秒左右完成加载,有时高达2000毫秒(在2020年10月21日测量)。重新加载大约在500毫秒完成。

传输大小约为44KB时,Vanilla应用程序可以在300-500ms内一致加载,而不是缩小,每个脚本、样式表和图标都作为单独的文件。重新加载在100-200ms完成;同样,根本没有优化(例如,使用资产散列/无限缓存)。

公平地说,我的实现遗漏了原始版本中的许多特性,不过我怀疑完全等效的克隆文件的传输大小远低于100KB。

不幸的是,很难找到无可争议的、客观的代码质量衡量标准(除了代码样式、linting等琐碎的东西之外),唯一被普遍接受的评估似乎是同行评审。

为了至少在一定程度上评估代码的质量,下面几节总结了关于代码库的相关事实,并根据我在该行业的经验总结了一些自以为是的声明。

这个应用程序可以通过处理/分派事件从外部自然增强(就像你可以自然地制作一些现有的HTML动画一样)。

所有源文件(HTML、CSS和JS)合并到2500行以下的代码,包括注释和空行。

公平地说,我的实现遗漏了原始版本中相当多的功能,不过我怀疑完全等效的克隆版本会远低于10000 LOC。

虽然在本研究中没有使用事件委托,但是在没有代码重复的情况下实现事件委托并不是一件容易的事。

通过构建步骤和最小的帮助器集消除冗余将进一步减少相对较低的代码大小(见上文)。

例如,与JSX相比,基本HTML和动态呈现之间的分离并不理想。

协调是冗长、脆弱和重复的,我至少不会推荐在没有经过良好测试的助手功能的情况下提出的技术。

在创建新元素时,必须正确记住安装行为。以某种方式实现自动化会很有帮助,例如(始终)观察选择器X的元素,并确保所需的行为一次安装在它们上面。

没有类型安全。我一直都是动态语言的支持者,但既然打字系统两全其美,我就不推荐充分使用它。

我们实际上被禁止使用不提供浏览器版本的NPM依赖项,因为我们不能使用CommonJS或ES6模块。

大多数框架免费处理大量的浏览器不一致问题,并通过大量的测试套件持续监控回归,使用普通的方法测试浏览器的成本肯定要高得多。

除了上述问题,我相信代码库组织良好,有明确的修复错误和功能开发的路径。由于没有第三方代码,因此很容易找到和修复错误,并且没有要解决的依赖限制。

一定程度的DOM API知识是必需的,但我相信这应该是任何Web开发人员的目标。

如果没有生产使用,客观地评估所发现技术的通用性是不可能的。然而,根据我的经验,我无法想象有哪种场景不适用挂载函数、基于事件的数据流等。毕竟,基本原则为既定的框架提供了动力:

这项研究的结果是一个可以工作的待办事项应用程序,它具有不错的UI/UX和原始TeuxDeux应用程序的大部分功能,该应用程序只使用标准的Web技术构建,整体性能更好,代码量和带宽只有一小部分。

通过几个简单的概念,代码库似乎是可以管理的,尽管它在某些领域相当冗长甚至凌乱,这可以通过少量的帮助器函数和简单的构建步骤(例如,SCSS和TypeScript)来缓解。

这项研究的方法有助于发现模式和技术,这些模式和技术至少与特定主题的基于框架的方法相当,而不会偏离到构建自定义框架。

后者的一个显著例外是以简明的方式呈现可变数量的元素。我无法消除基本但有效的协调所涉及的繁琐。这方面需要进一步的研究,但目前看来这是(可能是外部的)通用实用程序的有效候选者。

在考虑缺点时,请记住,所有单独的部分都是独立的、高度解耦的、可移植的,并且与Web平台一致。根据定义,由此产生的实现不能生锈,因为任何依赖项都不会过时。

另一个要持保留态度的想法是:我认为框架使简单的任务变得更加简单,但困难的任务(例如,实现横切关注点或性能优化)往往更加困难。

预先设置一些约束迫使我挑战关于普通web开发的假设和先入为主的观念,避免通用的实用工具,用现成的东西来做事情,这是相当解放的。

正如评估中详细描述的那样,如果允许构建步骤,研究可能会更有说服力。现代JavaScript和SCSS可以将大多数不必要的冗长部分减少到最低限度。

最后,这个案例研究不会质疑使用依赖关系或框架-它们确实在许多领域提供了很大的价值。这是一个受限的实验,旨在发现普通Web开发的新方法,并有望激励该领域的创新和进一步研究。

I‘我很乐意听取有关案例研究的任何方面的反馈和意见。它在一些重要的领域仍有欠缺,例如

.