[译] 用 Shadow DOM v1 和 Custom Elements v1 实现一个原生 Web Component

  • 原文地址:Make a Native Web Component with Custom Elements v1 and Shadow DOM v1
  • 原文作者:Pearl Latteier
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:newraina
  • 校对者:CoderMing

假如你有一个小表单或者组件要在网站的好几个地方或者好几个项目里用,你希望它们都能有统一的样式和行为,但是,你也希望它们能有些灵活性:也许你的表单需要根据容器元素的不同有各种大小,或者组件要在不同的项目里显示不同的文字和图标。你知道你需要什么吗?你需要一个 web component!

Web components 是可以重用和共享的自定义 HTML 元素。和原生 HTML 元素一样,它们有属性,有方法,有事件监听器,能嵌套,能兼容各种 JavaScript 框架。

怎么样,是不是很厉害?没有 jQuery,没有难以维护的面条代码,它就是一个良好封装过的带 UI 和功能的组件了。

介绍一下 Mini-Form 组件

我们要实现一个叫 “mini-form” 的 web component。(Custom element 的名字必须用小写字母开头,并且至少有一个连字符。要了解更多可以阅读相关标准。)它是一个很简单的表单组件:让用户提交投诉意见,并且能确认是否收到了用户的输入(实际上并不真的干什么)。这个组件能自适应它容器元素的大小和标题的长度。它有一个基本的 material design 样式;你可以给每个组件实例指定颜色主题。组件的代码托管在 github.com/pearlbea/mi…,在线示例请见这里。

定义 Custom Element

Web components 可以用一些新的 web 标准来实现。其中最重要的是最新修订过的 Custom Elements 标准。(要了解更多关于新的 Custom Elements V1 标准,可以阅读 Eric Bidelman 的文章)要创建一个 custom element,我们需要两个东西:一个定义元素行为的类,以及一个告诉浏览器如何关联 DOM 元素标签和刚才那个类的定义。新建一个叫 mini-form.js 的文件,把下面的类和定义代码放进去:

class MiniForm extends HTMLElement {constructor() {super();}
}
window.customElements.define('mini-form', MiniForm);
复制代码

constructor 里,对 super() 不带参数的调用必须放在第一行。它会为组件设置正确的原型链和 this 的值。(更多信息可以参考 Mozilla Developer Network 关于 super 的文章。)

其他准备工作

新建文件的时候,还要创建:一个 index.html,用来实际引用组件;一个 mini-form-test.html,用来写测试用例,因为组件是你写的。先在这两个文件里写上基本的 HTML5 样板代码。

你还需要一些 polyfill。我们使用的 web 标准非常新,还没被所有浏览器支持,至少到目前为止,polyfill 是必须的。对于我们这个简单的组件,只需要两个 polyfill:custom elements 和 shadydom,可以用 Bower 安装:

bower install --save webcomponents/custom-elements
bower install --save webcomponents/shadydom
复制代码

把这两个 polyfills 放在 index.htmlmini-form-test.html 的 head 里,(或者用你习惯的构建工具打包在一起,都行,无所谓。)同时,也要把 mini-form.js 引用进每一个 HTML 文件里。index.html 现在差不多是下面的样子:

<!doctype html>
<html lang="eng"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"><script src="bower_components/shadydom/shadydom.min.js"></script><script src="bower_components/custom-elements/custom-elements.min.js"></script><script src="mini-form.js"></script></head><body></body>
</html>
复制代码

注意:shadydom polyfill 要放在 custom elements polyfill 前面。不然,你可能会看到 Element#attachShadow 不存在的报错。(猜猜我是怎么知道的。)shadow DOM 的其他内容后面再说。

编写测试用例

在真的开始写组件之前,我们先写一些测试。我们要测试这个组件能不能在 DOM 中渲染出一个 div,现在它还通不过测试,毕竟我们的组件还几乎不存在。不过,一旦我们渲染出了一个 div 元素,我们就能体会到目睹测试通过的乐趣。

