HTML5组件编程

发表于2017-08-28 11:36  |  次阅读  |  0条评论  |   作者:siru90

在软件开发中使用组件,通常能减少重复造轮子的开发,提升开发效率。 使用组件的成本一般很低,无非是一个import,一个实例化。
在web前端,组件却有较高的使用成本,以至于有时候会选择不用组件,而是重新发开。前端组件主要的问题包括:
众多外部资源的依赖,可以有js库依赖,dom依赖,css样式依赖,图片依赖等。
以bootstrap为例,要使用bootstrap的功能必须有以下几个步骤:

插入样式
<link rel="stylesheet" href="css/bootstrap.min.css">

插入依赖js
<script src="js/jquery-1.9.1.js"></script>
<script src="js/bootstrap.js"></script>
封装并不严格。例如,外部脚本还是可以修改组件内部结构,外部样式可以影响组件的样式。
随着基于HTML5的webapp的发展,前端的组件也被提升到了新的高度。为了解决前端组件存在的诸多问题,W3C提出了web component提案,提案包括了以下四个部分:

HTML template: 定义了<template>标签,方便基于DOM的模板操作。
Custom Element: 使开发者可以自己定义并扩展html标签。
Shadown DOM: 在一个DOM节点内部创建一个黑盒子,使内部样式和DOM和外部环境隔离。
Html Import: 打包组件,并通过<link>标签导入组件。
下面分别阐述下各部分的功能。

 写在前面
web component标准仍然在开发中,当你看到这个的时候,可能代码已经坏掉了
chrome是唯一实现了web component的浏览器,并且需要手动打开chrome对web component的支持。
方法是访问chrome://flags,打开如下的开关。 

 打开chrome对web component的支持 

 html import也开了吧,后面要用 



 Template
模板在前端已经用的比较多了,常见的有John Resig的 micro template, handlerbars 等。这些都是基于字符串的模板,要通过innerHTML转变为DOM节点。

它的缺陷是:
字符串的模板容易被XSS攻击。
每次innerHTML,浏览器都要通过parse,转换为DOM树,浪费计算,没有clone节点高效。
web component标准提供了 <template> 标签,通过DOM来定义模板。 使用时只需把以前的html标签包裹在 <template> 内即可。 <template> 内部的元素不渲染,不拉取,不执行。通过该节点的content属性,可以获取到内部元素的文档碎片。使用时,克隆该节点即可。

<template id="tmpl">
    <h1></h1>
    <p></p>
</template>
<script type="text/javascript">
    var $frag = document.getElementById('tmpl').content.cloneNode(true);
    $frag.querySelector('h1').textContent = 'hi';
    $frag.querySelector('p').textContent = 'This is nice';
    document.body.appendChild($frag);
</script> 


Custom Element
讲自定义标签,我们先从HTML控件说起吧。
举个例子, html里面有个`<select>` 标签。select标签有一些用起来很爽的地方,比如可以通过disabled禁用下拉菜单项,可以通过optgroup标签将下拉菜单分组。稍微修改一下markup就会得到完全不同的行为,不写一行js。还有,它在IOS上是个滚轮,在android上是个面板。开发者不用关心任何其他的事情。

类似的组件在微软的WPF XAML,adobe Flex,QT的QML等技术中大行其道。为什么web开发中,不能用js实现自己的标签呢,而非要堆放div呢?
在没有web component提案之前,angularjs提供自定义标签的能力,但是方式不同。angularjs使用模板把自定义标签替换为了div等html标签。下面是之前做的一个angularjs组件accordion,你们感受下。
<accordion>
    <pane title="Latest News">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </pane>
    <pane title="oh my god">
        Phasellus et velit tellus...
    </pane>
</accordion>

实现代码看这里 http://jsbin.com/xexot/1
自定义元素赋予开发者开发自定义标签的能力。具体方法如下:
使用 document.register 定义一个自定义标签,定义之后就可以像正常标签一样使用。
var XFoo = document.register('x-foo', {
    prototype: {
        // 这里的方法会出现在标签的原型上面
        // 这个参数空非必填
    }
}); 



Note
自定义标签名字一定要包含一个 -符号哦
实例化有三种方法:
new XFoo();
// or
document.createElement('x-foo');
<x-foo></x-foo>
另外,自定义标签提供了几个回调方法,在生命周期中被调用,例如当加到dom上,从dom取下。

createdCallback,创建后调用
enteredViewCallback,插入document调用
leftViewCallback,移出document调用
attributeChangedCallback,属性更改调用
更多关于技巧详见参考:

扩展原生标签
Element upgrade
:unresolved选择器 


Shadow DOM
Shadow DOM给予了web组件封装内部实现的方法。虽然之前的代码可以通过一些js的设计模式来实现来实现较好的API层的封装,但这里的封装主要指的是DOM和CSS层的封装,比如操作组件内部节点,使用全局样式影响组件内部的样式。
HTMLElement.createShadowRoot() 方法是创建shadow root节点的方法。 创建了shadow root后,原来节点内部的DOM将不再展示,而是展示shadow root上的节点。shadow root内部的DOM结构,样式独立了。

<button>Hello</button>
<script type="text/javascript">
    var host = document.querySelector('button');
    var root = host.createShadowRoot();
    root.textContent = '<span>Awesome</span>';
    // host.shadowRoot === root
</script> 
 
 需要注意的是

在外部文档,使用 span{color:red;} 是不能给该节点样式的。
在外部文档,试图querySelector是取不到节点的。
访问内部元素必须要通过host的 shadowRoot 属性。

