React-原生-Web-组建构建指南-全-

React 原生 Web 组建构建指南(全)

原文:Building Native Web Components

协议:CC BY-NC-SA 4.0

一、制作您的第一个 Web 组件

欢迎构建您的第一个 web 组件。本章讨论了创建第一个 web 组件所需的各种工具、技术、设计和开发概念。您将学习什么是 web 组件,以及什么是 web 浏览器支持、设计系统和组件驱动开发(CDD)。

什么是 Web 组件?

在高层次上,Web 组件是孤立的部分(块的种类),用户界面(UI)可以通过属性和事件(来自这些块的输入和输出)与其他元素进行通信。以<video>元素为例。我们可以在浏览器的任何技术中使用这个元素,我们可以传递像widthheight这样的属性,并监听像onclick这样的事件。

更严格地说,我们可以说 web 组件是一组 web 平台 API(应用编程接口),允许我们构建 HTML 标签,这些标签将跨现代 Web 浏览器工作,并可以与任何 JavaScript 技术(React、Angular、Vue.js 等)一起使用。).

Web 组件有四个主要规范:

  • 自定义元素

  • 影子天赋

  • 是模块

  • HTML 模板

我将在接下来的章节中更深入地讨论这些规范。

Web 组件的历史

目前,Web 组件在前端环境中无处不在。三个最流行的框架(Angular、React 和 Vue.js)使用 Web 组件作为其架构的一部分。情况并不总是如此。Web 组件是随着时间一点点发展起来的。第一个重大进展是在 2010 年用 AngularJS ( https://angularjs.org )实现的,这是一个框架,它引入了指令的概念,作为一种创建自己的标签的方法,用它们自己的特性来构建 ui。后来,在 2011 年,亚历克斯·罗素在 Fronteers 会议上发表了题为“Web 组件和模型驱动的视图”的演讲,阐述了一些现在普遍使用的关键概念和想法。12013 年,谷歌通过 Polymer 向前迈出了又一大步,这是一个基于 web 组件(使用 web APIs)的库,它已经成为一个为更好的 Web 构建库、工具和标准的工具。

为什么要使用 Web 组件?

今天,所有前端开发人员都面临着两个重大问题,它们会消耗公司的精力、时间和财务。这些如下。

遗产

遗留是软件开发中的一个众所周知的问题,指的是必须在某个时候更新的旧代码库,以便与新的 JavaScript 项目和工具一起操作。

框架变动

JavaScript 的工具及其框架生态系统正在快速变化。为一个新项目选择正确的框架可能会令人紧张和疲惫,因为我们无法猜测这个框架会持续多久。这种相关性问题以及它如何影响对一组可能很快过时的工具的培训和开发的投资,被称为框架变动。

请记住,web 组件是一组 Web 平台规范。因此,它们可能会在 web 浏览器中使用很长一段时间,并提供许多好处,包括:

  • Web 组件是可重用的,并且在框架之间工作。

  • Web 组件可以在所有主流的 web 浏览器中运行。

  • Web 组件易于维护,并为未来做好了准备,这主要是因为它们基于 web 平台规范。

Web 组件生态系统的基本概念

在本书中,我将会用到一些与技术、方法或模式相关的术语,当我们在 web 应用中使用 Web 组件时,我们可以应用这些术语。这些如下。

设计系统

设计系统是可重用组件、指南和工具的目录或集合,允许组织中的团队构建数字产品以更有效地工作,并为他们的所有产品应用一致的品牌。这种方法的一些例子如下:

组件驱动开发

组件驱动开发意味着通过构建独立的组件来设计软件应用。每个组件都有一个接口或 API 来与系统的其余部分进行通信。使用这种方法的一些优点是

  • 更快的开发:将开发分成组件允许你用小范围和小目标构建模块化的部分。这意味着您可以更快地开发,并更快地让测试部分在其他系统中重用。

  • 更简单的维护:当您必须添加或更新应用的功能时,您只需更新组件,而不必重构应用中更重要的部分。

  • 可重用性:模块化组件允许可重用的功能,并且可以扩展来构建多个应用,消除了反复重写它们的需要。

  • 测试驱动开发(TDD) :实现单元测试来验证每个模块化组件的功能变得更加容易。

  • 更好地理解系统:当系统由模块化组件组成时,它变得更容易掌握、理解和操作。

对 Web 组件的浏览器支持

在撰写本文时(2020 年初),所有主流 web 浏览器都支持 Web 组件(见图 1-1 )。

img/494195_1_En_1_Fig1_HTML.jpg

图 1-1

支持 Web 组件主要规范的主流浏览器

入门指南

要开始使用 Web 组件构建应用,您必须了解并安装一些技术和工具。

cmder(仅适用于 Windows)

cmder是一个 Windows 的终端模拟器。默认情况下,Windows 操作系统附带一个对开发没有用的终端(命令提示符)。这就是为什么我们需要cmder,这是一个模拟器,我们可以使用它在我们的终端中流畅地运行命令。

要访问该仿真器,请转到cmder.net并下载最新版本。

将文件解压到您的C:/位置。

进入系统属性➤环境变量,编辑路径变量,如图 1-2 。

img/494195_1_En_1_Fig2_HTML.jpg

图 1-2

系统属性中的环境变量首选项

cmder位置添加到Path变量中,如图 1-3 。

img/494195_1_En_1_Fig3_HTML.jpg

图 1-3

Path系统属性中的变量首选项

从选择命令提示符运行cmder,测试环境变量(图 1-4 )。

img/494195_1_En_1_Fig4_HTML.jpg

图 1-4

从选择命令提示符运行cmder

Node.js

Node.js 是一个 JavaScript 运行时环境。大多数使用 JavaScript 的项目使用 Node 来安装依赖项并创建脚本来自动化开发工作流。

您必须在计算机上安装节点。可以从 https://nodejs.org/en/ 下载。

下载安装程序后,运行它并按照说明进行操作。

给麦克的

如果您使用的是 Mac,请遵循图 1-5 中所示的 Node.js 的安装说明。

img/494195_1_En_1_Fig5_HTML.jpg

图 1-5

Node.js Mac 安装

然后打开你的终端运行$node –v。如果一切正常,您将在您的终端中看到节点版本,如图 1-6 所示。

img/494195_1_En_1_Fig6_HTML.jpg

图 1-6

终端中的节点版本

对于 Windows

要安装 Node.js for Windows,请遵循图 1-7 中所示的安装程序说明。

img/494195_1_En_1_Fig7_HTML.jpg

图 1-7

Node.js Windows 安装

然后,当你完成后,打开cmder并运行$ node –v

新公共管理

当你安装 Node.js 的时候,你也安装了npmnpm是 Node.js 的包管理器,允许用户在他们的 JavaScript 项目中安装依赖项和运行小脚本。

适用于 Mac 和 Windows

通过$ npm –v检查终端上运行的npm版本。如果一切正常,你会在你的终端看到npm版本,如图 1-8 。

img/494195_1_En_1_Fig8_HTML.jpg

图 1-8

npm终端中的版本

谷歌 Chrome

Chrome 是一个网页浏览器,它为网页组件提供了出色的支持,并包括 Chrome DevTools,这是一个为开发者提供的便利功能。你可以从 www.google.com/chrome/ 下载安装 Chrome。

适用于 Mac 和 Windows

要安装 Chrome for Mac 和 Windows,请运行安装程序并遵循相关步骤。Chrome 安装成功后,打开它,你会看到一个欢迎屏幕(图 1-9 )。

img/494195_1_En_1_Fig9_HTML.jpg

图 1-9

谷歌浏览器安装

Chrome DevTools(铬 DevTools)

Chrome DevTools 是谷歌 Chrome 浏览器中包含的一套网络开发工具。作为一名开发者,这个工具可以帮助你诊断应用中的问题,并使它变得更快。要打开,按 Command+Option+J (Mac)或 Control+Shift+J (Windows、Linux、Chrome OS),直接跳到控制台面板(图 1-10 )。

img/494195_1_En_1_Fig10_HTML.jpg

图 1-10

Google Chrome DevTools

灯塔

Lighthouse 是一个开源的自动化工具,用于提高网页质量。Lighthouse 可以在 Chrome 的 DevTools 中找到。 2 进入审计页签访问(图 1-11 )。

img/494195_1_En_1_Fig11_HTML.jpg

图 1-11

Google chrome devtools audit tab(Google chrome devtools 审核选项卡)

某视频剪辑软件

本书中的一些例子将使用 Vue.js 框架。Vue.js 是一个简单明了的 JavaScript 框架。Vue 主要面向视图层,但是您可以添加您需要的内容,并使用其生态系统中的所有工具构建强大的渐进式 web 应用。

在你的项目中使用 Vue 真的很简单。您只需在您的index.html中添加以下内容,如清单 1-1 所示。

<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

Listing 1-1Adding Vue from the cdn Development Version

或者添加产品版本,如清单 1-2 所示。

<!-- production version, optimized for size and speed -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

Listing 1-2Adding Vue from the cdn Production Version

CLI 视图

Vue CLI 是一个用于快速 Vue.js 开发的完整系统。多亏了这个工具,我们可以在处理 Webpack、EsLint 和其他工具时避免一些额外的工作,并专注于在我们的应用中构建业务逻辑。您必须在您的终端中运行以下命令,将它安装到您的系统中:$npm install -g @vue/cli

如果一切正常,您将在您的终端中看到 Vue CLI 版本,如图 1-12 所示。

img/494195_1_En_1_Fig12_HTML.jpg

图 1-12

终端中的 Vue CLI 版本

饭桶

Git 是一个版本控制系统,旨在处理我们项目中的不同变更。我们将使用 Git 来操作我们的 web 应用项目,并处理每章中概述的连续步骤。可以从 https://git-scm.com/downloads 下载安装 Git。

适用于 Mac 和 Windows

要安装,请运行安装程序并按照步骤操作。完成后,打开cmder/terminal并运行$ git –version

如果一切正常,您将在您的终端中看到 Git 版本,如图 1-13 所示。

img/494195_1_En_1_Fig13_HTML.jpg

图 1-13

终端中的 Git 版本

Firebase(火力基地)

Firebase 是一个云服务,可以帮助你自动化后端开发。您可以将 Firebase 理解为一个无需后端知识就可以保存数据、资产和验证用户身份的地方。Firebase 很强大,谷歌也支持它。对于我们的项目,您必须通过$npm install -g firebase-tools在您的终端中安装 Firebase CLI。

此外,您必须在 https://firebase.google.com/ 注册并创建一个新项目。我创建了项目“新闻-书籍-网页组件”(图 1-14 )。我将使用这个项目来连接和发布本书涵盖的所有功能。

img/494195_1_En_1_Fig14_HTML.jpg

图 1-14

Firebase web 控制台项目概述

在本书中,我们将使用认证、数据库和托管来增强我们的应用。

Firebase 认证

Firebase Authentication 是一项服务,允许我们在应用中使用身份验证系统,以处理安全和服务器相关的问题。

您可以通过开发➤认证从您的 web 控制台( https://console.firebase.google.com )访问 Firebase 认证(图 1-15 )。

img/494195_1_En_1_Fig15_HTML.jpg

图 1-15

Firebase web 控制台身份验证

Firebase 数据库

Firebase Database 是一项服务,我们可以通过它添加一个远程数据库来保存我们的用户数据。此外,它是我们应用中处理实时信息的一个极好的选项,这意味着我们可以从移动或桌面设备打开我们的应用,并且会显示相同的信息。

您可以在开发➤数据库中的 web 控制台( https://console.firebase.google.com )中找到 Firebase 数据库(图 1-16 )。

img/494195_1_En_1_Fig16_HTML.jpg

图 1-16

Firebase web 控制台数据库

Firebase 托管

Firebase Hosting 是一个托管服务,你可以用它来服务你所有的静态文件,连接你的域,并快速获得一个 SSL 证书。它也易于部署。

你可以通过开发➤主机从你的网络控制台( https://console.firebase.google.com )找到 Firebase 主机(图 1-17 )。

img/494195_1_En_1_Fig17_HTML.jpg

图 1-17

Firebase web 控制台托管

Visual Studio 代码

Visual Studio Code 是一个免费的代码编辑器,它通过一组集成的工具以及通过插件扩展它们的可能性来帮助开发。可以从 https://code.visualstudio.com/ 下载 Visual 工作室代码。

适用于 Mac 和 Windows

要安装 Visual Studio 代码,只需运行安装程序并按照步骤操作。然后从应用/程序列表中打开 Visual Studio 代码。

有许多代码编辑器可用,但我们在本书中打算使用 Visual Studio 代码,主要是因为它是免费的,工作流畅,并且有一个大的插件生态系统。

开发我们的第一个 Web 组件

现在我们将创建我们的第一个 web 组件,一个我们称之为vanilla-placeholder-component的占位符。有了这个组件,你可以用红色背景和单词“placeholder”填充网页上的块,如图 1-18 所示。

img/494195_1_En_1_Fig18_HTML.jpg

图 1-18

占位符组件

这个组件在我们的 HTML 中的基本用法如清单 1-3 所示。

<vanilla-placeholder-content></vanilla-placeholder-content>

Listing 1-3Using vanilla-placeholder-component

我们可以添加一些属性,如清单 1-4 所示。

<vanilla-placeholder-content height="100px" width="50px"></vanilla-placeholder-content>

Listing 1-4Using vanilla-placeholder-component with Attributes

我们的组件接受高度和宽度属性来定制大小,但是如果我们不提供该信息,我们将默认为两者指定 100 像素。

首先我们必须创建一个文件index.html并用一个基本结构填充它,如清单 1-5 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - vanilla-placeholder</title>
</head>
<body>
</body>
</html>

Listing 1-5index.html—Basic Structure

有了这段代码,我们就有了一个正文中什么都没有的基本 HTML 页面。因此,我们将在</body>之前添加一些带有<script></script>标签的 JavaScript,并且我们将添加基本结构来创建一个定制组件,如清单 1-6 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - vanilla-placeholder</title>
</head>
<body>
<script>
class VanillaPlaceholderContent extends HTMLElement {
    constructor() {}
}
customElements.define('vanilla-placeholder-content', VanillaPlaceholderContent);
</script>
</body>
</html>

Listing 1-6Adding a Custom Component in index.html

这样,我们将定义我们的标签<vanilla-placeholder-content>并创建一个继承自HTMLElement ( https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement )的 JavaScript 类,并给我们定义组件的机会。

最后,我们将向VanillaPlaceholderContent类添加一些代码,如清单 1-7 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - vanilla-placeholder</title>
</head>
<body>
<script>
class VanillaPlaceholderContent extends HTMLElement {

    constructor() {
        super();
        const placeholder = document.createElement('template');
        const height = this.getAttribute('height') || '100px';
        const width = this.getAttribute('width') || '100px';
        placeholder.innerHTML = VanillaPlaceholderContent.template(height, width);

        this.appendChild(document.importNode(placeholder.content, true));
    }

    static template (height, width) {
        return `
        <style>
        .placeholder {
            background-color: red;
            width: ${height};
            height: ${width};
        }
        </style>
        <div class="placeholder">Placeholder</div>`;
    }

}
customElements.define('vanilla-placeholder-content', VanillaPlaceholderContent);
</script>
</body>
</html>

Listing 1-7Adding Component Logic to vanilla-placeholder-component

一般来说,我们使用constructor()来初始化我们的组件,使用this.getAttribute(''),我们检查我们是否得到了一些属性,比如高度和宽度。接下来,我们使用template()方法来创建我们的元素和样式,最后,我们使用this.appendChild(document.importNode(placeholder.content, true));将它们添加到我们的 UI 中。

我们可以在网络浏览器中看到结果(图 1-19 )。

img/494195_1_En_1_Fig19_HTML.jpg

图 1-19

web 浏览器中的占位符组件

如果有些事情暂时难以理解,也不要担心。在接下来的章节中,你将会学到更多关于这个 API 的知识,以及它为什么有用。

您可以在$git checkout chap-1访问本书( https://github.com/carlosrojaso/apress-book-web-components )的源代码。

摘要

在本章中,您学习了以下内容:

  • 什么是 Web 组件,当前主流浏览器的支持是什么

  • 什么是设计系统,我们可以在网上找到一些例子

  • 什么是组件驱动开发(CDD ),在我们的软件应用中使用这种方法有什么好处

二、自定义元素

在本章中,我们将探索 Web 组件集中的自定义元素规范。您将了解什么是定制元素,如何创建它们,以及定制元素的生命周期是什么。然后,我们将为我们的集合构建一个新的 web 组件。

什么是自定义元素?

自定义元素是一种机制,web 开发人员可以使用它来创建新的 HTML 标记。我们可以使用CustomElementRegistry对象来创建我们的标签。例如,我们可以定义一个random-icon-placeholder,如清单 2-1 所示。

class randomIconPlaceholder extends HTMLElement {
    constructor(){...}
}
customElements.define('random-icon-placeholder', randomIconPlaceholder);

Listing 2-1Defining a Web Component with CustomElements

这里,我们使用小写的名称,由连字符(kebab-case)分隔,这是为自定义标记指定名称所必需的。此外,我们正在使用一个从HTMLElement扩展而来的类。HTMLElement是 HTML 中的主对象,文档对象模型(DOM)中的任何元素都会继承它的属性。(你可以在 https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement 找到更多信息)。)

有两种类型的自定义元素:自主的和自定义的。一个自治的定制元素不会继承另一个标准的 HTML 元素,比如<p><a><br>等等。我们可以说上一个例子中的<random-icon-placeholder>(清单 2-1 )是一个自治的定制元素。

定制的内置元素从另一个标准 HTML 元素继承而来。例如,我们可以定义一个元素来扩展<p>元素,如清单 2-2 所示。

class randomParagraphSizePlaceholder extends HTMLParagraphElement {
    constructor(){...}
}

customElements.define('random-paragraph-size-placeholder',
randomParagraphSizePlaceholder, {extends: p});

Listing 2-2Defining a Customized CustomElement

现在我们可以在 HTML 文档中使用这个元素,如清单 2-3 所示。

<p is="random-paragraph-size-placeholder">Some text</p>

Listing 2-3Using a Customized CustomElement

你可以在 https://html.spec.whatwg.org/multipage/indices.html#element-interfaces 找到要继承的接口列表。

自定义元素的生命周期挂钩

当我们定义一个定制元素时,我们可以使用生命周期挂钩在组件生命周期的特定时刻运行代码。在我们的自定义元素中有四个主要的时刻可以使用。

  • constructor:创建或升级元素实例时触发。它对于初始化变量、添加事件侦听器或创建影子 DOM 非常有用。

  • connectedCallback:每次在文档中追加自定义元素时触发。这将在每次移动节点时发生,并且可能在元素的内容被完全解析之前发生。

  • attributeChangedCallback (attrName, oldVal, newVal):每次添加、删除或更改定制元素的属性时都会调用这个函数。注意到变化的观察属性是用static get observedAttributes方法指定的。

  • disconnectedCallback:每次自定义元素从文档的 DOM 断开时调用。

我们可以在图 2-1 中看到 Web 组件生命周期的所有前述方法。

img/494195_1_En_2_Fig1_HTML.jpg

图 2-1

Web 组件生命周期

构建自定义元素

为了学习如何使用customElements对象和生命周期挂钩,我们将创建randomParagraphSizePlaceholder组件。这个简单的组件生成一个 12 到 50px 之间的随机数,并接收属性'text'

要在我们的 HTML 文档中使用这个组件,我们必须调用<random-paragraph-size-placeholder>,如清单 2-4 所示。

<random-paragraph-size-placeholder text="My Personal Text"></random-paragraph-size-placeholder>

Listing 2-4Using random-paragraph-size-placeholder

接下来,我们必须为自治的定制元素创建一个通用结构,如清单 2-5 所示。

class RandomParagraphSizePlaceholder extends HTMLElement {
    constructor(){...}
}
customElements.define('random-paragraph-size-placeholder', RandomParagraphSizePlaceholder);

Listing 2-5Declaring random-paragraph-size-placeholder Component

这样,web 浏览器就知道我们想要注册一个新的 HTML 标签。

稍后,我们将添加生命周期挂钩作为方法,我们将添加console.log(),以了解方法何时被触发。(参见清单 2-6 。)

class RandomParagraphSizePlaceholder extends HTMLElement {
    constructor(){
      console.log(`contructor.`)
    }
   connectedCallback() {
        console.log(`connectedCallback hook`);
    }
    disconnectedCallback() {
        console.log(`disconnectedCallback hook`);
    }

 attributeChangedCallback(attrName, oldVal, newVal) {
        console.log(`attributeChangedCallback hook`);
    }
}
customElements.define('random-paragraph-size-placeholder', RandomParagraphSizePlaceholder);

Listing 2-6Defining

random-paragraph-size-placeholder Component

为了让attributeChangedCallback()方法正确工作,我们必须添加静态方法observedAttributes()并返回我们想要观察的属性。在本例中,我们只有'text'属性,如清单 2-7 所示。

static get observedAttributes() {
        return ['text'];
    }

Listing 2-7Adding Text to Be Observed in attributeChangedCallback

接下来,我们将在构造函数中添加基本逻辑,生成随机数并将它们发送给template方法,如清单 2-8 所示。

constructor() {
        console.log(`constructor hook`);
        super();
        const placeholder = document.createElement('template');
        const myText = this.getAttribute('text') || 'Loren Ipsum';
        const randomSize = Math.floor((Math.random() * (50 - 12 + 1)) + 12);
        placeholder.innerHTML = RandomParagraphSizePlaceholder.template(myText, randomSize);
        this.appendChild(document.importNode(placeholder.content, true));
    }

Listing 2-8Adding the Logic to Initialize random-paragraph-size-placeholder

总之,代码将如清单 2-9 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - random-paragraph-size-placeholder</title>
</head>
<body>
<div id="parent">
    <random-paragraph-size-placeholder text="My Personal Text"></random-paragraph-size-placeholder>
</div>
<button id="myButton" onclick="removeElement()">Remove Element</button>
<script>
class RandomParagraphSizePlaceholder extends HTMLElement {

    constructor() {
        console.log(`constructor hook`);
        super();
        const placeholder = document.createElement('template');
        const myText = this.getAttribute('text') || 'Loren Ipsum';
        const randomSize = Math.floor((Math.random() * (50 - 12 + 1)) + 12);
        placeholder.innerHTML = RandomParagraphSizePlaceholder.template(myText, randomSize);

        this.appendChild(document.importNode(placeholder.content, true));
    }

    static get observedAttributes() {
        return ['text'];
    }

    set text(val) {
        if (val) {
            this.setAttribute(`text`, val);
        } else {
            this.setAttribute(`text`, ``);
        }
    }

    get text() {
        return this.getAttribute('text');
    }

    connectedCallback() {
        console.log(`connectedCallback hook`);
    }
    disconnectedCallback() {
        console.log(`disconnectedCallback hook`);
    }

    attributeChangedCallback(attrName, oldVal, newVal) {
        console.log(`attributeChangedCallback hook`);
        console.log(`attrName`, attrName);
        console.log(`oldVal`, oldVal);
        console.log(`newVal`, newVal);
    }

    static template (myText, randomSize) {
        return `
        <div style="font-size:${randomSize}px">${myText}</div>`;
    }
}
customElements.define('random-paragraph-size-placeholder', RandomParagraphSizePlaceholder);

const element = document.querySelector('random-paragraph-size-placeholder');

function removeElement() {
    const parentElement = document.getElementById('parent')
    parentElement.removeChild(element);
    const myButton = document.getElementById('myButton');
    myButton.disabled = true;
}
</script>
</body>
</html>

Listing 2-9Final Code for random-paragraph-size-placeholder

您还会注意到,我添加了一个额外的函数removeElement,来看看当我从 DOM 中移除组件时disconnectedCallback()是如何被触发的。您可以在$git checkout chap-2获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

摘要

在本章中,您学习了以下内容:

  • 什么是CustomElementRegistry对象以及如何使用它

  • 自定义元素的两种主要类型是什么

  • 什么是生命周期挂钩,什么时候触发

三、HTML 模板

在这一章中,我们将研究 HTML 模板,Web 组件集中的另一个规范。您将学习什么是 HTML 模板,以及如何在 web 组件中使用 HTML 模板。然后,我们将为我们的集合构建一个新的 web 组件。

什么是 HTML 模板?

HTML 模板规范定义了<template>元素,以创建在我们的定制元素中不使用的标记片段,直到我们稍后在运行时激活它们。这些片段可以通过脚本克隆并插入到 HTML 中。

<template>中的内容具有以下属性:

  • 内容只有在激活后才会呈现。<template>中的标记是隐藏的,不会呈现。

  • 内容不会有副作用。脚本、图像和媒体标签在激活之前不会运行。

  • 该内容将不被视为在文档对象模型(DOM)中。使用getElementById()querySelector()不会返回模板的子节点。

清单 3-1 中说明了使用<template>的基本方法。

<template id="my-error-message">
    <p>
        Some error messages.
    </p>
</template>

Listing 3-1Basic Example of Using <template>

我的段落在 DOM 中是隐藏的,如图 3-1 所示。

img/494195_1_En_3_Fig1_HTML.jpg

图 3-1

使用模板在网页中隐藏段落

如果我想显示我的内容,我必须用代码激活它,如清单 3-2 所示。

let myTemplate = document.getElementById('my-error-message');
let myContent = myTemplate.content;
document.body.appendChild(myContent);

Listing 3-2Activating Content in <template>

这样,我们就可以激活我们文档中的内容,如图 3-2 所示。

img/494195_1_En_3_Fig2_HTML.jpg

图 3-2

激活网页中的template

时间

除了<template>,我们还可以在内容中利用<slot>。插槽允许你在模板中定义占位符,如图 3-3 所示。结合其他 Web 组件规范,插槽在元素内插入标记时非常有用。

清单 3-3 中概述了使用<slot>的基本方法。

img/494195_1_En_3_Fig3_HTML.jpg

图 3-3

用消息填充error-component中的槽

<p>
    <slot>This is a default message</slot>
</p>

Listing 3-3Using <slot>

现在,我们将创建一个组件来处理应用中的错误和警告消息。用这个组件,你可以发送一种错误或警告消息,以及你想在<error-component></error-component>之间显示的消息。如果您正在发送错误消息,您将会在红色背景下看到该消息。黄色背景下将显示一条警告消息。

首先,我们将为组件创建基本结构,如清单 3-4 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - error-component</title>
</head>
<body>
<script>
class ErrorComponent extends HTMLElement {
    constructor() {
        super();
    }
}
customElements.define('error-component', ErrorComponent);
</script>
</body>
</html>

Listing 3-4Basic Structure of error-component

现在我们将创建静态方法template(),用它我们将生成我们的标记,如清单 3-5 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - error-component</title>
</head>
<body>
<script>
class ErrorComponent extends HTMLElement {
    constructor() {
        super();
    }

   static template () {
        return `
        <template class="warning-type">
            <style>
                .warning {
                    background-color: yellow;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="warning">
                <slot>Error component<slot>
            </div>
        </template>
        <template class="error-type">
            <style>
                .error {
                    background-color: red;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="error">
                <slot>Error component<slot>
            </div>
        </template>
        <template class="none-type">
            <style>
                .none {
                    background-color: gray;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="none">
                <slot>Error component<slot>
            </div>
        </template>
        `;
    }

}
customElements.define('error-component', ErrorComponent);
</script>
</body>
</html>

Listing 3-5Adding the template() Method

在我们的标记中,我们有三个<template>块——每个块对应一种我们可以接收的消息:错误、警告和无。此外,我们在每个标签中添加了<slot>,它将接受我们在标签之间传递的值,如清单 3-6 所示。

<error-component>Value that the slot going to take</error-component>

Listing 3-6Passing Error Messages with Slots

最后,我们将使用生命周期挂钩connectedCallback()来处理选择使用哪个模板的逻辑,如清单 3-7 所示。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo - error-component</title>
</head>
<body>
<script>
class ErrorComponent extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this.root = this.attachShadow({mode: 'open'});
        this.templates = document.createElement('div');
        this.container = document.createElement('div');
        this.root.appendChild(this.templates);
        this.root.appendChild(this.container);
        this.templates.innerHTML = ErrorComponent.template();
        const kind = this.getAttribute(`kind`) || `none`;

        const template = this.templates.querySelector(`template.${kind}-type`);
        if (template) {
            const clone = template.content.cloneNode(true);
            this.container.innerHTML = '';
            this.container.appendChild(clone);
        }
    }

   static template () {
        return `
        <template class="warning-type">
            <style>
                .warning {
                    background-color: yellow;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="warning">
                <slot>Error component<slot>
            </div>
        </template>
        <template class="error-type">
            <style>
                .error {
                    background-color: red;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="error">
                <slot>Error component<slot>
            </div>
        </template>
        <template class="none-type">
            <style>
                .none {
                    background-color: gray;
                    padding: 15px;
                    color: black;
                }
            </style>
            <div class="none">
                <slot>Error component<slot>
            </div>
        </template>
        `;
    }

}
customElements.define('error-component', ErrorComponent);
</script>
</body>
</html>

Listing 3-7Initializing Properties in connectedCallback()

这里,我们使用'this'在组件中进行引用,并且使用方法connectedCallback()来初始化这些属性。

我们也在使用影子 DOM 'this.attachShadow({mode: 'open'});'。影子 DOM 是下一章的主题,但是你可以把这里的这个看作是特定于我们的组件的受保护的 DOM 树。

在这个逻辑中,我们获得了'kind'属性,并呈现了正确的<template>,无论它是错误、警告还是无。结果如图 3-4 所示。

img/494195_1_En_3_Fig4_HTML.jpg

图 3-4

使用 Chrome 上的错误组件

您可以在$git checkout chap-3通过回购( https://github.com/carlosrojaso/apress-book-web-components )获取相关代码。

摘要

在本章中,您学习了以下内容:

  • 什么是<template>以及如何在我们的 web 组件中使用它

  • 什么是<slot>以及如何在我们的 web 组件中使用它

  • 如何创建 web 组件来处理错误和警告

四、影子 DOM

在这一章中,你将会熟悉 Shadow DOM,Web 组件集合中的另一个规范。您将了解什么是 Shadow DOM 以及如何在 web 组件中使用它。接下来,我们将为我们的集合构建一个新的 web 组件。

什么是影子 DOM?

影子 DOM 规范定义了一种封装 web 组件的机制。我们在 web 组件内部创建的标记和样式保护它免受外部 DOM 操作和全局 CSS 规则的影响。

例如,考虑 HTML 5 <video>标签。如果我们想要一个视频播放器,我们创建类似于清单 4-1 所示的东西。

<video width="640" height="480" controls>
  <source src="myVideo.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>

Listing 4-1Using video tag

然而,当你查看网页浏览器中呈现的内容时(图 4-1 ,你可以看到 CSS 样式、div 和输入的复杂组合,它们被封装用于外部修改,并且你只能看到标签<video>。这就是影子王国的力量。

img/494195_1_En_4_Fig1_HTML.jpg

图 4-1

<video>谷歌浏览器中的标签

以下是影子 DOM 的一些好处:

  • 影子 DOM 创建了一个独立的 DOM,允许我们在 web 组件中操作 DOM,而不用担心外部节点。

  • Shadow DOM 创建了一个作用域 CSS,这意味着我们可以创建更多的通用规则,而不用担心命名冲突。

清单 4-2 显示了使用影子 DOM 的基本方法。你可能记得我们在第三章中使用了这个方法,在清单 3-7 中,我们给我们的 web 组件添加了一个阴影 DOM,来激活/停用我们例子中的模板。

let shadowElement = element.attachShadow({mode: 'open'});

Listing 4-2Attaching a Shadow DOM

前面代码片段中的attachShadow()接收一个可以是'open''closed'的模式。'open'表示可以从主上下文访问影子 DOM,'closed'表示不能。

阴影根

影子根是我们的影子 DOM 创建的 DOM 中的根节点(图 4-2 )。

img/494195_1_En_4_Fig2_HTML.jpg

图 4-2

影子 DOM 中的影子根节点

阴影树

影子树就是我们的影子 DOM 创建的 DOM 树(图 4-3 )。

img/494195_1_En_4_Fig3_HTML.jpg

图 4-3

阴影中的阴影树

阴影边界

阴影 DOM 结束和全局 DOM 继续的界限(图 4-4 )是阴影边界。

img/494195_1_En_4_Fig4_HTML.jpg

图 4-4

我们的 web 应用 DOM 中的阴影边界

影子主机

影子主机是影子 DOM 附加到的全局 DOM 节点(图 4-5 )。

img/494195_1_En_4_Fig5_HTML.jpg

图 4-5

我们的 web 应用 DOM 中的影子主机

构建社会共享组件

为了使用 Shadow DOM,我们将构建一个名为<social-share-component>的简单组件,为我们的应用添加社交网络链接。这个组件接收两个参数,'socialNetwork''user',其中'tw'表示 Twitter,'fb'表示脸书。首先,我们将定义我们的组件,如清单 4-3 所示。

class SocialShareComponent extends HTMLElement {}
customElements.define('social-share-component', SocialShareComponent);

Listing 4-3Defining SocialShareComponent

接下来,我们将在组件中定义一些 getters 和 setters,来处理'socialNetwork''user'参数,如清单 4-4 所示。

class SocialShareComponent extends HTMLElement {

    get socialNetwork() {
        return this.getAttribute('socialNetwork') || 'tw';
    }

    set socialNetwork(newValue) {
        this.setAttribute('socialNetwork', newValue);
    }

    get user() {
        return this.getAttribute('user') || 'none';
    }

    set user(newValue) {
        this.setAttribute('user', newValue);
    }
}
customElements.define('social-share-component', SocialShareComponent);

Listing 4-4Defining SocialShareComponent

此外,我们将定义一些静态方法,向我们的组件添加标记和样式,如清单 4-5 所示。

class SocialShareComponent extends HTMLElement {

    get socialNetwork() {
        return this.getAttribute('socialNetwork') || 'tw';
    }

    set socialNetwork(newValue) {
        this.setAttribute('socialNetwork', newValue);
    }

    get user() {
        return this.getAttribute('user') || 'none';
    }

    set user(newValue) {
        this.setAttribute('user', newValue);
    }

    static twTemplate(user) {
        return `
        ${SocialShareComponent.twStyle()}
        <span class="twitter-button">
            <a href="https://twitter.com/${user}">
                Follow @${user}
            </a>
        </span>`;
    }

    static twStyle() {
        return `
        <style>
        a {
            height: 20px;
            padding: 3px 6px;
            background-color: #1b95e0;
            color: #fff;
            border-radius: 3px;
            font-weight: 500;
            font-size: 11px;
            font-family:'Helvetica Neue', Arial, sans-serif;
            line-height: 18px;
            text-decoration: none;
        }

        a:hover {  background-color: #0c7abf; }

        span {
            margin: 5px 2px;
        }
        </style>`;
    }

    static fbTemplate(user) {
        return `
        ${SocialShareComponent.fbStyle()}
        <span class="facebook-button">
            <a href="https://facebook.com/${user}">
                Follow @${user}
            </a>
        </span>`;
    }

    static fbStyle() {
        return `
        <style>
        a {
            height: 20px;
            padding: 3px 6px;
            background-color: #4267b2;
            color: #fff;
            border-radius: 3px;
            font-weight: 500;
            font-size: 11px;
            font-family:'Helvetica Neue', Arial, sans-serif;
            line-height: 18px;
            text-decoration: none;
        }

        a:hover {  background-color: #0c7abf; }

        span {
            margin: 5px 2px;
        }
        </style>`;
    }
}
customElements.define('social-share-component', SocialShareComponent);

Listing 4-5Static Methods to Add Markup and Styles

最后,我们将构建我们的constructor()方法,如清单 4-6 所示,用它我们将在组件的根中附加阴影 DOM,并在它后面附加一个 div 元素,它将作为我们的标记和样式的容器。

class SocialShareComponent extends HTMLElement {

    constructor() {
        super();

        this.root = this.attachShadow({mode: 'open'});
        this.container = document.createElement('div');
        this.root.appendChild(this.container);

        switch(this.socialNetwork) {
            case 'tw':
                this.container.innerHTML = SocialShareComponent.twTemplate(this.user);
                break;
            case 'fb':
                this.container.innerHTML = SocialShareComponent.fbTemplate(this.user);
                break;
        }
    }

    get socialNetwork() {
        return this.getAttribute('socialNetwork') || 'tw';
    }

    set socialNetwork(newValue) {
        this.setAttribute('socialNetwork', newValue);
    }

    get user() {
        return this.getAttribute('user') || 'none';
    }

    set user(newValue) {
        this.setAttribute('user', newValue);
    }

    static twTemplate(user) {
        return `
        ${SocialShareComponent.twStyle()}
        <span class="twitter-button">
            <a href="https://twitter.com/${user}">
                Follow @${user}
            </a>
        </span>`;
    }

    static twStyle() {
        return `
        <style>
        a {
            height: 20px;
            padding: 3px 6px;
            background-color: #1b95e0;
            color: #fff;
            border-radius: 3px;
            font-weight: 500;
            font-size: 11px;
            font-family:'Helvetica Neue', Arial, sans-serif;
            line-height: 18px;
            text-decoration: none;
        }

        a:hover {  background-color: #0c7abf; }

        span {
            margin: 5px 2px;
        }
        </style>`;
    }

    static fbTemplate(user) {
        return `
        ${SocialShareComponent.fbStyle()}
        <span class="facebook-button">
            <a href="https://facebook.com/${user}">
                Follow @${user}
            </a>
        </span>`;
    }

    static fbStyle() {
        return `
        <style>
        a {
            height: 20px;
            padding: 3px 6px;
            background-color: #4267b2;
            color: #fff;
            border-radius: 3px;
            font-weight: 500;
            font-size: 11px;
            font-family:'Helvetica Neue', Arial, sans-serif;
            line-height: 18px;
            text-decoration: none;
        }

        a:hover {  background-color: #0c7abf; }

        span {
            margin: 5px 2px;
        }
        </style>`;
    }
}
customElements.define('social-share-component', SocialShareComponent);

Listing 4-6Adding a constructor()

in SocialShareComponent

这里我们使用switch()来处理我们需要使用的标记和样式,这取决于'socialNetwork'参数。结果如图 4-6 所示。

img/494195_1_En_4_Fig6_HTML.jpg

图 4-6

在谷歌浏览器中使用social-share-component

您可以在$git checkout chap-4获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

摘要

在本章中,您学习了以下内容:

  • 什么是影子 DOM 以及如何在我们的 web 组件中使用它

  • 什么是影像根、影像树、影像边界和影像宿主

  • 如何使用 Shadow DOM 创建一个 web 组件,在我们的 web 应用中添加社交网络

五、ES 模块

在这一章中,我将讨论 ES 模块,Web 组件集中的另一个规范。您将学习什么是 es 模块,以及如何在 web 组件中使用 ES 模块。然后,我们将为我们的集合构建一个新的 web 组件。

什么是 ES 模块?

ES 模块规范定义了一种机制,通过该机制可以在我们的项目中通过不同的文件共享变量和函数。ES6 现在提供了 ES 模块。在此之前,如果您想要共享某个东西,您可以将它添加到全局上下文中,并使它无论是否被使用都可用。考虑清单 5-1 中的代码。

var pi = 3.1415;
var euler = 2.7182;

function getCircumference(radius) {
  return 2 * pi * radius;
}

function getCalcOneYear(interestRate, currentVal) {
  return currentVal * (euler ** interestRate);
}

console.log(getCircumference(2)); // 12.566
console.log(getCalcOneYear(0.3, 100)); // 134\. 98466170045035

Listing 5-1Using Constants in main.js

main.js中,我们有两个值pieuler,它们是函数getCircumferencegetCalcOneYear所需要的。但是如果我们在应用的不同地方的不同函数中需要pieuler呢?

为了更容易地共享这些值,我们可以创建一个新文件math-constants.js,并使用'export',告诉 JavaScript 我们可以导入该值。如清单 5-2 所示。

export const pi = 3.1415;
export const euler = 2.7182;

Listing 5-2Exporting Values in file math-constants.js

现在我们可以在其他文件中使用这些值,在 HTML 文件中使用type="module",如清单 5-3 ,或者在 JS 文件中使用import,如清单 5-4 。

import {pi, euler} from "./math-constants.js";

Listing 5-4Using ES Modules in JS Scripts

<script type="module" src="./math-constants.js"></script>

Listing 5-3Using ES Modules in HTML

图 5-1 提供了 ES 模块的图形视图。

img/494195_1_En_5_Fig1_HTML.jpg

图 5-1

ES 模块的图形表示

构建 MathOperationsComponent 组件

为了练习使用 Shadow DOM,我们将构建一个名为<math-operations-component>的简单组件,在我们的应用中添加社交网络链接。该组件接收两个参数,operation'initialValue',其中,getCircumference'接收半径为的圆周;getCalcOneYear获取一年的复利,有两个参数,利率和当前值;而'getLog2'返回2的自然对数值。

首先,我们将定义我们的组件,如清单 5-5 所示。

class MathOperationsComponent extends HTMLElement {

    constructor() {
    }

}
customElements.define('math-operations-component', MathOperationsComponent);

Listing 5-5Defining MathOperationsComponent

在与index.html相同的层中创建一个新的math-constants.js文件,并创建常量pieulerln2,使用'export'允许将这些值作为一个模块使用,如清单 5-6 所示。

export const pi = 3.1415;
export const euler = 2.7182;
export const ln2 = 0.693;

Listing 5-6Defining MathOperationsComponent

现在,在文件index.html中,我们必须添加一些东西,以便使用组件中的模块,如清单 5-7 所示。

<script type="module">
import {pi, euler, ln2} from './math-constants.js';

class MathOperationsComponent extends HTMLElement {

    constructor() {
    }

}
customElements.define('math-operations-component', MathOperationsComponent);
</script>

Listing 5-7Using Modules in MathOperationsComponent

前面代码片段中的第一个是我们代码中模块的脚本标记中的type="module"。第二个是“导入”,用于我们在math-constants.js中声明的常量。

现在,在我们的组件中,我们将创建getCircumferencegetCalcOneYeargetLN2,以返回我们在调用中发送的参数所需的值。这显示在清单 5-8 中。

    getCircumference(radius) {
        return 2 * pi * radius;
    }

    getCalcOneYear(interestRate, currentVal) {
        return currentVal * (euler ** interestRate);
    }

    getLN2() {
        return ln2;
    }

Listing 5-8Using Modules in MathOperationsComponent

注意,这里我们使用的是从模块中导入的常量。

最后,我们在constructor()中添加逻辑,以处理我们在组件的'operation''initialValue'属性中发送的参数,并为我们希望在文档中显示信息的模板和样式创建方法,如清单 5-9 所示。

<script type="module">
import {pi, euler, ln2} from './math-constants.js';

class MathOperationsComponent extends HTMLElement {

    constructor() {
        super();
        this.root = this.attachShadow({mode: 'open'});
        this.container = document.createElement('div');
        this.root.appendChild(this.container);

        switch(this.getAttribute('operation')) {
            case 'getCircumference':
                const radius = this.getAttribute('initialValue');
                this.container.innerHTML = MathOperationsComponent.getTemplate(this.getCircumference(radius));
                break;
            case 'getCalcOneYear':
                const [interestRate, currentVal] = this.getAttribute('initialValue').split(',');
                this.container.innerHTML = MathOperationsComponent.getTemplate(this.getCalcOneYear(interestRate, currentVal));
                break;
            case 'getLog2':
                this.container.innerHTML = MathOperationsComponent.getTemplate(this.getLN2());
            break;
        }
    }

    getCircumference(radius) {
        return 2 * pi * radius;
    }

    getCalcOneYear(interestRate, currentVal) {
        return currentVal * (euler ** interestRate);
    }

    getLN2() {
        return ln2;
    }

    static getTemplate(value) {
        return `
        ${MathOperationsComponent.getStyle()}
        <div>
            ${value}
        </div>
        `;
    }

    static getStyle() {
        return `
        <style>
            div {
                padding: 5px;
                background-color: yellow;
                color; black;
            }
        </style>`;
    }
}
customElements.define('math-operations-component', MathOperationsComponent);
</script>

Listing 5-9Using Modules in MathOperationsComponent

为了运行我们的代码,由于浏览器和模块的安全策略,我们必须使用静态服务器。我们可以初始化一个从项目根目录运行的节点应用

$npm init

以及回答终端中的问题。

后来,在package.json文件中,我添加了两件事,如清单 5-10 所示。

...
"scripts": {
    "start": "serve",
    ...
  },
"devDependencies": {
      "serve": "¹¹.3.2"
    }
...

Listing 5-10Adding dependencies and npm Script in Package.json

这条指令将在我们的项目中安装'serve'包,并使用'npm run start'运行一个本地服务器。

然后,要运行我们的示例,您必须进入我们的math-operations-component文件夹并运行

$npm install

稍后,运行

$npm run start

现在转到http://localhost:5000,就这样。您可以看到我们的组件正在运行(图 5-2 )。

img/494195_1_En_5_Fig2_HTML.jpg

图 5-2

在本地服务器上使用 Google Chrome 中的 ES 模块

你可以在$git checkout chap-5访问这本书的源代码( https://github.com/carlosrojaso/apress-book-web-components )。

摘要

在本章中,您学习了以下内容:

  • 什么是 ES 模块以及如何在 web 组件中使用它们

  • 如何创建 ES 模块

  • 如何使用 ES 模块创建一个 web 组件,在我们的 web 应用中添加数学函数

六、组件架构

在本章中,你将学习如何设计组件,并使它们在 web 应用中协同工作。我们将把我们的 web 应用连接到应用编程接口(API ),并为我们的组件定义数据流。

我们的 NoteApp 应用

我们将创建一个简单的 notes 应用,允许用户做笔记,如图 6-1 所示。在用户添加信息后,当用户单击按钮时,将使用带有“标题”和“描述”的模态和表单创建一个新的注释。我们必须将这些信息添加到一个注释列表中,向我们显示用户过去创建的所有注释,并从该列表中删除注释。

img/494195_1_En_6_Fig1_HTML.jpg

图 6-1

NoteApp 模型

现在,我们可以利用这个初始模型,考虑如何将元素分割成小块,这将使我们的开发更容易,并更好地面向将来可以使用的组件。

在图 6-2 中,你可以看到我们有三个主要组件(simple-form-modal-componentnote-list-componentnote-list-item-component),我们可以在app.js的逻辑中使用它们来实现我们的目标,即拥有一个注释列表、删除注释的能力以及允许添加新注释。

img/494195_1_En_6_Fig2_HTML.jpg

图 6-2

识别我们应用中的组件

定义好组件后,我们可以考虑组件的层次结构(如图 6-3 ),看看它们之间的关系。

img/494195_1_En_6_Fig3_HTML.jpg

图 6-3

识别应用中组件的层次结构

我们可以看到主要元素是app.js。我们将添加两个兄弟组件,simple-form-modal-componentnote-list-component,在note-list-component中,我们将有几个note-list-item-component元素是note-list-component的子元素。清楚地理解这种关系将有助于我们在接下来的步骤中做出其他的架构决策。

Web 组件之间的通信

当我们使用组件时,通常我们需要一种方法在父母和孩子之间发送和接收数据(如图 6-4 所示),以更新或发送用户或其他组件在我们应用的业务逻辑中的某个时刻所做的更改的通知。为此,我们使用属性获取数据,并使用事件将数据发送给其他组件。

img/494195_1_En_6_Fig4_HTML.jpg

图 6-4

识别组件之间的通信

在图 6-5 中,你可以看到我们将使用idxnote属性定义来自note-list-component的传递数据,并使用delEvent事件接收数据。

img/494195_1_En_6_Fig5_HTML.jpg

图 6-5

设计note-list-componentnote-list-item-component之间的通信机制

这意味着在我们的NoteListComponent中,我们将创建我们需要的<note-list-item-component>元素,并传递一个对象note和一个数字idx,如清单 6-1 所示。

class NoteListComponent extends HTMLElement {

    constructor() {
    }

    render() {
      return `
        <note-list-item-component note='${JSON.stringify(note)}' idx='${idx}'></note-list-item-component>`;
    }
}
customElements.define('note-list-component', NoteListComponent);

Listing 6-1Defining NoteListComponent

我们将使用JSON.stringify()来正确传递对象note,并使用JSON.parse()来接收子组件中的数据。你可以在 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringifyhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse 了解更多这些方法。

现在,在我们的NotelistItemComponent中,我们将创建一个自定义事件,NoteListComponent可以监听并知道用户何时想要从列表中删除一个注释,如清单 6-2 所示。

class NoteListItemComponent extends HTMLElement {

    constructor() {
    }

    handleDelete() {
    this.dispatchEvent(new CustomEvent('delEvent', {bubbles: true, detail:  {idx: this.idx}}));
    }
}
customElements.define('note-list-item-component', NoteListItemComponent);

Listing 6-2Adding a Custom Event in NoteListItemComponent

这里,我们创建了一个handleDelete()方法,它使用CustomEvent()创建一个事件,并在detail中发送我们需要的数据。通过这种方式,父节点将知道需要从列表中删除什么项目。你可以在 https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent 中了解更多关于CustomEvent的信息。

通常,有了属性和事件,我们可以在大多数中小规模的场景中处理组件之间的通信。尽管如此,如果你的应用更复杂,组件之间的交互更复杂,你将需要一个事件总线,你可以在 www.npmjs.com/package/js-event-bus 找到,或者像 Redux 这样的模式,你可以在 https://github.com/reduxjs/redux 找到。

notes list component

在上一节中,您了解了如何让我们的 web 组件进行通信。现在我们将构建这些组件。首先,处理注释的更自然的方式是接收一组注释,并为每个元素创建一个条目。然后,我们将获取属性notes并使用方法JSON.parse()将其转换成一个对象。我们使用_notes,以避免与组件中的设置器发生冲突,并在每次更新时再次呈现所有注释(参见清单 6-3 )。

class NoteListComponent extends HTMLElement {

    constructor() {
      super();

      this._notes = JSON.parse(this.getAttribute('notes')) || [];
      this.root = this.attachShadow({mode: 'open'});
      this.root.innerHTML = this.render();
    }

    render() {
      let noteElements = '';
      this._notes.map(
      (note, idx) => {
        noteElements += `
        <note-list-item-component note='${JSON.stringify(note)}' idx='${idx}'></note-list-item-component>`;
      }
    );
    return `
      ${noteElements}`;
    }

    get notes(){
      return this._notes;
    }

    set notes(newValue) {
      this._notes = newValue;
      this.root.innerHTML = this.render();
    }
}
customElements.define('note-list-component', NoteListComponent);

Listing 6-3getter and setter in NoteListComponent

此外,我们在 render 方法中使用了一个map(),迭代 object notes 并创建我们需要的每个项目,并用项目列表更新 Shadow DOM。

记住,每当我们需要从列表中删除一个项目时,我们都会收到一个delEvent事件。因此,我们必须在这里处理这种行为,为该事件添加一个监听器,并从列表中删除该元素,如清单 6-4 所示。

class NoteListComponent extends HTMLElement {

    constructor() {
      super();

      this._notes = JSON.parse(this.getAttribute('notes')) || [];
      this.root = this.attachShadow({mode: 'open'});
      this.root.innerHTML = this.render();
      this.handleDelEvent = this.handleDelEvent.bind(this);
    }

    connectedCallback() {
      this.root.addEventListener('delEvent', this.handleDelEvent);
    }

    disconnectedCallback () {
      this.root.removeEventListener('delEvent', this.handleDelEvent);
    }

    handleDelEvent(e) {
      this._notes.splice(e.detail.idx, 1);
      this.root.innerHTML = this.render();
    }

    render() {
      let noteElements = '';
      this._notes.map(
      (note, idx) => {
        noteElements += `
        <note-list-item-component note='${JSON.stringify(note)}' idx='${idx}'></note-list-item-component>`;
      }
    );
    return `
      ${noteElements}`;
    }

    get notes(){
      return this._notes;
    }

    set notes(newValue) {
      this._notes = newValue;
      this.root.innerHTML = this.render();
    }
}
customElements.define('note-list-component', NoteListComponent);

Listing 6-4Adding a Listener in NoteListComponent

这里,我们在connectedCallback()中添加监听器,在disconnectedCallback()中移除监听器,以避免在移除组件时出现不必要的监听器。此外,我们在构造函数中使用bind(),在构造函数中表明我们正在定义handleDelEvent(),并确保当我们想要在组件中传递方法时,它不会变得未定义。经过这些修改,我们的组件就完成了。

NoteListItemComponent

现在,NoteListComponent创建了一个包含<note-list-item-component>元素的列表,并传递了一个note对象和一个idx数字,该数字相当于音符数组中的位置。我们将在NoteListItemComponent中添加 getters 和 setters,并在constructor()中初始化这些属性。记住,笔记是一个对象,使用JSON.parse(),我们将把这些数据转换成一个对象(参见清单 6-5 )。

class NoteListItemComponent extends HTMLElement {

  constructor() {
    super();

    this._note = JSON.parse(this.getAttribute('note')) || {};
    this.idx = this.getAttribute('idx') || -1;
    this.root = this.attachShadow({mode: 'open'});
  }

  get note() {
    return this._note;
  }

  set note(newValue) {
    this._note = newValue;
  }

  get idx() {
    return this._idx;
  }

  set idx(newValue) {
    this._idx = newValue;
  }

  handleDelete() {
    this.dispatchEvent(new CustomEvent('delEvent', {bubbles: true, detail: {idx: this.idx}}));
}

customElements.define('note-list-item-component', NoteListItemComponent);

Listing 6-5Adding Getters and Setters in NoteListItemComponent

我们将添加这个组件的模板和样式,为注释获得一个好的项目(参见清单 6-6 )。

class NoteListItemComponent extends HTMLElement {

  constructor() {
    super();

    this._note = JSON.parse(this.getAttribute('note')) || {};
    this.idx = this.getAttribute('idx') || -1;
    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = this.getTemplate();
  }

  get note() {
    return this._note;
  }

  set note(newValue) {
    this._note = newValue;
  }

  get idx() {
    return this._idx;
  }

  set idx(newValue) {
    this._idx = newValue;
  }

  handleDelete() {
    this.dispatchEvent(new CustomEvent('delEvent', {bubbles: true, detail: {idx: this.idx}}));

  getStyle() {
    return `
    <style>
      .note {
        background-color: #ffffcc;
        border-left: 6px solid #ffeb3b;
      }
      div {
        margin: 5px 0px 5px;
        padding: 4px 12px;
      }
    </style>
    `;
  }

  getTemplate() {
    return`
    ${this.getStyle()}
    <div class="note">
      <p><strong>${this._note.title}</strong> ${this._note.description}</p><br/>
      <button type="button" id="deleteButton">Delete</button>
    </div>`;
  }
}
customElements.define('note-list-item-component', NoteListItemComponent);

Listing 6-6Adding the Template and Styles in NoteListItemComponent

有了这些改进,我们的<note-list-item-component>将看起来像图 6-6 。

img/494195_1_En_6_Fig6_HTML.jpg

图 6-6

note-list-item谷歌浏览器中的组件

最后,我们将为触发delEvent事件的删除按钮添加一个事件监听器。与NoteListComponent一样,我们必须在connectedCallback()中添加监听器,并在disconnectedCallback()中移除它,如清单 6-7 所示。

class NoteListItemComponent extends HTMLElement {

  constructor() {
    super();

    this._note = JSON.parse(this.getAttribute('note')) || {};
    this.idx = this.getAttribute('idx') || -1;
    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = this.getTemplate();
    this.handleDelete = this.handleDelete.bind(this);
  }

  connectedCallback() {
    this.delBtn.addEventListener('click', this.handleDelete);
  }

  disconnectedCallback () {
    this.delBtn.removeEventListener('click', this.handleDelete);
  }

  get note() {
    return this._note;
  }

  set note(newValue) {
    this._note = newValue;
  }

  get idx() {
    return this._idx;
  }

  set idx(newValue) {
    this._idx = newValue;
  }

  handleDelete() {
    this.dispatchEvent(new CustomEvent('delEvent', {bubbles: true, detail: {idx: this.idx}}));

  getStyle() {
    return `
    <style>
      .note {
        background-color: #ffffcc;
        border-left: 6px solid #ffeb3b;
      }
      div {
        margin: 5px 0px 5px;
        padding: 4px 12px;
      }
    </style>
    `;
  }

  getTemplate() {
    return`
    ${this.getStyle()}
    <div class="note">
      <p><strong>${this._note.title}</strong> ${this._note.description}</p><br/>
      <button type="button" id="deleteButton">Delete</button>
    </div>`;
  }
}
customElements.define('note-list-item-component', NoteListItemComponent);

Listing 6-7Adding an Event Listener in NoteListItemComponent

现在当用户点击删除按钮时,NoteListItemComponent将发送自定义事件,并且NoteListComponent将知道必须从列表中删除的项目是什么。

SimpleFormModalComponent

准备好NoteListComponentNoteListItemComponent后,我们现在需要一种简单的方法在我们的应用中添加新的笔记。这就是为什么我们要创建SimpleFormModalComponent,一个允许用户输入标题和描述的表单。该组件将与app.js通信,我们将使用一个 open 属性,以了解何时显示或隐藏模态,以及用户何时在表单中插入数据。我们将通过自定义事件addEvent传递该数据,如图 6-7 所示。

img/494195_1_En_6_Fig7_HTML.jpg

图 6-7

识别simple-form-modal-componentapp.js之间的通信

我们将开始定义我们的组件,添加 setters 和 getters,如清单 6-8 所示。

class SimpleFormModalComponent extends HTMLElement {

  constructor() {
      super();

      this.root = this.attachShadow({mode: 'open'});
      this.container = document.createElement('div');
      this.container.innerHTML = this.getTemplate();
      this.root.appendChild(this.container.cloneNode(true));

      this._open = this.getAttribute('open') || false;
  }
  get open() {
    return this._open;
  }

  set open(newValue) {
    this._open = newValue;
  }
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 6-8Adding Getters and Setters in SimpleFormModalComponent

现在我们将添加显示和隐藏组件所需的模板和样式(参见清单 6-9 )。

class SimpleFormModalComponent extends HTMLElement {

  constructor() {
      super();

      this.root = this.attachShadow({mode: 'open'});
      this.container = document.createElement('div');
      this.container.innerHTML = this.getTemplate();
      this.root.appendChild(this.container.cloneNode(true));

      this._open = this.getAttribute('open') || false;
  }
  get open() {
    return this._open;
  }
  set open(newValue) {
    this._open = newValue;
  }
  getTemplate() {
      return `
      ${this.getStyle()}
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button><button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>`;
  }

  getStyle() {
      return `
      <style>
        .modal {
          display: none;
          position: fixed;
          z-index: 1;
          padding-top: 100px;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          overflow: auto;
          background-color: rgb(0,0,0);
          background-color: rgba(0,0,0,0.4);
        }
        .modal-content {
          background-color: #fefefe;
          margin: auto;
          padding: 20px;
          border: 1px solid #888;
          width: 50%;
        }
        .close {
          color: #aaaaaa;
          float: right;
          font-size: 28px;
          font-weight: bold;
        }

        .close:hover,
        .close:focus {
          color: #000;
          text-decoration: none;
          cursor: pointer;
        }
      </style>`;
  }
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 6-9Adding the Template and Styles in SimpleFormModalComponent

通过这种增强,我们将展示一个有两个输入和两个按钮的表单。modal类将元素放在所有东西的顶部,并用透明的灰色背景显示在窗口的中间。此外,因为默认显示是"none",这意味着当我们将属性设置为"block"时,我们将隐藏该元素并使其可见。为了处理这种行为,我们将创建方法showModal()并在 open 属性的 setter 中触发它,如清单 6-10 所示。

class SimpleFormModalComponent extends HTMLElement {
  constructor() {
      super();

      this.root = this.attachShadow({mode: 'open'});
      this.container = document.createElement('div');
      this.container.innerHTML = this.getTemplate();
      this.root.appendChild(this.container.cloneNode(true));

      this._open = this.getAttribute('open') || false;
      this.modal = this.root.getElementById("myModal");
  }
  get open() {
    return this._open;
  }
  set open(newValue) {
    this._open = newValue;
    this.showModal(this._open);
  }
  showModal(state) {
    if(state) {
      this.modal.style.display = "block";
    } else {
      this.modal.style.display = "none"
    }
  }
  getTemplate() {
      return `
      ${this.getStyle()}
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button><button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>`;
  }

  getStyle() {
      return `
      <style>
        .modal {
          display: none;
          position: fixed;
          z-index: 1;
          padding-top: 100px;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          overflow: auto;
          background-color: rgb(0,0,0);
          background-color: rgba(0,0,0,0.4);
        }
        .modal-content {
          background-color: #fefefe;
          margin: auto;
          padding: 20px;
          border: 1px solid #888;
          width: 50%;
        }
        .close {
          color: #aaaaaa;
          float: right;
          font-size: 28px;
          font-weight: bold;
        }

        .close:hover,
        .close:focus {
          color: #000;
          text-decoration: none;
          cursor: pointer;
        }
      </style>`;
  }
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 6-10Adding showModal() in SimpleFormModalComponent

最后,我们将为按钮创建事件,并在用户添加注释时发送一个自定义事件(参见清单 6-11 )。

class SimpleFormModalComponent extends HTMLElement {

  constructor() {
      super();

      this.root = this.attachShadow({mode: 'open'});
      this.container = document.createElement('div');
      this.container.innerHTML = this.getTemplate();
      this.root.appendChild(this.container.cloneNode(true));

      this._open = this.getAttribute('open') || false;

      this.modal = this.root.getElementById("myModal");
      this.addBtn = this.root.getElementById("addBtn");
      this.closeBtn = this.root.getElementById("closeBtn");

      this.handleAdd = this.handleAdd.bind(this);
      this.handleCancel = this.handleCancel.bind(this);

  }

  connectedCallback() {
    this.addBtn.addEventListener('click', this.handleAdd);
    this.closeBtn.addEventListener('click', this.handleCancel);
  }

  disconnectedCallback () {
    this.addBtn.removeEventListener('click', this.handleAdd);
    this.closeBtn.removeEventListener('click', this.handleCancel);
  }

  get open() {
    return this._open;
  }

  set open(newValue) {
    this._open = newValue;
    this.showModal(this._open);
  }

  handleAdd() {
    const fTitle = this.root.getElementById('ftitle');
    const fDesc = this.root.getElementById('fdesc');
    this.dispatchEvent(new CustomEvent('addEvent', {detail: {title: fTitle.value, description: fDesc.value}}));

    fTitle.value = '';
    fDesc.value = '';
    this.open = false;
  }

  handleCancel() {
    this.open = false;
  }

  showModal(state) {
    if(state) {
      this.modal.style.display = "block";
    } else {
      this.modal.style.display = "none"
    }
  }

  getTemplate() {
      return `
      ${this.getStyle()}
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button><button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>`;
  }

  getStyle() {
      return `
      <style>
        .modal {
          display: none;
          position: fixed;
          z-index: 1;
          padding-top: 100px;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          overflow: auto;
          background-color: rgb(0,0,0);
          background-color: rgba(0,0,0,0.4);
        }
        .modal-content {
          background-color: #fefefe;
          margin: auto;
          padding: 20px;
          border: 1px solid #888;
          width: 50%;
        }
        .close {
          color: #aaaaaa;
          float: right;
          font-size: 28px;
          font-weight: bold;
        }

        .close:hover,
        .close:focus {
          color: #000;
          text-decoration: none;
          cursor: pointer;
        }
      </style>`;
  }
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 6-11Adding Events in SimpleFormModalComponent

这个代码类似于我们为NoteListComponentNoteListItemComponent做的代码。我们在connectedCallback()中添加监听器,在disconnectedCallback()中删除它,并在handleAdd()中发送定制事件。现在我们有了一个模态(见图 6-8 )。

img/494195_1_En_6_Fig8_HTML.jpg

图 6-8

simple-form-modal-component在谷歌浏览器中

添加 API

通常,当您使用 web 应用时,您必须将应用连接到服务。我们将使用 API Rest 来连接开放的 API https://jsonplaceholder.typicode.com/ 。这个 API 拥有所有可以在实际应用中使用的 HTTP 方法(GETPOSTPUTPATCHDELETE)。

调用 API 方法在我们应用的所有部分都很常见。因此,我们可能会在几个组件中重用这些方法。这就是为什么我们要构建一个带有调用的小模块,并且在将来,只导入它并使用我们需要的函数(参见清单 6-12 )。

const apiUrl= 'https://jsonplaceholder.typicode.com';
export const notesDataApi = {
    createTask(task) {
        return fetch(`${apiUrl}/posts/`, {
            method: 'POST',
            body: JSON.stringify(task),
            headers: {
              "Content-type": "application/json; charset=UTF-8"
            }
          });
        },
    deleteTask(id) {
        return fetch(`${apiUrl}/posts/${id}`, {method: 'DELETE'});
        },
    getTasks(id) {
        return fetch(`${apiUrl}/users/${id}/posts`);
        }
};

Listing 6-12Creating notes-data-api.js

在本模块中,我们将创建创建、获取和删除任务的函数。这些函数将使用 Mozilla ( https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API )向 API 发出 HTTP 请求,并返回我们需要的数据。

现在我们已经有了应用所需的所有小部分,我们将把所有的东西放在一起。首先,我们将创建一个index.html并调用我们正在使用的所有组件和模块,如清单 6-13 所示。

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script async src="./simple-form-modal-component/simple-form-modal-component.js"></script>
  <script async type="module" src="./note-list-component/note-list-component.js"></script>
  <script async src="./note-list-item-component/note-list-item-component.js"></script>
  <link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body>

  <h2>Notes App</h2>
  <button class="fab" id="myBtn">+</button>
  <simple-form-modal-component></simple-form-modal-component>
  <note-list-component></note-list-component>
</body>
</html>

Listing 6-13Creating index.html

index.html中,我们添加了一个按钮,当用户必须添加新的笔记时,它将显示模态,我们将为index.html创建一个小逻辑app.js,它将连接所有的部分(参见清单 6-14 )。

import { notesDataApi } from './utils/notes-data-api.js';
const formModal = document.querySelector('simple-form-modal-component');
const noteList = document.querySelector('note-list-component');

notesDataApi.getTasks(1)
  .then((res) => res.json())
  .then((items) => {
    const allNotes = items.map((item)=>({title: item.title, description: item.body}));
    noteList.notes = allNotes;
  });

formModal.addEventListener('addEvent', function(e) {
  let notes = noteList.notes;

  notes.push({"title": e.detail.title, "description": e.detail.description});
  noteList.notes = notes;
});

const myBtn = document.getElementById('myBtn');
myBtn.addEventListener('click', function() {
  formModal.open = !formModal.open;
});

Listing 6-14Creating app.js

这里,我们为在 index.html 中添加的按钮添加登录。该按钮将向SimpleFormModalComponent传递一个状态,以显示或隐藏模态。我们正在为定制事件addEvent添加一个监听器,以获取新便笺的数据,并将该数据传递给NoteListComponent。我们使用note-data-api模块获取 API 中的所有注释,并将这些数据发送给NoteListComponent,默认情况下填充虚拟注释。最后,我们将添加一个样式文件,以改善我们的按钮在index.html中的外观,如清单 6-15 所示。

.fab {
  width: 50px;
  height: 50px;
  background-color: black;
  border-radius: 50%;
  border: 1px solid black;
  transition: all 0.1s ease-in-out;

  font-size: 30px;
  color: white;
  text-align: center;
  line-height: 50px;

  position: fixed;
  right: 20px;
  bottom: 20px;
}

.fab:hover {
  box-shadow: 0 6px 14px 0 #666;
  transform: scale(1.05);
}

Listing 6-15Creating style.css

现在我们的 NoteApp 完成了,看起来如图 6-9 所示。

img/494195_1_En_6_Fig9_HTML.jpg

图 6-9

谷歌浏览器中的 NoteApp

你可以在$git checkout chap-6访问这本书的源代码( https://github.com/carlosrojaso/apress-book-web-components )。

摘要

在本章中,您学习了

  • 如何在真实的 web 应用中设计组件

  • 如何让所有组件与属性和自定义事件一起工作

  • 如何将我们的应用连接到 API

七、分发 Web 组件

在这一章中,您将学习如何在npm中使用我们的 web 组件。您还将了解 web 组件 API 提供的浏览器支持,如何添加 polyfills 来支持更多的 web 浏览器,以及如何添加 Webpack 和 Babel 来处理和准备我们的 Web 组件以供发布。

发布到状态预防机制

在第六章中,我们创建了三个组件:<simple-form-modal-component><note-list-component><note-list-item-component>。现在我们将通过 www.npmjs.com使这些组件可用。npm 是一个包库,我们可以使用命令$npm install <package>轻松地将它添加到我们的项目中。

首先,我们需要一个帐户来发布包(见图 7-1 )。

img/494195_1_En_7_Fig1_HTML.jpg

图 7-1

npmjs.com中创建用户账户

接下来,我们必须使用$npm adduser(图 7-2 )将我们的账户与终端连接起来。

img/494195_1_En_7_Fig2_HTML.jpg

图 7-2

将我们的终端与npm连接

现在我们必须把我们的组件分离出来,做成模块,其结构如图 7-3 所示。

img/494195_1_En_7_Fig3_HTML.jpg

图 7-3

结构来发布组件

我们为组件创建一个package.json文件,如清单 7-1 所示。

{
  "name": "apress-simple-form-modal-component",
  "version": "1.0.1",
  "description": "simple form modal component",
  "main": "src/index.js",
  "module": "src/index.js",
  "directories": {
    "src": "src"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/carlosrojaso/apress-book-web-components.git"
  },
  "author": "Carlos Rojas",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/carlosrojaso/apress-book-web-components/issues"
  },
  "homepage": "https://github.com/carlosrojaso/apress-book-web-components#readme"
}

Listing 7-1package.json for simple-form-modal-component

在这个package.json文件中,我们定义了组件在npmjs.com中的名称以及文件的结构。npm包名应该是唯一的。因此,您必须确保您为package.json中的name属性选择的名称不会在npmjs.com中使用。

现在我们将创建一个名为 src 的目录,我们将在其中定位我们的代码源。然后,我们将把我们的simple-form-modal-component.js移到那里,并创建一个新文件index.js,如清单 7-2 所示。

export * from './simple-form-modal-component';

Listing 7-2index.js for simple-form-modal-component

这只是我们用来导入simple-form-modal-componet.js中所有内容的一行。因此,在我们的文件simple-form-modal-component.js中,我们必须添加单词导出,以使我们的组件作为一个模块可用,如清单 7-3 所示。

export class SimpleFormModalComponent extends HTMLElement {
...
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 7-3Converting simple-form-modal-component in a Module

好了,我们现在已经准备好组件了。下一步是运行

$npm publish

现在我们已经发布了我们的组件(见图 7-4 )。

img/494195_1_En_7_Fig4_HTML.jpg

图 7-4

发布simple-form-modal-component

您可以在$git checkout chap-7-1获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

这个过程对于<note-list-component><note-list-item-component>是相似的。如果你想看修改,你可以在$git checkout chap-7-2$git checkout chap-7-3看到。

现在我们可以很容易地在项目中使用我们的组件。现在,我们将使用服务unpkg.comunpkgnpm包的cdn,方便插入我们的包,如清单 7-4 所示。

<!DOCTYPE html>
<html>
<head>

<meta name="viewport" content="width=device-width, initial-scale=1">

<script async type="module" src="http://unpkg.com/apress-simple-form-modal-component@1.0.1/src/simple-form-modal-component.js"></script>

<script async type="module" src="http://unpkg.com/apress-book-web-components-note-list@1.0.1/src/note-list-component.js"></script>
<script async type="module" src="http://unpkg.com/apress-note-list-item-component@1.0.1/src/note-list-item-component.js"></script>

<script async type="module" src="./app.js"></script>

<link rel="stylesheet" type="text/css" href="./style.css">

</head>
<body>

<h2>Notes App</h2>

<button class="fab" id="myBtn">+</button>

<simple-form-modal-component></simple-form-modal-component>

<note-list-component></note-list-component>

</body>
</html>

Listing 7-4package.json

for simple-form-modal-component

您可以在$git checkout chap-7-4访问代码( https://github.com/carlosrojaso/apress-book-web-components )。

有了这些修改,我们不需要项目中的组件文件,因为我们是从unpkg.com导入的。在接下来的部分中,您将学习添加其他工具来从我们的本地环境中运行一切,使用没有unpkg.comnpm

旧的网络浏览器支持

Web 组件在主流浏览器或 Webkit 以及所有基于 Chrome 的 web 浏览器中都有出色的支持(见图 7-5 )。但是如果我们必须支持 IE 11 这样的网络浏览器会怎么样呢?

img/494195_1_En_7_Fig5_HTML.jpg

图 7-5

支持 Web 组件主要规范的主流浏览器

如果你去我能使用吗(caniuse.com)并搜索我们需要使用 Web 组件的每个规范,你会发现对于 ES6,IE 11 的支持是有限的(图 7-6 )。

img/494195_1_En_7_Fig6_HTML.jpg

图 7-6

支持 ES6 1 的网页浏览器版本

IE 11 不提供对自定义元素的支持(图 7-7 )。

img/494195_1_En_7_Fig7_HTML.jpg

图 7-7

支持自定义元素的网页浏览器版本 2

IE 11 不提供对 HTML 模板的支持(图 7-8 )。

img/494195_1_En_7_Fig8_HTML.jpg

图 7-8

支持 HTML 模板的网页浏览器版本 3

IE 11 不提供对影子 DOM 的支持(图 7-9 )。

img/494195_1_En_7_Fig9_HTML.jpg

图 7-9

支持影子 DOM 的网页浏览器版本 4

IE 11 不提供对 ES 模块的支持(图 7-10 )。

img/494195_1_En_7_Fig10_HTML.jpg

图 7-10

支持 ES 模块的网络浏览器版本 5

IE 11 是一个还不流行的网络浏览器;因此,我们可能会在某个项目的某个时候遇到问题。幸运的是,我们可以用 polyfills 解决这些问题。

多填充物

聚合填充用于通过模拟这些功能的库将缺失的功能添加到 web 浏览器中。对于 web 组件,我们可以将一个可靠的包添加到我们的项目中,以支持更多的 web 浏览器。您可以在 https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs 访问项目,并使用unpkg.com(列表 7-5 )快速添加聚合填充。

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script async src="https://unpkg.com/browse/@webcomponents/webcomponentsjs@2.4.3/webcomponents-bundle.js"></script>
<script async type="module" src="http://unpkg.com/apress-simple-form-modal-component@1.0.1/src/simple-form-modal-component.js"></script>
<script async type="module" src="http://unpkg.com/apress-book-web-components-note-list@1.0.1/src/note-list-component.js"></script>
<script async type="module" src="http://unpkg.com/apress-note-list-item-component@1.0.1/src/note-list-item-component.js"></script>
<script async type="module" src="./app.js"></script>
<link rel="stylesheet" type="text/css" href="./style.css">
</head>
<body>

<h2>Notes App</h2>
<button class="fab" id="myBtn">+</button>
<simple-form-modal-component></simple-form-modal-component>
<note-list-component></note-list-component>
</body>
</html>

Listing 7-5Adding webcomponentsjs Polyfills

通过这次修改,我们现在对缺少 web 组件 API 的 Web 浏览器有了更好的支持。

您可以在$git checkout chap-7-5获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

网络包和巴别塔

使用聚合填充,我们现在拥有了项目中以前缺少的功能。尽管如此,我们要求 IE 11 理解我们在 ES6 中使用的 JS。为了实现这一点,我们将使用 Babel 作为 transpiler,并在 ES5 中转换我们的代码。Webpack 将处理所有的依赖关系,并把所有的东西放在一个没有 es 模块的 web 浏览器可以找到的地方。您可以在babeljs.io and webpack.js.org阅读关于这些工具的更多信息。

首先,在每个组件中,我们将通过npm安装我们需要的所有过程所需的工具。为此,请运行以下命令:

$ npm install rimraf webpack webpack-cli babel-core babel-loader babel-preset-env path serve copyfiles --save-dev

使用这个命令,您将向 package.json 添加所有工具。

现在我将创建一个webpack.config.js文件并添加一些设置(列表 7-6 )。

var path= require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.js',
    library: 'apressSimpleFormModalComponent',
    libraryTarget: 'umd'
  }
};

Listing 7-6Adding webpack.config.js in simple-form-modal-component Project

在这里,我们说把文件放在./src/index.js中并解析所有的依赖关系,把它们放在./dist/index.js中,并把它作为一个umd格式的库来处理。通用模块定义,或 UMD,是一种在 web 浏览器中添加 JS 模块之前创建模块的方法。

现在我们将在我们的项目中添加一些npm脚本(清单 7-7 )。

"scripts": {
    "clean": "rimraf dist",
    "build": "npm run clean && webpack --mode production",
    "cpdir": "copyfiles -V \"./dist/*.js\" \"./example\"",
    "start": "npm run build && npm run cpdir && serve example"
  }

Listing 7-7Adding npm Scripts in package.json

从先前的构建中删除任何生成的代码。cpdir复制./example文件夹中的./dist目录。build使用webpack创建 transpiled 文件并开始运行本地服务器,以查看我们编译的组件在示例中的运行情况。

现在我将创建一个示例文件夹,并将我们完整的 NoteApp 项目复制到其中,您可以在分支chap-7-5中找到。为了测试我们的构建,我们必须修改index.html,如清单 7-8 所示。

<!DOCTYPE html>
<html>

<head>

<meta name="viewport" content="width=device-width, initial-scale=1">

<script async src="https://unpkg.com/browse/@webcomponents/webcomponentsjs@2.4.3/webcomponents-bundle.js"></script>

<script async src="./dist/index.js"></script>

<script async type="module" src="http://unpkg.com/apress-book-web-components-note-list@1.0.1/src/note-list-component.js"></script>

<script async type="module" src="http://unpkg.com/apress-note-list-item-component@1.0.1/src/note-list-item-component.js"></script>

<script async type="module" src="./app.js"></script>

<link rel="stylesheet" type="text/css" href="./style.css">
</head>
...

Listing 7-8Loading Our simple-form-modal-component from ./dist/index.js

有了这个,每次我们运行一个新的构建,我们就可以测试编译后的文件是否仍然像预期的那样工作。

现在,您可以运行命令$npm run start并转到 localhost:5000,查看一切是否如预期的那样工作(图 7-11 )。

img/494195_1_En_7_Fig11_HTML.jpg

图 7-11

从编译后的文件运行simple-form-modal

它像预期的那样工作——现在我们可以在旧的 web 浏览器中使用它,如 IE 11——并且使用 ES5,这是 IE 11 应该理解的 JS 规范。

您可以使用$git checkout chap-7-6访问代码( https://github.com/carlosrojaso/apress-book-web-components )。

这个过程对于<note-list-component><note-list-item-component>是相似的。如果你想看到修改,你可以从$git checkout chap-7-7$git checkout chap-7-8访问它们。

您可以在$git checkout chap-7找到使用聚合填充和编译组件的最终示例。

摘要

在本章中,您学习了

  • 如何发布到npmjs.com

  • 如何让我们的组件对旧的 web 浏览器可用

  • 如何用 Babel 和 Webpack 编译我们的组件

八、Polymer

在本章中,你将学习如何用 Polymer 构建 web 组件,为什么用 Polymer 代替 VanillaJS,如何在我们的 web 组件中使用LitElement,以及如何使用lit-html

Polymer 已经存在很长时间了,从 Polymer 版本 1x 和 2x 开始。该项目的重点是使用 Polymer CLI 和PolymerElements构建一个完整的框架来制作完整的项目。有了 Polymer 3x,仍然可以使用 Polymer CLI 和PolymerElements。然而,该项目现在面向使用LitElementLitHtml库来构建组件,并使它们在所有 JS 项目中可用。这就是为什么我们要关注这些库。

像谷歌、YouTube、可口可乐、麦当劳和 BBVA 这样的大公司,以及其他许多公司,都在用 Polymer 建造项目。

入门指南

为了开始构建我们的组件,Polymer 提供了两个惊人的“启动器”,让我们用LitElement开发组件,并为我们提供一些方便的工具,用于林挺、测试和生成文档。你不必使用启动器,但它会使事情变得更容易。可以在 https://github.com/PolymerLabs/lit-element-starter-js 获得 JS 首发,在 https://github.com/PolymerLabs/lit-element-starter-ts 获得 TS 首发。我将使用 JS Starter 来构建本章中的例子。

首先,通过运行$ git clone(在 https://github.com/PolymerLabs/lit-element-starter-js.git 可用)在你的机器中克隆项目。

安装依赖项,运行在项目文件夹$ npm install中。

就这样。现在,您可以通过执行$ npm run serve来运行本地服务器。

转到http://localhost:8000/dev/查看如图 8-1 所示运行的示例 web 组件。

img/494195_1_En_8_Fig1_HTML.jpg

图 8-1

你好,世界!例子

你可以通过执行$npm run lint来运行ESLint

终端将显示在您的组件中发现的所有代码样式问题(参见图 8-2 )。

img/494195_1_En_8_Fig2_HTML.jpg

图 8-2

正在运行ESLint

您可以通过执行$ npm run test来运行 Karma、Chai 和 Mocha 测试。

终端将显示我们为组件编写的所有测试(见图 8-3 )。

img/494195_1_En_8_Fig3_HTML.jpg

图 8-3

运行测试

执行$ npm run docs可以生成单据。

使用$npm run docs:serve,您可以看到创建的文档。

这些任务使用 eleventy,一个静态站点生成器,根据我们在文件夹docs-src中创建的模板来生成漂亮的文档(图 8-4 )。

img/494195_1_En_8_Fig4_HTML.jpg

图 8-4

运行文档

如您所见,这个 starter 是构建组件的良好起点。在接下来的部分中,我们将为每个 web 组件使用一个新项目来迁移<simple-form-modal-component><note-list-component><note-list-item-component>

列表元素

LitElement是一个库,它为我们创建 web 组件提供了一个基类,而不用担心当我们只使用 JS 时必须处理低级的复杂性,例如每次更新时呈现元素。

LitElement使用lit-html来处理我们组件中的模板。lit-html是一个模板库,我们可以独立地将它添加到我们的 JS 项目中,高效地呈现带有数据的 HTML 模板。

性能

在前面的章节中,你可能还记得我们用类中的 setters 和 getters 创建了属性。在一个LitElement中,我们只需要声明get properties()静态方法中的所有属性,如清单 8-1 所示。

static get properties() {
  return { propertyName: options};
}

Listing 8-1Declaring Properties

LitElement将为我们妥善处理更新和转换。在选项中,我们可以添加以下值:

  • Attribute:该值表示属性是否与某个属性相关联,或者是相关属性的自定义名称。

  • hasChanged:这个函数采用一个oldValuenewValue,返回一个Boolean来表示一个属性在被设置时是否已经改变。

  • Type:这是一种属性与属性之间转换的提示。可以用StringNumberBooleanArrayObject

清单 8-2 中给出了一个在<simple-form-modal-component>中声明属性的例子。

static get properties() {
    return {
      open: {
        type: Boolean,
        hasChanged(newVal, oldVal) {
          if (oldVal !== newVal) {
            return true;
          }
          else {
            return false;
          }
        }
      }
    };
}

Listing 8-2Declaring Properties in <simple-form-modal-component>

这里,我们将open属性创建为Boolean,并在属性改变时检查逻辑。你可以在 https://lit-element.polymer-project.org/guide/properties 找到属性的所有机制。

模板

当发生变化时,呈现和更新 DOM 是一项困难的任务,这会影响处理它的函数中代码的性能。解决了这种复杂性,并为我们提供了一种构建模板的便捷方式。

使用模板很简单:只需在组件中使用render()方法,并用html标签函数返回一个模板文本,如清单 8-3 所示。

import { LitElement, html } from 'lit-element';
class MyComponent extends LitElement {
  render(){
    return html`
      <div>
        My Component content
      </div>
    `;
  }
}
customElements.define('my-component, MyComponent);

Listing 8-3Using Templates

如果我们必须使用一个属性,我们必须在模板文本中用this.prop符号调用它,如清单 8-4 所示。

import { LitElement, html } from 'lit-element';
class MyComponent extends LitElement {
static get properties() {
    return {
      myString: { type: String }
    };
  }
  render(){
    return html`
      <div>
        My Component content with ${this.myString}
      </div>
    `;
  }
}
customElements.define('my-component, MyComponent);

Listing 8-4Using a Property in Templates

如果我们想要绑定一个属性,我们可以直接从模板文本传递它,如清单 8-5 所示。

import { LitElement, html } from 'lit-element';
class MyComponent extends LitElement {
static get properties() {
    return {
      myId: { type: String }
    };
  }
  render(){
    return html`
      <div id=”${this.myId}”>
        My Component content
      </div>
    `;
  }
}

Listing 8-5Binding an Attribute in Templates

如果我们想绑定一个属性,我们可以用模板文字中的.prop符号传递它,如清单 8-6 所示。

import { LitElement, html } from 'lit-element';
class MyComponent extends LitElement {
static get properties() {
    return {
      myValue: { type: String }
    };
  }
  render(){
    return html`
      <input type="checkbox" .value="${this.myValue}"/>
    `;
  }
}

Listing 8-6Binding a Property in Templates

如果我们想将一个clickHandler绑定到一个点击事件,我们可以用模板文字中的$click符号传递它,如清单 8-7 所示。

import { LitElement, html } from 'lit-element';
class MyComponent extends LitElement {
  render(){
    return html`
      <button @click="${this.clickHandler}">click</button>
    `;
  }
  clickHandler(e) {
    console.log(e.target);
  }
}

Listing 8-7Binding a clickHandler in a Click Event

您可以在 https://lit-element.polymer-project.org/guide/templateshttps://lit-html.polymer-project.org/guide 找到模板的所有机制。

风格

使用 Polymer 在 web 组件中添加样式非常简单。简单地在静态方法styles()中添加你的选择器和属性,如清单 8-8 所示。

import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {
  static get styles() {
    return css`
      div { color: blue; }
    `;
  }
  render() {
    return html`
      <div>Content in Blue!</div>
    `;
  }}

Listing 8-8Adding Styles in Web Components

你可以在 https://lit-element.polymer-project.org/guide/styles 了解更多风格。

事件

您可以直接在模板中添加事件监听器,如清单 8-9 所示。

import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  render() {
    return html`<button @click="${this.handleEvent}">click</button>`;
  }

  handleEvent(e) {
   console.log(e);
  }
}

Listing 8-9Adding an Event in the Template

您也可以在组件中直接添加监听器,在生命周期的某个方法中,如清单 8-10 所示。

import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  constructor() {
    super();
    this.addEventListener('DOMContentLoaded', this.handleEvent);
  }

  handleEvent() {
   console.log('It is Loaded');
  }
}

Listing 8-10Adding an Event in the Component

您可以在 https://lit-element.polymer-project.org/guide/events 了解更多活动。

生存期

从 Web 组件标准中继承了默认的生命周期回调,以及一些可用于在组件中添加逻辑的附加方法。

  • connectedCallback:当一个组件被添加到文档的 DOM 时,这个函数被调用(清单 8-11 )。

  • disconnectedCallback:当一个组件从文档的 DOM 中删除时,这个函数被调用(清单 8-12 )。

import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  connectedCallback() {
    super.connectedCallback();
    console.log('added');
  }
}

Listing 8-11connectedCallback

  • adoptedCallback:当一个组件被移动到一个新的文档时,这个函数被调用(清单 8-13 )。
import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  disconnectedCallback() {
    super.disconnectedCallback();
    console.log('removed');
  }
}

Listing 8-12disconnectedCallback

  • attibuteChangedCallback:当一个组件属性改变时调用(清单 8-14 )。
import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  adoptedCallback() {
    super.disconnectedCallback();
    console.log('moved');
  }
}

Listing 8-13adoptedCallback

  • firstUpdated:这在你的组件第一次被更新和渲染后被调用(清单 8-15 )。
import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  attributeChangedCallback(name, oldVal, newVal) {
    super.attributeChangedCallback(name, oldVal, newVal);
    console.log('attribute change: ', name, newVal);
  }
}

Listing 8-14attibuteChangedCallback

  • updated:当元素的 DOM 被更新和呈现时,这个函数被调用(清单 8-16 )。
import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  firstUpdated(changedProperties) {
    console.log('first updated');
  }
}

Listing 8-15firstUpdated

import { LitElement, html } from 'lit-element';

class MyComponent extends LitElement {

  updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => {
      console.log(`${propName} changed. oldValue: ${oldValue}`);
    });
  }
}

Listing 8-16updated

你可以在 https://lit-element.polymer-project.org/guide/lifecycle 中阅读更多关于生命周期的内容。

Polymer 建筑

在本节中,我们将使用 Polymer 启动器构建我们的<simple-form-modal-component><note-list-component><note-list-item-component>

正如您在清单 8-17 中看到的,我们在 VanillaJS 有我们的SimpleFormModalComponent

export class SimpleFormModalComponent extends HTMLElement {

  constructor() {
      super();

      this.root = this.attachShadow({mode: 'open'});
      this.container = document.createElement('div');
      this.container.innerHTML = this.getTemplate();
      this.root.appendChild(this.container.cloneNode(true));

      this._open = this.getAttribute('open') || false;

      this.modal = this.root.getElementById("myModal");
      this.addBtn = this.root.getElementById("addBtn");
      this.closeBtn = this.root.getElementById("closeBtn");

      this.handleAdd = this.handleAdd.bind(this);
      this.handleCancel = this.handleCancel.bind(this);

  }

  connectedCallback() {
    this.addBtn.addEventListener('click', this.handleAdd);
    this.closeBtn.addEventListener('click', this.handleCancel);
  }

  disconnectedCallback () {
    this.addBtn.removeEventListener('click', this.handleAdd);
    this.closeBtn.removeEventListener('click', this.handleCancel);
  }

  get open() {
    return this._open;
  }

  set open(newValue) {
    this._open = newValue;
    this.showModal(this._open);
  }

  handleAdd() {
    const fTitle = this.root.getElementById('ftitle');
    const fDesc = this.root.getElementById('fdesc');
    this.dispatchEvent(new CustomEvent('add-event', {bubbles: true, composed:true, detail: {title: fTitle.value, description: fDesc.value}}));

    fTitle.value = '';
    fDesc.value = '';
    this.open = false;
  }

  handleCancel() {
    this.open = false;
  }

  showModal(state) {
    if(state) {
      this.modal.style.display = "block";
    } else {
      this.modal.style.display = "none"
    }
  }

  getTemplate() {
      return `
      ${this.getStyle()}
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button><button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>`;
  }

  getStyle() {
      return `
      <style>
        .modal {
          display: none;
          position: fixed;
          z-index: 1;
          padding-top: 100px;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          overflow: auto;
          background-color: rgb(0,0,0);
          background-color: rgba(0,0,0,0.4);
        }
        .modal-content {
          background-color: #fefefe;
          margin: auto;
          padding: 20px;
          border: 1px solid #888;
          width: 50%;
        }
      </style>`;
  }
}
customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-17SimpleFormModalComponent in VanillaJS

我们有一个为组件返回 HTML 的getTemplate()方法,一个返回我们在组件中使用的样式规则的getStyle()方法,一些我们添加到组件中处理组件中一些逻辑的方法,以及一些更新组件中属性的 setter 和 getter。我们可以使用相同的原则快速创建一个LitElement,并使我们的代码更短,因为LitElement为我们处理一些底层的事情。

我们可以使用LitElement语法从属性开始,如清单 8-18 所示。

import {LitElement, html, css} from 'lit-element';

export class SimpleFormModalComponent extends LitElement {
static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }

  constructor() {
    super();
    this.open = false;
  }
}

window.customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-18Adding Properties in SimpleFormModalComponent

with LitElement

在这里,我们将我们的'open'属性添加为Boolean,并将这个属性初始化为constructor()中的false

接下来,我们将采用方法getTemplate()并在LitElement中进行迁移,如清单 8-19 所示。

import {LitElement, html, css} from 'lit-element';

export class SimpleFormModalComponent extends LitElement {
static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }

  constructor() {
    super();

    this.open = false;
  }

  render() {
    return html`
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button>
            <button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>
    `;
  }
}

window.customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-19Adding render()

in SimpleFormModalComponent with LitElement

正如你所看到的,这几乎是相同的代码,但是我们使用了render()方法并返回一个 HTML 标签文字而不是一个字符串。

现在我们要对getStyle()做同样的事情,将它移动到styles() getter,如清单 8-20 所示。

import {LitElement, html, css} from 'lit-element';

export class SimpleFormModalComponent extends LitElement {
static get styles() {
    return css`
      .modal {
        display: none;
        position: fixed;
        z-index: 1;

        padding-top: 100px;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgb(0,0,0);
        background-color: rgba(0,0,0,0.4);
      }
      .modal-content {
        background-color: #fefefe;
        margin: auto;
        padding: 20px;
        border: 1px solid #888;
        width: 50%;
      }
    `;
}

static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }

  constructor() {
    super();
    this.open = false;
  }

  render() {
    return html`
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn">Add</button>
            <button type="button" id="closeBtn">Close</button>
          </form>
        </div>
      </div>
    `;
  }
}

window.customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-20Adding render() in SimpleFormModalComponent with LitElement

现在我们将使用@click符号为按钮添加处理程序方法,如清单 8-21 所示。

import {LitElement, html, css} from 'lit-element';

export class SimpleFormModalComponent extends LitElement {
static get styles() {
    return css`
      .modal {
        display: none;
        position: fixed;
        z-index: 1;
        padding-top: 100px;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgb(0,0,0);
        background-color: rgba(0,0,0,0.4);
      }
      .modal-content {
        background-color: #fefefe;
        margin: auto;
        padding: 20px;
        border: 1px solid #888;
        width: 50%;
      }
    `;
}

static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }

  constructor() {
    super();
    this.open = false;
  }

  render() {
    return html`
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn" @click=${this.handleAdd}>Add</button>
            <button type="button" id="closeBtn" @click=${this.handleCancel}>Close</button>
          </form>
        </div>
      </div>
    `;
  }

  handleAdd() {
    const fTitle = this.shadowRoot.getElementById('ftitle');
    const fDesc = this.shadowRoot.getElementById('fdesc');
    this.dispatchEvent(new CustomEvent('addEvent', {detail: {title: fTitle.value, description: fDesc.value}}));

    fTitle.value = '';
    fDesc.value = '';
    this.open = false;
  }

  handleCancel() {
    this.open = false;
  }
}
window.customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-21Adding styles()

in SimpleFormModalComponent with LitElement

最后,我们将为'open'的更新添加showModal()方法,如清单 8-22 所示。

import {LitElement, html, css} from 'lit-element';

export class SimpleFormModalComponent extends LitElement {
  static get styles() {
    return css`
      .modal {
        display: none;
        position: fixed;
        z-index: 1;
        padding-top: 100px;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgb(0,0,0);
        background-color: rgba(0,0,0,0.4);
      }
      .modal-content {
        background-color: #fefefe;
        margin: auto;
        padding: 20px;
        border: 1px solid #888;
        width: 50%;
      }
    `;
  }

  static get properties() {
    return {
      open: {
        type: Boolean,
        hasChanged(newVal, oldVal) {
          if (oldVal !== newVal) {
            return true;
          }
          else {
            return false;
          }
        }
      }
    };
  }

  constructor() {
    super();
    this.open = false;
  }

  render() {
    return html`
      <div id="myModal" class="modal">
        <div class="modal-content">
          <form id="myForm">
            <label for="ftitle">Title:</label><br>
            <input type="text" id="ftitle" name="ftitle"><br>
            <label for="fdesc">Description:</label><br>
            <textarea id="fdesc" name="fdesc" rows="4" cols="50"></textarea><br/>
            <button type="button" id="addBtn" @click=${this.handleAdd}>Add</button>
            <button type="button" id="closeBtn" @click=${this.handleCancel}>Close</button>
          </form>
        </div>
      </div>
    `;
  }

  handleAdd() {
    const fTitle = this.shadowRoot.getElementById('ftitle');
    const fDesc = this.shadowRoot.getElementById('fdesc');
    this.dispatchEvent(new CustomEvent('addEvent', {detail: {title: fTitle.value, description: fDesc.value}}));

    fTitle.value = '';
    fDesc.value = '';
    this.open = false;
  }

  handleCancel() {
    this.open = false;
  }

  showModal(state) {
    const modal = this.shadowRoot.getElementById("myModal");
    if(state) {
      modal.style.display = "block";
    } else {
      modal.style.display = "none"
    }
  }

  updated(){
    this.showModal(this.open);
  }
}

window.customElements.define('simple-form-modal-component', SimpleFormModalComponent);

Listing 8-22Adding render() in SimpleFormModalComponent with LitElement

这里,我们使用'open'中的选项hasChanged()来检查更新何时被触发,使用updated()方法,我们使用方法showModal()来显示/隐藏模态。现在,我们的<simple-form-modal-component>准备好了。

您可以通过以下方式访问本书的代码( https://github.com/carlosrojaso/apress-book-web-components )

$git checkout chap-8.

我在dev/文件夹中创建了一个例子,在docs-src/文件夹中创建了文档,在test/文件夹中创建了一些基本测试,然后你可以运行下面的代码:

$ npm install

$ npm run serve

要查看在 localhost:8080/dev 中运行的示例,请运行

$ npm run docs

$ npm run docs:serve

我们将继续使用我们在前面章节中写的<note-list-component>,如清单 8-23 。

export class NoteListComponent extends HTMLElement {
  static get observedAttributes() { return ['notes']; }

  constructor() {
    super();

    this._notes = JSON.parse(this.getAttribute('notes')) || [];
    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = this.render();

    this.handleDelEvent = this.handleDelEvent.bind(this);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    switch(name) {
      case 'notes':
        this.note = JSON.parse(newValue);
        this.root.innerHTML = this.render();
        break;
    }
  }

  connectedCallback() {
    this.root.addEventListener('del-event', this.handleDelEvent);
  }

  disconnectedCallback () {
    this.root.removeEventListener('del-event', this.handleDelEvent);
  }

  handleDelEvent(e) {
    this._notes.splice(e.detail.idx, 1);
    this.root.innerHTML = this.render();
  }

  render() {
    let noteElements = '';
    this._notes.map(
      (note, idx) => {
        noteElements += `
        <note-list-item-component note='${JSON.stringify(note)}' idx='${idx}'></note-list-item-component>`;
      }
    );
    return `
      ${noteElements}`;
  }

  get notes(){
    return this._notes;
  }

  set notes(newValue) {
    this._notes = newValue;
    this.root.innerHTML = this.render();
  }
}
customElements.define('note-list-component', NoteListComponent);

Listing 8-23NoteListComponent

in VanillaJS

我们将根据清单 8-24 为组件添加属性。

import {LitElement, html, css} from 'lit-element';

export class NoteListComponent extends LitElement {
static get properties() {
    return {
      notes: {
        type: Array,
        attribute: true,
        reflect: true,
      }
    };
  }

  constructor() {
    super();
    this.notes = this.getAttribute("notes") || [];
  }
}

Listing 8-24Adding Properties in NoteListComponent with LitElement

这里,我们将 notes 属性定义为一个数组,并添加了options属性,以使该属性作为一个属性正确工作。在constructor()中,我们用'notes'中的值或者一个空数组来初始化this.notes

现在我们要迁移render()方法,它类似于LitElement,如清单 8-25 所示。

import {LitElement, html, css} from 'lit-element';

export class NoteListComponent extends LitElement {
static get properties() {
    return {
      notes: {
        type: Array,
        attribute: true,
        reflect: true,
      }
    };
  }

  constructor() {
    super();
    this.notes = this.getAttribute("notes") || [];
  }

  render() {
    return html`
      ${this.notes.map((note, idx) => {
        return html` <note-list-item-component
          note="${JSON.stringify(note)}"
          idx="${idx}"
        ></note-list-item-component>`;
      })}
    `;
  }
}

Listing 8-25Adding a Template in NoteListComponent with LitElement

这里,我们使用map()来迭代注释,并以 HTML 标记文字的形式返回所有注释,以使我们的模板正确工作。

每次值发生变化时,我们都必须更新注释,但是要做到这一点,我们必须做一些特殊的事情,因为当作为引用传递时,hasChanged()选项不会检测数组中何时发生变化。为了解决这个问题,我们将使用attributeChangedCallback(),如清单 8-26 所示。

import {LitElement, html, css} from 'lit-element';

export class NoteListComponent extends LitElement {
static get properties() {
    return {
      notes: {
        type: Array,
        attribute: true,
        reflect: true,
      }
    };
  }

  constructor() {
    super();
    this.notes = this.getAttribute("notes") || [];
  }

  attributeChangedCallback() {
    this.notes = [...this.notes];
    super.attributeChangedCallback();
  }

  render() {
    return html`
      ${this.notes.map((note, idx) => {
        return html` <note-list-item-component
          note="${JSON.stringify(note)}"
          idx="${idx}"
        ></note-list-item-component>`;
      })}
    `;
  }
}

Listing 8-26Adding attributeChangedCallback

in NoteListComponent with LitElement

这里,在attributeChangedCallback()中,我们检测到组件的变化。如果出现这种情况,我们可以使用spread操作符,用一个新的数组(不是引用)更新this.notes,通过这个小小的修正,我们的组件将再次正常工作。

最后,我们将为'del-event'handleDelEvent()函数添加事件监听器,如清单 8-27 所示。经过这些修改,我们的组件就准备好了。

import {LitElement, html, css} from 'lit-element';

export class NoteListComponent extends LitElement {
static get properties() {
    return {
      notes: {
        type: Array,
        attribute: true,
        reflect: true,
      }
    };
  }

  constructor() {
    super();
    this.notes = this.getAttribute("notes") || [];
    this.addEventListener("del-event", this.handleDelEvent);
  }

  attributeChangedCallback() {
    this.notes = [...this.notes];
    super.attributeChangedCallback();
  }

  handleDelEvent(e) {
    this.notes.splice(e.detail.idx, 1);
    this.requestUpdate();
  }

  render() {
    return html`
      ${this.notes.map((note, idx) => {
        return html` <note-list-item-component
          note="${JSON.stringify(note)}"
          idx="${idx}"
        ></note-list-item-component>`;
      })}
    `;
  }
}

Listing 8-27Adding the Event Listener in NoteListComponent with LitElement

您可以在$git checkout chap-8-1获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

我在dev/文件夹中创建了一个例子,在docs-src/文件夹中创建了文档,在test/文件夹中创建了一些基本测试,然后你可以运行下面的代码:

$ npm install

$ npm run serve

要查看在localhost:8080/dev中运行的示例,请运行以下命令:

$ npm run docs

$ npm run docs:serve

现在我们要迁移我们在前面章节中写的最后一个组件<note-list-item-component>(见清单 8-28 )。

export class NoteListItemComponent extends HTMLElement {
  static get observedAttributes() { return ['note', 'idx']; }

  constructor() {
    super();

    this._note = JSON.parse(this.getAttribute('note')) || {};
    this.idx = this.getAttribute('idx') || -1;
    this.root = this.attachShadow({mode: 'open'});
    this.root.innerHTML = this.getTemplate();

    this.delBtn = this.root.getElementById('deleteButton');
    this.handleDelete = this.handleDelete.bind(this);
  }

  connectedCallback() {
    this.delBtn.addEventListener('click', this.handleDelete);
  }

  disconnectedCallback () {
    this.delBtn.removeEventListener('click', this.handleDelete);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    switch(name) {
      case 'note':
        this.note = JSON.parse(newValue);
        this.handleUpdate();
        break;
      case 'idx':
        this.idx = newValue;
        this.handleUpdate();
        break;
    }
   }

  get note() {
    return this._note;
  }

  set note(newValue) {
    this._note = newValue;
  }

  get idx() {
    return this._idx;
  }

  set idx(newValue) {
    this._idx = newValue;
  }

  handleDelete() {
    this.dispatchEvent(new CustomEvent('del-event', {bubbles: true,  composed: true, detail: {idx: this.idx}}));
  }

  handleUpdate() {
    this.root.innerHTML = this.getTemplate();
    this.delBtn = this.root.getElementById('deleteButton');
    this.handleDelete = this.handleDelete.bind(this);
    this.delBtn.addEventListener('click', this.handleDelete);
   }

  getStyle() {
    return `
    <style>
      .note {
        background-color: #ffffcc;
        border-left: 6px solid #ffeb3b;
      }
      div {
        margin: 5px 0px 5px;
        padding: 4px 12px;
      }
    </style>
    `;
  }

  getTemplate() {
    return`
    ${this.getStyle()}
    <div class="note">
      <p><strong>${this._note.title}</strong> ${this._note.description}</p><br/>
      <button type="button" id="deleteButton">Delete</button>
    </div>`;
  }
}
customElements.define('note-list-item-component', NoteListItemComponent);

Listing 8-28NoteListComponent in VanillaJS

迁移该组件的过程类似于我们之前遵循的步骤。因此,我将跳过解释,只向您展示LitElement(列表 8-29 )中的完整组件。

import { LitElement, html, css } from "lit-element";

/**
 * A Note List Item Component
 */
export class NoteListItemComponent extends LitElement {
  static get properties() {
    return {
      /**
       * The attribute is an Object.
       */
      note: {
        type: Object,
        attribute: true,
        reflect: true,
      },
      /**
       * The attribute is a number.
       */
      idx: {
        type: Number,
        attribute: true,
        reflect: true,
      }
    };
  }

  static get styles() {
    return css`
      .note {
        background-color: #ffffcc;
        border-left: 6px solid #ffeb3b;
      }

      div {
        margin: 5px 0px 5px;
        padding: 4px 12px;
      }
    `;
  }

  constructor() {
    super();
    this.note = JSON.parse(this.getAttribute('note')) || {};
    this.idx = this.getAttribute('idx') || -1;
  }

  render() {
    return html`
    <div class="note">
      <p><strong>${this.note.title}</strong> ${this.note.description}</p><br/>
      <button type="button" id="deleteButton" @click="${this.handleDelete}">Delete</button>
    </div>
    `;
  }

  handleDelete() {
    this.dispatchEvent(new CustomEvent('del-event', {bubbles: true,  composed: true, detail: {idx: this.idx}}));
  }
}

window.customElements.define('note-list-item-component', NoteListItemComponent);

Listing 8-29NoteListItemComponent with LitElement

您可以在$git checkout chap-8-2获取这本书的代码( https://github.com/carlosrojaso/apress-book-web-components )。

太棒了。现在,我们所有的组件都是由 Polymer 制成的。

摘要

在本章中,您学习了

  • 如何在我们的组件中使用LitElementlit-html

  • 如何将香草中的成分迁移到 Polymer 中

  • litElement的主要特点是什么

九、使用 Vue.js

Vue.js 是一个开源的、渐进式的 JavaScript 框架,用于构建用户界面,旨在以增量方式采用。Vue.js 的核心库只关注视图层,很容易获取并与其他库或现有项目集成。

您将学会利用它的特性来构建快速、高性能的 web 应用。贯穿本章,我们要开发一个 app,你会学到一些关键概念,了解如何集成 Web 组件和 Vue.js。

Vue.js 有哪些主要特点?

Vue.js 拥有构建单页面应用的框架应该拥有的所有特性。一些功能比其他功能突出,如下所示:

  • 虚拟 DOM :虚拟 DOM 是原始 HTML DOM 的轻量级内存树表示,可以在不影响原始 DOM 的情况下进行更新。

  • 组件:用于在 Vue.js 应用中创建可重用的定制元素。

  • Templates : Vue.js 提供了基于 HTML 的模板,将 DOM 与 Vue 实例数据绑定在一起。

  • 路由:页面之间的导航通过vue-router实现。

  • 轻量级 : Vue.js 相对于其他框架是一个轻量级的库。

Vue.js 中有哪些组件?

组件是可重用的元素,我们用它来定义它们的名字和行为。要理解这个概念,请看图 9-1 。

img/494195_1_En_9_Fig1_HTML.jpg

图 9-1

web 应用中的组件

您可以在图 9-1 中看到,我们有六个不同级别的组件。组件 1 是组件 2 和组件 3 的父级,也是组件 4、组件 5 和组件 6 的祖父级。因此,我们可以制作一个层次树来表达这种关系(图 9-2 )。

img/494195_1_En_9_Fig2_HTML.jpg

图 9-2

组件层次结构

现在,认为每个组件都可以是我们想要的——列表、图像、按钮、文本或我们定义的任何东西。清单 9-1 显示了定义简单 Vue 组件的基本方法。

const app = createApp({...})
app.component('my-button', {
  data: function () {
    return {
      counter: 0
    }
  },
  template: '<button v-on:click="counter++">Clicks {{ counter }}.</button>'
})}

Listing 9-1Declaring a Vue’s Component

我们可以将它作为新的 HTML 标签添加到我们的应用中,如清单 9-2 所示。

<div id="app">
  <my-button></my-button>
</div>

Listing 9-2Using a Vue’s Component

Vue 组件中的生命周期挂钩有哪些?

组件被创建和销毁。总的来说,这些过程被称为一个生命周期,我们可以使用一些方法在该周期的特定时刻运行功能。考虑出现在清单 9-3 中的组件。

<script>
export default {
  name: 'HelloWorld',
  created: function () {
    console.log('Component created.');
  },
  mounted: function() {
    fetch('https://randomuser.me/api/?results=1')
    .then(
      (response) => {
        return response.json();
      }
    )
    .then(
      (reponseObject) => {
        this.email = reponseObject.results[0].email;
      }
    );
    console.log('Component is mounted.');
  },
  props: {
    msg: String,
    email:String
  }
}
</script>

Listing 9-3Using Life Cycle Hooks in a Vue Component

这里,我们用方法created()mounted()添加了一个 email 属性。这些方法被称为生命周期挂钩。我们将使用这些来在组件中的特定时刻执行操作,例如,当我们的组件被挂载时调用一个 API,并在那时收到电子邮件。

生命周期挂钩是任何重要 Vue 组件的重要组成部分(见图 9-3 )。

img/494195_1_En_9_Fig3_HTML.jpg

图 9-3

Vue 组件的生命周期

创建前

beforeCreate钩子在组件初始化的早期运行。你可以使用清单 9-4 中的方法。

Vue.createApp({
  beforeCreate: function () {
    console.log('Initialization is happening');
})

Listing 9-4Using beforeCreate in a Vue Component

创造

当你的组件被初始化时,created钩子运行。您将能够访问 React 数据,并且事件是活动的。你可以使用清单 9-5 中的方法。

Vue.createApp({
  created: function () {
    console.log('The Component is created');
})

Listing 9-5Using created in a Vue Component

即将挂载

beforeMount钩子在初始渲染发生之前和模板或渲染函数被编译之后运行。你可以使用清单 9-6 中的方法。

Vue.createApp({
  beforeMount: function () {
    console.log('The component is going to be Mounted');
  }
})

Listing 9-6Using beforeMount in a Vue Component

安装好的

mounted钩子提供了对 React 组件、模板和呈现的 DOM ( via. this.$el)的完全访问。你可以使用清单 9-7 中的方法。

Vue.createApp({
  mounted: function () {
    console.log('The component is mounted');
  }
})

Listing 9-7Using mounted in a Vue Component

更新前

beforeUpdate钩子在组件上的数据改变之后运行,更新周期开始,就在 DOM 被修补和重新呈现之前。你可以使用清单 9-8 中的方法。

Vue.createApp({
  beforeUpdate: function () {
    console.log('The component is going to be updated');
  }
})

Listing 9-8Using beforeUpdate in a Vue Component

更新

在组件上的数据改变和 DOM 重新呈现之后,updated钩子运行。你可以使用清单 9-9 中的方法。

Vue.createApp({
  updated: function () {
    console.log('The component is updated');
  }
})

Listing 9-9Using updated in a Vue Component

销毁前

就在组件被销毁之前调用了beforeDestroy钩子。您的组件仍将完全存在并正常工作。你可以使用清单 9-10 中的方法。

Vue.createApp({
  beforeDestroy: function () {
    console.log('The component is going to be destroyed');
  }
})

Listing 9-10Using beforeDestroy in a Vue Component

破坏

当连接到钩子上的所有东西都被销毁时,这个钩子就会被调用。您可以使用destroyed钩子来执行最后的清理。你可以使用清单 9-11 中的方法。

Vue.createApp({
  destroyed: function () {
    console.log('The component is destroyed');
  }
})

Listing 9-11Using destroyed in a Vue Component

Vue 组件之间的通信

组件之间通常必须共享信息。对于这些基本场景,我们可以使用 props 或 ref 属性,如果你想把数据传递给子组件;发射器,如果你要把数据传递给一个父组件;以及双向数据绑定,使子节点和父节点之间的数据同步(参见图 9-4 )。

img/494195_1_En_9_Fig4_HTML.jpg

图 9-4

父组件和子组件之间的通信

如图 9-4 所示,如果你想要一个父组件与子组件通信,你可以使用 props 或者 ref 属性。

什么是属性?

属性是可以在组件上注册的自定义属性。当一个值被传递给一个适当属性时,它就成为组件实例的一个属性。你可以在清单 9-12 中看到基本结构。

const app = createApp({...})
app.component('some-item', {
  props: ['somevalue'],
  template: '<div>{{ somevalue }}</div>'
})

Listing 9-12Declaring Props in a Vue Component

现在您可以像清单 9-13 中那样传递值。

<some-item somevalue="value for prop"></some-item>

Listing 9-13Using Props in a Vue Component

什么是 Ref 属性?

ref 属性用于注册对元素或子组件的引用。该引用将注册在父组件的$refs对象下。 1 基本结构见清单 9-14 。

<input type="text" ref="email">

<script>
    const input = this.$refs.email;
</script>

Listing 9-14Using a Ref in a Vue Component

发出事件

如果想和有家长的孩子沟通,可以用$emit(),这是推荐的传递信息的方法。调用 prop 函数是传递信息的另一种方法(图 9-5 ),但这是一种不好的做法,因为当你的项目在增长时,这可能会令人困惑,这也是我跳过这一点的原因。

img/494195_1_En_9_Fig5_HTML.jpg

图 9-5

与具有父组件的子组件通信

在 Vue 中,可以使用$emit方法将数据发送给父组件。在清单 9-15 中,您可以看到发出事件的基本结构。

const app = createApp({...})
app.component('child-custom-component', {
  data: function () {
    return {
      customValue: 0
    }
  },
  methods: {
    giveValue: function () {
      this.$emit('give-value', this.customValue++)
    }
  },
  template: `
    <button v-on:click="giveValue">
      Click me for value
    </button>
  `
})

Listing 9-15Using a Ref in a Vue Component

使用双向数据绑定

促进组件间通信的一个简单方法是使用双向数据绑定。在下面的场景中,Vue.js 为我们进行组件之间的通信(图 9-6 )。

img/494195_1_En_9_Fig6_HTML.jpg

图 9-6

组件之间的双向通信

双向数据绑定意味着 Vue.js 为你同步数据属性和 DOM。对数据属性的更改会更新 DOM,对 DOM 的修改会更新数据属性。因此,数据是双向流动的。在清单 9-16 中,您可以看到使用双向数据绑定的基本结构。

const app = Vue.createApp({})
app.component('my-component', {
  props: {
    myProp: String
  },
  template: `
    <input
      type="text"
      :value="myProp"
      @input="$emit('update: myProp, $event.target.value)">
  `
})
<my-component v-model:myProp="Some Value"></my-component>

Listing 9-16Using v-model in a Vue Component

材料网组件

在第八章中,我们使用 VanillaJS 和 Polymer 构建了一些 web 组件。这是学习这些技术基础的一个极好的方法。尽管如此,对于我们的 VueNoteApp,我们将使用一个更健壮的 Web 组件目录,由 Google 维护,它实现了材料设计并使用LitElement构建。你可以在 https://github.com/material-components/material-components-web-components 找到这些组件。

使用这些组件有助于我们提高生产率,因为我们有正确的材料设计指南,并提高质量,因为它们经过更多测试,在我们的用户界面中有更多使用选项。

建筑 VueNoteApp

本节我们要搭建一个完整的笔记 App,如图 9-7 所示,用 Vue,采用材质设计。

img/494195_1_En_9_Fig7_HTML.jpg

图 9-7

vuemoteapp 设计

创建新的 Vue 项目

首先,我们将使用 Vue CLI 在 Vue 3 中创建新项目。确保您使用的是最新版本的 Vue CLI,并运行以下命令:

$npm update -g @vue/cli

要创建项目,请在终端中运行以下命令:

$vue create note-app

选择 Vue 3,如图 9-8 所示。

img/494195_1_En_9_Fig8_HTML.jpg

图 9-8

CLI 视图中的 pick vue 3

安装完所有依赖项后,转到文件夹项目。

$cd note-app

添加材料 Web 组件

首先,我们将安装应用中要使用的 web 组件。

安装mwc-button(图 9-9 )。

img/494195_1_En_9_Fig9_HTML.jpg

图 9-9

mwc-button组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-button

https://github.com/material-components/material-components-web-components/tree/master/packages/button 可以看到它的所有属性。

安装mwc-dialog(图 9-10 )。

img/494195_1_En_9_Fig10_HTML.jpg

图 9-10

mwc-dialog组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-dialog

https://github.com/material-components/material-components-web-components/tree/master/packages/dialog 可以看到它的所有属性。

安装mwc-fab(图 9-11 )。

img/494195_1_En_9_Fig11_HTML.jpg

图 9-11

mwc-fab组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-fab

https://github.com/material-components/material-components-web-components/tree/master/packages/fab 可以看到它的所有属性。

安装mwc-icon-button(图 9-12 )。

img/494195_1_En_9_Fig12_HTML.jpg

图 9-12

mwc-icon-button组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-icon-button

https://github.com/material-components/material-components-web-components/tree/master/packages/icon-button 可以看到它的所有属性。

安装mwc-list(图 9-13 )。

img/494195_1_En_9_Fig13_HTML.jpg

图 9-13

mwc-list组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-list

你可以在 https://github.com/material-components/material-components-web-components/tree/master/packages/list 中看到它的所有属性。

安装mwc-textfield(图 9-14 )。

img/494195_1_En_9_Fig14_HTML.jpg

图 9-14

mwc-textfield组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-textfield

https://github.com/material-components/material-components-web-components/tree/master/packages/textfield 可以看到它的所有属性。

安装mwc- top-app-bar(图 9-15 )。

img/494195_1_En_9_Fig15_HTML.jpg

图 9-15

mwc-top-app-bar组件

然后在您的终端中运行以下命令:

$npm install –-save @material/mwc-top-app-bar

https://github.com/material-components/material-components-web-components/tree/master/packages/top-app-bar 可以看到它的所有属性。

现在我们将安装 Polyfills 来支持旧的 web 浏览器。

安装以下依赖项:

$npm install --save-dev copy-webpack-plugin @webcomponents/webcomponentsjs

copy-webpack-plugin添加到 Vue 的 Webpack 配置文件中。为此,我们必须创建一个新文件vue.config.js,并添加清单 9-17 中的代码。

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  configureWebpack: {
    plugins: [
      new CopyWebpackPlugin({
        patterns: [{
          context: 'node_modules/@webcomponents/webcomponentsjs',
          from: '**/*.js',
          to: 'webcomponents'
        }]
      })
    ]
  }
};

Listing 9-17Using copy-webpack-plugin with webcomponentsjs

copy-webpack-plugin现在会在构建时将所有需要的 JS 文件复制到 webcomponents 目录中。

现在,在public/index.html中,我们将添加一些行来检查 web 浏览器是否支持customElements或使用 Polyfills(参见清单 9-18 )。

<!DOCTYPE html>
<html lang="en">
  <head>
...
    <script src="webcomponents/webcomponents-loader.js"></script>
    <script>
      if (!window.customElements) { document.write('<!--'); }
    </script>
    <script src="webcomponents/custom-elements-es5-adapter.js"></script>
    <!-- ! DO NOT REMOVE THIS COMMENT -->

...
  </body>
</html>

Listing 9-18Adding webcomponentjs in VueNoteApp

components/HelloWorld.vue中,我们将添加mwc-button(见清单 9-19 )。

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <ul>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
    </ul>
    <h3>Essential Links</h3>
    <ul>
      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
    </ul>
    <h3>Ecosystem</h3>
    <ul>
      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
    </ul>
    <mwc-button id="myButton" label="Click Me!" @click="handleClick" raised></mwc-button>
  </div>
</template>

<script>
import '@material/mwc-button';

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  methods: {
    handleClick() {
      console.log('click');
    }
  },
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Listing 9-19Using mwc-button in HelloWorld.vue

要进行测试,请运行以下命令:

$npm run serve

您应该在 VueNoteApp 中看到该组件,如图 9-16 所示。

img/494195_1_En_9_Fig16_HTML.jpg

图 9-16

HelloWorld.vue中增加mwc-button

您可以在$git checkout v1.0.1从 GitHub 资源库访问该网站。

添加标题

我们将添加mwc-top-app-bar并创建一些空组件,以更好的结构组织我们的文件。我们必须修改Apps.vue并在这里添加mwc-top-app-bar组件,如清单 9-20 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <mwc-top-app-bar centerTitle>
    <mwc-icon-button icon="menu" slot="navigationIcon"></mwc-icon-button>
    <div slot="title">VueNoteApp</div>
    <mwc-icon-button icon="help" slot="actionItems"></mwc-icon-button>
    <div><!-- content --></div>
  </mwc-top-app-bar>
</template>

<script>
import '@material/mwc-top-app-bar';
import '@material/mwc-icon-button';

export default {
  name: 'App',
}
</script>

Listing 9-20Using mwc-button in HelloWorld.vue

此时,可以使用$npm run serve。结果将看起来如图 9-17 所示。

img/494195_1_En_9_Fig17_HTML.jpg

图 9-17

添加mwc-top-app-bar

此外,我们将创建两个空组件,views/Dashboard.vue(参见清单 9-21 )和 views/About.vue(参见清单 9-22 ),以允许我们的应用拥有不同的视图。

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

Listing 9-22About.vue

<template>
  <div>
    Dashboard
  </div>
</template>
<script>
export default {
  name: 'Dashboard'
}
</script>
<style>

</style>

Listing 9-21Dashboard.vue

您可以从位于$git checkout v1.0.2的 GitHub 库访问这段代码。

添加 Vue 路由

Vue Router 是一个用于单页面应用的官方路由插件,设计用于 Vue.js 框架内。路由是在单页应用中从一个视图跳到另一个视图的一种方式,无需刷新 web 浏览器。在 VueNoteApp 中集成 Vue 路由很容易。

安装插件。

$ vue add router

main.js中增加VueRouter,如清单 9-23 所示。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Listing 9-23Adding Router in main.js

编辑router/index.js文件,为DashboardAbout添加路由,如清单 9-24 所示。

import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '../views/Dashboard.vue'
const routes = [
  {
    path: '/',
    name: 'Dashboard',
    component: Dashboard
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

Listing 9-24Adding Router in router/index.js

如果你看一下/about路线,我们使用import()来装载组件。这是因为我们在用户使用视图时加载组件,而不是在启动应用时加载所有内容。这被称为延迟加载,它对性能有好处。现在我们必须在App.vue中添加一些东西。占位符<router-view>是组件将要被装载的地方,这取决于路线,<router-link>是在模板中改变路线的一种方式(参见清单 9-25 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <mwc-top-app-bar centerTitle>
    <div slot="title"><router-link to="/">VueNoteApp</router-link></div>
    <div slot="actionItems"><router-link to="/About">About</router-link></div>
    <div><router-view/></div>
  </mwc-top-app-bar>
</template>

<script>
import '@material/mwc-top-app-bar';
import '@material/mwc-icon-button';

export default {
  name: 'App',
  methods: {
    handleAbout() {
      this.$router.push('About');
    }
  },
}
</script>

<style>
  a, a:visited {
    color:white;
    text-decoration:none;
    padding: 5px;
  }
</style>

Listing 9-25Adding Routes in App.vue

经过这些小小的改动,您应该会看到带有路线的标题,您可以通过点击链接来更改这些路线,如图 9-18 所示。

img/494195_1_En_9_Fig18_HTML.jpg

图 9-18

添加路线

您可以从位于$git checkout v1.0.3的 GitHub 库访问代码。

现在我们要给views/Dashboard.vue添加一些元素(见清单 9-26 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list multi>
      <mwc-list-item twoline>
        <span>Item 0</span>
        <span slot="secondary">Secondary line</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
      <mwc-list-item twoline>
        <span>Item 1</span>
        <span slot="secondary">Secondary line</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
      <mwc-list-item twoline>
        <span>Item 2</span>
        <span slot="secondary">Secondary line</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
      <mwc-list-item twoline>
        <span>Item 3</span>
        <span slot="secondary">Secondary line</span>
      </mwc-list-item>
    </mwc-list>
    <mwc-fab class="floatButton" mini icon="add"></mwc-fab>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';

export default {
  name: 'Dashboard'
}
</script>
<style scoped>
.floatButton {
  position: fixed;
  bottom: 20px;
  right: 20px;
}
</style>

Listing 9-26Adding Web Components in Dashboard.vue

我们添加了mwc-list-items来显示注释,并添加了一个mwc-fab来拥有一个浮动按钮,我们将使用它来添加新的注释。

我们还添加了views/About.vue,如清单 9-27 所示。

<template>
  <div class="about">
    <h1>Building Native Web Components</h1>
    <h2><i>Front-End Development with Polymer and Vue.js</i></h2>
    <p>
    Start developing single-page applications (SPAs) with modern architecture. This book shows you how to create, design, and publish native web components, ultimately allowing you to piece together those elements in a modern JavaScript framework.<br/><br/>

    Building Native Web Components dives right in and gets you started building your first web component. You'll be introduced to native web component design systems and frameworks, discuss component-driven development and understand its importance in large-scale companies.
    You'll then move on to building web components using templates and APIs, and custom event lifecycles. Techniques and best practices for moving data, customizing, and distributing components are also covered. Throughout, you'll develop a foundation to start using Polymer, Vue.js, and Firebase in your day-to-day work.<br/><br/>

    Confidently apply modern patterns and develop workflows to build agnostic software pieces that can be reused in SPAs. Building Native Web Components is your guide to developing small and autonomous web components that are focused, independent, reusable, testable, and works with all JavaScript frameworks, modern browsers, and libraries.
    </p>
  </div>
</template>
<style scoped>
.about {
  background-color: white;
  text-justify:auto;
  padding: 30px;
}
</style>

Listing 9-27Adding Routes in About.vue

我们正在添加一些关于这本书的信息,在图 9-19 中,你可以看到外观上的改进,这更符合我们的目标。

img/494195_1_En_9_Fig19_HTML.jpg

图 9-19

Dashboard.vue中添加 Web 组件

您可以从位于$git checkout v1.0.4.的 GitHub 库访问代码

删除注释

有了mwc-listmws-list-item,我们可以在Dashboard.vue中以更令人愉快的方式看到我们的注释,但是我们的注释在代码中是静态的。这就是为什么我们要在utils/DummyData.js中创建一个模块,如清单 9-28 所示。

export const notesData = [
  {id: 1, title: "Note 1", description: "Loren Ipsum"},
  {id: 2, title: "Note 2", description: "Loren Ipsum"},
  {id: 3, title: "Note 3", description: "Loren Ipsum"},
  {id: 4, title: "Note 4", description: "Loren Ipsum"},
  {id: 5, title: "Note 5", description: "Loren Ipsum"},
  {id: 6, title: "Note 6", description: "Loren Ipsum"},
  {id: 7, title: "Note 7", description: "Loren Ipsum"}
];

Listing 9-28DummyData module

这个模块只是一个简单的数组,带有一些我们可以在Dashboard.vue中导入的注释,但是它很方便,因为现在我们可以动态地加载这些数据,如清单 9-29 所示。将来,我们可以很容易地为 API 或 Firebase 替换它。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" mini icon="add"></mwc-fab>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import { notesData } from '../utils/DummyData';

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: notesData
    }
  }
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }
</style>

Listing 9-29DummyData module

现在,在Dashboard.vue中,我们在属性注释中导入notesData,在模板中,我们使用directive v-for迭代所有注释,并为每个注释创建一个mwc-list-item,如图 9-20 所示。

img/494195_1_En_9_Fig20_HTML.jpg

图 9-20

Dashboard.vue中增加notesData

用户在插入新便笺时可能会出错。这就是为什么我们必须允许他们删除笔记。为此,我们必须修改数组并移除元素。JavaScript 有一个方法可以帮助我们做到这一点:SpliceSplice方法允许我们改变数组中的内容。

我们将创建handleDelete(id)方法并将其添加到 Delete 按钮,我们将向其传递我们想要删除的项目索引。使用Splice,我们可以从数组中移除内容,如清单 9-30 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" mini icon="add"></mwc-fab>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import { notesData } from '../utils/DummyData';

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: notesData
    }
  },
  methods: {
    handleDelete(id) {
      const noteToDelete = this.notes.findIndex((item) => (item.id === id));
      this.notes.splice(noteToDelete, 1);
    }
  },
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }
</style>

Listing 9-30DummyData Module

您可以从位于$git checkout v1.0.5的 GitHub 库访问相关代码。

添加新注释

在这一点上,我们可以动态地加载我们的笔记,并且我们可以从我们的列表中删除笔记。现在我们将添加一个添加新注释的机制,使用我们之前在右下角添加的fab按钮。为了实现这一点,我们将使用mwc-dialog,显示一个用户可以添加新注释或取消并返回注释列表的模态(见图 9-21 )。

img/494195_1_En_9_Fig21_HTML.jpg

图 9-21

Dashboard.vue中的mwc-dialog

此外,我们将添加库uuid。有了这个库,我们可以为新的笔记生成新的惟一 id。运行以下命令:

$npm install –save uuid

在模板中,我们将添加mwc-dialog,如清单 9-31 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" @click="handleAdd" mini icon="add"></mwc-fab>
    <mwc-dialog id="dialog" heading="Add Note">
      <div class="formFields">
        <mwc-textfield
          id="text-title"
          outlined
          minlength="3"
          label="Title"
          required>
        </mwc-textfield>
      </div>
      <div class="formFields">
      <mwc-textfield
        id="text-description"
        outlined
        minlength="3"
        label="Description"
        required>
      </mwc-textfield>
      </div>
      <div>
      <mwc-button
        id="primary-action-button"
        slot="primaryAction"
        @click="handleAddNote">
        Add
      </mwc-button>
      <mwc-button
        slot="secondaryAction"
        dialogAction="close"
        @click="handleClose">
        Cancel
      </mwc-button>
      </div>
    </mwc-dialog>
  </div>
</template>

Listing 9-31Adding mwc-dialog

这里,我们在mwc-dialog中添加了一个表单,它使用mwc-textfieldmwc-button组件来允许新笔记进入并触发handleAddNote方法或handleClose方法来处理用户选择的内容。接下来,我们将在Dashboard.vue中添加这个逻辑,如清单 9-32 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" @click="handleAdd" mini icon="add"></mwc-fab>
    <mwc-dialog id="dialog" heading="Add Note">
      <div class="formFields">
        <mwc-textfield
          id="text-title"
          outlined
          minlength="3"
          label="Title"
          required>
        </mwc-textfield>
      </div>
      <div class="formFields">
      <mwc-textfield
        id="text-description"
        outlined
        minlength="3"
        label="Description"
        required>
      </mwc-textfield>
      </div>
      <div>
      <mwc-button
        id="primary-action-button"
        slot="primaryAction"
        @click="handleAddNote">
        Add
      </mwc-button>
      <mwc-button
        slot="secondaryAction"
        dialogAction="close"
        @click="handleClose">
        Cancel
      </mwc-button>
      </div>
    </mwc-dialog>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-textfield';
import { notesData } from '../utils/DummyData';
import { v4 as uuidv4 } from 'uuid';

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: notesData
    }
  },
  methods: {
    handleDelete(id) {
      const noteToDelete = this.notes.findIndex((item) => (item.id === id));
      this.notes.splice(noteToDelete, 1);
    },
    handleAdd() {
      const formDialog = this.$el.querySelector('#dialog');
      formDialog.show();
    },
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        this.notes.push({id: newIndex, title: txtTitle.value, description: txtDescription.value});

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
    handleClose() {
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const formDialog = this.$el.querySelector('#dialog');

      txtTitle.value ='';
      txtDescription.value = '';
      formDialog.close();
    }
  },
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }

  .formFields {
    margin: 15px;
  }
