2021 年的 Web 标准进展

Google DevFest 2021 成都站


Web 组件

前情提要


服务端渲染


Template to Shadow DOM

<host-element>
  <template shadowroot="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

<host-element>
  #shadow-root (open)
  <slot><h2>Light content</h2>
  </slot>
</host-element>

Shadow DOM to Template

const html = element.getInnerHTML({
  includeShadowRoots: true,
  closedRoots: [shadowRoot1, shadowRoot2]
});

console.log(html);

组件初始化

<menu-toggle>
  <template shadowroot="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>

class MenuToggle extends HTMLElement {
  constructor() {
    var button = super().shadowRoot?.firstElementChild;

    if (!button) {
      const shadow = this.attachShadow({ mode: 'open' });
      shadow.innerHTML = `<button><slot></slot></button>`;
      button = shadow.firstChild;
    }
    button.addEventListener('click', toggle);
  }
}
customElements.define('menu-toggle', MenuToggle);

私有 Shadow DOM

class MenuToggle extends HTMLElement {
  constructor() {
    var { shadowRoot } = super().attachInternals();

    if (!shadowRoot) {
      shadowRoot = this.attachShadow({ mode: 'open' });
      shadowRoot.innerHTML = `<button><slot></slot></button>`;
    }
    shadowRoot.firstChild.addEventListener('click', toggle);
  }
}
customElements.define('menu-toggle', MenuToggle);

仅限于解析器 —— 为了安全

const div = document.createElement('div');
const template = document.createElement('template');

template.setAttribute('shadowroot', 'open'); // 啥也没做
div.appendChild(template);

console.log(div.shadowRoot); // null

const html = `
    <div>
      <template shadowroot="open"></template>
    </div>
  `;
const div = document.createElement('div');
div.innerHTML = html;

console.log(div.shadowRoot); // null
const document = new DOMParser().parseFromString(html, 'text/html', {
  includeShadowRoots: true
});
const { shadowRoot } = document.querySelector('div');

console.log(shadowRoot); // # DocumentFragment

CSS 渲染

<nineties-button>
  <template shadowroot="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

Polyfill 补丁

for (const template of document.querySelectorAll('template[shadowroot]')) {
  const mode = template.getAttribute('shadowroot');
  const shadowRoot = template.parentNode.attachShadow({ mode });

  shadowRoot.append(template.content);
  template.remove();
}

数据绑定(难产)

<template>
  <section>
    <h1>{{name}}</h1>
    Email: <a href="mailto:{{email}}">{{email}}</a>
  </section>
</template>
const template = document.currentScript.previousElementSibling;

const tree = template.createInstance({
  name: 'Ryosuke Niwa',
  email: '[email protected]'
});
document.body.prepend(tree);

tree.update({ name: 'Ryosuke Niwa', email: '[email protected]' });

表单


Event Submitter

https://github.com/idea2app/event-submitter-polyfill


HTML

<form>
  <input name="data" />

  <button type="submit" data-name="first">Fisrt</button>
  <button type="submit" data-name="second">Second</button>
</form>
document.querySelector('form')?.addEventListener('submit', event => {
  event.preventDefault();

  const { name } = event.submitter.dataset,
    { data } = event.target.elements;

  fetch(`/api/${name}`, { data: data.value });
});

React

import React, { FormEvent } from 'react';
import { render } from 'react-dom';

function handleSubmit(event: FormEvent<HTMLFormElement>) {
  event.preventDefault();

  const { name } = event.nativeEvent.submitter.dataset,
    { data } = event.currentTarget.elements;

  fetch(`/api/${name}`, { data: data.value });
}

render(
  <form onSubmit={handleSubmit}>
    <input name="data" />

    <button type="submit" data-name="first">
      Fisrt
    </button>
    <button type="submit" data-name="second">
      Second
    </button>
  </form>,
  document.body
);

FormData 事件

const form = document.querySelector('form');

form?.addEventListener('formdata', ({ formData }) => {
  formData.append('my-input', myInputValue);
});

标准提案作者博文译文


Web 表单组件


Element Internals

class MyCounter extends HTMLElement {
  static formAssociated = true;

  private value_ = 0;
  private internals_ = this.attachInternals();

  get value() {
    return this.value_;
  }
  set value(v) {
    this.value_ = v;
  }

  get form() {
    return this.internals_.form;
  }
  get name() {
    return this.getAttribute('name');
  }
  get type() {
    return this.localName;
  }
  get validity() {
    return this.internals_.validity;
  }
  get validationMessage() {
    return this.internals_.validationMessage;
  }
  get willValidate() {
    return this.internals_.willValidate;
  }

  checkValidity() {
    return this.internals_.checkValidity();
  }
  reportValidity() {
    return this.internals_.reportValidity();
  }
}
customElements.define('my-counter', MyCounter);

HTML

<template>
  <div contenteditable="true"></div>
</template>
const { content } = document.currentScript.previousElementSibling;

class MyInput extends HTMLInputElement {
  constructor() {
    super().attachShadow({ mode: 'open' }).append(content.cloneNode(true));
  }
}
customElements.define('my-input', MyInput, { extends: 'input' });
<input is="my-input" />

WebCell

import { component, mixinForm } from 'web-cell';

@component({
  tagName: 'my-counter',
  renderTarget: 'shadowRoot'
})
export class MyCounter extends mixinForm() {
  render() {
    return <div contenteditable="true" />;
  }
}

上一篇
脱离 IDE,徒手 Kotlin! 脱离 IDE,徒手 Kotlin!
Java、PHP 作为中国软件外包行业的扛把子,自然是码农最多、甲方最爱的后端技术栈,我自主创业以来也有不少甲方要求 Java + Spring 的架构。 同时,JSer 一直很反感 Java 庞大的代码量,即便后来大行其道的 TypeSc
2022-01-06
下一篇
开源给中国的精神启示 开源给中国的精神启示
COSCon 2021 开源之功利 开源是“用爱发电”? 开源是一种诞生自计算机软硬件行业的 新型生产力、生产关系 于己 Git 记录代码成长 GitHub 绿点是工程师最好的简历 开源项目开发、管理流程称得上业界最佳实践 于司
2021-10-25
目录