用现代JavaScript和Web组件构建一个Web应用程序

    浏览器中的JavaScript已经进化了。希望利用最新特性的开发人员可以选择减少框架,减少麻烦。通常保留在前端框架中的选项,例如基于组件的方法,现在在普通的旧式JavaScript中是可行的。

    在这篇文章中,我将展示所有最新的JavaScript特性,使用带有网格和搜索过滤器的作者数据的UI。为了简单起见,一旦介绍了一项技术,我将转到下一项技术,以便不赘述这一点。出于这个原因,UI将有一个Add选项和一个下拉搜索过滤器。作者模型将有三个字段:姓名、电子邮件和一个可选主题。表单验证主要是为了展示这种无需框架的技术,而不是详细介绍。

    曾经勇敢的语言已经成长为许多现代功能,如代理、导入/导出、可选链操作符和web组件。这完全符合<一个href="//www.shaoxingby.com/learn-jamstack/">Jamstack,因为应用程序通过HTML和普通JavaScript在客户端上呈现。

    为了专注于应用程序,我将省略API,但我会指出这种集成可以在应用程序中发生的位置。

    开始

    该应用程序是一个典型的JavaScript应用程序,有两个依赖项:http-server和Bootstrap。代码将只在浏览器中运行,因此除了一个用于托管静态资产的后端外,没有其他后端。代码是<一个href="https://github.com/sitepoint-editors/framework-less-web-components">在GitHub上给你玩。

    假设你有<一个href="//www.shaoxingby.com/quick-tip-multiple-versions-node-nvm/">安装了最新的节点LTS在机器上:

    mkdirframework-less-web-componentscdframework-less-web-componentsnpm初始化

    这应该是一个单package.json文件中放置依赖项的位置。

    安装两个依赖项:

    npm我http-server bootstrap@next—save-exact

    如果你觉得http服务器不是一个依赖项,而是这个应用程序运行的一个要求,有一个选项来安装它全局通过NPM I -g http-server.无论采用哪种方式,此依赖项都不会传递给客户端,而只是为客户端提供静态资产。

    打开package.json文件和设置入口点通过“开始”:“http服务器”脚本.继续,并通过启动应用程序npm开始,这将使http://localhost:8080/对浏览器可用。任何index . html放在根文件夹中的文件自动由HTTP服务器托管。您所要做的就是刷新页面以获得最新的位。

    文件夹结构如下所示:

    组件┳┣━┓┃┣━━App.js┃┣━━AuthorForm.js┃┣━━AuthorGrid.js┃┗━━ObservableElement.js┣━┓┃模型┣━━actions.js┃┗━━observable.js┣━━index . html┣━━index.js┗━━package.json

    这是每个文件夹的含义:

    • 组件的HTML web组件App.js和继承的自定义元素ObservableElement.js
    • 模型: app状态和监听UI状态变化的突变index . html:主要的静态资产文件,可以托管在任何地方

    要在每个文件夹中创建文件夹和文件,请执行以下命令:

    mkdir组件模型触摸components/App.js组件/AuthorForm.js组件/AuthorGrid.js组件/ObservableElement.js模型/actions.js模型/observable.js index.html index.js

    集成Web组件

    简而言之,web组件就是自定义的HTML元素。它们定义了可以放入标记中的自定义元素,并声明了呈现组件的回调方法。

    下面是一个自定义web组件的快速概述:

    HelloWorldComponent扩展HTMLElementconnectedCallback//回调方法innerHTML“Hello, World !”//定义自定义元素窗口customElements定义“hello world”HelloWorldComponent//标记可以通过以下方式使用自定义web组件:/ / < hello world > < / hello world >

    如果您觉得需要更温和的web组件介绍,请查看<一个href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements">中数条.起初,它们可能感觉很神奇,但对回调方法的良好掌握使这一点变得非常清楚。

    主要的index . html静态页面声明HTML web组件。我将使用Bootstrap来设置HTML元素的样式,并引入index.js资产,成为应用程序进入JavaScript的主要入口和网关。

    打开index . html把这些整理好:

    <!文档类型超文本标记语言><超文本标记语言><><字符集utf - 8><的名字视窗内容宽度=设备宽度,初始= 1><链接hrefnode_modules /引导/ dist / css / bootstrap.min.cssrel样式表><标题>Framework-less组件标题>><身体><模板idhtml-app><div容器><h1>作者h1><author-form>author-form><author-grid>author-grid><页脚fixed-bottom小><ptext-center mb-0>按Enter键添加一个作者条目p><ptext-center小>与C Rp>页脚>div>模板><模板idauthor-form><形式><div行mt-4><div上校><输入类型文本表单控件占位符的名字aria-label的名字>div><div上校><输入类型电子邮件表单控件占位符电子邮件aria-label电子邮件>div><div上校><选择form-selectaria-label主题><选项>主题选项><选项>JavaScript选项><选项>HTMLElement选项><选项>ES7 +选项>选择>div><div上校><选择form-select搜索aria-label搜索><选项>搜索选项><选项>所有选项><选项>JavaScript选项><选项>HTMLElement选项><选项>ES7 +选项>选择>div>div>形式>模板><模板idauthor-grid><表格表mt-4><thead><tr><th>的名字th><th>电子邮件th><th>主题th>tr>thead><tbody>tbody>表格>模板><模板id作者行的><tr><道明>道明><道明>道明><道明>道明>tr>模板><导航导航栏Navbar -expand-lg Navbar -light bg-dark><divcontainer-fluid><一个navbar-brand文字光线href/>带有可观察对象的无框架组件一个>div>导航><html-app>html-app><脚本类型模块srcindex.js>脚本>身体>超文本标记语言>

    密切注意脚本标记类型属性设置为模块.这就是在浏览器中使用普通JavaScript解锁导入/导出的方法。的模板标记id定义启用web组件的HTML元素。我将应用程序分解为三个主要组件:html-appauthor-form,author-grid.因为JavaScript中还没有定义任何东西,所以应用程序将在没有任何自定义HTML标记的情况下呈现导航栏。

    开始时要简单,把这个放进去ObservableElement.js.它是所有作者组件的父元素:

    出口默认的ObservableElement扩展HTMLElement

    然后,定义html-app组件App.js

    出口默认的应用程序扩展HTMLElementconnectedCallback模板文档getElementById“html-app”窗口requestAnimationFrame= >常量内容模板内容firstElementChild版本真正的列表末尾内容

    注意使用出口违约来声明JavaScript类。这是我通过模块当我引用主脚本文件时键入。要使用web组件,请从HTMLElement并定义connectedCallback类方法。浏览器负责其余的工作。我使用requestAnimationFrame在下次在浏览器中重新绘制之前渲染主模板。

    这是一种常见的技术,你会看到web组件。首先,通过元素ID获取模板。然后,克隆模板via版本.最后,列表末尾内容进入DOM。如果遇到web组件无法呈现的问题,请确保首先检查克隆的内容是否附加到DOM中。

    接下来,定义AuthorGrid.jsweb组件。这一个将遵循类似的模式,并对DOM进行一些操作:

    进口ObservableElement”。/ ObservableElement.js '出口默认的AuthorGrid扩展ObservableElementconnectedCallback模板文档getElementById“author-grid”rowTemplate文档getElementById“作者行的”常量内容模板内容firstElementChild版本真正的列表末尾内容表格querySelector“表”updateContentupdateContent表格风格显示作者?.长度??0= = =0?“没有”''表格querySelectorAll“tbody tr”forEachr= >r删除

    我定义了主体this.table元素。querySelector.因为这是一个类,所以可以使用.的updateContent方法在网格中没有要显示的作者时,通常会删除主表。的<一个href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining">可选链操作符?.)和空合并负责设置显示风格到无。

    看一下进口语句,因为它在文件名中引入了带有完全限定扩展名的依赖项。如果您习惯了Node开发,这就是它与浏览器实现的不同之处,后者遵循标准,需要一个文件扩展名,如. js.向我学习,确保在浏览器中工作时输入文件扩展名。

    接下来,AuthorForm.js组件有两个主要部分:呈现HTML和将元素事件连接到表单。

    若要渲染表单,请打开AuthorForm.js

    进口ObservableElement”。/ ObservableElement.js '出口默认的AuthorForm扩展ObservableElementconnectedCallback模板文档getElementById“author-form”常量内容模板内容firstElementChild版本真正的列表末尾内容形式querySelector“形式”形式querySelector“输入”焦点resetForm输入输入forEach= >价值''班级名册删除“有效”输入0焦点

    焦点引导用户在表单中可用的第一个输入元素上开始输入。确保放置任何DOM选择器列表末尾否则这个技巧就行不通了。的resetForm现在不使用,但当用户按下Enter键时,会重置表单的状态。

    通过连接事件addEventListener方法将此代码附加到connectedCallback方法。这可以添加到的末尾connectedCallback方法:

    形式addEventListener键盘按键的e= >如果e关键= = =“进入”常量输入形式querySelectorAll“输入”常量选择形式querySelector“选择”控制台日志'按Enter键:'+输入0价值+“|”+输入1价值+“|”+选择价值= = =“主题”?''选择价值resetForm输入形式addEventListener“改变”e= >如果e目标匹配“select.search”& &e目标价值= = !“搜索”控制台日志'过滤通过:'+e目标价值

    类附加的典型事件侦听器this.form元素。的改变事件使用事件委托侦听表单中的所有更改事件,但只针对select.search元素。这是将单个事件委托给父元素中尽可能多的目标元素的有效方法。有了这些,在表单中输入任何内容并点击Enter将表单重置为零状态。

    要在客户端上呈现这些web组件,请打开index.js把这个放进去:

    进口AuthorForm“。/组件/ AuthorForm.js”进口AuthorGrid“。/组件/ AuthorGrid.js”进口应用程序“。/组件/ App.js”窗口customElements定义“author-form”AuthorForm窗口customElements定义“author-grid”AuthorGrid窗口customElements定义“html-app”应用程序

    现在可以在浏览器中刷新页面,并使用UI。打开开发人员工具,在单击并键入表单时查看控制台消息。按下选项卡键可以帮助您在HTML文档中的输入元素之间导航。

    验证表单

    通过摆弄表单,您可能会注意到,当姓名和电子邮件都是必需的,主题是可选的时,它需要任意输入。无框架的方法可以是HTML验证和一些JavaScript的组合。幸运的是,Bootstrap通过添加/删除CSS类名使此操作变得简单班级名册web API。

    AuthorForm.js组件,找到console.log输入键事件处理程序,寻找日志与“按Enter”,并把这个放在它的正上方:

    如果isValid输入返回

    然后,定义isValid中的类方法AuthorForm.这可能会超过resetForm方法:

    isValid输入isInvalid输入forEach= >如果价值& &checkValidity班级名册删除“无效”班级名册添加“有效”其他的班级名册删除“有效”班级名册添加“无效”isInvalid真正的返回isInvalid

    在普通JavaScript中,调用checkValidity使用内置的<一个href="//www.shaoxingby.com/using-the-html5-constraint-api-for-form-validation/">HTML验证器,因为我用type = "电子邮件".为了检查必需的字段,一个基本的真理检查通过i.value.的班级名册web API添加或删除CSS类名,这样Bootstrap样式就可以完成它的工作。

    现在,继续前进,再试一次这个应用程序。尝试输入无效数据现在会被标记,有效数据现在会重置表单。

    可见

    是时候说说这种方法的细节了,因为web组件和事件处理程序只能带我到这里了。制作这个应用程序政府主导的,我需要一种方法来跟踪UI状态的更改。事实证明,可观察对象非常适合这种情况,因为当状态发生变化时,它们可以向UI发出更新。可以将可观察对象视为sub/pub模型,其中订阅者侦听更改,而发布者在UI状态下触发更改。这简化了在没有任何框架的情况下构建复杂而令人兴奋的ui所需的推拉代码的数量。

    打开obserable.js下的文件模型把这个放进去:

    常量cloneDeepx= >JSON解析JSONstringifyx常量冻结状态= >对象冻结cloneDeep状态出口默认的initialState= >听众常量代理代理cloneDeepinitialState目标的名字价值= >目标的名字价值的听众forEachl= >l冻结代理返回真正的代理addChangeListenercb= >听众cbcb冻结代理返回= >听众听众过滤器埃尔= >埃尔= = !cb返回代理

    乍一看这可能很可怕,但它做了两件事:劫持setter来捕捉突变,以及添加侦听器。在ES6+中,代理类启用围绕initialState对象。它可以拦截像这样的基本操作方法,该方法在对象发生更改时执行。返回真正的让JavaScript内部机制知道突变成功了。的代理设置一个处理程序对象,其中包含得到定义。因为我只在乎突变的状态对象有陷阱。所有其他功能(如读取)都直接转发到原始状态对象。

    侦听器保留一个已订阅的回调列表,这些回调希望在发生突变时得到通知。该回调在添加侦听器后执行一次,并返回侦听回调以供将来引用。

    冻结而且cloneDeep函数的存在是为了防止基础状态对象发生任何进一步的变化。这使得UI状态更具可预测性,并且在某种程度上是无状态的,因为数据只向一个方向移动。

    现在,去actions.js把这些整理好:

    出口默认的状态= >常量addAuthor作者= >如果作者返回状态作者...状态作者...作者常量changeFiltercurrentFilter= >状态currentFiltercurrentFilter返回addAuthorchangeFilter

    这是一个可测试的JavaScript对象,它对状态执行实际的变化。为了简洁起见,我将放弃编写单元测试,而将其留给读者作为练习。

    为了从web组件中触发突变,它们需要在全局中注册window.applicationContext对象。这使得这个带有突变的状态对象对应用程序的其余部分可用。

    打开总管index.js文件,并将其添加到我注册的自定义元素的上方:

    进口observableFactory”。/模型/ observable.js '进口actionsFactory”。/模型/ actions.js '常量INITIAL_STATE作者currentFilter“所有”常量observableStateobservableFactoryINITIAL_STATE常量行动actionsFactoryobservableState窗口applicationContext对象冻结observableState行动

    有两个对象可用:代理observableState行动与突变。的INITIAL_STATE用初始数据引导应用程序。这将设置初始的零配置状态。对象的操作突变在可观察状态中执行,并通过更改对象为所有侦听器触发更新observableState对象。

    因为突变没有通过连接到web组件applicationContext然而,UI不会跟踪任何更改。web组件将需要HTML属性来改变和显示状态数据。这是接下来的事情。

    观察到的属性

    对于web组件,状态的变化可以通过属性web API跟踪。这些都是getAttributesetAttribute,hasAttribute.有了这个武器库,在DOM中持久化UI状态会更有效。

    打开ObservableElement.js然后把它取出来,用下面的代码代替:

    出口默认的ObservableElement扩展HTMLElement得到作者如果hasAttribute“作者”返回返回JSON解析getAttribute“作者”作者价值如果构造函数observedAttributes包括“作者”setAttribute“作者”JSONstringify价值得到currentFilter如果hasAttribute“current-filter”返回“所有”返回getAttribute“current-filter”currentFilter价值如果构造函数observedAttributes包括“current-filter”setAttribute“current-filter”价值connectAttributes窗口applicationContextobservableStateaddChangeListener状态= >作者状态作者currentFilter状态currentFilterattributeChangedCallbackupdateContent

    我故意在current-filter属性。这是因为属性web API只支持小写名称。getter/setter在这个web API和类的期望之间进行映射,这是驼峰情况。

    connectAttributes方法添加自己的侦听器来跟踪状态突变。有一个attributeChangedCallback可用的,当属性改变时触发,并且web组件更新DOM中的属性。这个回调也调用updateContent告诉web组件更新UI。ES6+ getter/setter声明状态对象中相同的属性。这就是为什么this.authors,例如,可访问的web组件。

    注意使用constructor.observedAttributes.这是我现在可以声明的自定义静态字段,所以父类ObservableElement可以跟踪web组件关心的属性。这样,我就可以选择状态模型的哪个部分与web组件相关。

    我将利用这个机会充实实现的其余部分,通过每个web组件中的观察对象跟踪和更改状态。这就是当状态发生变化时使UI“活跃起来”的原因。

    回到AuthorForm.js并做出这些改变。代码注释将告诉您将它放在哪里(或者您可以咨询<一个href="https://github.com/sitepoint-editors/framework-less-web-components/blob/master/components/AuthorForm.js">回购):

    //在顶部,类声明的正下方静态得到observedAttributes返回“current-filter”//在输入事件处理程序中,resetForm的正上方addAuthor的名字输入0价值电子邮件输入1价值主题选择价值= = =“主题”?''选择价值//在选择事件处理程序中,console.log的正下方changeFiltere目标价值//在connectedCallback方法的最后超级connectAttributes//这些helper方法位于类的底部addAuthor作者窗口applicationContext行动addAuthor作者changeFilter过滤器窗口applicationContext行动changeFilter过滤器updateContent//获取状态突变以同步搜索过滤器//使用下拉菜单获得一个不错的效果,并重置窗体如果currentFilter= = !“所有”形式querySelector“选择”价值currentFilterresetForm形式querySelectorAll“输入”

    在Jamstack中,您可能需要调用后端API来持久化数据。我建议对这些类型的调用使用helper方法。一旦持久化状态从API返回,就可以在应用程序中更改它。

    最后,找到AuthorGrid.js并连接可观察属性(最终的文件是<一个href="https://github.com/sitepoint-editors/framework-less-web-components/blob/master/components/AuthorGrid.js">在这里):

    //在顶部,类声明的正下方静态得到observedAttributes返回“作者”“current-filter”//在connectedCallback方法的最后超级connectAttributes//这个helper方法可以放在updateContent的正上方getAuthorRow作者常量的名字电子邮件主题作者常量元素rowTemplate内容firstElementChild版本真正的常量元素querySelectorAll“td”0textContent的名字列1textContent电子邮件列2textContent主题如果currentFilter= = !“所有”& &主题= = !currentFilter元素风格显示“没有”返回元素//在updateContent内部,在最后作者地图一个= >getAuthorRow一个forEache= >表格querySelector“身体”列表末尾e

    每个web组件都可以跟踪不同的属性,这取决于UI中呈现的内容。这是一种分离组件的好方法,因为它只处理自己的状态数据。

    继续,让它在浏览器中旋转。打开开发人员工具并检查HTML。您将看到DOM中设置的属性,例如current-filter,在web组件的根。当你点击和按输入注意,应用程序会自动跟踪DOM中的状态。

    陷阱

    对于pièce de résistance,请确保打开开发人员工具,转到JavaScript调试器并找到AuthorGrid.js.然后,在任意位置设置断点updateContent.选择一个搜索筛选器。注意到浏览器多次点击这段代码了吗?这意味着更新UI的代码不是一次运行,而是每次状态发生变化时都运行。

    这是因为里面的代码ObservableElement

    窗口applicationContextobservableStateaddChangeListener状态= >作者状态作者currentFilter状态currentFilter

    目前,只有两个侦听器在状态发生变化时触发。如果web组件跟踪多个状态属性,比如this.authors,这将触发更多的更新到UI。这将导致UI更新效率低下,并可能在监听器和DOM更改足够多时导致延迟。

    要补救这个问题,就要敞开心扉ObservableElement.js和home在HTML属性设置:

    //这个可以放到可观察元素类之外常量equalDeepxy= >JSONstringifyx= = =JSONstringifyy//在作者的setter中如果构造函数observedAttributes包括“作者”& &equalDeep作者价值//在currentFilter setter里面如果构造函数observedAttributes包括“current-filter”& &currentFilter= = !价值

    这增加了一层防御编程来检测属性更改。当web组件意识到它不需要更新UI时,它会跳过设置属性。

    现在回到带有断点的浏览器,更新状态应该达到updateContent只有一次。

    <我mg decoding="async" src="https://uploads.sitepoint.com/wp-content/uploads/2021/03/1615242402framework_less_web_components_2.jpg" alt="缺乏跑一次的梗"loading="lazy">

    最后演示

    这是应用程序将看起来像观察对象和web组件:

    <我mg decoding="async" src="https://uploads.sitepoint.com/wp-content/uploads/2021/03/1615242394framework_less_web_components_1.png" alt="最后演示"loading="lazy">

    别忘了,你可以找到<一个href="https://github.com/sitepoint-editors/framework-less-web-components">在GitHub上完整的代码

    结论

    通过web组件和可观察对象的无框架应用程序可以很好地构建功能丰富的ui,而无需任何依赖。这使得应用程序的有效负载对客户来说是轻量级和快速的。

    <一个side class="flex space-x-4">

    分享本文

    Baidu