更多关于技巧详见参考:
insertion point: <content>标签
:host 选择器
^ ^^ combinator
resetStyleInheritance 属性
multiple shadow root
use css variable to cross shadow boundry
::content pseudo element
shadow insertion point: <shadow>标签
Element.getDistributedNodes()
Element.getDestinationInsertionPoints()
Event model in shadow root



 Html Import
html import使<link>标签可以加载html文档,并同时加载html内的所有内容包括js, css, DOM,

<link rel="import" href="/path/to/import/file.html">
使用html import最大的好处就是现在可以一步导入多个资源了。像bootstrap这样的库,就可以一步导入。依赖多个资源的组件也可以一次打包。

例如,导入bootstrap就可以如下:
<link rel="import" href="bootstrap.html">
bootstrap.html
<link rel="stylesheet" href="css/bootstrap.min.css">
<script src="js/jquery-1.9.1.js"></script>
<script src="js/bootstrap.js"></script>
Note

几个注意点是:
导入的html文件,不用很规范,没有head body都是可以的。可以是一堆script标签,也可以是内联了js, css的html。
import文档内的js在import时执行,js的作用域是同一个window对象
import文档内的js异步加载,并顺序执行。
import文档内的js仅执行一遍,有些像python的import语句
import文档内的dom和css,对主文档没有作用,除非手动插入:

// 获取主文档
document
// 获取import文档
document.currentScript.ownerDocument
html import遵循同源策略,需要CORS实现跨域。本地开发需要本地server。 


示例下面,我们通过学到的组件技术来制作实例 --- 一个钟,使用的时候只需要 <o-clock></o-clock> 就行了。示例代码放附件里了。 

 clock组件
使用template实现模版首先,我们写出DOM结构。如下。这里我用template来做,把结构包在<template> 标签里面就好了。<templateid="tmpl"><divclass="clock"><div><spanclass="hours"></span>:<spanclass="minutes"></span><spanclass="seconds"></span></div><divclass="date"></div></div></template> 

使用document.register实现自定义元素
我们的元素需要继承 HTMLElement 的原型,因为我们需要这个节点上面的方法 appencChild, innerHTML 等

var proto = Object.create(HTMLElement.prototype);
但这个原型我们需要扩展一下。
首先是初始化,这个在createdCallback里面做,我们把内部的几个dom节点存下来,避免多次重复取值
proto.createdCallback = function (){
    var $tmpl = document.getElementById('tmpl');
    this.appendChild($tmpl.content.cloneNode(true));

    this.$hours = this.querySelector('.hours');
    this.$minutes = this.querySelector('.minutes');
    this.$seconds = this.querySelector('.seconds');
    this.$date = this.querySelector('.date');
};
加上update方法,调用时候刷新钟表的展示时间
proto.update = function (){
    var time = new Date();

    this.$hours.textContent = padZero(time.getHours());
    this.$minutes.textContent = padZero(time.getMinutes());
    this.$seconds.textContent = padZero(time.getSeconds());
    this.$date.textContent = padZero(time.getSeconds());
    this.$date.textContent = time.toDateString();
};


当clock节点加到DOM上的时候,我们启动定时器每标刷新,这个在enteredViewCallback里面做
proto.enteredViewCallback = function init(){
    this.update();
    var self = this;
    this._timer = setInterval(function (){
        self.update();
    }, 1000);
};

从节点移除的时候,需要取消定时器
proto.leftViewCallback = function (){
    clearInterval(this._timer);
};

最后使用该原型注册新元素
document.register('o-clock', {
    prototype: proto
});

使用shadow DOM隐藏内部实现
这时候我们的组件已经可以用了。但是由于不是shadow DOM外部的样式,脚本仍然可以轻易干扰组件的工作。我们把它变成一个shadow DOM。这里需要修改初始化方法。 之前是 this.appendChild 现在,创建shadow root后需要 root.appendChild

proto.createdCallback = function (){
    var $tmpl = document.getElementById('tmpl');
    var root = this.createShadowRoot();
    root.appendChild($tmpl.content.cloneNode(true));

    this.$hours = root.querySelector('.hours');
    ...
};
这样修改后的DOM查看器中可以看到结构中多了一个document fragment

 对比非shadow DOM的结构。 



使用html import打包组件
最后,试试html import打包吧。

需要注意的是,被导入文档中document引用的问题。我们的组件初始化的时候,用到了 document.getElementById('tmpl') 来获取文档中的template节点。但是在html import的时候,这个 document 指的是声明导入的文档的document对象。这里必须使用 document.currentScript.ownerDocument.getElementById('tmpl') 来指明是被导入的文档的document。

修改这个以后,我们的clock组件就可以用了。
<!DOCTYPE html>
<html>
<head>
    <link rel="import" href="shadow-DOM.html">
</head>
<body>
    <o-clock></o-clock>
</body>
</html>
这样的前端组件,是不是Simple and beautiful~~

参考
http://w3c.github.io/webcomponents/explainer/
http://www.html5rocks.com/en/tutorials/webcomponents/template/
http://www.html5rocks.com/en/tutorials/webcomponents/customelements/
http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom/
http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/
http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-301/
http://www.html5rocks.com/en/tutorials/webcomponents/imports/
本站关键字:sunny90 web开发 数据库 移动开发 服务器 Nginx Mysql PHP
Copyright © sunny90版权所有 power by sunny90.com  
湘ICP备14012284号-1,粤公网安备 44030602000307号