我最近需要构建一个 UI,而且(很长一段时间以来第一次)我没有选择使用 React.js,这是我目前最喜欢的 UI 解决方案。 因此,我查看了内置的浏览器 API 提供的功能,发现使用 自定义元素(aka Web Components)可能正是这个 React 开发者需要的解决方案。
自定义元素可以提供与 React 组件相同的通用好处,而不会绑定到特定框架实现。 自定义元素为我们提供了一个新的 HTML 标签,我们可以通过原生浏览器 API 对其进行编程控制。
让我们谈谈基于组件的 UI 的好处
- 封装 - 限制在该组件的关注点将保留在该组件的实现中
- 可重用性 - 当 UI 被分成更通用的部分时,它们更容易分解成模式,而这些模式更容易重复使用
- 隔离 - 由于组件被设计为封装的,因此您获得了隔离的额外好处,这使您可以更容易地将错误和更改范围限定到应用程序的特定部分
用例
您可能想知道谁在生产环境中使用自定义元素。 值得注意的是
- GitHub 正在为他们的模态对话框、自动完成和显示时间使用自定义元素。
- YouTube 的 新 Web 应用程序 是用 Polymer 和 Web 组件构建的。
与组件 API 的相似之处
在尝试比较 React 组件与自定义元素时,我发现 API 非常相似
- 它们都是不是“new”的类,可以扩展基类
- 它们都继承了挂载或渲染生命周期
- 它们都通过 props 或属性接收静态或动态输入
演示
因此,让我们构建一个列出有关 GitHub 存储库的详细信息的小型应用程序。