</style>

Listing 9-32Adding mwc-dialog

这里,我们使用this.$el.querySelector()来选择 web 组件,并使用它们的方法来执行逻辑,例如formDialog.show()formDialog.close(),以显示和隐藏mwc-dialog。此外,在handleAddNote()中,我们使用uuidv4()方法为用户在表单中输入的数据生成一个新的索引,并使用this.notes.push()在本地数组中添加新的笔记,我们使用该数组保存所有笔记。通过这些修改,您可以添加注释(参见图 9-22 )。

img/494195_1_En_9_Fig22_HTML.jpg

图 9-22

添加新注释

您可以从位于$git checkout v1.0.6的 GitHub 库访问相关代码。

Challenge Exercise

在我们的应用中添加一个操作按钮和功能来编辑注释。

  • 新增一个按钮,编辑,类似于删除。

  • 当用户点击编辑时,应用将打开一个类似于新笔记的对话窗口,但选择了笔记中的信息。

  • 用户可以进行更改并再次保存数据,注释应该会更新。

添加 Firebase

如果您尝试重新加载应用,您会发现我们丢失了所有笔记。我们需要一个外部持久性系统来保存我们的数据,并在我们所有的客户端之间进行同步。

Firebase 数据库为我们提供了一个完美的解决方案,可以为我们的所有客户实时保持数据同步,我们可以使用 Firebase 的 JavaScript 软件开发工具包(SDK)轻松保存数据。

