原始 JavaScript 组件模式
大约一年前,我开始深入研究 Web 组件。我非常喜欢获取自定义元素的引用,然后直接在自定义元素上调用方法并设置值的想法。之后,我研究了 Polymer 3.0,它包含了许多便捷的功能和最佳实践。这些功能和最佳实践主要体现在模板、生命周期管理和属性/特性反射方面。我放弃了 Polymer 3.0,转而使用 lit-element,最后只使用了 lit-html。我继续这个过程,不断剥离技术,同时保留了我所学的模式、方案和最佳实践。最终,我得到了一种类似原生 JavaScript 组件模式(我可能需要一个更具体的名字)的东西。
这种模式甚至没有使用 Web 组件,因为我想要一个可以跨浏览器部署的东西,无需 polyfill 或任何需要传递给浏览器的额外代码。这倒不是什么难事,也不应该成为在新项目中使用 Web 组件的障碍,只是我想要一个可以在任何地方使用的东西。
下面是一个非常简单的组件示例。它使用 ES6 类和一个简单的模板字面量来生成标记。它在构造函数内部做了一些巧妙的操作,这段代码本质上是一个样板,用于确保每个 DOM 元素只有一个 JavaScript 对象代表它。它通过设置一个 data-ref 属性并随机生成一个 ID 来实现这一点。然后,当使用 ExampleComponent 类时,如果提供的 DOM 元素已经存在该类的实例,则构造函数将返回对已存在对象的引用。这允许将一个 DOM 元素多次传递给该类的构造函数,并且该类的实例始终只有一个。
export default class ExampleComponent {
init(container) {
this.container = container;
this.render();
}
render() {
this.container.innerHTML = ExampleComponent.markup(this);
}
static markup({}) {
return `
<h1>Hello, World!</h1>
`;
}
constructor(container) {
// The constructor should only contain the boiler plate code for finding or creating the reference.
if (typeof container.dataset.ref === 'undefined') {
this.ref = Math.random();
ExampleComponent.refs[this.ref] = this;
container.dataset.ref = this.ref;
this.init(container);
} else {
// If this element has already been instantiated, use the existing reference.
return ExampleComponent.refs[container.dataset.ref];
}
}
}
ExampleComponent.refs = {};
document.addEventListener('DOMContentLoaded', () => {
new ExampleComponent(document.getElementById('example-component'))
});
你会注意到,这会在标签中渲染静态的“Hello, World!”值<h1>
。但是,如果我们想要一些动态值怎么办?首先,我们将更新类,如下所示:
export default class ExampleComponent {
set title(title) {
this.titleValue = title;
this.render();
}
get title() {
return titleValue;
}
init(container) {
this.container = container;
this.titleValue = this.container.dataset.title;
this.render();
}
render() {
this.container.innerHTML = ExampleComponent.markup(this);
}
static markup({title}) {
return `
<h1>${title}</h1>
`;
}
constructor(container) {
// The constructor should only contain the boiler plate code for finding or creating the reference.
if (typeof container.dataset.ref === 'undefined') {
this.ref = Math.random();
ExampleComponent.refs[this.ref] = this;
container.dataset.ref = this.ref;
this.init(container);
} else {
// If this element has already been instantiated, use the existing reference.
return ExampleComponent.refs[container.dataset.ref];
}
}
}
ExampleComponent.refs = {};
document.addEventListener('DOMContentLoaded', () => {
new ExampleComponent(document.getElementById('example-component'))
});
现在,我们使用构造函数传入的容器 DOM 元素的 data-title 属性来初始化值。此外,我们还提供了 setter 和 getter 方法来获取和更新值,并且每当值更新时,我们都会重新渲染组件。
但是,如果我们希望子组件作为该组件的一部分进行渲染该怎么办?
export default class ExampleComponent {
set title(title) {
this.titleValue = title;
this.render();
}
get title() {
return titleValue;
}
init(container) {
this.container = container;
this.titleValue = this.container.dataset.title;
this.render();
}
render() {
this.container.innerHTML = ExampleComponent.markup(this);
this.pageElement = this.container.querySelector('.sub-component-example');
new AnotherExampleComponent(this.pageElement);
}
static markup({title}) {
return `
<h1>${title}</h1>
<div class="sub-component-example"></div>
`;
}
constructor(container) {
// The constructor should only contain the boiler plate code for finding or creating the reference.
if (typeof container.dataset.ref === 'undefined') {
this.ref = Math.random();
ExampleComponent.refs[this.ref] = this;
container.dataset.ref = this.ref;
this.init(container);
} else {
// If this element has already been instantiated, use the existing reference.
return ExampleComponent.refs[container.dataset.ref];
}
}
}
ExampleComponent.refs = {};
document.addEventListener('DOMContentLoaded', () => {
new ExampleComponent(document.getElementById('example-component'))
});
注意,这次我们在标记方法中添加了一个具有唯一类名的 div。然后在 render 方法中,我们获取了该元素的引用,并使用该 DOM 元素初始化 AnotherExampleComponent。注意:我这里没有提供 AnotherExampleComponent 的实现。最后,如果我们希望组件将事件从组件传播到父组件,或者任何已初始化或引用该组件的代码,该怎么办?
export default class ExampleComponent {
set title(title) {
this.titleValue = title;
this.render();
}
get title() {
return titleValue;
}
init(container) {
this.container = container;
this.titleValue = this.container.dataset.title;
this.render();
}
render() {
this.container.innerHTML = ExampleComponent.markup(this);
this.pageElement = this.container.querySelector('.sub-component-example');
this.clickMeButton = this.container.querySelector('.click-me');
new AnotherExampleComponent(this.pageElement);
this.addEventListeners();
}
static markup({title}) {
return `
<h1>${title}</h1>
<button class="click-me">Click Me</div>
<div class="sub-component-example"></div>
`;
}
addEventListeners() {
this.clickMeButton().addEventListener('click', () =>
this.container.dispatchEvent(new CustomEvent('click-me-was-clicked')));
}
constructor(container) {
// The constructor should only contain the boiler plate code for finding or creating the reference.
if (typeof container.dataset.ref === 'undefined') {
this.ref = Math.random();
ExampleComponent.refs[this.ref] = this;
container.dataset.ref = this.ref;
this.init(container);
} else {
// If this element has already been instantiated, use the existing reference.
return ExampleComponent.refs[container.dataset.ref];
}
}
}
ExampleComponent.refs = {};
document.addEventListener('DOMContentLoaded', () => {
new ExampleComponent(document.getElementById('example-component'))
});
请注意,我们现在添加了一个 addEventListeners 方法,用于监听组件内的事件。当按钮被点击时,它会在容器上调度一个自定义名称的事件,这样客户端代码就可以监听容器上一组自定义命名的事件,而无需了解组件本身的实现细节。也就是说,容器是客户端代码和实现之间的边界。类本身永远不应该超出其容器的范围,客户端代码也永远不应该进入容器内部获取数据或事件。所有数据和事件都应通过容器调度的 getter 方法和事件接口提供给客户端。
所有这些关注点分离、封装和组件化开发,在原生 JS 中都可以实现,无需依赖任何库、框架或 polyfill。我一直强调,方案和模式永远比框架和库更好。我们也不需要 Web 组件来实现这一点。那么,Web 组件和库的优势究竟体现在哪里呢?
首先,Web 组件是对平台的增强,它将这里介绍的方案和模式转化为平台的规则。这意味着,对于 Web 组件,这里展示的封装和关注点分离不会被客户端代码破坏,因为平台会强制执行。因此,如果可以使用 Web 组件,这些最佳实践应该针对 Web 组件进行更新(一篇关于此的博客文章即将发布!)。
其次,库也能派上用场。所以,如果你的数据预算中有足够的空间来交付给客户端的代码量,那么有一些库可以帮到我们。目前,这种方案只需要实际的项目代码,因为不需要任何库。这种方案的主要问题是渲染标记。目前,重新渲染的成本很高,而且用简单的模板文字来表示复杂的视图可能很复杂。但是,我们可以使用带标记的模板文字库(例如 hyperHTML 或 lit-html)来简化渲染过程并加快重新渲染过程。请记住,虽然 hyperHTML 已经投入生产一年多了,但 lit-html 目前正处于 1.0 版本的准备阶段。
我在我的博客上有同样的帖子,在其中我更多地讨论了框架上最新和最伟大的 Web 开发模式。
文章来源:https://dev.to/megazear7/the-vanilla-javascript-component-pattern-37la