如果我要用 React 来处理这个问题,我会定义一个像这样的简单组件
<Repository name="charliewilco/obsidian" />
此组件接受一个 props - 存储库的名称 - 并且我们像这样实现它
class Repository extends React.Component {
state = {
repo: null
};
async getDetails(name) {
return await fetch(`https://api.github.com/repos/${name}`, {
mode: 'cors'
}).then(res => res.json());
}
async componentDidMount() {
const { name } = this.props;
const repo = await this.getDetails(name);
this.setState({ repo });
}
render() {
const { repo } = this.state;
if (!repo) {
return <h1>Loading</h1>;
}
if (repo.message) {
return <div className="Card Card--error">Error: {repo.message}</div>;
}
return (
<div class="Card">
<aside>
<img
width="48"
height="48"
class="Avatar"
src={repo.owner.avatar_url}
alt="Profile picture for ${repo.owner.login}"
/>
</aside>
<header>
<h2 class="Card__title">{repo.full_name}</h2>
<span class="Card__meta">{repo.description}</span>
</header>
</div>
);
}
}
查看 CodePen 上 Charles (@charliewilco) 的 React 演示 - GitHub。
为了进一步分解,我们有一个组件,它有自己的状态,即仓库详细信息。 最初,我们将其设置为 null
,因为我们还没有任何这些数据,因此我们在获取数据时会有一个加载指示器。
在 React 生命周期中,我们将使用 fetch 从 GitHub 获取数据,设置卡片,并在获取数据后使用 setState()
触发重新渲染。 UI 采取的所有这些不同状态都表示在 render()
方法中。
定义/使用自定义元素
使用自定义元素这样做有点不同。 就像 React 组件一样,我们的自定义元素将接受一个属性 - 再次是存储库的名称 - 并管理它自己的状态。
我们的元素将如下所示
<github-repo name="charliewilco/obsidian"></github-repo>
<github-repo name="charliewilco/level.css"></github-repo>
<github-repo name="charliewilco/react-branches"></github-repo>
<github-repo name="charliewilco/react-gluejar"></github-repo>
<github-repo name="charliewilco/dotfiles"></github-repo>
查看 CodePen 上 Charles (@charliewilco) 的 自定义元素演示 - GitHub。
首先,我们定义和注册自定义元素只需创建一个扩展 HTMLElement
类的类,然后使用 customElements.define()
注册元素的名称。
class OurCustomElement extends HTMLElement {}
window.customElements.define('our-element', OurCustomElement);
我们可以这样调用它
<our-element></our-element>
这个新元素没什么用,但是使用自定义元素,我们可以使用三种方法来扩展此元素的功能。 这些几乎类似于 React 用于其组件 API 的 生命周期方法。 与我们最相关的两个生命周期方法是 disconnectedCallBack
和 connectedCallback
,由于这是一个类,它带有一个构造函数。
名称 | 调用时间 |
---|---|
构造函数 |
创建或升级元素的实例。 有助于初始化状态、设置事件监听器或创建 Shadow DOM。 有关您可以在 constructor 中执行的操作的限制,请参见规范。 |
connectedCallback |
元素被插入 DOM。 有助于运行设置代码,例如获取资源或渲染 UI。 通常,您应该尝试将工作推迟到此时 |
disconnectedCallback |
元素从 DOM 中删除时。 有助于运行清理代码。 |
为了实现我们的自定义元素,我们将创建该类并设置与该 UI 相关的一些属性
class Repository extends HTMLElement {
constructor() {
super();
this.repoDetails = null;
this.name = this.getAttribute("name");
this.endpoint = `https://api.github.com/repos/${this.name}`
this.innerHTML = `<h1>Loading</h1>`
}
}
在我们的构造函数中调用 super()
,this
的上下文就是元素本身,所有 DOM 操作 API 都可以使用。 到目前为止,我们已将默认存储库详细信息设置为 null
,从元素的属性中获取了存储库名称,创建了一个要调用的端点,这样我们以后就不必定义它,最重要的是,将初始 HTML 设置为加载指示器。
为了获取有关该元素存储库的详细信息,我们需要向 GitHub 的 API 发出请求。 我们将使用 fetch
,并且,由于它是基于 Promise 的,我们将使用 async
和 await
来使我们的代码更易读。 您可以在 此处 了解有关 async
/await
关键字的更多信息,以及有关浏览器 fetch API 的更多信息 此处。 您也可以 在 Twitter 上给我发消息 了解我是否更喜欢它而不是 Axios 库。(提示,这取决于我的早餐是喝茶还是喝咖啡。)
现在,让我们向此类添加一个方法,以向 GitHub 请求有关存储库的详细信息。
class Repository extends HTMLElement {
constructor() {
// ...
}
async getDetails() {
return await fetch(this.endpoint, { mode: "cors" }).then(res => res.json());
}
}
接下来,让我们使用 connectedCallback
方法和 Shadow DOM 来使用此方法的返回值。 使用此方法将与我们调用 React 示例中的 Repository.componentDidMount()
时类似。 相反,我们将覆盖最初给 this.repoDetails
的 null
值 - 我们将在开始调用模板创建 HTML 时使用它。
class Repository extends HTMLElement {
constructor() {
// ...
}
async getDetails() {
// ...
}
async connectedCallback() {
let repo = await this.getDetails();
this.repoDetails = repo;
this.initShadowDOM();
}
initShadowDOM() {
let shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.innerHTML = this.template;
}
}
您会注意到我们正在调用与 Shadow DOM 相关的方法。 除了是 Marvel 电影中被拒绝的标题之外,Shadow DOM 有自己的 丰富的 API 值得一看。 然而,对我们的目的来说,它将抽象化向元素添加 innerHTML
的实现。
现在,我们将 innerHTML
的值设置为 this.template
的值。 现在让我们定义它
class Repository extends HTMLElement {
get template() {
const repo = this.repoDetails;
// if we get an error message let's show that back to the user
if (repo.message) {
return `<div class="Card Card--error">Error: ${repo.message}</div>`
} else {
return `
<div class="Card">
<aside>
<img width="48" height="48" class="Avatar" src="${repo.owner.avatar_url}" alt="Profile picture for ${repo.owner.login}" />
</aside>
<header>
<h2 class="Card__title">${repo.full_name}</h2>
<span class="Card__meta">${repo.description}</span>
</header>
</div>
`
}
}
}
就是这样。 我们定义了一个自定义元素,它管理自己的状态,获取自己的数据,并将该状态反映回用户,同时为我们提供了一个可以在应用程序中使用的 HTML 元素。
在完成此练习后,我发现自定义元素的唯一必要依赖项是浏览器的原生 API,而不是框架来额外解析和执行。 这使得它成为一个更可移植和可重用的解决方案,具有与您已经喜欢和使用以谋生的框架相似的 API。
当然,使用这种方法也有缺点。 我们正在讨论各种浏览器支持问题,以及一些一致性的缺乏。 此外,使用 DOM 操作 API 可能会非常混乱。 有时它们是赋值。 有时它们是函数。 有时这些函数会接受回调,有时不会。 如果你不信,看看向通过 document.createElement()
创建的 HTML 元素添加一个类,这是使用 React 的五大理由之一。 基本的实现并不复杂,但它与其他类似的 document
方法不一致。
真正的问题是:它是否能够弥补不足? 也许吧。 React 仍然非常擅长它被设计为非常擅长的东西:虚拟 DOM、管理应用程序状态、封装以及将数据传递到树中。 在该框架内部使用自定义元素几乎没有任何动力。 另一方面,自定义元素只是因为构建浏览器应用程序而可用。
要查看如何使用各种库生成 Web 组件,请查看 https://github.com/elmsln/wcfactory
以及此视频,https://www.youtube.com/playlist?list=PLJQupiji7J5cAv7Jfr1V8FvUTx_jJrmCl ,以查看其实际操作。
您试过 StencilJs 吗? 您对此有何看法?
我没有尝试过。 我主要坚持使用 React。 看起来很有趣!