要开始,请转到firebase.google.com并使用您的 Google 帐户登录。

接下来,进入控制台(图 9-23 )。

img/494195_1_En_9_Fig23_HTML.jpg

图 9-23

Firebase 主控台连结

创建一个新项目(图 9-24 )。

img/494195_1_En_9_Fig24_HTML.jpg

图 9-24

添加项目按钮

接下来,您将选择项目的名称(图 9-25 )。

img/494195_1_En_9_Fig25_HTML.jpg

图 9-25

在 Firebase 中创建新项目

选择您是否希望在您的项目中使用 Google Analytics 集成(图 9-26 )。

img/494195_1_En_9_Fig26_HTML.jpg

图 9-26

向新的 Firebase 项目添加 Google Analytics

只需几分钟,您就可以开始使用您的新项目。

当您的项目在 Firebase 控制台中准备就绪时,您将需要一些信息来连接我们的应用和 Firebase。

为此,请转到项目概述➤项目设置。(参见图 9-27 。)

img/494195_1_En_9_Fig27_HTML.jpg

图 9-27

Firebase 项目概述

现在点击网络应用图标(图 9-28 )。

img/494195_1_En_9_Fig28_HTML.jpg

图 9-28

Firebase 项目设置视图

Firebase 将要启动一个向导(图 9-29 )。

