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; } } }
来源:oschina
链接:https://my.oschina.net/u/131191/blog/4722558