浏览器中的JavaScript已经发生了变化。想利用最新功能的开发者可以选择无框架的方式,而不那么麻烦。通常保留给前端框架的选项,如基于组件的方法,现在在普通的旧JavaScript中也是可行的。
在这个案例中,我将展示所有最新的JavaScript功能,使用一个以作者数据为特征的网格和搜索过滤器的用户界面。为了简单起见,一旦介绍了一种技术,我就会转到下一种技术,这样就不会重复说明问题。为此,用户界面将有一个添加选项,以及一个下拉式搜索过滤器。作者模型将有三个字段:姓名、电子邮件和一个可选的主题。表单验证将被包括在内,主要是为了展示这种无框架的技术,而不是彻底的。
曾经的痞子语言已经成长为具有许多现代特征的语言,如代理、导入/导出、可选链式运算符和网络组件。这完全符合Jamstack的要求,因为该应用通过HTML和vanilla JavaScript在客户端进行渲染。
我将不谈API,以保持对应用程序的关注,但我将指出这种集成可以在应用程序中发生。
开始使用
该应用是一个典型的JavaScript应用,有两个依赖性:一个http服务器和Bootstrap。代码只在浏览器中运行,所以除了托管静态资产外,没有后端。该代码在GitHub上供你玩耍。
假设你的机器上安装了最新的Node LTS。
mkdir framework-less-web-components
cd framework-less-web-components
npm init
这应该是一个单一的package.json 文件,在其中放置依赖项。
要安装两个依赖项。
npm i http-server bootstrap@next --save-exact
- http-server:一个HTTP服务器,用于托管Jamstack中的静态资产。
- Bootstrap:一套时尚、强大的CSS样式,以方便网络开发。
如果你觉得http-server 不是一个依赖项,而是这个应用运行的一个要求,可以选择通过npm i -g http-server 全局安装它。无论哪种方式,这个依赖性都不会被运送到客户端,而只是向客户端提供静态资产。
打开package.json 文件,通过"start": "http-server" 在scripts 下设置入口点。继续通过npm start 来启动应用程序,这将使http://localhost:8080/ 对浏览器可用。任何放在根文件夹中的index.html 文件都会自动被HTTP服务器托管。你所做的就是刷新页面以获得最新的信息。
文件夹结构看起来像这样。
┳
┣━┓ components
┃ ┣━━ App.js
┃ ┣━━ AuthorForm.js
┃ ┣━━ AuthorGrid.js
┃ ┗━━ ObservableElement.js
┣━┓ model
┃ ┣━━ actions.js
┃ ┗━━ observable.js
┣━━ index.html
┣━━ index.js
┗━━ package.json
这就是每个文件夹的用途。
components:带有App.js和自定义元素的HTML网页组件,这些元素继承于ObservableElement.jsmodel:应用状态和监听UI状态变化的突变。index.html:主要的静态资产文件,可以在任何地方托管。
要创建每个文件夹中的文件夹和文件,请运行以下程序。
mkdir components model
touch components/App.js components/AuthorForm.js components/AuthorGrid.js components/ObservableElement.js model/actions.js model/observable.js index.html index.js
集成网络组件
简而言之,Web组件是自定义HTML元素。它们定义了可以放在标记中的自定义元素,并声明了一个渲染组件的回调方法。
这里有一个关于自定义网页组件的快速介绍。
class HelloWorldComponent extends HTMLElement {
connectedCallback() { // callback method
this.innerHTML = 'Hello, World!'
}
}
// Define the custom element
window.customElements.define('hello-world', HelloWorldComponent)
// The markup can use this custom web component via:
// <hello-world></hello-world>
如果你觉得你需要一个更温和的网络组件介绍,请查看MDN的文章。一开始,他们可能会觉得很神奇,但对回调方法的良好掌握使这一点变得非常清楚。
主index.html 静态页面声明了HTML网页组件。我将使用Bootstrap对HTML元素进行样式设计,并引入index.js ,该资产将成为应用程序的主要入口和进入JavaScript的门户。
猛然打开index.html 文件,把这个东西放进去。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<title>Framework-less Components</title>
</head>
<body>
<template id="html-app">
<div class="container">
<h1>Authors</h1>
<author-form></author-form>
<author-grid></author-grid>
<footer class="fixed-bottom small">
<p class="text-center mb-0">
Hit Enter to add an author entry
</p>
<p class="text-center small">
Created with ❤ By C R
</p>
</footer>
</div>
</template>
<template id="author-form">
<form>
<div class="row mt-4">
<div class="col">
<input type="text" class="form-control" placeholder="Name" aria-label="Name">
</div>
<div class="col">
<input type="email" class="form-control" placeholder="Email" aria-label="Email">
</div>
<div class="col">
<select class="form-select" aria-label="Topic">
<option>Topic</option>
<option>JavaScript</option>
<option>HTMLElement</option>
<option>ES7+</option>
</select>
</div>
<div class="col">
<select class="form-select search" aria-label="Search">
<option>Search by</option>
<option>All</option>
<option>JavaScript</option>
<option>HTMLElement</option>
<option>ES7+</option>
</select>
</div>
</div>
</form>
</template>
<template id="author-grid">
<table class="table mt-4">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Topic</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</template>
<template id="author-row">
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</template>
<nav class="navbar navbar-expand-lg navbar-light bg-dark">
<div class="container-fluid">
<a class="navbar-brand text-light" href="/">
Framework-less Components with Observables
</a>
</div>
</nav>
<html-app></html-app>
<script type="module" src="index.js"></script>
</body>
</html>
密切注意script 标签,其type 属性设置为module 。这就是在浏览器中解锁导入/导出vanilla JavaScript的原因。带有id 的template 标签定义了启用网络组件的HTML元素。我把这个应用程序分成了三个主要组件。html-app,author-form, 和author-grid 。因为还没有在JavaScript中定义任何东西,所以该应用程序将在没有任何自定义HTML标签的情况下渲染导航栏。
为了简单起见,把这个放在ObservableElement.js 。它是所有作者组件的父元素。
export default class ObservableElement extends HTMLElement {
}
然后,在App.js 中定义html-app 组件。
export default class App extends HTMLElement {
connectedCallback() {
this.template = document
.getElementById('html-app')
window.requestAnimationFrame(() => {
const content = this.template
.content
.firstElementChild
.cloneNode(true)
this.appendChild(content)
})
}
}
注意使用export default 来声明JavaScript类。这是我在引用主脚本文件时通过module 类型启用的能力。要使用网络组件,从HTMLElement 继承并定义connectedCallback 类方法。浏览器会处理剩下的事情。我在浏览器中使用requestAnimationFrame ,在下一次重绘之前渲染主模板。
这是你会看到的网页组件的常见技术。首先,通过一个元素ID抓取模板。然后,通过cloneNode 克隆模板。最后,appendChild ,将新的content 到DOM中。如果你遇到任何问题,网络组件不能渲染,请确保首先检查克隆的内容是否被追加到DOM中。
接下来,定义AuthorGrid.js 网络组件。这个组件将遵循类似的模式,对DOM进行一些操作。
import ObservableElement from './ObservableElement.js'
export default class AuthorGrid extends ObservableElement {
connectedCallback() {
this.template = document
.getElementById('author-grid')
this.rowTemplate = document
.getElementById('author-row')
const content = this.template
.content
.firstElementChild
.cloneNode(true)
this.appendChild(content)
this.table = this.querySelector('table')
this.updateContent()
}
updateContent() {
this.table.style.display =
(this.authors?.length ?? 0) === 0
? 'none'
: ''
this.table
.querySelectorAll('tbody tr')
.forEach(r => r.remove())
}
}
我用一个querySelector 来定义主this.table 元素。因为这是一个类,可以通过使用this 来保持对目标元素的良好引用。当网格中没有作者要显示时,updateContent 方法主要是将主表nuk掉。可选的链式运算符(?.)和null coalescing负责将display 样式设置为无。
看一下import 语句,因为它带来了文件名中带有完全合格扩展名的依赖关系。如果你习惯于Node开发,这就是它与浏览器实现的不同之处,它遵循标准,其中确实需要一个文件扩展名,如.js 。向我学习,在浏览器中工作时一定要放上文件扩展名。
接下来,AuthorForm.js 组件有两个主要部分:渲染HTML和将元素事件连接到表单。
要渲染表单,请打开AuthorForm.js 。
import ObservableElement from './ObservableElement.js'
export default class AuthorForm extends ObservableElement {
connectedCallback() {
this.template = document
.getElementById('author-form')
const content = this.template
.content
.firstElementChild
.cloneNode(true)
this.appendChild(content)
this.form = this.querySelector('form')
this.form.querySelector('input').focus()
}
resetForm(inputs) {
inputs.forEach(i => {
i.value = ''
i.classList.remove('is-valid')
})
inputs[0].focus()
}
}
focus ,引导用户在表单中第一个可用的输入元素上开始输入。请确保将任何DOM选择器放在appendChild之后,否则这个技术将无法工作。resetForm 现在还没有使用,但是当用户按下Enter键时,将重置表单的状态。
通过addEventListener ,在connectedCallback 方法中添加这段代码来连接事件。这可以被添加到connectedCallback 方法的最末端。
this.form
.addEventListener('keypress', e => {
if (e.key === 'Enter') {
const inputs = this.form.querySelectorAll('input')
const select = this.form.querySelector('select')
console.log('Pressed Enter: ' +
inputs[0].value + '|' +
inputs[1].value + '|' +
(select.value === 'Topic' ? '' : select.value))
this.resetForm(inputs)
}
})
this.form
.addEventListener('change', e => {
if (e.target.matches('select.search')
&& e.target.value !== 'Search by') {
console.log('Filter by: ' + e.target.value)
}
})
这些是典型的事件监听器,被附加到DOM中的this.form 元素上。change 事件使用事件委托来监听表单中的所有变化事件,但只针对select.search 元素。这是一种有效的方法,可以将一个事件委托给父元素中尽可能多的目标元素。有了这个方法,在表单中输入任何东西并按下回车键,表单就会恢复到零状态。
为了让这些Web组件在客户端呈现,打开index.js ,把这个放进去。
import AuthorForm from './components/AuthorForm.js'
import AuthorGrid from './components/AuthorGrid.js'
import App from './components/App.js'
window.customElements.define('author-form', AuthorForm)
window.customElements.define('author-grid', AuthorGrid)
window.customElements.define('html-app', App)
现在可以随意在浏览器中刷新页面,玩玩用户界面。打开你的开发者工具,当你在表单中点击和输入时,看看控制台的信息。按Tab键应该可以帮助你在HTML文档的输入元素之间进行导航。