img/494195_1_En_9_Fig29_HTML.jpg

图 9-29

Firebase web 应用向导

最后你会看到我们的firebaseConfig总结(图 9-30 )。

img/494195_1_En_9_Fig30_HTML.jpg

图 9-30

Firebase 配置摘要

复制这些信息。当我们为项目创建firebase.js文件时,您将需要它。

我们需要在控制台中做的最后一件事是创建一个新的数据库,以及在没有身份验证的情况下访问它的安全规则。(我们这样做是为了让我们的应用简单。)

转到数据库(图 9-31 )。

img/494195_1_En_9_Fig31_HTML.jpg

图 9-31

Firebase 数据库链接

选择创建实时数据库(图 9-32 )。

img/494195_1_En_9_Fig32_HTML.jpg

图 9-32

Firebase 安全规则

选择测试模式下的开始。在这种模式下,我们可以在没有身份验证的情况下将数据写入数据库。这在开发中很方便,但在生产中不安全。现在我们可以回到我们的应用。

在终端中运行以下命令:

$npm install firebase --save

创建一个新的firebase.js文件并粘贴来自 Firebase 项目的数据,如清单 9-33 所示。

import Firebase from 'firebase';

let config = {
  apiKey: "xxx",
  authDomain: "xxx",
  databaseURL: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};

