如何用现代JavaScript和Web组件构建Web应用程序

95 阅读7分钟

浏览器中的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.js
  • model:应用状态和监听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的原因。带有idtemplate 标签定义了启用网络组件的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文档的输入元素之间进行导航。

继续阅读:在SitePoint 上用现代JavaScript和Web组件构建一个Web应用程序