五、开发天气小部件

嘿! 视频通话应用做得很好。希望你给你的朋友打了一些电话。 在前一章中,我们使用 SimpleWebRTC 框架构建了一个视频通话应用。 知道可以用 JavaScript 构建所有这些很酷的应用是不是很棒? 您可以直接从浏览器访问用户的设备硬件。

到目前为止,您已经自己构建了完整的应用,因此您完全了解应用的结构,比如 HTML 和 CSS 中使用的类和 id,以及 JavaScript 中使用的类、函数和服务。 但在现实世界中,你很少独自工作。 如果有的话,您的团队规模从几个成员到数百名开发人员,在全球范围内工作。 在这种情况下,您将不了解整个 web 应用。 在本章中,你是一个庞大的 web 应用项目的一部分。 这是你工作的第一周,然后你的经理走进来,把本周的任务交给你。

你能构建一个天气小部件吗?

所以,你的项目有大约 40 个开发人员在 web 应用的不同部分工作,一个新的需求就会突然出现。 他们需要在网站的某些区域显示天气插件。 天气小部件需要有响应,这样它才能挤进 web 应用的任何部分的任何可用空间。

我们当然可以建立一个天气小部件,但有一个问题。 我们不知道 web 应用的其余部分! 例如,HTML 中使用的类和 ID 是什么,因为 CSS 创建的样式总是全局的? 如果我们意外地使用了一个已经在 web 应用的其他部分的 HTML 中使用的类,我们的小部件将继承那个 DOM 元素的样式,这是我们真的需要避免的!

另一个问题是,我们会创建<div>。 例如:

<div class="weather-container">
  <div class="temperature-area">
  ....
  </div>
  <div>...</div>
  <div>...</div>
  <!-- 10 more divs -->
</div>

除了 CSS 文件和一些 JS 文件之外,我们还拥有使小部件工作所需的所有逻辑。 但我们要如何将它传递给团队的其他成员(考虑到我们没有在小部件中重用 web 应用中使用的任何其他类名或 id)?

