使用Web Component和Event开发Web应用

余生颓废 提交于 2020-11-18 17:57:54

http://tommwq.tech/blog/2020/11/18/218

 

前一阵用Web Component和Event做了个自己用的Web工具。顺便封装了一个框架,用来写一些简单的Web应用。

下面是一个例子,展示了基本的用法。 

Listing 1: todolist.html
<!DOCTYPE html>
<html>
  <head>
    <title>TODO List</title>
    <meta charset="utf-8"/>
    <script src="wce.js"></script>
    <script src="server.js"></script>
  </head>
  <template id="todo-item-template">
    <style>
      span.done {
          text-decoration: line-through;
      }
      input.done {
          visibility: hidden;
      }
    </style>
    <div>
      <input type="checkbox" class="<!--#data-status-->" id="checkbox" data-id="<!--#data-id-->>"/>
      <span class="<!--#data-status-->" id="text"><!--#data-content--></span>
    </div>
  </template>
  <template id="todo-list-template">
    <div>
      <input id="content"/>
      <button id="create">新建待做项</button>
    </div>
    <div id="todo-items">
    </div>
  </template>
  <body>
    <script src="todolist.js"></script>
    <todo-list id="todo-list" data-todo-list=""></todo-list>
    <script src="main.js"></script>
  </body>
</html>
Listing 2: todolist.js
class TodoItem extends Component {

    static get observedAttributes() {
        return ['data-status', 'data-content'];
    }

    connectedCallback() {
        super.connectedCallback();
        let cb = this.child("checkbox")
        cb.addEventListener("click", () => {
            let finish = cb.checked;
            this.setAttribute("data-status", finish ? "done" : "");

            if (finish) {
                let todoItemId = this.getAttribute("data-id");
                document.dispatchEvent(new CustomEvent("finish-todo-item", {
                    detail: {
                        todoItemId: todoItemId
                    }
                }));
            }
        });
    }

    update(attributeName, oldValue, newValue) {
        super.update(attributeName, oldValue, newValue);
    }
}

class TodoList extends Component {

    static get observedAttributes() {
        return ["data-todo-list"];
    }

    connectedCallback() {
        super.connectedCallback();

        this.child("create").addEventListener("click", (e) => {
            let content = this.child("content").value;
            if (content) {
                document.dispatchEvent(new CustomEvent("create-todo-item", {
                    detail: {
                        content: content
                    }
                }));
            }
        });
    }

    update(attributeName, oldValue, newValue) {
        super.update(attributeName, oldValue, newValue);

        let todoList;

        switch (attributeName) {
        case "data-todo-list":
            if (newValue == "") {
                break;
            }
            todoList = JSON.parse(newValue);
            Component.removeAllChildren(this.child("todo-items"));
            for (let todoItem of todoList) {
                let item = document.createElement("todo-item");
                item.setAttribute("data-id", todoItem.id);
                item.setAttribute("data-content", todoItem.content);
                item.setAttribute("data-status", todoItem.finish ? "done" : "");
                this.child("todo-items").appendChild(item);
            }
            break;
        default:
            break;
        }
    }
}


(() => {
    Component.define(TodoItem);
    Component.define(TodoList);
})();

这个框架有如下的几个特性:

  • 使用Web Component开发组件。
  • 使用<template>标签编写Web Component界面。
  • 组件样式写在<template>标签中。
  • 将<template>标签置于<head>标签和<body>标签之间。
  • Web Component组件的类采用PascalCase方式命名(如:TodoList),对应的<template>标签的id为Web Component组件类名对应的KebabCase方式命名,并增加-template后缀(如:todo-list-template)。
  • 数据保存在Web Component组件对应标签的属性中,属性名采用data-前缀。如果属性是Javascript对象,将对象转换为JSON字符串作为属性值。
  • Web Component内部的标签,其属性或innerHTML/textContent可以采用类似“<!–#XYZ–>”的属性模板,其中XYZ是Web Component组件对应标签属性名(如:data-content)。
  • 数据更新通过自定义事件通知服务器,document对象负责接收自定义事件,并通知服务器进行数据。
  • 容器或根组件只维护集合,不维护集合中的具体元素。具体元素由其对应的组件维护。