Firebase.initializeApp(config)

export const fireApp = Firebase;

Listing 9-33Adding Firebase

导入main.js,如清单 9-34 所示。

import Firebase from 'firebase';

import './firebase';
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')...

Listing 9-34Importing Firebase in main.js

您可以从位于$git checkout v1.0.7的 GitHub 库访问相关代码。

现在,在Dashboard.vue中,我们必须使用生命周期mounted()方法,并恢复我们实时数据库中的所有笔记。我们还必须更新saveNote()deleteNote(),更新 Firebase 中的新注释。

下面,我们从firebase.js导入 Fireapp 以在我们的应用中保留一个引用:

const db = fireApp.database().ref();

现在,使用db.push,我们向 Firebase 添加数据,使用remove(),我们可以从 Firebase 中删除数据(清单 9-35 )。欲了解更多信息,您可以通过 https://firebase.google.com/docs/reference/js/firebase.database 访问相关文件。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" @click="handleAdd" mini icon="add"></mwc-fab>
    <mwc-dialog id="dialog" heading="Add Note">
      <div class="formFields">
        <mwc-textfield
          id="text-title"
          outlined
          minlength="3"
          label="Title"
          required>
        </mwc-textfield>
      </div>
      <div class="formFields">
        <mwc-textfield
          id="text-description"
          outlined
          minlength="3"
          label="Description"
          required>
        </mwc-textfield>
      </div>
      <div>
        <mwc-button
          id="primary-action-button"
          slot="primaryAction"
          @click="handleAddNote">
          Add
        </mwc-button>
        <mwc-button
          slot="secondaryAction"
          dialogAction="close"
          @click="handleClose">
          Cancel
        </mwc-button>
      </div>
    </mwc-dialog>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-textfield';
