一起构建 Web 组件!第八部分:流言终结者版
到目前为止,在本系列中,我们已经介绍了底层 Web 组件标准、旧版浏览器的polyfill,以及它们使用原始 JavaScript和各种 不同的帮助库的实现。
今天,我们将回顾一些最近出现的关于 Web Components 及其使用的有害误区。考虑到这项技术本身还很年轻,以及从v0
Web Components 规范到v1
其广泛应用的转变已经显著地改变了整个行业格局,而且是朝着更好的方向发展,这些误解中的许多都是可以理解的。
让我们稍微了解一下 Web 自身的组件模型,并了解它们如何使开发更容易并改善用户、开发人员和管理人员的体验。
- 误区:浏览器不支持 Web Components
- 误区:Web 组件无法接受复杂数据
- 误区:Web Components 无法实现模板化
- 误区:Web 组件无法进行服务器端渲染
- 误解:Web Components 是 Google 的专有技术
- 误区:需要 Polymer 才能使用 Web Components
- 误区:你需要使用 HTML 导入
- 误区:你需要使用 Shadow DOM
- 误区:你需要框架来编写应用程序
- 误区:你不能在框架中使用 Web Components
- 误区:Web 社区已经不再使用 Web Components
误区:浏览器不支持 Web Components
有时一张图片胜过 1024 个字:
此截图取自https://webcomponents.org,截取时间为 2019 年 2 月,当时 Firefox 版本为 65.0.1。它显示所有主流浏览器都支持 Web Components 规范,Edge 即将支持,无需 polyfill。(Web Components 也可以支持到 IE11,但你不应该这么做。)
但布丁的真伪,难道不是在实践中检验的吗?或者说,平台API的可靠性,难道不是在部署中检验的吗?如果 Web 组件不受支持,我们就不会指望它们被广泛使用,当然也不会被大型团队使用。然而,Twitter、GitHub、dev.to、麦当劳、Salesforce、ING(PDF 链接)、SAP以及许多其他公司都在面向公众的核心业务页面中使用 Web 组件。我在Forter的日常工作中,我们也使用 Web 组件。事实上,2018 年,所有报告的 Chrome 页面加载中有 10% 使用了 Web 组件。
显然,Web 组件不仅仅是一项潜在的、有趣的未来技术。它如今已被您以及像您一样的用户在网络上广泛使用。
误区:Web 组件无法接受复杂数据
最近我看到有人说,Web 组件只能接受字符串形式的数据,因此无法接受复杂的对象。这种误解尤其危险,因为就像任何善意的谎言一样,它只有一半是真的。这种误导性的观点源于对DOM及其工作原理的根本误解。
以下是一个简短的回顾。如果你对 DOM 与HTML / attrs 与 props比较满意,可以跳过此部分。
<input id="text-input" placeholder="Enter Your Text"/>
HTML 元素和属性是 HTML 规范的一部分,大致构成了文档对象模型 (Document Object Model)D
的一部分DOM
。上例中,<input>
元素有两个属性,id
分别为值“text-input”和placeholder
值“Enter Your Text”。由于 HTML 文档本身就是字符串,因此属性名称及其值也都是字符串。
当浏览器解析文档时,它会创建与每个 HTML 元素对应的 JavaScript 对象,并使用相应属性 (attribute) 的值初始化该对象的某些属性 (property)。这棵对象树构成了OM
。DOM
属性存在于 JavaScript 对象中。
下面是我们输入的 DOM 节点的伪代码示例:
Object HTMLInputElement {
tagName: 'INPUT',
placeholder: 'Enter Your Text',
id: 'text-input'
...
}
严格来说,元素可以拥有属性 (attribute),但不能拥有特性 (property),因为元素是文档的一部分,而不是 DOM 树。我的意思是,给定页面的 DOM 与该页面的 HTML 不同;相反,DOM源自HTML 文档。
您可以在开发工具元素/检查器面板中检查任何 DOM 节点的属性。Chrome 会在选项properties
卡中显示所有 DOM 属性(查看 CSS 规则旁边),Firefox 则会在Show DOM Properties
上下文菜单中显示它们。您也可以在检查节点时进行求值$0
,或者使用 DOM API,例如document.querySelector('my-element').someProp
;
在我们刚刚开始的输入中,DOM 对象的id
属性是text-input
。
const input = document.getElementById('text-input');
console.log(input.id); // 'text-input'
console.log(input.getAttribute('id')); // 'text-input'
input.id = 'by-property';
console.log(input.getAttribute('id')); // 'by-property'
input.setAttribute('id', 'by-attribute');
console.log(input.id); // 'by-attribute'
对于许多属性/特性对,其中一个的更改会反映在另一个的更改中,但并非所有属性/特性对都是如此。例如,HTMLInputElement
的value
特性 表示当前值,而value
属性 仅表示初始值。
一些开发人员似乎有这样的理由:
- 属性只能是字符串
- HTML 元素只有属性 (attribute),没有特性 (property)
- 自定义元素是 HTML 元素
- 因此 Web 组件只能接受属性中的字符串
这种推理在每个人都 100% 禁用 JavaScript 的世界中成立,但我们并非生活在这样的世界。在我们的世界中,DOM 是 Web 平台中丰富且利用率高的部分。
自定义元素确实是与文档绑定的 HTML 元素,但它们也是 DOM 节点,在 DOM 树的分支上摆动。它们可以具有语义字符串属性,也可以使用 JavaScript 和 DOM接受复杂的嵌套数据作为属性。
以下是如何仅使用 DOM API 实现此目的的示例:
const input = document.createElement('country-input');
input.countries = [
{name: 'Afghanistan', dialCode: '+93', countryCode: 'AF'},
{name: 'Albania', dialCode: '+355', countryCode: 'AL'},
/* ... */
];
那么 - Web 组件只能接受字符串吗?简直是胡扯!一派胡言!胡扯!从第一天起,DOM 的全部表现力就可用于自定义元素。
如果您认为只能使用裸 DOM API 来设置这些属性...请再想一想!
误区:Web Components 无法实现模板化
和之前的谬论一样,这种误解也有一定的道理。最广泛采用的 Web 组件规范是<template>
element,用于实现高效的静态模板,并且适用于所有主流浏览器。本文中我想讨论的模板类型使用了所谓的“动态模板”,或者说包含可变部分的模板。
<template id="person-template">
<figure>
<img alt="{{picture.alt}}" src="{{picture.src}}"/>
<figcaption>{{name}}</figcaption>
</figure>
</template>
我们将首先讨论一些提议的功能,然后展示一些您今天可以运行的示例。
模板实例化是一项拟议的 Web 组件规范,它提供了一种未来定义 DOM 模板的方法,其中包含用于动态内容的插槽。希望它能很快让我们为自定义元素编写声明式模板。以下模型演示了它在实践中可能的样子:
<template type="with-for-each" id="list">
<ul>
{{foreach items}}
<li class={{ type }} data-value={{value}}>{{label}}</li>
{{/foreach}}
</ul>
</template>
<script>
const list = document.getElementById('list');
customElements.define('awesome-web-components', class extends HTMLElement {
#items = [
{ type: 'description', value: 'awesome', label: "Awesome!!" },
{ type: 'technology', value: 'web-components', label: "Web Components!!" }
];
template = list.createInstance({ items: this.#items });
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(this.template);
}
set items(items) {
this.#items = items;
this.template.update(items);
}
get items() {
return this.#items;
}
});
</script>
with-for-each
。这个例子只是为了激发一下兴趣。更多信息 请参阅提案。
模板实例化在实现时将会非常有用,但目前,我们需要依赖库。
这是否意味着 Web 组件无法实现模板?简直荒谬!现在有很多方法和库可用,从lit-html、HyperHTML或hybrids到slim.js或svelte等等。
举几个例子来说明这一点:
使用 lit-html 进行模板
import { LitElement, html } from 'lit-element';
const itemTemplate = ({ value, label, type }) => html`
<li class=${type} data-value=${value}>${label}</li>`
customElements.define('awesome-web-components', class extends LitElement {
items = [/* ... */]
render() {
return html`<ul>${items.map(itemTemplate)}</ul>`;
}
});
使用混合模板
import { define, html } from 'hybrids';
const itemTemplate = ({ value, label, type }) => html`
<li class=${type} data-value=${value}>${label}</li>`;
define('awesome-web-components', {
items: { get: () => [/*...*/] },
render: ({ items }) => html`<ul>${items.map(itemTemplate)}</ul>`
});
使用 Slim.js 进行模板化
import { Slim } from 'slim-js';
import { tag, template } from 'slim-js/Decorators';
import 'slim-js/directives/repeat.js'
@tag('awesome-web-components')
@template(`
<ul>
<li s:repeat="items as item"
bind:class="item.type"
bind:data-value="item.value">
{{ item.label }}
</li>
</ul>`)
class MyTag extends Slim {
onBeforeCreated() {
this.items = [/*...*/]
}
}
使用 Svelte 进行模板
<ul>
{#each items as item}
<li class="{item.type}" data-value="{item.value}">{item.label}</li>
{/each}
</ul>
<script>
export default {
data() {
return {
items: [/*...*/]
}
}
}
</script>
值得一提的是,其中一些示例展示了使用构建时转译来渲染模板(尤其是 svelte 模板)的方法。但您并不限于此;混合模板、lit-element 和其他模板都可以在浏览器中运行动态模板。您可以将 lit-element 示例(进行一些小修改以解析裸模块说明符)粘贴到浏览器控制台中,它就可以正常工作。
通过多种模板方法,您还可以声明性地将复杂数据作为属性传递:
import { html } from 'lit-html';
const propPassingTemplate = html`
<takes-complex .data=${{ like: { aTotal: ['boss'] } }}></takes-complex>`;
那么,你能编写动态的声明式模板吗?Web 组件提供了一个简单的模板机制,无需繁琐的转译步骤。此外,生态系统中存在许多不同的、自成体系的方法,随着这些标准的声名鹊起,还会有更多方法出现。
误区:Web 组件无法进行服务器端渲染
服务器端渲染是一种技术,当请求到达时,客户端 JavaScript(或类似代码)会在服务器上执行,并生成初始响应,其中包含一些原本不可用的内容,除非上述客户端代码下载、解析并执行完毕。一般来说,实现服务器端渲染有两个原因:
- 让你的应用页面可以被可能不运行 JavaScript 的搜索引擎索引
- 减少首次内容绘制的时间
你能在 Web 组件应用中实现这些目标吗?毫无疑问。
您可以使用 Google的Puppeteer(在您的服务器上运行无头 Chrome 或 Firefox)来渲染组件内容,以供网络爬虫使用。captaincodeman提供了一个用 Go 编写的、功能齐全的SSR和SEO示例。
因此,出于 SEO 目的,有一些方法可以在服务器上运行基于自定义元素的客户端 JS。那么如何减少加载时间呢?
好吧,关于在服务器端运行模板是否更快,目前似乎还没有定论。如果目标是减少FCP时间,您可以选择在请求时计算数据,同时使用轻量级的静态应用外壳来构建客户端应用。在这种 SSR 中,您需要一些服务器端代码来计算初始状态,例如Apollo Elements GraphQL 应用中的以下示例:
async function ssr(file, client) {
// Instantiate a version of the client-side JS on the server.
const cache = new InMemoryCache();
const link = new SchemaLink({ schema: server.schema, context });
const client = new ApolloClient({ cache, link, ssrMode: true });
// Calculate the initial app state.
await client.query({ query: initialQuery });
const serializedState = JSON.stringify(client.extract());
// Inject said state into the app with a static `<script>` tag
const dom = await JSDOM.fromFile(file);
const script = dom.window.document.createElement('script');
script.innerHTML =
`window.__APOLLO_STATE__ = ${serializedState}`;
dom.window.document.head.append(script);
// Send the modified index.html to the client
return dom.serialize();
}
app.get(/^(?!.*(\.)|(graphi?ql).*)/, async function sendSPA(req, res) {
// SSR All the Things
const index = path.resolve('public', 'index.html');
const body = await ssr(index, client);
// 👯♀️👯♂️
res.send(body);
});
对于像 redux 这样的不同状态容器执行相同的操作留给读者练习。(或者,像...谷歌一下)
你会注意到,这些代码并非特定于 Web 组件或任何特定的模板库。当你的组件升级并连接到其状态容器时,它们会获取其属性并根据其实现进行渲染。
关于这个问题还有很多话要说,而且由于 lit-html 团队已将 SSR 的工作列为 2019 年的优先事项,所以在短期内,情况只会有所好转。亲爱的读者,我并不介意告诉您,我并非专家。如果您想了解更多详情,请关注Trey Shugart、Kevin P Schaaf和Justin Fagnani 。
那么,你能在你的 Web Components 应用里实现所有东西的 SSR 吗?好吧,别指望有什么一站式解决方案。现在还处于早期阶段,牛路还很新。尽管如此,目前的基本设施已经在生产环境中使用,而且很快就会有很多新功能推出。但这真的可能吗?当然!
tl;dr:这些技术和库还处于早期阶段,但在基于 wc 的应用程序中实现 SSR 的目标是完全可能的。
好的,我这就打电话。
误解:Web Components 是 Google 的专有技术
虽然现代 Web 组件的故事始于 Google(我听说是在其一个数据中心的地下室举行的一次秘密会议上),但它的发展已经超越了任何一家公司的界限。
即:
- HTML 模块提案已被微软采纳。
- 模板实例化提案已被苹果搁置。(对美国人来说,“搁置”的意思是“提交审议”)。
- VSCode 团队正在带头标准化 Web 组件的 IDE 工具。
open-wc
(警告:我是一名贡献者)是一个与任何大公司都无关的社区项目。
Web 组件规范是具有多个实现和利益相关者的开放标准。
误区:需要 Polymer 才能使用 Web Components
这很有趣。早在 2013 年那个黑暗年代,使用“Web 组件”的唯一方法是使用 Polymer 库,当时它的功能集 polyfill/模板系统/构建工具/包管理器/工具箱于一身。原因很简单:Polymer 项目发明了 Web 组件的现代概念,而 Polymer 库(版本 0)是它们的原型实现。
从那时起,情况发生了翻天覆地的变化。polyfill 多年前就从 Polymer 库及其自成体系的模板系统中分离出来,现在已被许多独立项目使用。
如果这对您来说是新鲜事,请快速阅读我的Polymer Library 帖子的第一部分,其中阐明了 Polymer 项目和 Polymer Library 之间的区别。
所以,你不需要 Polymer 来使用 Web 组件。如果你只支持常用浏览器(Edgeium 发布前不支持 Edge),你甚至不需要 Polyfill。
想要证据吗?在 Chrome、Firefox 或 Safari 中打开新标签页,然后将此代码片段粘贴到控制台中:
customElements.define('the-proof', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>:host { display: block; }</style>
You just used web components without Polymer
`;
}
});
document.body.innerHTML = `
<the-proof>You Can't use web components without Polymer!!</the-proof>
`;
tl;dr: polyfill是独立的,Polymer 项目甚至建议不要在新项目中使用 Polymer 库。
误区:你需要使用 HTML 导入
2015 年吸引我关注 Web 组件的原因之一,就是在 HTML 文件中编写复杂组件的概念。如今已不复存在的 HTML Imports 规范让我们能够做到这一点,它的具体实现如下:
<link rel="import" href="/my-component.html">
<my-component></my-component>
HTML Imports 引起了许多开发者的共鸣,因为它标志着 Web 开发回归以文档为中心的理念,而非如今我们许多人不得不遵循的“现代”以脚本为中心的理念。正因如此,当 HTML Imports 规范被弃用,转而采用模块时,对于 Web 组件社区的许多成员来说,这无疑是苦乐参半的。
是的,你没看错。HTML导入根本就不存在。1
如今,Web 组件和应用程序作者最有可能使用 JavaScript 模块来打包和导入他们的组件:
<script type="module" src="/my-component.js"></script>
<my-component></my-component>
这种方法为我们现有的大量工具选项打开了大门,这意味着您不需要在项目中使用 Polymer 工具。
但你也不仅限于模块:<good-map>
它是一个原生的 Google 地图 Web 组件包装器,以脚本而非模块的形式分发。如果你访问过这个代码库(我希望你访问过),不要被(可选的)旧版 HTML 导入所困扰,也不要因为上次更新是两年前而感到担忧,Web 组件规范意味着它仍然可以正常工作。
tl;dr:HTML 导入不仅是不必要的,而且实际上您不应该在您的项目中使用它们。
误区:你需要使用 Shadow DOM
这是最容易破除的迷思之一。最近用过 GitHub 吗?你肯定用过没有 Shadow DOM 的 Web 组件。用你常用的浏览器打开https://github.com,然后在控制台中粘贴这段代码:
const isCustomElement = ({ tagName }) => tagName.includes('-');
const usesShadowDom = ({ shadowRoot }) => !!shadowRoot;
const allElements = Array.from(document.querySelectorAll('*'))
console.log("All Custom Elements", allElements.filter(isCustomElement));
console.log("Uses Shadow Dom", allElements.filter(usesShadowDom));
Shadow DOM 是 Web 组件的秘密武器,我强烈建议您充分利用它。然而,有时您可能不想将组件的所有样式都封装到文档2的其余部分。在这种情况下,避免使用 Shadow DOM 很简单——只需不选择启用即可!
这是一个简单的可复制粘贴的示例:
customElements.define('without-shadow', class extends HTMLElement {
constructor() {
super();
// no call to `this.attachShadow`
this.innerHTML = `<p>A Custom Element Without Shadow DOM</p>`
this.style.color = 'rebeccapurple';
}
});
document.body.innerHTML = `<without-shadow></without-shadow>`;
因此,虽然我认为您应该使用 Shadow DOM,但很高兴知道您不必这样做。
误区:你需要框架来编写应用程序
你可能听说过“Web 组件非常适合按钮之类的叶节点,但构建真正的应用则需要框架”之类的说法。当然,如果你要构建复选框或卡片之类的叶节点,Web 组件无疑是首选(参见下一个误区),但你可能不知道的是,你确实可以用它们构建完整的应用。
我使用 Apollo GraphQL 和 Web 组件构建了一个演示应用,它在 Lighthouse 测试中得分很高。此外,我还创建了pwa-starter-kit示例应用。它使用 Web 组件和 Redux 3来管理状态,并具备客户端路由、集成测试以及所有应用相关的功能。在 Forter,我们正在构建不使用框架的原型和内部应用,目前为止,效果非常良好。
还有更多例子。(想知道 GitHub 使用的是哪个 JS 框架吗?)
现在,我突然觉得,说你永远不应该使用框架和说你总是需要框架一样错误。框架本身并没有错。框架或许是你项目的正确选择,但不要让任何人告诉你,编写 Web 应用需要框架。
tl;dr:框架很棒,但它们不是绝对的要求,即使对于尖端的工作流程也是如此。
误区:你不能在框架中使用 Web Components
这个很简单。只需浏览https://custom-elements-everywhere.com 10 秒钟就能消除它。
即使是自定义元素支持最差的框架也在缓慢但坚定地努力改善这种情况,并且提供了解决方法。
tl;dr:Web 组件💓喜欢💓框架。
误区:Web 社区已经不再使用 Web Components
如果你已经读完了整篇文章,你可能会挠头想“这难道不明显吗?”然而,从网上声称WC已死的言论来看,这个说法确实值得进一步完善。
我们已经见证了大大小小的组织是如何部署 Web 组件的。我们也见证了您自己在过去一小时内如何在热门网站上使用 Web 组件。我们也见证了在所有浏览会话中,超过 10% 的页面加载会加载包含自定义元素的页面。而这一切仅仅是个开始。
2018 年,Web 组件领域经历了名副其实的“寒武纪大爆发”,涌现出大量新想法和代码——从Firefox 63 版本全面支持,到Edge 宣布即将发布,再到 hybrids 和haunted等创新库的发布(想想 Web 组件的 React hooks),再到Angular Elements等项目,它们改进了元素和框架之间本已强大的互操作性。我们说的可不是那些在编译器背后高谈阔论的浏览器实现者!正如我们上面所见,大大小小的公司开发者以及社区志愿者都纷纷采用了它。
那么,我们应该如何看待那些有时坚持认为“Web 组件尚未出现”的声音呢?
结论
如果你一直在等待 Web 组件“到来”才开始尝试,我现在就给你机会。现在正是 Web 开发者的黄金时代,未来只会更加光明。
Web 组件让我们能够编写和发布可复用的 Web 内容,并构建依赖关系和工具链越来越少的模块化应用。如果您还没有尝试过这种令人耳目一新的开发方式,希望您很快就能尝试一下。
致谢
很多人帮助我写这篇文章,我非常感激。
感谢(排名不分先后) Polymer Slack 上的westbrook、Dzintars、stramel、 Thomas 、 tpluscode 和 Corey Farell;以及团队中的lars、 Passle和daKmoRopen-wc
;WeAllJS slack 上的 Dan Luria(他将这篇文章描述为“早午餐鸡尾酒 - 既令人愉快又逐渐更具挑战性”);我的好朋友 Justin Kaufman;以及我亲爱的妻子 Rachel。
尾注
- 请继续关注,因为随着HTML 模块提案的推出,用 HTML 编写 HTML 的日子又回来了。
- 大多数情况下,你会希望使用
<slot>
元素来实现这种用例。当你的项目因某种原因无法使用影子 DOM polyfill 时,零影子 DOM 方法最适合你。返回 - 不喜欢 Redux 或 Apollo?使用其他状态容器(例如 MobX 等),或者不使用(例如中介器或减数分裂模式)状态容器——你还有其他选择。返回