我所知道的HTML——现成轮子都堆成山了,新特性还算新吗?(中篇)

594 阅读10分钟

往期回顾

  1. 我所知道的HTML——现成轮子都堆成山了,新特性还算新吗?(上篇)

(如果您正巧因为首页推荐的功能点进此文章,由衷地建议您先回顾往期内容,这将有助您接下来的阅读体验。)

QQ图片20240823121106.png

前言

在上一篇文章中,我们聊了HTML5新特性所带来的网页在展示表现层面上的提升。在本篇文章中,我们将继续深入,聊一聊HTML5新特性所带来的网页在功能层面上的提升,并结合一些代码示例,帮助大家更好地掌握和消化知识点。

在本篇文章中,我们将会一起学习如下内容:

  • 拖动和放置:(Drag and drop) API
  • 桌面通知: Notifications
  • 历史管理:History
  • 多任务: Web Worker
  • 跨窗口通信:PostMessage
  • 全双工通信协议: Websocket
  • 离线应用:本地存储 localStorage 和 sessionStorage 和 manifest
  • 跨域资源共享(CORS): Access-Control-Allow-Origin以及衍生的网络安全问题

拖动和放置

新的全局属性:draggable

HTML 拖放(Drag and Drop)接口使应用程序能够在浏览器中使用拖放功能。例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。

对于网站、扩展以及 XUL 应用程序,你可以自定义什么元素是可拖拽的、可拖拽元素产生的反馈类型,以及可放置的元素。——MDN

如何让一个元素能够被拖动?设置这个元素的draggable属性为true即可。

<div class="drag-item" draggable="true">Drag Me</div>

可拖动.gif

👆从上方的动图可以看到,确实有一个可拖拽元素的半透明快照跟随着鼠标指针。👆

同理,如果我们不期望一个元素能够被拖动,则设置draggable属性为false即可。

不可拖动.gif

当我们不对draggable属性人为地设置值时,它的默认值为"auto",表示使用浏览器定义的默认行为。

什么是浏览器定义的默认行为呢?

拖动行为是浏览器的默认行为是指:只有选择的文本图像链接可以被拖动。

👇我们拿掘金的个人主页试一试,请看下方动图👇:

默认拖动.gif

至此,我们对draggable属性已经有了一个基本的认知:

  • true:表示元素可以被拖动
  • false:表示元素不可以被拖动
  • "auto":表示拖动行为遵循浏览器的默认行为

那其实可以看到,上面展示的可拖动元素,实际上并不能被有效地放置。如果我们期望实现一个将某一指定元素拖动后放置在另一指定区域的完整过程,我们就要继续学习DragEvent了。

拖拽事件,有效操作的必要条件

DragEvent 是一个表示拖、放交互的一个DOM event 接口。用户通过将指针设备(例如鼠标)放置在触摸表面上并且然后将指针拖动到新位置(诸如另一个 DOM 元素)来发起拖动。应用程序可以按应用程序特定的方式自由解释拖放交互。——MDN

当我们拖动一个元素的时候,我们可以选择几种模式:

  • copy

    • 在新位置生成源项的副本
  • move

    • 将项目移动到新位置
  • link

    • 在新位置建立源项目的链接
  • none

    • 项目可能禁止拖放(译者注:还与 effectAllowed 设置的值相关)

这其实和复制粘贴挺相似的,复制操作将当前对象的数据存入操作系统的剪切板中,粘贴操作再从剪切板中取到数据

因此咱们这里的拖拽操作也需要保存一下当前被拖动对象的信息,这样方便我们在放置的时候,能够正确的将被拖动对象放置。

我们使用element.dataTransfer.setData("text/plain", ev.target.id);去保存被拖动的对象的信息。

👇一起来看一个完整的代码例子,帮助我们消化理论知识吧👇:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drag and Drop Example</title>
    <style>
        #dropzone {
            width: 200px;
            height: 200px;
            border: 2px dashed #ccc;
            margin: 20px;
            position: relative;
            display: flex;

        }

        .desc {
            position: absolute;
            bottom: 0;
        }

        .drag-item {
            width: 50px;
            height: 50px;
            background-color: #ccc;
            margin: 10px;
            display: inline-block;
            cursor: move;
        }
    </style>
</head>