测试差不多是这个样子:

suite('<mini-form>', () => {let component = document.querySelector('mini-form');test('renders div', () => {assert.isOk(component.querySelector('div'));});
});
复制代码

为了运行测试,我们要用到 Polymer Project 创建的 web component tester 工具。用 NPM 安装好 web-component-tester 之后,在 mini-form-test.html 文件的 head 标签里加上 node_modules/web-component-tester/browser.js,polyfills 和 mini-form.js 也应该在页面上了。

你还要在 body 里加上 mini-form 的实例,就像这样:

<body><mini-form></mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');test('renders div', () => {assert.isOk(component.shadowRoot.querySelector('div'));});});</script>
</body>
复制代码

好了,跑测试吧!在命令行中输入 wct,web component tester 会启动你安装的所有浏览器运行测试。然后,你会看到一个测试失败的提示:

test/mini-form-test.html » <mini-form> » renders div expected null to be truthy
复制代码

如果你遇到了其他问题,可以在这里看看到这一步,你的代码应该是什么样子。

编写模版

现在我们可以来扩充组件的实现并让测试通过了。

class MiniForm extends HTMLElement {constructor() {super();}connectedCallback() {this.innerHTML = this.template;}get template() {return `<div>This is a div</div>`;}
}
复制代码

上面的代码新增了一个返回最简单模板的 getter。然后,在 connectedCallback 中,模板赋给了组件的 innerHTML。connectedCallback 方法是custom element 生命周期的一部分,当组件插入到 DOM 中时会被调用。

再跑一遍测试,噢耶!这次肯定能通过!当然,这个组件最后不会仅仅只显示一个 div。我们要写更多的测试,看着它们测试失败,再靠代码实现让它们最终都能通过。

// mini-form-test.html
test('renders input', function() {assert.isOk(component.querySelector('input[type="text"]'));
});test('renders button', function() {assert.isOk(component.querySelector('button'));
});// mini-form.js
get template() {return `<div><input type="text" name="complaint" /><button>Submit</button></div>`;
}
复制代码

增加样式和 Shadow DOM

到目前为止,mini-form 组件还不是很好看,是时候加一点样式了。不管用在哪里,组件的样式都应该在所有的实例间保持统一。我们并不希望组件所在页面的 CSS 或者 JS 会影响到组件,也不希望组件的样式或行为影响到了它所处的页面。可以通过把组件的内容封装在 Shadow DOM 里来实现这一点。

Shadow DOM 和你早已熟悉和喜爱的 DOM 很像。它有相同的树形结构和工作方式,只是:它不会和父级 DOM 相互影响;也不会成为它所附属元素的子元素。

我们要修改 mini-form 来让它支持 Shadow DOM。

connectedCallback() {this.initShadowDom();
}initShadowDom() {let shadowRoot = this.attachShadow({mode: 'open'});shadowRoot.innerHTML = this.template;
}
复制代码

我们不再把模板内容直接赋给组件自身的 innerHTML,而是创建一个 shadowRoot 作为中介:给组件关联上一个 Shadow DOM,然后把模板内容赋给这个 Shadow DOM 的 innerHTML。

这样做会破坏掉所有的测试,不过,改起来也很简单,只要在 DOM 查询上加上刚定义过的 shadowRoot 即可。

test('renders div', () => {assert.isOk(component.shadowRoot.querySelector('div'));
});
test('renders input', () => {assert.isOk(component.shadowRoot.querySelector('input'));
});
test('render button', () => {assert.isOk(component.shadowRoot.querySelector('button'));
});
复制代码

跑一遍测试,确保全都通过之后,我们来加上 Material Design 的样式。