import { fireApp } from'../firebase'
import { v4 as uuidv4 } from 'uuid';

const db = fireApp.database().ref();

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: []
    }
  },
  mounted() {
    db.once('value', (notes) => {
      notes.forEach((note) => {
        this.notes.push({
          id: note.child('id').val(),
          title: note.child('title').val(),
          description: note.child('description').val(),
          ref: note.ref
        })
      })
    });
  },
  methods: {
    handleDelete(id) {
      const noteToDelete = this.notes.findIndex((item) => (item.id === id));
      const noteRef = this.notes[noteToDelete].ref;
      if(noteRef) {
        noteRef.remove();
      }
      this.notes.splice(noteToDelete, 1);
    },
    handleAdd() {
      const formDialog = this.$el.querySelector('#dialog');
      formDialog.show();
    },
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        const newItem = {id: newIndex, title: txtTitle.value, description: txtDescription.value};
        this.notes.push(newItem);
        db.push(newItem);

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
    handleClose() {
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const formDialog = this.$el.querySelector('#dialog');

      txtTitle.value ='';
      txtDescription.value = '';
      formDialog.close();
    }
  },
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }

  .formFields {
    margin: 15px;
  }
</style>

Listing 9-35Adding Firebase in Dashboard.vue

现在我们可以在 Firebase 中看到我们的数据存储(图 9-33 )。