如果它是一个简单的 JavaScript 模块,我们只需构建一个 ES6 模块,团队可以导入和使用它,因为 ES6 模块中的变量的范围不会泄漏(你应该只使用letconst; 您真的不希望意外地使用var创建全局变量。 但 HTML 和 CSS 的情况就不一样了。 它们的作用域总是全局的,并且总是需要小心处理(你不希望团队中的其他人意外地篡改你的小部件)!

所以,让我们开始吧! 我们将为 DOM 元素想出一些非常随机(而且很酷!)的类名和 id, 没有其他人在您的团队能想到的,然后编写一个 10 页的readme文件记录天气小部件的工作的注意事项,然后花时间在仔细更新readme文件当我们做一些改进和错误修正的小部件。 另外,一定要记住所有的类名和 id !

关于最后一段,不! 我们绝对不会这么做! 我想起来就起鸡皮疙瘩了! 相反,我们将学习 web 组件,我们将编写一个简单的 ES6 模块,你的其他团队成员应该导入它,然后他们应该简单地在他们的 HTML 文件中添加以下 DOM 元素:

<x-weather></x-weather>

就是这样! 您需要构建一个 DOM 元素(例如<input><p><div>元素),它将显示一个 Weather Widget。 x-weather是一个新的 HTML5自定义元素,我们将在本章中构建。 它将克服我们在以前的方法中可能面临的所有问题。

web 组件介绍

Web 组件是一组四种不同的技术,可以一起使用,也可以单独使用,以构建可重用的用户界面小部件。 就像我们可以用 JavaScript 创建可重用的模块一样,我们也可以用 web 组件技术创建可重用的 DOM 元素。 构成网络组件的四种技术是:

  • 自定义元素
  • HTML 模板
  • 影子 DOM
  • HTML 进口

创建 web 组件的目的是为开发人员提供简单的 api,以构建高度可重用的 DOM 元素。 有很多 JavaScript 库和框架通过将整个 web 应用组织成更简单的组件来提供可重用性,比如 React、Angular、Vue、Polymer 等等。 在下一章中,我们将通过将多个独立的 React 组件放在一起来构建一个完整的 web 应用。 然而,尽管有所有可用的框架和库,web 组件仍然有很大的优势,因为它们是由浏览器本地支持的,这意味着没有额外的库来增加你的小部件的大小。

对于我们的小部件,我们将使用自定义元素和阴影 DOM。 在我们开始构建我们的小部件之前,让我们快速了解一下其他两个小部件,我们将在本章中不使用它们。

Web components are a new standard and they are actively being implemented by all the browser vendors. However, at the time of writing this book, only Chrome supports all the features of the web components. If you want to check whether your browser supports web components, visit: http://jonrimmer.github.io/are-we-componentized-yet/.

你应该只在本章的项目中使用 Chrome,因为其他浏览器还没有对 web 组件的适当支持。 在本章的最后,我们将讨论如何添加腻子来让 web 组件在所有浏览器中工作。

HTML 模板

HTML 模板是一个简单的<template>标签,我们可以在 DOM 中添加它。 然而,即使我们在 HTML 中添加它,<template>元素的内容也不会被渲染。 如果它包含任何外部资源,如图像、CSS 和 JS 文件,它们也不会加载到我们的应用中。

因此,template 元素只包含一些 HTML 内容,这些内容稍后可以被 JavaScript 使用。 例如,假设你有以下模板元素:

<template id="image-template">
  <div>
    <h2>Javascript</h2>
    <img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/4621/javascript.png" alt="js-logo" style="height: 50px; width: 50px;">
  </div>
</template>

该元素保存着浏览器无法呈现的div。 然而,我们可以使用 JavaScript 创建一个div的引用,如下所示:

const $template = document.querySelector('#image-template');

现在,我们可以对这个引用进行任何更改,并将其添加到 DOM 中。 更好的是,我们可以对这个元素进行深度复制,这样就可以在多个地方使用它。 深层副本是对象的副本,其中对副本的更改不会反映在原始副本中。 默认情况下,当我们使用=操作符赋值时,JavaScript 总是做一个对象的浅拷贝。 $template是 DOM 元素的一个浅拷贝,我们称之为 DOM 元素的引用。 因此,对$template的任何更改都反映在 DOM 中。 但是如果我们对$template做一个深度拷贝,那么对该深度拷贝的更改将不会在 DOM 中反映出来,因为它不会影响$template

要对 DOM 元素进行深度克隆,可以使用document.importNode()方法。 它接受两个参数:第一个是需要克隆的 DOM 元素,第二个是一个布尔值,用于判断是否需要深度复制。 如果第二个参数为 true,它将创建元素的深层副本。 参见以下代码:

const $javascript = document.importNode($template.content, true);
$body.appendChild($javascript);

在这里,我在$javascript对象中复制了模板元素($template.content)的内容,并在 DOM 元素中添加了$javascript$javascript的任何修改都不会影响$template

对于一个更详细的例子,我在:https://jsfiddle.net/tgf5Lc0v/设置了一个 JSFiddle。 检查一下,看看模板元素的实际工作情况。

HTML 进口

HTML 导入很简单。 它们允许您在另一个 HTML 文档中导入一个 HTML 文档,其方式与包含 CSS 和 JS 文件相同。 import 语句如下所示:

<link rel="import" href="file.html">

当我们在一个使用 Webpack 等构建工具的环境中工作时,HTML 导入有很多好处; 例如,交付 web 组件以便跨 web 应用使用。

For more information regarding using the HTML imports feature, refer to the html5rocks tutorial at: https://www.html5rocks.com/en/tutorials/webcomponents/imports/.

我们不打算在天气小部件中使用 HTML 模板和 HTML 导入的主要原因是它们更侧重于使用 HTML 文件。 我们将在本章中使用的构建系统(Webpack)在 JavaScript 文件中工作得更好。 因此,我们将继续本章的其余部分,学习自定义元素和阴影 DOM。

构建天气小部件

对于本章,我们需要一个服务器来获取给定位置的天气信息。 在浏览器中,我们可以使用 navigator 对象来检索用户的精确地理位置(latitudelongitude)。 然后,利用这些坐标,我们需要找到该地区的名称及其天气信息。 为此,我们需要使用第三方天气提供商和谷歌 Maps API,我们在第 3 章,事件注册应用中使用过谷歌 Maps API。 我们将在这个项目中使用的天气供应商是Dark Sky

让我们为天气小部件设置服务器。 在书代码中打开Chapter05\Server目录。 在服务器目录中,首先运行npm install来安装所有依赖项。 您需要获得 Dark Sky 和谷歌 Maps 的 API 密钥。 您可能已经有谷歌 Map API 键,因为我们最近使用过它。 要为这两个服务生成 API 密钥,请执行以下步骤:

一旦你得到了两个键,在根目录Server中创建一个.env文件,并按以下格式添加键:

DARK_SKY_KEY=DarkSkySecretKey
GMAP_KEY=GoogleMapAPIKey

添加密钥后,在终端的Server根目录下运行npm start即可启动服务器。 服务器将在http://localhost:3000/URL 上运行。

服务器已经准备好了。 让我们为项目设置启动器文件。 打开Chapter05\Starter文件,然后在该目录下运行npm install以安装所有依赖项。 在项目根目录下创建一个.env文件,并在其中添加以下行:

NODE_ENV=dev
SERVER_URL=http://localhost:3000

正如我们在前一章中所做的,我们应该设置NODE_ENV=production来生成产品构建。 SERVER_URL将包含我们刚刚设置的项目服务器的 URL。 NODE_ENVSERVER_URL将作为我们应用中的 JavaScript 代码的全局变量(我使用了webpack.config.js中定义的 Webpack 插件)。

最后,在终端中执行npm run watch,启动 Webpack 开发服务器。 您的项目将在http://localhost:8080/中运行(项目 URL 将在终端中打印)。 目前,web 应用将显示三种文本:大、中、小。 它有三个不同大小的容器,用来存放 Weather Widget。 在项目的最后,天气小部件看起来如下所示:

天气小部件的工作

让我们为天气小部件的工作制定策略。 因为我们的 Weather Widget 是一个 HTML 自定义元素,所以它应该像其他原生 HTML 元素一样工作。 例如,考虑<input>元素:

<input type="text" name="username">

这将呈现一个正常的文本输入。 但是,我们可以使用具有不同属性的相同<input>元素,如下所示:

<input type="password" name="password">

它将呈现一个密码字段,而不是隐藏所有输入文本内容的文本字段。 同样,对于我们的 Weather Widget,我们需要显示给定位置的当前天气条件。 准确定位用户位置的最佳方法是使用 HTML5 地理位置,它将直接从浏览器获取用户当前的纬度和经度信息。

但是,我们应该让其他开发人员可以定制我们的小部件。 他们可能想手动设置 Weather Widget 的位置。 因此,我们将把检索位置的逻辑留给其他开发人员。 相反,我们可以手动接受latitudelongitude作为天气小部件的属性。 我们的天气元素如下所示:

<x-weather latitude="40.7128" longitude="74.0059" />

现在,我们可以从各自的属性中读取latitudelongitude,并在我们的小部件中设置天气信息,其他开发人员可以通过简单地更改latitudelongitude属性的值来轻松定制位置。

获取地理位置

在开始构建小部件之前,让我们看一下检索用户地理位置的步骤。 在你的src/js/home.js文件中,你应该看到一个导入语句,它将把 CSS 导入到 web 应用中。 在该 import 语句下面,添加以下代码:

window.addEventListener('load', () => {
  getLocation();
});

function getLocation() {
}

这将在页面加载完成时调用getLocation()函数。 在这个函数中,我们必须首先检查navigator.geolocation方法在浏览器中是否可用。 如果它是可用的,我们可以使用navigator.geolocation.getCurrentPosition()方法检索用户的地理位置。 该方法接受两个函数作为参数。 当成功检索到位置时将调用第一个函数,如果无法检索到位置则调用第二个函数。

在你的home.js文件中,添加以下功能:

function getLocation() {
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(showPosition, errorPosition);
  } else {
    console.error("Geolocation is not supported by this browser.");
  }
}