<style>@import 'https://fonts.googleapis.com/icon?family=Material+Icons';@import 'https://code.getmdl.io/1.3.0/material.indigo-pink.min.css';@import 'http://fonts.googleapis.com/css?family=Roboto:300,400,500,700';.mdl-card {width: 100%;}.mdl-button {margin-top: 10px;}i {margin-right: 5px;}
</style>
<div class="mdl-card mdl-shadow--2dp"><header class="mdl-layout__header"><div class="mdl-layout__header-row"><i class="material-icons">mood_bad</i><div class="mdl-layout-title">complaint box</div></div></header><div class="mdl-card__supporting-text"><input type="text" class="mdl-textfield__input" /></div><div class="mdl-card__actions"><button class="mdl-button mdl-button--raised mdl-button--accent">Submit</button></div>
</div>
复制代码

在浏览器里打开组件的 index.html 看一下,页面虽然还需要打磨,但是已经有一个好看的输入框和一个漂亮的粉色按钮了。

(没看到粉色按钮?可以来这里看下到这一步,代码应该是什么样子。)

在内部 DOM 中创建 <slot>

Shadow DOM 有个很棒的特性:<slot> 元素,它让组件可以把它实际的子元素插入到内部结构中。这个能力让 web components 变得异常灵活。<slot> 元素扮演了一个占位符的角色,使用组件的人可以自己填充内容。对于我们这个组件来说,我们将用 slot 让我们自己(或者组件未来的用户)有能力为表单每一个实例提供不同的文字提示或者问题。第一步,先写好测试:

<body><mini-form>What?!</mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');...test('renders prompt', () => {let index = component.innerText.indexOf('What?!');assert.isAtLeast(index, 0);});});</script>
</body>
复制代码

上面的测试检查了 <mini-form> 标签之间的文本内容是不是在组件中显示出来了。运行一下测试,可以看到测试失败了。

为了让测试通过,在模板中加一个 <slot>

<div class="mdl-card mdl-shadow--2dp"><div class="mdl-card__supporting-text"><h4><slot></slot></h4><input type="text" rows="3" class="mdl-textfield__input" name="prompt" /></div>...
</div>
复制代码

再跑一遍测试,这次通过了!试试在 index.htmlmini-form 标签之间写点东西,然后在浏览器里看一下效果。到这一步的代码在这里。

实现主题化

组件需要能允许我们为每一个实例指定一个颜色主题。为了让主题化和我们在用的 material design CSS 配合得好,用户能用的主题会被限制在这里列出的几种里。我们给组件新增一个 theme 属性,用户设置一个字符串值来指定主题。

给这个新特性写点测试。

<body><mini-form theme="blue-green">What?!</mini-form><script>suite('<mini-form>', function() {let component = document.querySelector('mini-form');...test('applies color theme to button', () => {let button = component.shadowRoot.querySelector('button');let buttonColor = window.getComputedStyle(button).getPropertyValue('background-color');assert.equal(buttonColor, 'rgb(105, 240, 174)');});test('applies color theme to header', () => {let header = component.shadowRoot.querySelector('header');let headerColor = window.getComputedStyle(header).getPropertyValue('background-color');assert.equal(headerColor, 'rgb(33, 150, 243)');});});</script>
</body>
复制代码

跑一遍测试,确定一下它们通过没有。没通过吧?很好。修改组件的代码来获取和使用 theme 属性。

get theme() {return this.getAttribute('theme') || 'indigo-pink';
}get template() {return `<style>@import 'https://code.getmdl.io/1.3.0/material.${this.theme}.min.css';...</style>...`;
}
复制代码

我们从 <mini-form> 标签上获取 theme 属性,把它或者它的默认值 indigo-pink 用在 CSS 的地址里。如果我们给 theme 属性赋了这个 CSS 类库实际并没有的主题值,CSS 的地址就不会生效,组件就会很难看。解决这个问题需要写的代码(和它的测试用例!),我打算交给你自己来完成。

跑一下测试,哎呀,并没有全部通过。因为 Firefox 不支持 Shadow DOM,在 Firefox 里跑的测试失败了。我们已经用上了 shadydom polyfill,但它并不支持 CSS 封装,有另一个叫 shadycss 的 polyfill 能解决这个问题。跟上面一样,之后你自己完成。