img/494195_1_En_9_Fig33_HTML.jpg

图 9-33

Firebase 数据库控制台

当我们刷新时,我们的数据将被保存(图 9-34 )。

img/494195_1_En_9_Fig34_HTML.jpg

图 9-34

vuemoteapp 在 firebase 中的持久性

您可以从位于$git checkout chap-9的 GitHub 库访问相关代码。

摘要

在本章中,您学习了以下内容:

  • Vue.js 只关注视图层,很容易获取并与其他库或现有项目集成。

  • 组件的创建和销毁过程统称为生命周期。我们可以用一些方法在特定时刻运行函数。这些方法被称为生命周期挂钩。生命周期挂钩是任何严肃组件的重要组成部分。

  • 组件之间通常需要共享信息。对于这些基本场景,我们可以使用 props、ref属性、发射器和双向数据绑定。

  • Vue Router 是一个用于单页面应用的官方路由插件,设计用于在Vue.js框架内使用。路由是在单页应用中从一个视图跳到另一个视图的一种方式。

  • 我们可以使用 Firebase 数据库来保持我们的笔记在所有客户端之间同步。

十、发布

恭喜你!我们现在有了自己的 web 应用,使用 Vue.js 和 Web 组件,但我们希望与全世界分享我们的应用。Firebase 托管可以帮助我们公开我们的应用,Firebase 身份验证允许我们添加一个身份验证系统,并注册用户和每个用户的相关注释。