<body>

    <div id="dropzone" draggable="false">
        <p class="desc">Drop Here</p>
    </div>
    <div id="dropitem" class="drag-item" draggable="true">Drag Me</div>

    <script>
        const dropzone = document.getElementById('dropzone');
        const dragItem = document.getElementById('dropitem');

        dragItem.addEventListener('dragstart', e => {
            e.dataTransfer.setData('text/html', e.target.outerHTML);
            e.dataTransfer.effectAllowed = 'move';

        });


        dropzone.addEventListener('dragover', e => {
            e.preventDefault();
        });

        dropzone.addEventListener('drop', e => {
            e.preventDefault();
            const data = e.dataTransfer.getData('text/html');
            const newItem = document.createElement('div');
            newItem.innerHTML = data;
            e.target.appendChild(newItem);

        });
    </script>
</body>

</html>

这个示例创建了一个可拖动灰块和一个可放置区域。通过监听一系列的交互事件完成整个拖动和放置的过程。我们这里一起分析一下代码:

  1. 当元素被拖动时,触发dragstart事件,我们通过addEventListener,给这个事件加上回调函数,在回到函数中,使用dataTransfer.setData保存被拖动的元素的信息,并设置拖动模式为move
  2. 当元素结束拖动时,即元素被拖进一个有效的放置目标时,触发dragover事件,同样的,我们给这个事件也加上回调函数,在回调函数中通过 e.preventDefault();来阻止对这个事件的其他处理过程(如触点事件或指针事件)。
  3. 最后,当拖动结束时,即元素被放置到有效的放置目标上时,触发drop事件,我们给这个事件也加上回调函数,在回调函数中通过 e.preventDefault();来阻止对这个事件的其他处理过程(如触点事件或指针事件)。并根据dataTransfer.getData中获得的元素信息,创建一个新的元素,挂载到放置区域上。

👇可以通过下方动图查看示例实际的运行结果👇:

完整拖动例子.gif

第三方库

当我们使用React作为实现项目的技术框架时,我们一般不会去利用HTML原生操作去实现元素的拖拽,而是会使用第三方库:react-dnd

我们也可以写一个简单的demo:

import React, { useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Space, Button } from "antd";
const DragItem = ({ text }) => {
  const [{ isDragging }, drag] = useDrag({
    type: "item",
    item: () => ({ text }),
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });

  return (
    <div
      ref={drag}
      style={{
        opacity: isDragging ? 0.4 : 1,
        border: "solid 2px black",
        padding: 10,
      }}
    >
      {text}
    </div>
  );
};

const DropZone = ({ onDrop }) => {
  const [{ isOver, canDrop }, drop] = useDrop({
    accept: "item",
    drop: (item) => onDrop(item.text),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  });

  return (
    <div
      ref={drop}
      style={{
        padding: "20px",
        backgroundColor: isOver && canDrop ? "lightgreen" : "white",
      }}
    >
      Drop Here
    </div>
  );
};

const DragAndDropExample = () => {
  const [droppedText, setDroppedText] = useState("");

  const onDrop = (text) => {
    setDroppedText(text);
  };

  return (
    <DndProvider backend={HTML5Backend}>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "20px" }}
      >
        <Space>
          <DragItem text="Alice" />
          <DragItem text="Bob" />
        </Space>
      </div>
      <DropZone onDrop={onDrop} />
      <p>Dropped Text: {droppedText}</p>
    </DndProvider>
  );
};

export default DragAndDropExample;


👇可以通过下方动图查看示例实际的运行结果👇:

完整拖动react例子.gif

桌面通知

Notifications API 允许网页控制向最终用户显示系统通知——这些都在顶层浏览上下文窗口之外,因此即使用户已经切换标签页或移动到不同的应用程序也可以显示。该 API 被设计成与不同平台上的现有通知系统兼容。——MDN

我们可以写一个简单的示例来介绍一下这个API:

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notifications Example</title>
</head>

<body>

    <button id="notify-btn">通知我</button>

    <script>
        document.getElementById('notify-btn').addEventListener('click', function () {
            if ('Notification' in window) {
                if (Notification.permission === "granted") {
                    // 检查是否已授予通知权限;如果是的话,创建一个通知
                    const notification = new Notification("欢迎来到我的网站!");
                } else if (Notification.permission !== "denied") {
                    // 我们需要征求用户的许可
                    Notification.requestPermission().then((permission) => {
                        // 如果用户接受,我们就创建一个通知
                        if (permission === "granted") {
                            const notification = new Notification("欢迎来到我的网站!");
                            // …
                            notification.onclick = function () {
                                window.focus();
                                notification.close();
                            };
                        };
                    })
                }
            }
            else {
                alert('桌面通知不被支持。');
            }
        });
    </script>

</body>

</html>

当我们点击按钮时,会弹出一个需要用户授权的弹窗

image.png