index.html 里,给 mini-form 标签增加一个 theme 属性。然后你就能在浏览器里看到你的艺术创作了。

处理事件

组件已经很好看了,但还什么都干不了。我们要干的最后一件事情,是给它加上事件处理的逻辑。当用户点击“Submit”按钮的时候,得发生点什么事情。代码要获取输入,显示一个成功或失败(如果输入为空)的提示。当用户接着聚焦进输入框的时候,错误信息需要消失掉。

给这些事件逻辑写上测试。

let input = component.shadowRoot.querySelector('input[type="text"]');
let button = component.shadowRoot.querySelector('button');
let errorMsg = component.shadowRoot.querySelector('.error');test('displays an error message on submit', () => {button.click();let index = errorMsg.innerText.indexOf('Don\'t you have something to say?');assert.isAtLeast(index, 0);
});
test('clears error message on focus', () => {input.focus();let index = errorMsg.innerText.indexOf('Don\'t you have something to say?');assert.isAtLeast(index, -1);
});
test('displays a success message on submit', () => {input.value = 'Some text';button.click();let index = component.shadowRoot.querySelector('.mdl-card').innerText.indexOf('Thank you.');assert.isAtLeast(index, 0);
});
复制代码

在组件代码里,给用户会与之发生交互的两个元素:输入框和按钮绑定事件监听器。

当用户聚焦进输入框,我们希望清空可能在显示的任何错误提示。首先,在模板里新增一个错误提示,并且创建一个带有 visibility: hidden 属性的 CSS 类 hide

<div class="mdl-card__supporting-text"><h4><slot></slot></h4><input type="text" rows="3" class="mdl-textfield__input" name="question" /><div class="error hide">Don't you have something to say?</div>
</div>
复制代码

给输入框绑定一个事件监听器,处理它的聚焦事件。

connectedCallback() {this.initShadowDom();this.addFocusListener();
}
get input() {return this.shadowRoot.querySelector('input');
}
get errorMessage() {return this.shadowRoot.querySelector('.error');
}
addFocusListener() {this.input.addEventListener('focus', e => {this.hideErrorMessage();});
}
hideErrorMessage() {this.errorMessage.className = 'error hide';
}
复制代码

上面的代码给输入框元素创建了一个 getter、一个在 connectedCallback 里调用的绑定聚焦事件监听的方法、还有一个在事件监听中用来隐藏错误提示的方法。

接着,给按钮增加点击事件的事件监听和处理点击的逻辑。

connectedCallback() {this.initShadowDom();this.addFocusListener();this.addClickListener();
}
get button() {return this.shadowRoot.querySelector('button');
}
get card() {return this.shadowRoot.querySelector('.mdl-card');
}
get message() {// this could be a separate component and probably should be if you make it more complicatedreturn `<div><div class="mdl-card__title"><h4>Thank you.</h4></div><div class="mdl-card__supporting-text">We have received your complaint.</div><div class="mdl-card__actions"></div></div>`;
}
addClickListener() {this.button.addEventListener('click', e => {this.getUserInput();});
}
getUserInput() {this.input.value.length > 0 ? this.handleSuccess() : this.displayErrorMessage();
}
handleSuccess() {// You could call a method to save the user's answer herethis.displaySuccessMessage();
}
displaySuccessMessage() {this.card.innerHTML = this.message;
}
displayErrorMessage() {this.errorMessage.className = 'error';
}
复制代码

跑一遍测试,看它们是不是全都通过!也有可能只是大部分通过:在 Firefox 里,样式的测试用例依然会失败。恭喜,你有一个能工作的 web component 了!

全部的代码在这里。

还可以做很多很多事情来完善和扩展这个组件。除了我早就提到过的,你还可以给头部标题的文本、图标加上 slot,或者美化、保存用户的输入内容。

觉得还不够的话,可以写一个你自己的组件,在 Twitter 上私信给我。祝编程愉快!