function showPosition(position) {
  const latitude = position.coords.latitude;
  const longitude = position.coords.longitude;

  console.log(latitude);
  console.log(longitude);
}

function errorPosition(error) {
  console.error(error);
}

在 Chrome 浏览器中打开应用。 该页面应该请求您访问您的位置的许可,就像它在前一章访问摄像机和麦克风一样。 如果你点击允许,你应该看到你当前的latitudelongitude打印在 Chrome 的控制台。

上述代码执行以下操作:

  • 首先,getLocation()函数将使用navigator.getlocation.getCurrentPosition(showPosition, errorPosition)方法来获取用户的位置。
    • 如果在页面请求权限时单击 Allow,那么它将调用showPosition函数,并将position对象作为参数。
    • 如果你点击 Block,那么它将以error对象作为参数调用errorPosition函数。
  • 对象在position.coords属性中包含用户的经度和纬度。 这个函数将在控制台中打印纬度和经度。

For more information regarding the usage of the geolocation, refer to the MDN page: https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/Using_geolocation.

创建天气自定义元素

我们有地理位置。 那么,让我们开始创建自定义元素。 目前,你的文件夹结构如下:

.
├── index.html
├── package.json
├── src
   ├── css
      └── styles.css
   └── js
       └── home.js
└── webpack.config.js

我们希望保持自定义元素独立于其他 JavaScript 模块。 在src/js目录下,在CustomElements/Weather/Weather.js路径下创建一个文件。 请注意,我在文件夹和文件名中使用了大写字母作为首字母(PascalCase)。 您可以对文件和文件夹使用 PascalCase,它将导出整个类。 这只是为了方便地识别项目文件夹中的类,不需要严格遵守规则。

现在,你的文件夹结构将变成:

.
├── index.html
├── package.json
├── src
   ├── css
      └── styles.css
   └── js
       ├── CustomElements
          └── Weather
              └── Weather.js
       └── home.js
└── webpack.config.js

