JavaScript一个>用现代JavaScript和Web组件构建一个Web应用程序 卡米洛·雷耶斯一个>一个ddress> 2021年4月5日 分享 浏览器中的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服务器一个>:在Jamstack中托管静态资产的HTTP服务器<一个href="https://www.npmjs.com/package/bootstrap">引导一个>:一套圆滑,功能强大的CSS样式,以简化web开发 如果你觉得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扩展HTMLElement{connectedCallback(){//回调方法这.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"><链接href="node_modules /引导/ dist / css / bootstrap.min.css"rel="样式表"><标题>Framework-less组件标题>头><身体><模板id="html-app"><div类="容器"><h1>作者h1><author-form>author-form><author-grid>author-grid><页脚类="fixed-bottom小"><p类="text-center mb-0">按Enter键添加一个作者条目p><p类="text-center小">与C Rp>页脚>div>模板><模板id="author-form"><形式><div类="行mt-4"><div类="上校"><输入类型="文本"类="表单控件"占位符="的名字"aria-label="的名字">div><div类="上校"><输入类型="电子邮件"类="表单控件"占位符="电子邮件"aria-label="电子邮件">div><div类="上校"><选择类="form-select"aria-label="主题"><选项>主题选项><选项>JavaScript选项><选项>HTMLElement选项><选项>ES7 +选项>选择>div><div类="上校"><选择类="form-select搜索"aria-label="搜索"><选项>搜索选项><选项>所有选项><选项>JavaScript选项><选项>HTMLElement选项><选项>ES7 +选项>选择>div>div>形式>模板><模板id="author-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"><div类="container-fluid"><一个类="navbar-brand文字光线"href="/">带有可观察对象的无框架组件一个>div>导航><html-app>html-app><脚本类型="模块"src="index.js">脚本>身体>超文本标记语言> 密切注意脚本标记类型属性设置为模块.这就是在浏览器中使用普通JavaScript解锁导入/导出的方法。的模板标记id定义启用web组件的HTML元素。我将应用程序分解为三个主要组件:html-app,author-form,author-grid.因为JavaScript中还没有定义任何东西,所以应用程序将在没有任何自定义HTML标记的情况下呈现导航栏。开始时要简单,把这个放进去ObservableElement.js.它是所有作者组件的父元素:出口默认的类ObservableElement扩展HTMLElement{} 然后,定义html-app组件App.js:出口默认的类应用程序扩展HTMLElement{connectedCallback(){这.模板=文档.getElementById(“html-app”)窗口.requestAnimationFrame(()= >{常量内容=这.模板.内容.firstElementChild.版本(真正的)这.列表末尾(内容)})}} 注意使用出口违约来声明JavaScript类。这是我通过模块当我引用主脚本文件时键入。要使用web组件,请从HTMLElement并定义connectedCallback类方法。浏览器负责其余的工作。我使用requestAnimationFrame在下次在浏览器中重新绘制之前渲染主模板。这是一种常见的技术,你会看到web组件。首先,通过元素ID获取模板。然后,克隆模板via版本.最后,列表末尾新内容进入DOM。如果遇到web组件无法呈现的问题,请确保首先检查克隆的内容是否附加到DOM中。接下来,定义AuthorGrid.jsweb组件。这一个将遵循类似的模式,并对DOM进行一些操作:进口ObservableElement从”。/ ObservableElement.js '出口默认的类AuthorGrid扩展ObservableElement{connectedCallback(){这.模板=文档.getElementById(“author-grid”)这.rowTemplate=文档.getElementById(“作者行的”)常量内容=这.模板.内容.firstElementChild.版本(真正的)这.列表末尾(内容)这.表格=这.querySelector(“表”)这.updateContent()}updateContent(){这.表格.风格.显示=(这.作者?.长度??0)= = =0?“没有”:''这.表格.querySelectorAll(“tbody tr”).forEach(r= >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扩展ObservableElement{connectedCallback(){这.模板=文档.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下的文件模型把这个放进去:常量cloneDeep=x= >JSON.解析(JSON.stringify(x))常量冻结=状态= >对象.冻结(cloneDeep(状态))出口默认的initialState= >{让听众=[]常量代理=新代理(cloneDeep(initialState),{集:(目标,的名字,价值)= >{目标[的名字]=价值的听众.forEach(l= >l(冻结(代理)))返回真正的}})代理.addChangeListener=cb= >{听众.推(cb)cb(冻结(代理))返回()= >听众=听众.过滤器(埃尔= >埃尔= = !cb)}返回代理} 乍一看这可能很可怕,但它做了两件事:劫持setter来捕捉突变,以及添加侦听器。在ES6+中,代理类启用围绕initialState对象。它可以拦截像这样的基本操作集方法,该方法在对象发生更改时执行。返回真正的让JavaScript内部机制知道突变成功了。的代理设置一个处理程序对象,其中包含集得到定义。因为我只在乎突变的状态对象集有陷阱。所有其他功能(如读取)都直接转发到原始状态对象。侦听器保留一个已订阅的回调列表,这些回调希望在发生突变时得到通知。该回调在添加侦听器后执行一次,并返回侦听回调以供将来引用。的冻结而且cloneDeep函数的存在是为了防止基础状态对象发生任何进一步的变化。这使得UI状态更具可预测性,并且在某种程度上是无状态的,因为数据只向一个方向移动。现在,去actions.js把这些整理好:出口默认的状态= >{常量addAuthor=作者= >{如果(!作者)返回状态.作者=[...状态.作者,{...作者}]}常量changeFilter=currentFilter= >{状态.currentFilter=currentFilter}返回{addAuthor,changeFilter}} 这是一个可测试的JavaScript对象,它对状态执行实际的变化。为了简洁起见,我将放弃编写单元测试,而将其留给读者作为练习。为了从web组件中触发突变,它们需要在全局中注册window.applicationContext对象。这使得这个带有突变的状态对象对应用程序的其余部分可用。打开总管index.js文件,并将其添加到我注册的自定义元素的上方:进口observableFactory从”。/模型/ observable.js '进口actionsFactory从”。/模型/ actions.js '常量INITIAL_STATE={作者:[],currentFilter:“所有”}常量observableState=observableFactory(INITIAL_STATE)常量行动=actionsFactory(observableState)窗口.applicationContext=对象.冻结({observableState,行动}) 有两个对象可用:代理observableState和行动与突变。的INITIAL_STATE用初始数据引导应用程序。这将设置初始的零配置状态。对象的操作突变在可观察状态中执行,并通过更改对象为所有侦听器触发更新observableState对象。因为突变没有通过连接到web组件applicationContext然而,UI不会跟踪任何更改。web组件将需要HTML属性来改变和显示状态数据。这是接下来的事情。 观察到的属性 对于web组件,状态的变化可以通过属性web API跟踪。这些都是getAttribute,setAttribute,hasAttribute.有了这个武器库,在DOM中持久化UI状态会更有效。打开ObservableElement.js然后把它取出来,用下面的代码代替:出口默认的类ObservableElement扩展HTMLElement{得到作者(){如果(!这.hasAttribute(“作者”))返回[]返回JSON.解析(这.getAttribute(“作者”))}集作者(价值){如果(这.构造函数.observedAttributes.包括(“作者”)){这.setAttribute(“作者”,JSON.stringify(价值))}}得到currentFilter(){如果(!这.hasAttribute(“current-filter”))返回“所有”返回这.getAttribute(“current-filter”)}集currentFilter(价值){如果(这.构造函数.observedAttributes.包括(“current-filter”)){这.setAttribute(“current-filter”,价值)}}connectAttributes(){窗口.applicationContext.observableState.addChangeListener(状态= >{这.作者=状态.作者这.currentFilter=状态.currentFilter})}attributeChangedCallback(){这.updateContent()}} 我故意在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的正下方这.changeFilter(e.目标.价值)//在connectedCallback方法的最后超级.connectAttributes()//这些helper方法位于类的底部addAuthor(作者){窗口.applicationContext.行动.addAuthor(作者)}changeFilter(过滤器){窗口.applicationContext.行动.changeFilter(过滤器)}updateContent(){//获取状态突变以同步搜索过滤器//使用下拉菜单获得一个不错的效果,并重置窗体如果(这.currentFilter= = !“所有”){这.形式.querySelector(“选择”).价值=这.currentFilter}这.resetForm(这.形式.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”)列[0].textContent=的名字列[1].textContent=电子邮件列[2].textContent=主题如果(这.currentFilter= = !“所有”& &主题= = !这.currentFilter){元素.风格.显示=“没有”}返回元素}//在updateContent内部,在最后这.作者.地图(一个= >这.getAuthorRow(一个)).forEach(e= >这.表格.querySelector(“身体”).列表末尾(e)) 每个web组件都可以跟踪不同的属性,这取决于UI中呈现的内容。这是一种分离组件的好方法,因为它只处理自己的状态数据。继续,让它在浏览器中旋转。打开开发人员工具并检查HTML。您将看到DOM中设置的属性,例如current-filter,在web组件的根。当你点击和按输入注意,应用程序会自动跟踪DOM中的状态。 陷阱 对于pièce de résistance,请确保打开开发人员工具,转到JavaScript调试器并找到AuthorGrid.js.然后,在任意位置设置断点updateContent.选择一个搜索筛选器。注意到浏览器多次点击这段代码了吗?这意味着更新UI的代码不是一次运行,而是每次状态发生变化时都运行。这是因为里面的代码ObservableElement:窗口.applicationContext.observableState.addChangeListener(状态= >{这.作者=状态.作者这.currentFilter=状态.currentFilter}) 目前,只有两个侦听器在状态发生变化时触发。如果web组件跟踪多个状态属性,比如this.authors,这将触发更多的更新到UI。这将导致UI更新效率低下,并可能在监听器和DOM更改足够多时导致延迟。要补救这个问题,就要敞开心扉ObservableElement.js和home在HTML属性设置://这个可以放到可观察元素类之外常量equalDeep=(x,y)= >JSON.stringify(x)= = =JSON.stringify(y)//在作者的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"> 分享本文 下一个 交互式数据可视化与现代JavaScript和D3一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/ajanes/">亚当•琼斯一个>一个rticle> 使用现代JavaScript语法的最佳实践一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/mdavidgreen/">大卫·格林一个>一个rticle> 一个现代JavaScript应用程序的剖析一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/jkolce/">詹姆斯Kolce一个>一个rticle> 现代JavaScript开发很困难一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/njacques/">尼尔森雅克一个>一个rticle> 如何让现代PHP更现代?预处理!一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/assertchris/">克里斯托弗·皮特一个>一个rticle> 升级你的应用到Angular 1.5组件及更高版本!一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/jraj/">杰拉吉一个>一个rticle>
交互式数据可视化与现代JavaScript和D3一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/ajanes/">亚当•琼斯一个>
使用现代JavaScript语法的最佳实践一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/mdavidgreen/">大卫·格林一个>
一个现代JavaScript应用程序的剖析一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/jkolce/">詹姆斯Kolce一个>
如何让现代PHP更现代?预处理!一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/assertchris/">克里斯托弗·皮特一个>
升级你的应用到Angular 1.5组件及更高版本!一个><一个类="文本-sm text-gray-400" href="//www.shaoxingby.com/author/jraj/">杰拉吉一个>