相关链接

  • webcomponents.org,关于 web components 最重要的信息来源
  • Web Components v1 — the next generation Google 的 Web 更新动向,Taylor Savage 编写
  • Custom Elements v1: Reusable Web Components Google 的 Web 基础知识,Eric Bidelman 编写
  • Shadow DOM v1: Self-Contained Web Components Google 的 Web 基础知识,Eric Bidelman 编写
  • Custom Elements That Work Anywhere Rob Dodson 编写
  • Polymer,一个 web component 库
  • Skate,也是一个 web component 库
  • web-component-tester,一个测试 web components 的工具
有任何问题或想法,都可以在 twitter @bendyworks 或者 Facebook 上联系我们。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/252778.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php 原生文件下载

1.整个网页的html界面源码下载: xiazai.php <html> <head> <meta charset "utf-8"> <title></title> </head> <body> <form method"post" action"xiazai.php"> <input type"submit&quo…

红外线摄像机的选择与使用及原理

红外线摄像机的选择与使用及原理 用户使用红外灯首先要仔细阅读使用说明书&#xff0c;特别是为保证人身设备安全的注意事项。检查前面所讲述的配套性方面是否达到要求&#xff0c;应考虑到的影响因素是否考虑到&#xff0c;如未达到要求&#xff0c;可及时调整所用器材。 红…

asp 之 让实体中字段类型为DateTime的字段仅仅显示日期不显示时间

在我们平时的工作开发中。我们一般会遇到这种一个问题&#xff1a;某个实体的某个字段是DateTime类型的&#xff0c;但是我们在界面上仅仅想让它显示日期不显示时间&#xff01;一个订单实体&#xff1a;//订单类public class order{//订单IDpublic int id{get;set;}//物品IDpu…

JQ的异步文件上传

一,view代码 <form role"form"><div class"form-group"><label for"keyinput">选择文件&#xff1a;</label><input type"file" name"upfile" id"upfile" /></div><div c…

红外成像与微光成像的区别

在现有的安防技术中,微光和红外成像是运用最广的夜视技术.而微光成像主要运用在反恐侦查,部队作战的夜视仪中、而红外夜视成像主要用于监控摄像机的夜间监控较多.   微光成像技术微光夜视技术又称像增强技术&#xff0c;是通过带像增强管的夜视镜&#xff0c;对夜天光照亮的微…

实体类和数据表的映射异常(XXX is not mapping[ ])

在使用SSH框架开发过程&#xff0c;使用hibernate框架提供的工具类实现与数据库数据交互&#xff0c;在执行cmd操作时&#xff0c;如果出现以下异常&#xff1a; org.hibernate.hql.ast.QuerySyntaxException: xxx is not mapped [from xxx] 或者 nested exception is org.hibe…

Linux下配置LVM

1 LVM介绍LVM(Logical Volume Manager)逻辑卷管理&#xff0c;它是Linux环境下对磁盘分区进行管理的一种机制&#xff0c;LVM是建立在硬盘和分区之上的一个逻辑层&#xff0c;来提高磁盘分区管理的灵活性。通过LVM系统管理员可以轻松管理磁盘分区&#xff0c;逻辑卷管理器的技术…

Python3 配置文件(configparser)(转载)

本文由 Luzhuo 编写,转发请保留该信息. 原文: http://blog.csdn.net/rozol/article/details/72793304 以下代码以Python3.6.1为例 Less is more! configparser 可以读写和解析注释文件, 但是没有写入注释的功能 1 #!/usr/bin/env python2 # codingutf-83 __author__ Luzhuo4 _…

激光摄像机的原理及应用

近年来&#xff0c;在安防监控领域&#xff0c;以目前视频监控技术的发展情况&#xff0c;室内监控和白天正常环境下的监控已不是难题&#xff0c;但社会环境的发展日新月异&#xff0c;城市的发展、森林资源的不断流失、大型项目的建设、边防安全的守护等&#xff0c;这些环境…

Object.defineProperty 详解

