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

63 阅读19分钟

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

原文:Building Native Web Components

协议:CC BY-NC-SA 4.0

七、分发 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 编译我们的组件

Footnotes 1

https://caniuse.com/es6

  2

https://caniuse.com/custom-elementsv1

  3

https://caniuse.com/template

  4

https://caniuse.com/shadowdomv1

  5

https://caniuse.com/es6-module

 

八、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 数据库来保持我们的笔记在所有客户端之间同步。

Footnotes 1

https://v3.vuejs.org/api/instance-properties.html#refs