添加 Firebase 身份验证

通过其软件开发工具包,Firebase Authentication 为我们提供了一种在 JavaScript 应用中添加身份验证的安全方法。但是,我们必须选择我们的应用将支持的登录提供商。你可以选择最流行的,如脸书和谷歌,或将登录集成到一个自定义的认证系统。为了简单起见,我们将只选择电子邮件和密码。

首先,我们将转到 Firebase web 控制台并选择身份验证(参见图 10-1 )。

img/494195_1_En_10_Fig1_HTML.jpg

图 10-1

Firebase web 控制台中的身份验证链接

在身份验证中(图 10-2 ,我们将选择登录方式。

img/494195_1_En_10_Fig2_HTML.jpg

图 10-2

Firebase web 控制台中的身份验证部分

现在启用电子邮件/密码(图 10-3 )。

img/494195_1_En_10_Fig3_HTML.jpg

图 10-3

在 Firebase web 控制台中启用电子邮件/密码身份验证

好了,web 控制台到此为止。

回到我们的代码。我们需要一个表单的新组件,我们可以输入电子邮件和密码,以执行身份验证过程。

创建一个新的components/Login.vue文件并添加一个模板,如清单 10-1 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div class="formContainer">
    <div class="formLogin">
      <div class="formFields">
        <h1>{{actionText}}</h1>
        <mwc-textfield
            id="text-email"
            outlined
            minlength="3"
            label="Email"
            required>
          </mwc-textfield>
        </div>
        <div class="formFields">
          <mwc-textfield
            id="text-password"
            type="password"
            outlined
            minlength="3"
            label="Password"
            required>
          </mwc-textfield>
        </div>
        <div class="actionButtons">
          <mwc-button
            id="link-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="goToLink">
            {{linkButton}}
          </mwc-button>
        </div>
        <div class="actionButtons">
          <mwc-button
            raised
            id="primary-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="submitAction">
            {{actionButton}}
          </mwc-button>
          <mwc-button
            raised
            slot="secondary-action-button"
            class="actionButton"
            @click="handleClear">
            Clear
          </mwc-button>
        </div>
      </div>
  </div>
</template>
<script>
import '@material/mwc-textfield';
import '@material/mwc-button';
export default {
  name: 'Login',
  data() {
    return {
      actionText: 'Login',
      actionButton: 'Send',
      linkButton: 'Register'
    }
  },
  methods: {
    goToLink () {
      switch(this.linkButton) {
      case 'Login':
        this.actionText= 'Login';
        this.actionButton= 'Send';
        this.linkButton = 'Register';
        break;
      case 'Register':
        this.actionText= 'Register';
        this.actionButton= 'Register';
        this.linkButton = 'Login';
        break;
      }
    },

}
</script>
<style scoped>
  .formContainer {
    display:flex;
  }

  .formLogin {
    margin:auto;
  }

  .formFields {
    margin: 15px;
  }

  .actionButtons {
    text-align:center;
  }

  .actionButton {
    margin: 10px;
  }
</style>

Listing 10-1Adding a Template in Login.vue

这样,我们就可以添加一个表单来登录和注册用户。我们对两种操作使用相同的视图,actionTextactionButtonlinkButton切换用户在视图中可以看到的文本,如图 10-4 所示。

img/494195_1_En_10_Fig4_HTML.jpg

图 10-4

登录组件

下一步是添加认证特性(清单 10-2 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div class="formContainer">
    <div class="formLogin">
      <div class="formFields">
        <h1>{{actionText}}</h1>
        <mwc-textfield
            id="text-email"
            outlined
            minlength="3"
            label="Email"
            required>
          </mwc-textfield>
        </div>
        <div class="formFields">
          <mwc-textfield
            id="text-password"
            type="password"
            outlined
            minlength="3"
            label="Password"
            required>
          </mwc-textfield>
        </div>
        <div class="actionButtons">
          <mwc-button
            id="link-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="goToLink">
            {{linkButton}}
          </mwc-button>
        </div>
        <div class="actionButtons">
          <mwc-button
            raised
            id="primary-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="submitAction">
            {{actionButton}}
          </mwc-button>
          <mwc-button
            raised
            slot="secondary-action-button"
            class="actionButton"
            @click="handleClear">
            Clear
          </mwc-button>
        </div>
      </div>
  </div>
</template>
<script>
import '@material/mwc-textfield';
import '@material/mwc-button';
import { fireApp } from '../firebase';

const auth = fireApp.auth();

export default {
  name: 'Login',
  data() {
    return {
      actionText: 'Login',
      actionButton: 'Send',
      linkButton: 'Register'
    }
  },
  methods: {
    goToLink () {
      switch(this.linkButton) {
      case 'Login':
        this.actionText= 'Login';
        this.actionButton= 'Send';
        this.linkButton = 'Register';
        break;
      case 'Register':
        this.actionText= 'Register';
        this.actionButton= 'Register';
        this.linkButton = 'Login';
        break;
      }
    },
    goToDashboard () {
      this.$router.push('/dashboard');
    },
    signInUser (email, password) {
      auth.signInWithEmailAndPassword(email,password)
      .then(
        () => {
        this.goToDashboard();
        }
      )
      .catch(
        // eslint-disable-next-line
        (error) => {console.log('Something happened.', error)}
      );
    },
    signUpUser (email, password) {
      auth.createUserWithEmailAndPassword(email,password)
      .then(
        // eslint-disable-next-line
        (user) => {console.log('User registered.', user)}
      )
      .catch(
        // eslint-disable-next-line
        (error) => {console.log('Something happened.', error)}
      );
    },
    submitAction () {

      let txtEmail = this.$el.querySelector('#text-email');
      let txtPassword = this.$el.querySelector('#text-password');
      const isValid = txtEmail.checkValidity() && txtPassword.checkValidity();

      if(isValid) {
        switch(this.actionText) {
          case 'Login':
            this.signInUser (txtEmail.value, txtPassword.value);

            txtEmail.value = '';
            txtPassword.value ='';
            break;
          case 'Register':
            this.signUpUser (txtEmail.value, txtPassword.value);

            txtEmail.value = '';
            txtPassword.value ='';
            break;
      }
      }
    }
  },
}
</script>
<style scoped>
  .formContainer {
    display:flex;
  }

  .formLogin {
    margin:auto;
  }

  .formFields {
    margin: 15px;
  }

  .actionButtons {
    text-align:center;
  }

  .actionButton {
    margin: 10px;
  }
</style>

Listing 10-2Adding a Firebase Authentication in Login.vue

这里,我们使用submitAction()方法来检查我们是否在应用中创建新用户或登录,并传递用户在表单中输入的电子邮件和密码。如果用户想要登录 app,我们使用 Firebase auth.signInWithEmailAndPassword()方法来检查用户是否存在于 Firebase 中。如果密码正确,我们将被重定向到仪表板视图。如果用户正在创建一个新帐户,我们使用 Firebase auth.createUserWithEmailAndPassword()方法将这个新用户添加到 Firebase。

最后,我们将修改我们的文件router/index.js来添加这个新组件(清单 10-3 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'

const routes = [
  {
    path: '',
    component: Login
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router

Listing 10-3Adding the Login Component in the Vue Router

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.8访问相关代码。

添加防护装置

现在,在App.vue中,我们必须添加一个机制,防止未登录的用户直接从 URL 访问Dashboard.vue。为了实现这一点,我们将使用 Vue 路由的一个名为 Guards 的功能。守卫主要用于通过重定向或取消导航来保护导航。可以在 https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards 处了解更多守卫信息。我们将把这个机制添加到我们的router/index.js文件中(清单 10-4 )。

import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import { fireApp } from '../firebase';

const routes = [
  {
    path: '',
    component: Login
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue'),
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth && !await fireApp.getCurrentUser()) {
    next('Login');
  } else {
    next();
  }
})

export default router

Listing 10-4Adding Navigation Guards in the Vue Router

在这里,我们在/Dashboard中使用一个路由元字段来加强这个路由中的认证,并且我们在 guard 中检查这一点。此外,我们正在检查来自fireAppgetCurrentUser()方法。我们必须在 firebase.js 中添加这个方法(清单 10-5 )。

import Firebase from 'firebase';

let config = {
  apiKey: "xxx",
  authDomain: "xxx",
  databaseURL: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};

Firebase.initializeApp(config)

Firebase.getCurrentUser = () => {
  return new Promise((resolve, reject) => {
      const unsubscribe = Firebase.auth().onAuthStateChanged(user => {
          unsubscribe();
          resolve(user);
      }, reject);
  })
};
export const fireApp = Firebase;

Listing 10-5Adding the getCurrentUser() Method

in firebase.js

getCurrentUser方法为我们提供了关于当前用户或拒绝的信息。最后,我们将在顶部添加一个注销链接,供用户关闭他们的会话。

App.vue中,我们将在<mwc-top-app-bar>(列表 10-6 )中添加一个链接。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <mwc-top-app-bar>
    <div slot="title"><router-link to="/">VueNoteApp</router-link></div>
    <div slot="actionItems"><router-link to="/About">About</router-link></div>
    <div slot="actionItems" v-if="logged" @click="handleLogout">Logout</div>
    <div><router-view/></div>
  </mwc-top-app-bar>
</template>

<script>
import '@material/mwc-top-app-bar';

import { fireApp } from './firebase';
const auth = fireApp.auth();

export default {
  name: 'App',
  data() {
    return {
      logged: false
    }
  },
  mounted() {
    fireApp.getCurrentUser()
      .then((user)=> {
        this.logged = user;
        this.$router.push('Dashboard');
        })
      .catch(() => {
        this.logged = false;
      });
  },
  methods: {
    handleAbout() {
      this.$router.push('About');
    },
    handleLogout() {
      auth.signOut()
        .then(()=>{
          this.$router.push('/login');
          this.logged= false;
          })
        .catch((error)=> {
          // eslint-disable-next-line
          console.log('error', error)
        });
    }
  },
}
</script>

<style>
  a, a:visited {
    color:white;
    text-decoration:none;
    padding: 5px;
  }
</style>

Listing 10-6Adding Logout Mechanism in App.vue

在这里,我们使用fireApp.currentUser()来检查在挂载Login.vue时用户是否登录(),我们是否可以直接跳转到Dashboard.vue,或者我们是否要求用户输入凭证。此外,我们创建了handleLogout(),这是一个使用auth.signOut关闭会话并将用户重定向到Login.vue的方法。当用户登录时,我们使用v-if指令显示注销链接,然后,仅当用户已经登录时,我们向用户显示该链接(图 10-5 )。

img/494195_1_En_10_Fig5_HTML.jpg

图 10-5

注销链接

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.9访问相关代码。

向数据中添加用户

如果您创建几个用户并添加或删除注释,您可以看到这些注释在所有帐户中共享。这是因为我们没有为每个用户过滤笔记。为了实现这一点,我们必须采取一些额外的步骤。

首先,我们将在用户创建的每个便笺中添加用户 ID,如清单 10-7 所示。

...
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        const newItem = {
          id: newIndex,
          title: txtTitle.value,
          description: txtDescription.value,
          userId: this.user.uid
        };
        this.notes.push(newItem);
        db.push(newItem);

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
...

Listing 10-7Adding userId Property

in New Notes

现在,数据在每个笔记和创建该笔记的用户之间有了关系(图 10-6 )。

img/494195_1_En_10_Fig6_HTML.jpg

图 10-6

Firebase web 控制台数据库数据

现在我们必须返回 Firebase web 控制台,更新我们的安全规则(图 10-7 )。

img/494195_1_En_10_Fig7_HTML.jpg

图 10-7

Firebase web 控制台数据库安全规则

这样,只有登录的会话才能在我们的数据库中读写数据。此外,我们用userId创建一个索引,以便更快地执行搜索/排序。

现在我们必须过滤笔记(清单 10-8 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" @click="handleAdd" mini icon="add"></mwc-fab>
    <mwc-dialog id="dialog" heading="Add Note">
      <div class="formFields">
        <mwc-textfield
          id="text-title"
          outlined
          minlength="3"
          label="Title"
          required>
        </mwc-textfield>
      </div>
      <div class="formFields">
        <mwc-textfield
          id="text-description"
          outlined
          minlength="3"
          label="Description"
          required>
        </mwc-textfield>
      </div>
      <div>
        <mwc-button
          id="primary-action-button"
          slot="primaryAction"
          @click="handleAddNote">
          Add
        </mwc-button>
        <mwc-button
          slot="secondaryAction"
          dialogAction="close"
          @click="handleClose">
          Cancel
        </mwc-button>
      </div>
    </mwc-dialog>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-textfield';
import { fireApp } from'../firebase'
import { v4 as uuidv4 } from 'uuid';

const db = fireApp.database().ref();
const auth = fireApp.auth();

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: [],
      user: null
    }
  },
  mounted() {
    this.isUserLoggedIn()
    .then(
      (user) => {
        this.user = user;
        this.updateLogged();
        this.getUserNotes();
      }
    )
    .catch(
      () => {
        this.$router.push('/login');
      }
    )
    ;
  },
  methods: {
    getUserNotes() {
      db.orderByChild('userId')
        .equalTo(this.user.uid)
        .once("value")
        .then(
          (notes) => {
            notes.forEach((note) => {
              this.notes.push({
                id: note.child('id').val(),
                title: note.child('title').val(),
                description: note.child('description').val(),
                userId: note.child('userId').val(),
                ref: note.ref
              })
            })
          }
        );
    },
    handleDelete(id) {
      const noteToDelete = this.notes.findIndex((item) => (item.id === id));
      const noteRef = this.notes[noteToDelete].ref;
      if(noteRef) {
        noteRef.remove();
      }
      this.notes.splice(noteToDelete, 1);
    },
    handleAdd() {
      const formDialog = this.$el.querySelector('#dialog');
      formDialog.show();
    },
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        const newItem = {
          id: newIndex,
          title: txtTitle.value,
          description: txtDescription.value,
          userId: this.user.uid
        };
        this.notes.push(newItem);
        db.push(newItem);

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
    handleClose() {
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const formDialog = this.$el.querySelector('#dialog');

      txtTitle.value ='';
      txtDescription.value = '';
      formDialog.close();
    },
    updateLogged() {
      this.$emit("update-logged", true);
    },
    isUserLoggedIn () {
        return new Promise(
          (resolve, reject) => {
            auth.onAuthStateChanged(function(user) {
              if (user) {
                resolve(user);
              }
              else {
                reject(user);
              }
            })
          }
        )
        ;
    }
  },
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }

  .formFields {
    margin: 15px;
  }
</style>

Listing 10-8Adding a Login in the Vue Router

这里,在getUserNotes()方法中,我们获取与一个用户相关联的注释,用户 ID 为db.orderByChild('userId').equalTo(this.user.uid).once("value")。这样,我们解决了笔记的问题,用户可以在每个帐户中拥有自己的笔记。

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.10访问相关代码。

发送到 Firebase 主机

首先,我们必须用我们的 Firebase 帐户验证我们的 Firebase CLI。为此,请运行以下命令:

$ firebase login

成功认证后,您可以开始使用 Firebase 工具将您的应用与 Firebase 连接起来。下一步是创建我们的产品包。来做这个实验

$ npm run build

这个命令创建了dist/文件夹,其中包含了我们所有优化的应用。

现在我们必须运行 Firebase CLI 向导来连接 VueNoteApp 和 Firebase(图 10-8 )。奔跑

img/494195_1_En_10_Fig8_HTML.jpg

图 10-8

Firebase CLI 选择要配置的服务

$ firebase init

选择主机(参见图 10-9 )。

img/494195_1_En_10_Fig9_HTML.jpg

图 10-9

Firebase CLI 选择 Firebase 项目设置

选择您在firebase.google.com中创建的项目——在我的例子中是apress-book-webcomponents(图 10-10 )。

img/494195_1_En_10_Fig10_HTML.jpg

图 10-10

Firebase CLI 选择公共目录

公共目录是dist/

好的。这样,我们就可以将我们的应用发送到 Firebase 主机了。为此,请运行以下命令:

$ npm run build
$ Firebase deploy

您将看到如图 10-11 所示的进度。

img/494195_1_En_10_Fig11_HTML.jpg

图 10-11

Firebase CLI 部署和获取公共 URL

最后,你会得到一个托管 URL。这是你的网址。现在可以用 https://apress-book-webcomponents.web.app 试试。

摘要

在本章中,您学习了

  • 如何在 VueNoteApp 中使用导航卫士

  • 如何在 Firebase 中启用电子邮件/密码验证

  • 如何将数据与用户帐户相关联

  • 如何准备 web 应用并将其发送到 Firebase 主机

最后的想法

如果你已经到了这一步,恭喜你!您现在知道如何在任何现有的 web 应用中构建、设计和使用 Web 组件。

如果您有任何意见或反馈,请随时通过iam@carlosrojas.dev联系我。

如果你有关于代码的问题,不要犹豫,在 https://github.com/carlosrojaso/apress-book-web-components/issues 查询这本书的 GitHub 知识库。

另外,为了更新,经常检查代码的官方存储库( https://github.com/carlosrojaso/apress-book-web-components )。

回头见,继续编程!

posted @ 2024-10-02 03:52  绝不原创的飞龙  阅读(59)  评论(0)    收藏  举报