最近想了解一下Vue是怎么实现数据双向绑定的&#xff0c;了解到是基于Object.definProperty,在此记录一下。 Object.defineProperty  顾名思义&#xff0c;就是给对象定义一个属性&#xff0c;总共有这么几种&#xff1a; value  属性的值writable  是否可改写&#xff0…

Java 实现排序

public class Sort {public static void main(String[] args) {int data[] {43,54,123,5,98,10,7,74,5,54};System.out.println("原先数组&#xff1a;");for(int d : data) {System.out.print(d " ");}System.out.println("\n");/*System.ou…

相机帧率和曝光时间的关系

文章转载自&#xff1a;http://blog.163.com/pluto_918/blog/static/203853902012111255634175/ 工业相机参数之帧率相关知识详解&#xff1a; 工业相机是机器视觉系统的重要组成部分之一&#xff0c;在机器视觉系统中有着非常重要的作用。工业相机已经被广泛应用于工业生产线…

班长的选举

/* Note:Your choice is C IDE */ #include "stdio.h" #include "string.h" void main() {int zs,ls,ww,zl;//定义一个给参选人员的票数int max;//int xuhao;//char name[5];//参选人员的名字zslswwzl0;//初始票数都为0printf("候选人名单如下\n"…

jquery刷新页面

window.location.reload()刷新当前页面. parent.location.reload()刷新父亲对象&#xff08;用于框架&#xff09; opener.location.reload()刷新父窗口对象&#xff08;用于单开窗口&#xff09; top.location.reload()刷新最顶端对象&#xff08;用于多开窗口&#xff09; 转…

Python 常量

总结&#xff1a;在Python中实际中没有严格的常量&#xff1b;知识程序员中约定俗成用变量名全部大写代表常量 常量的定义&#xff1a; 常量即指不变的量&#xff0c;如pai 3.141592653..., 或在程序运行过程中不会改变的量 举例&#xff0c;假如老男孩老师的年龄会变&#xff…

SlickOne 敏捷开发框架介绍(二) -- 多用户/多租户/SAAS软件基础框架实现

前言&#xff1a;在应用于集团版客户或SAAS平台服务的业务系统中&#xff0c;流程管理系统需要支持多用户组织模型。其中包括角色数据、流程定义数据和流程实例数据的多用户标识绑定。本文旨在全面描述如何基于SlickOne敏捷开发框架实现上述基础服务功能&#xff0c;形成一个完…

工业相机行曝光与全局曝光

工业相机行曝光与全局曝光 逐行曝光&#xff1a; 图1 逐行曝光模式 逐行曝光sensor 实现如图1逐行曝光模式所示。与全局曝光不同&#xff0c;逐行曝光从第一行开始曝光&#xff0c;一个行周期之后第二行才开始曝光。依次类推&#xff0c;经过N-1 行后第N 行开始曝光。第一行曝光…

【Alpha阶段汇总】成果展示与体验总结

一、燃尽图 二、软件截图 三、代码与图片、音乐素材仓库 git仓库 四、问题与总结 1.git提交问题 之前创建的仓库地址是http://git.oschina.net/8265559926/groupnet14 但是无论怎么输入都说找不到仓库 经反复思考&#xff0c;感觉可能是因为地址不是纯字母的原因。就重新注册了…

Accusoft结构化工具包FormSuite for Structured Forms常见问题解答(二)

FormSuite for Structured Forms是结构化的表单处理SDK和字符识别工具套包&#xff0c;包括表单处理工具FormFix和字符识别工具SmartZone。所有表格处理控件被设计为可以通过内存到内存的数据传输模式进行相互沟通。本文收集了一些FormSuite for Structured Forms常见问题及解答…

构建之法阅读笔记二

构建之法阅读笔记之二&#xff08;4-11章节&#xff09; 在这之前我不注重代码的规范性&#xff0c;也不是很注重代码风格的规范&#xff0c;也没有对自己写的代码进行审查的过程 根据书中所讲的这样写即使是自己的能力很强&#xff0c;写的代码只有自己可以看懂&#xff0c;别…