在 VSCode 中打开Weather.js文件。 所有原生 HTML 元素都是直接使用HTMLElement类(接口)或通过继承它的接口实现的。 对于我们的自定义天气元素,我们需要创建一个扩展HTMLElement的类。 通过扩展一个类,我们可以继承父类的属性和方法。 在你的Weather.js文件中,写以下代码:

class Weather extends HTMLElement {

}

根据自定义元素 v1 规范,自定义元素应该只使用类直接从HTMLElement扩展。 然而,我们使用的是带有env预设的babel-loader,它将所有类转换为函数。 这将导致自定义元素的问题,这些元素需要是类。 但是有一个插件可以用来解决这个问题:transform-custom-element-classes。 我已经在你的webpack.config.js文件中添加了这个插件,这样你就不会在本章中遇到任何问题。 你可以在 Webpack 配置文件的.js规则部分找到它。

让我们在Weather类的构造函数中声明初始类变量:

Class Weather extends HTMLElement {

  constructor() {
    super();

    this.latitude = this.getAttribute('latitude');
    this.longitude = this.getAttribute('longitude');
  }

}

注意,在构造函数的第一行中,我调用了super()方法。 这将调用父类HTMLElement的构造函数。 当你的类扩展另一个类时,总是在类的构造函数中添加super()。 这样,父类也将在类方法开始工作之前初始化。

两个类变量(属性),this.latitudethis.longitude,将使用this.getAttribute()方法从自定义天气元素中获取属性latlong的值。

我们还需要向自定义元素添加 HTML。 由于Weather类类似于我们之前使用的 DOM 元素的引用,this.innerHTML可以用来为 weather 元素添加 HTML。 在构造函数内部,添加以下行:

this.innerHTML = ` `;

现在,this.innerHTML是一个空的模板字符串。 我已经创建了自定义元素所需的 HTML 和 CSS。 你可以在图书代码的Chapter 05\WeatherTemplate目录中找到它。 复制weather-template.html文件的内容,并将其粘贴到模板字符串中。

测试自定义元素

我们的自定义元素现在包含显示内容所需的 HTML。 让我们来测试一下。 在你的Weather.js文件的末尾,添加以下行:

export default Weather;

这将导出整个Weather类,并使其可以在其他模块中使用。 我们需要将其导入到我们的home.js文件中。 在你的home.js文件中,在顶部添加以下代码:

import Weather from './CustomElements/Weather/Weather';

接下来,我们需要定义自定义元素,即将自定义元素与标记名称关联起来。 理想情况下,我们希望将我们的元素称为<weather>。 那太好了! 但是根据自定义元素规范,我们应该将元素命名为带有破折号-的元素。 为了简单起见,我们称元素为<x-weather>。 这样,每当我们看到一个以x-为前缀的元素时,我们就会立即知道它是一个自定义元素。

方法用于定义自定义元素。 customElements在全局window对象上可用。 它接受两个参数:

  • 第一个参数是一个字符串,应该包含自定义元素名
  • 第二个参数应该包含实现自定义元素的类

在你的窗口加载事件监听器的回调函数中添加customElements.define('x-weather', Weather),我们之前在home.js中添加了customElements.define('x-weather', Weather)来获取地理位置。 window.addEventListener现在看起来如下:

window.addEventListener('load', () => {
  customElements.define('x-weather', Weather);
  getLocation();
});

让我们添加自定义元素到我们的index.html文件。 在你的index.html文件中,在div.large-container元素中添加以下行:

<x-weather />

因为这是一个在 HTML 文件的变化,你必须手动重新加载页面在 Chrome。 现在,你应该得到一个显示加载消息的天气小部件,如下所示:

如果你使用 Chrome DevTools 检查元素,它应该如下结构:

如您所见,HTML 现在与样式一起附加在自定义元素中。 然而,我们面临着一个严重的问题。 样式的范围总是全局。 这意味着,如果有人在页面的 CSS 中添加.title类的样式,比如color: red;,它也会影响我们的 Weather Widget ! 或者,如果我们在页面中使用的任何类中添加样式,比如.large-container,在我们的小部件内,它将影响整个页面! 我们真的不希望发生这种事。 为了克服这个问题,让我们学习 web 组件的最后一个主题。

附加阴影 DOM

shadow DOM 提供了 DOM 和 CSS 之间的封装。 shadow DOM 可以附加到任何元素上,而附加这个 shadow DOM 的元素称为 shadow 根元素。 阴影 DOM 被认为是独立于 DOM 树的其余部分的; 因此,阴影根之外的样式不会影响阴影 DOM,反之亦然。

要将 shadow DOM 附加到元素上,我们只需要对该元素使用attachShadow()方法。 看看下面的例子:

const $shadowDom = $element.attachShadow({mode: 'open'});
$shadowDom.innerHTML = `<h2>A shadow Element</h2>`;

在这里,首先,我附加了一个名为$shadowDom的阴影 DOM 到$element。 在这之后,我添加了一些 HTML 到$shadowDom。 注意,我在attachShadow()方法中使用了参数{mode: 'open'}。 如果使用{mode: 'closed'},使用 JavaScript 从 shadow 根访问 shadow DOM 将被关闭,其他开发人员将无法使用 JavaScript 从 DOM 操作我们的元素。

我们需要开发人员使用 JavaScript 操作元素,以便为 Weather Widget 设置地理位置。 一般来说,开放模式被广泛使用。 只有当您想要完全阻止其他人对您的元素进行更改时,才使用关闭模式。

添加阴影 DOM 到我们的自定义天气元素,执行以下步骤:

  1. 给自定义元素附加一个 shadow DOM。 这可以通过在构造函数中添加以下行来实现:
this.$shadowRoot = this.attachShadow({mode: 'open'});
  1. this.innerHTML替换为this.$shadowRoot.innerHTML,你的代码应该如下所示:
this.$shadowRoot.innerHTML = ` <!--Weather template> `;
  1. 在 Chrome 浏览器中打开页面。 您应该看到相同的天气小部件; 然而,如果你使用 Chrome DevTools 检查元素,你的 DOM 树将结构如下:

您可以看到,通过指定x-weather作为影子根,<x-weather>元素的内容将与 DOM 的其余部分分离。 同样,天气元素内部定义的样式不会泄露到 DOM 的其他部分,阴影 DOM 外部的样式也不会影响天气元素。

通常,要访问一个元素的 shadow DOM,可以使用该元素的shadowRoot属性。 例如:

const $weather = document.querySelector('x-weather');
console.log($weather.shadowRoot);

这将打印整个影子 DOM 附加到控制台中影子根。 然而,如果您的影子根是关闭,那么它将简单地打印null

使用自定义元素

现在我们已经为天气小部件准备好了 UI。 下一步是从服务器检索数据并在 Weather Widget 中显示它。 一般来说,小部件,比如天气小部件,不会直接出现在 HTML 中。 就像我们在第 1 章,建立待办事项列表中的任务一样,开发人员通常从 JavaScript 创建元素,附加属性,并将其添加到 DOM 中。 此外,如果他们需要做任何更改,比如更改地理位置,他们将使用 JavaScript 中的元素引用来修改其属性。

这是很常见的,我们在所有的项目中都用这种方法修改了很多 DOM 元素。 现在,我们的自定义天气元素也会出现同样的情况。 扩展Weather类的HTMLElement接口为Weather类提供了称为生命周期回调的特殊方法。 生命周期回调是在某个事件发生时被调用的方法。

对于自定义元素,有四个生命周期回调方法可用:

  • connectedCallback():当元素插入到 DOM 或 shadow DOM 时调用。
  • attributeChangedCallback(attributeName, oldValue, newValue, namespace):当元素的观察属性被修改时调用。
  • disconnectedCallback():当元素从 DOM 或 shadow DOM 中移除时调用。
  • adoptedCallback(oldDocument, newDocument):当一个元素被采用到一个新的 DOM 中时调用。

对于我们的自定义元素,我们将使用前三个回调方法。 删除index.html文件中的<x-weather />元素。 我们将从 JavaScript 代码中添加它。

在你的home.js文件中,在showPosition()函数中,创建一个新的函数名:createWeatherElement()。 这个函数应该接受一个类名(HTML 类属性)作为参数,并用这个类名创建一个天气元素。 我们已经在latitudelongitude常量中有了地理位置。 showPosition()功能代码如下:

function showPosition() {
  ...
  function createWeatherElement(className) {
    const $weather = document.createElement('x-weather');
    $weather.setAttribute('latitude', latitude);
    $weather.setAttribute('longitude', longitude);
    $weather.setAttribute('class', className);
    return $weather;
  };
}

这个函数将返回一个带有三个属性的天气元素,它在 DOM 中看起来像下面的代码片段:

<x-weather latitude="13.0827" longitude="80.2707" class="small-widget"></x-weather>

要在所有大、中、小容器中添加 Weather Widget,请在前面的函数后添加以下代码:

const $largeContainer = document.querySelector('.large-container');
const $mediumContainer = document.querySelector('.medium-container');
const $smallContainer = document.querySelector('.small-container');

$largeContainer.appendChild(createWeatherElement('large'));
$mediumContainer.appendChild(createWeatherElement('medium'));
$smallContainer.appendChild(createWeatherElement('small'));

您应该看到 Weather Widgets 附加到所有三个不同大小的容器上。 我们最后的小部件应该如下所示:

天气小部件包含以下细节:

  • 城市的名字
  • 天气图标
  • 温度
  • 时间(小时:分钟:)
  • 天气状况摘要()

添加依赖模块

我们的 Weather Widget 需要向服务器发出一个 HTTP 请求。 为此,我们可以重用之前在第 3 章事件注册应用中构建的 APICall 模块。 此外,由于我们将使用 Dark Sky 服务来显示天气信息,我们可以使用他们的图标库 Skycons 来显示天气图标。 目前,Skycons 在 npm 中不可用。 您可以从图书代码Chapter05\weatherdependencies目录或完整的代码文件中获得这两个文件。

目前,你的 JS 文件夹结构如下:

.
├── CustomElements
   └── Weather
       └── Weather.js
└── home.js

CustomElements/Weather/services/api/apiCall.js路径下添加apiCall.js文件,在CustomElements/Weather/lib/skycons.js路径下添加skycons.js文件。 你的 JS 文件夹现在应该如下所示:

.
├── CustomElements
   └── Weather
       ├── lib
          └── skycons.js
       ├── services
          └── api
              └── apiCall.js
       └── Weather.js
└── home.js

检索和显示天气信息

在你的weather.js文件中,在顶部添加以下导入语句:

import apiCall from './services/api/apiCall';
import './lib/skycons';

Skycons 库将向窗口对象添加一个全局变量Skycons。 它用于在 canvas 元素中显示一个动画Scalable Vector Graphics(SVG)图标。 目前,所有的类变量,如latitudelongitude,都是在构造函数中创建的。 但是,最好只在将 Weather Widget 添加到 DOM 时创建它们。 让我们将变量移到connectedCallback()方法中,以便仅在将小部件添加到 DOM 时才创建变量。 您的Weather类现在将如下所示:

class Weather extends HTMLElement {
  constructor() {
    this.$shadowRoot = this.attachShadow({mode: 'open'});
    this.$shadowRoot.innerHTML = ` <!-- Weather widget HTML --> `;
  }

  connectedCallback() {
    this.latitude = this.getAttribute('latitude');
    this.longitude = this.getAttribute('longitude');
  }
}

同样,正如我们在前几章中创建 DOM 中元素的引用一样,让我们在 Weather Widget 的阴影 DOM 中创建元素的引用。 在connectedCallback()方法中,添加以下代码:

this.$icon = this.$shadowRoot.querySelector('#dayIcon');
this.$city = this.$shadowRoot.querySelector('#city');
this.$temperature = this.$shadowRoot.querySelector('#temperature');
this.$summary = this.$shadowRoot.querySelector('#summary');

启动本章中包含的服务器,并让它在http://localhost:3000/URL 中运行。 为了获取天气信息,API 端点如下所示:

http://localhost:3000/getWeather/:lat,long

其中,latlong为经度和纬度值。 如果你的(latlong)值是(13.135885480.286841),那么你的请求 URL 如下:

http://localhost:3000/getWeather/13.1358854,80.286841

API 端点的响应格式如下:

{
  "latitude": 13.1358854,
  "longitude": 80.286841,
  "timezone": "Asia/Kolkata",
  "offset": 5.5,
  "currently": {
    "summary": "Overcast",
    "icon": "cloudy",
    "temperature": 88.97,
    // More information about current weather
    ...
  },
  "city": "Chennai"
}

要在 weather Widget 中设置天气信息,在WeathersetWeather()中创建一个新方法,并添加以下代码:

setWeather() {
  if(this.latitude && this.longitude) {
    apiCall(`getWeather/${this.latitude},${this.longitude}`, {}, 'GET')
      .then(response => {
        this.$city.textContent = response.city;
        this.$temperature.textContent = `${response.currently.temperature}° F`;
        this.$summary.textContent = response.currently.summary;

        const skycons = new Skycons({"color": "black"});
        skycons.add(this.$icon, Skycons[response.currently.icon.toUpperCase().replace(/-/g,"_")]);
        skycons.play();
      })
      .catch(console.error);
    }
  }

另外,通过在connectedCallback()方法的末尾添加this.setWeather()来调用前面的方法。 在 Chrome 中打开页面,你应该看到天气小部件按照预期工作! 您将能够看到城市名称、天气信息和天气图标。 setWeather()方法的工作很简单,如下所示:

  • 首先,它将检查纬度和经度是否都可用。 否则,将不可能发出 HTTP 请求。
  • 使用apiCall模块,发出 GET 请求,responsePromise.then()链中可用。
  • 从 HTTP 请求的response开始,所需的数据,如城市名称、温度和摘要,都包含在各自的 DOM 元素中。
  • 对于天气图标,全局Skycons变量是一个构造函数,它创建一个对象,其中的所有图标都使用特定的颜色。 在我们的例子中,黑色。 构造函数的实例存储在skycons对象中。
  • 为了添加动画图标,我们使用add方法,canvas 元素(this.$icon)作为第一个参数,图标名称作为所需格式的第二个参数。 例如,如果 API 中的图标的值为cloudy-day,则对应的图标为Skycons['CLOUDY_DAY']。 为此,我们首先将整个字符串转换为大写,并使用正则表达式.replace(/-/g, "_")的 replace 方法将-替换为_