Notification.permission有以下几个状态:

  • granted

    • 用户已经明确地授予了当前来源显示系统通知的权限。
  • denied

    • 用户已经明确地拒绝了当前来源显示系统通知的权限。
  • default

    • 用户是否授予当前来源显示系统通知的权限的决定是未知的;在这种情况下,应用的行为与该值为 denied 的情况相同。

当我们点击“允许”之后,桌面通知才会显示。但是上述的例子你通过直接在资源管理器方式用浏览器直接打开文件路径的话:

image.png


运行之后吗,你会发现浏览器没有任何反应,哪怕你已经点了允许,你并不会看到有这么一个弹窗在桌面上弹出:

image.png

为什么会看不到弹窗呢?因为这个API的使用存在限制

安全上下文:  此项功能仅在一些支持的浏览器安全上下文(HTTPS)中可用。——MDN

当我们打开一个html文件,实际上此时页面是通过 file:// 协议加载的:

image.png

浏览器的同源策略限制了跨源资源访问,而桌面通知功能可能受到这种策略的影响。通过 file:// 协议打开的文件通常被认为是非同源的,因此可能无法使用某些Web API,包括通知API。

综上所述,点击“允许”之后,Notification.permission哪怕是granted也无效,不会有任何桌面通知出现。

我们可以把它放在本地工程里,以 http:// 协议加载即可,比如我把它放到我之前写的Next.js项目中:

image.png

实机演示动图:

桌面通知例子.gif

这样我们就看到了我们心心念念的桌面通知弹窗了😄。

还有一条备注:

此特性在 Web Worker 中可用。

第三方库

同样的,如果我们要在工作业务中使用的话,建议使用react-notification。 👇这里咱们也展示一个例子👇:

import React from "react";
import {
  NotificationContainer,
  NotificationManager,
} from "react-notifications";
import "react-notifications/lib/notifications.css";

const NotificationComponent = () => {
  const createNotification = (type) => {
    return () => {
      switch (type) {
        case "info":
          NotificationManager.info("Info message");
          break;
        case "success":
          NotificationManager.success("Success message", "Title here");
          break;
        case "warning":
          NotificationManager.warning(
            "Warning message",
            "Close after 3000ms",
            3000
          );
          break;
        case "error":
          NotificationManager.error("Error message", "Click me!", 5000, () => {
            alert("callback");
          });
          break;
      }
    };
  };

  return (
    <div>
      <button className="btn btn-info" onClick={createNotification("info")}>
        Info
      </button>
      <hr />
      <button
        className="btn btn-success"
        onClick={createNotification("success")}
      >
        Success
      </button>
      <hr />
      <button
        className="btn btn-warning"
        onClick={createNotification("warning")}
      >
        Warning
      </button>
      <hr />
      <button className="btn btn-danger" onClick={createNotification("error")}>
        Error
      </button>
      <NotificationContainer />
    </div>
  );
};

export default NotificationComponent;

👇我也将它放在了我写的Next.js项目上,实际操作可以看下方动图👇: 桌面通知rc例子.gif

历史管理

History API 通过 history 全局对象提供了对浏览器会话的历史记录(不要与 WebExtensions 的 history 混淆)的访问功能。它暴露了很多有用的方法和属性,使你可以在用户的历史记录中来回导航,而且可以操作历史记录栈中的内容。

它的基本使用方式有3种:

  • back:在历史记录中向后跳转

    history.back();
    
  • forward:在历史记录中向前跳转

    history.forward();
    
  • go:从会话历史记录中加载某一特定页面,该页面使用与当前页面的相对位置来标识(当前页面的相对位置为 0)。

    // -1 代表向后跳转1个页面
    history.go(-1);
    
    // 1 代表向前跳转1个页面
    history.go(1);
        
    // 3 代表向前跳转3个页面
    history.go(3);
    
    // 传 0 或者 不传,都代表刷新当前页面
    history.go(0);
    history.go();
    
    

    最后,还有一条备注:

    以通过查看 length 属性的值来确定历史记录栈中的页面数量。

    const numberOfEntries = history.length;
    

结语

令人没有想到的是,有关HTML5新特性的介绍,只分成上、下两篇的话,还是不够。

看样子需要3篇文章才能够完结这个系列。因此又拆了一下,毕竟写到这里的时候,正文字数也已经将近3000字。

在本篇文章里,我们介绍了一些新的Web API,在学习理论知识的同时,也结合了实际业务,编写了几个代码示例,加深了掌握。

这些新的API丰富了网页的功能,也给用户的交互行为提供了更多的可能。

在下一篇文章中,我会完结这个系列,和大家继续学习剩下的Web WorkerWebSocket 等内容。

期待与你在【下篇】相遇~