框架的优点有:

  • 充分利用浏览器自身功能。
  • 框架简单,体积小。
  • 模块化开发。
  • 将UI和逻辑分离。
  • 简化代码开发。
  • 大部分现代浏览器都支持。
  • 样式隔离。避免全局样式。

框架的缺点有:

  • 兼容性不好,老版本浏览器不支持。
  • 框架只提供了基础功能。
  • 缺少像for/if这样复杂的渲染功能。

框架代码如下:

Listing 3: wce.js
class Character {
    static isUpperCase(ch) {
        return ch.length == 1 && "A" <= ch && ch <= "Z";
    }

    static isLowerCase(ch) {
        return ch.length == 1 && "a" <= ch && ch <= "z";
    }
}

class Text {
    static pascalToKebab(text) {
        let s = "";

        for (let i = 0; i < text.length; i++) {
            let current = text.charAt(i);
            let previous = i > 0 ? text.charAt(i - 1) : "";
            let next = i + 1 < text.length ? text.charAt(i + 1) : "";

            if (Character.isLowerCase(current)) {
                s = s.concat(current);
                continue;
            }

            if (Character.isLowerCase(next)) {
                if (s.length > 0) {
                    s = s.concat("-");
                }
                s = s.concat(current.toLowerCase());
                continue;
            }

            s = s.concat(current.toLowerCase());
        }

        return s;
    }
}


class Component extends HTMLElement {

    static define(componentClass) {
        customElements.define(Text.pascalToKebab(componentClass.name), componentClass);
    }

    static descendants(node) {
        let result = [];
        if (node instanceof Node) {
            for (let c of node.children) {
                result.push(c);
            }
            for (let c of node.children) {
                for (let d of Component.descendants(c)) {
                    result.push(d);
                }
            }
        }
        return result;
    }

    constructor() {
        super();

        this.attributeSlots = {}; // componentAttribute: {element: X, elementAttribute: Y}
    }

    connectedCallback() {
        this.append(this.createContent());
    }

    disconnectedCallback() {
    }

    attributeChangedCallback(attributeName, oldValue, newValue) {
        this.update(attributeName, oldValue, newValue);
    }

    update(attributeName, oldValue, newValue) {
        if (attributeName in this.attributeSlots) {
            for (let slot of this.attributeSlots[attributeName]) {
                slot.element[slot.elementAttribute] = newValue;
            }
        }
    }

    componentName() {
        return this.constructor.name;
    }

    componentTagName() {
        return Text.pascalToKebab(this.componentName());
    }

    componentTemplateId() {
        return Text.pascalToKebab(this.componentName()).concat("-template");
    }

    static removeAllChildren(element) {
        while (element.firstChild) {
            element.removeChild(element.firstChild);
        }
    }

    createContent() {
        let template = document.getElementById(this.componentTemplateId());
        let content = template.content.cloneNode(true);

        for (let c of Component.descendants(content)) {
            let attrs = c.getAttributeNames();
            attrs.push("innerHTML");
            for (let attr of attrs) {
                if (attr == "class") {
                    attr = "className";
                }

                let value = c[attr];
                let result;
                let re = /^<!--#([a-zA-Z0-9-]*)-->$/g;

                if (attr == "href") {
                    let realValue = unescape(value);
                    let pos = window.location.href.lastIndexOf("/");
                    if (pos != -1) {
                        realValue = realValue.substring(pos + 1);
                    }
                    value = realValue;
                }

                result = re.exec(value);
                if (result == null) {
                    continue;
                }
                let componentAttribute = result[1];
                let slot = {
                    element: c,
                    elementAttribute: attr
                };
                if (componentAttribute in this.attributeSlots) {
                    this.attributeSlots[componentAttribute].push(slot);
                } else {
                    this.attributeSlots[componentAttribute] = [slot];
                };
            }
        }

        for (let name of this.getAttributeNames()) {
            this.update(name, "", this.getAttribute(name));
        }

        return content;
    }

    child(id) {
        let children = this.querySelector("#" + id);
        if (children instanceof HTMLCollection) {
            return children.item(children.length - 1);
        } else {
            return children;
        }
    }
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!