向小部件添加当前时间

我们的小部件仍然缺少时间。 与其他值不同,时间不依赖于 HTTP 请求,但需要每秒钟自动更新一次。 在你的天气类中,添加以下方法:

displayTime() {
  const date = new Date();
  const displayTime = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
  const $time = this.$shadowRoot.querySelector('#time');
  $time.textContent = displayTime;
}

displayTime()方法做以下工作:

  • date 对象是使用new Date()构造函数创建的。 new Date()构造函数创建一个date对象,其中包含作为参数传递的关于日期和时间的所有细节。 如果没有传递参数,它将创建一个对象,其中包含有关当前日期和时间(直到毫秒)的所有信息。 在我们的例子中,因为我们没有传递任何参数,所以它包含初始化时的所有日期和时间的细节。
  • 我们从 date 对象中获得小时、分钟和秒。 通过使用模板字符串,我们可以轻松地以所需的格式构造时间,并将其存储在displayTime常量中。
  • 最后,它将时间设置为 shadow DOM 中p#time($time)元素的文本内容。

the date object is an important concept and is part of everyday software development in JavaScript. To learn more about date objects, refer to the w3schools page at: https://www.w3schools.com/js/js_dates.asp.

这个方法用于设置一次时间,但我们需要每秒钟执行一次这个方法,以便用户可以看到小部件中的确切时间。 JavaScript 有一个叫做setInterval()的方法。 它用于在特定的时间间隔内重复执行函数。 setInterval()方法接受两个参数:

  • 第一个是需要在特定时间间隔内执行的函数
  • 第二个是时间间隔,单位是毫秒

然而,setInterval()会重复执行函数,即使由于某种原因 DOM 元素被从 DOM 中移除。 为了克服这个问题,您应该将setInterval()存储在一个变量中,然后使用disconnectedCallback()方法执行clearInterval(intervalVariable),这将清除 interval 函数。

要实现这一点,请使用以下代码:

connectedCallback() {
  ...
  this.ticker = setInterval(this.displayTime.bind(this), 1000);
}

disconnectedCallback() {
  clearInterval(this.ticker);
}

在 Chrome 中打开天气小部件,你应该看到小部件中的当前时间每秒钟更新一次,这对用户来说是正常的。

响应元素属性的更改

我们有一个完整的天气小部件,但是天气信息只有在小部件第一次添加到 DOM 时才被加载。 如果你尝试从 Chrome DevTools 或 JavaScript 改变属性latitudelongitude的值,值会改变,但天气小部件不会得到更新。 为了使天气元素响应latitudelongitude中的变化,我们需要将它们声明为观测属性。 为此,在您的Weather类中添加以下行:

static get observedAttributes() { return ['latitude', 'longitude']; }

这将创建一个静态 getterobservedAttributes(),它将返回所有属性名的数组,Weather Widget 应该侦听这些属性名的更改。 静态方法是Class的特殊方法,无需创建类实例对象即可访问。 对于所有其他方法,我们需要创建一个类的新实例(对象); 否则,我们将无法访问它们。 因为静态方法不需要实例,所以在这些方法中this对象将是undefined

Static methods are used to hold common (class variables and methods independent) functions associated with the class that can be used in other places outside the class.

由于我们将latitudelongitude标记为观察属性,因此无论何时使用任何方法修改它们,都会以修改后的属性名称、该属性的旧值和该属性的新值作为参数触发attributeChangedCallback()。 因此,让我们在Weather类中添加attributeChangedCallback():

attributeChangedCallback(attr, oldValue, newValue) {
  if (attr === 'latitude' && oldValue !== newValue) {
    this.latitude = newValue;
    this.setWeather();
  }
  if(attr === 'longitude' && oldValue !== newValue) {
    this.longitude = newValue;
    this.setWeather();
  }
}

这个方法很简单。 每当latitudelongitude属性的值发生变化时,它都会更新各自的类变量,并调用this.setWeather()将天气更新到新的地理位置。 你可以通过直接在 Chrome DevTools 的 DOM 树中编辑天气小部件的属性来测试。

使用 setter 和 getter 方法

当我们创建 DOM 元素的引用时,我们一直使用settersgetters。 如果我们有一个天气自定义元素的引用,我们只需设置或获取latitudelongitude,如下所示:

currentLatitude = $weather.lat;
$weather.lat = newLatitude;

在本例中,如果我们设置了新的latitudelongitude,则需要更新小部件。 要做到这一点,在你的Weather类中添加以下 setter 和 getter:

get long() {
  return this.longitude;
}

set long(long) {
  this.longitude = long;
  this.setWeather();
}

get lat() {
  return this.latitude;
}

set lat(lat) {
  this.latitude = lat;
  this.setWeather();
}

为了测试 setter 和 getter 是否正常工作,让我们删除(或注释)Weather Widget 附加到$smallContainer的那行。 取而代之的是,添加以下代码:

const $small = createWeatherElement('small');
$smallContainer.appendChild($small);
setTimeout(() => {
  console.log($small.lat, $small.long);
  $small.lat = 51.5074;
  $small.long = 0.1278;
  console.log($small.lat, $small.long);
}, 10000);

你应该会看到,10 秒钟后,小集装箱里的天气会自动转到伦敦。 旧的和新的地理位置也将打印在 Chrome DevTools 控制台。

您已经成功地完成了天气小部件! 在你的项目中使用它之前,你需要添加腻子,因为在写这本书的时候,只有 Chrome 支持 web 组件的所有功能。

修复浏览器兼容性

为了提高 Weather Widget 的浏览器兼容性,我们需要:https://github.com/webcomponents/webcomponentsjs库中的webcomponents.js库提供的一组腻子。 这些腻子使我们的小部件能够在大多数现代浏览器中工作。 要将这些腻子文件添加到我们的项目中,首先在终端中从项目根文件夹运行以下命令:

npm install -S webcomponents.js

这将安装并添加webcomponents.js到我们的项目依赖项。 之后,在你的home.js文件中导入它:

import 'webcomponents.js';

目前,我们正在监听窗口加载事件后初始化项目。 Webcomponents.js异步加载腻子,一旦准备好,它将触发一个'WebComponentsReady'事件。 因此,我们现在应该监听这个新事件,而不是 load 事件:

window.addEventListener('WebComponentsReady', () => {
  customElements.define('x-weather', Weather);
  getLocation();
});

现在,对于最后一部分,你需要在一个readme文件中记录如何使用天气自定义元素和 web 组件 polyfill,以便团队的其他成员知道如何将它添加到项目中。 但这一次,readme文档将少于一个单页,应该易于维护! 我将把readme部分留给你。 我打赌你已经在庆祝第五章的完成了。

需要知道的重要事情

在使用自定义元素时,需要知道一些有用的事情。 就像我们扩展了一般的HTMLElement界面一样,我们也可以扩展内置的元素,比如段落元素<p>,按钮元素<button>等等。 这样,我们就可以继承父元素中所有可用的属性和方法。 例如,要扩展 button 元素,请执行以下操作:

class PlasticButton extends HTMLButtonElement {
  constructor() {
    super();

    this.addEventListener("click", () => {
      // Draw some fancy animation effects!
    });
  }
}

这里,我们扩展了HTMLButtonElement接口,而不是HTMLElement接口。 此外,就像内置元素可以扩展一样,定制元素也可以扩展,这意味着我们可以通过扩展 Weather widget 类创建另一种类型的小部件。

尽管 JavaScript 现在支持类和扩展类,但它还不像其他面向对象语言那样支持私有或受保护的类变量和方法。 目前,所有的类变量和方法都是公共的。 一些开发人员在需要私有的变量和方法上添加下划线'_'前缀,这样他们就不会在扩展的类中意外地使用它们。

如果你有兴趣更多地使用 web 组件,你可能应该看看以下的库,它们是为了提高使用 web 组件和内置腻子的可用性和工作流程而创建的:

To learn more about extending native inbuilt HTML elements, refer to the following tutorial on the Google Developers page: https://developers.google.com/web/fundamentals/getting-started/primers/customelements.

总结

在本章中,您在学习 web 组件的同时为您的团队构建了一个天气小部件。 您创建了一个可重用的 HTML 自定义元素,它使用阴影 DOM 从文档的其余部分抽象 CSS,使小部件可以轻松地插入到项目的其余部分。 您还了解了一些方法,如地理定位和设置间隔。 但你在本章学到的最重要的东西是在团队环境中创建独立组件的优势。 通过创建一个可重用的天气组件,您可以使您自己和团队其他成员的工作更轻松。

到目前为止,我们一直致力于纯 JavaScript。 然而,现在有许多现代框架和库,这使得 JavaScript 编程更容易、更高效,并且在很大程度上具有可伸缩性。 大多数框架专注于将整个应用组织成更小的、独立的、可重用的组件,这是我们在本章的 web 组件中所经历的。 在下一章中,我们将使用 Facebook 创建的很棒的 UI 库——React.js来构建整个应用。