作为一名前端攻城狮,相信大家也都在关注着前端的一些新技术,近些年来前端组件化开发已为常态,我们经常把重用性高的模块抽离成一个个的组件,来达到复用的目的,这样减少了我们的维护成本,提高了开发的效率。但是都有一个缺点离不开框架本身,因为我们浏览器本身解析不了那些组件。那么有没有一种技术也可以达到这种效果呢?答案就是今天的主角 Web Components。
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。
目前 W3C 也在积极推动,并且浏览器的支持情况还不错。FireFox、Chrome、Opera 已全部支持,Safari 也大部分支持,Edge 也换成 webkit 内核了,离全面支持应该也不远了。蓝狮注册开户当然社区也有兼容的解决方案 webcomponents/polyfills。
WebComponents 三要素和生命周期
Button 组件示例
首先我们就从一个最简单的 Button 组件开始,我们可以通过在组件中传入 type 来改变按钮的样式,并且动态监听了数据的变化。
// html
按钮
三要素、生命周期和示例的解析
Custom elements(自定义元素):一组 JavaScript api,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。在上面例子中就指的是我们的自定义组件,我们通过 class CaiButton extends HTMLElement {} 定义我们的组件,通过 window.customElements.define(‘cai-button’, CaiButton) 挂载我们的已定义组件。
Shadow DOM(影子 DOM ):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。使用 const shadow = this.attachShadow({mode : ‘open’}) 在 WebComponents 中开启。
HTML templates(HTML 模板)slot :template 可以简化生成 dom 元素的操作,我们不再需要 createElement 每一个节点。slot 则和 vue 里面的 slot 类似,只是使用名称不太一样。
内部生命周期函数
connectedCallback : 当 WebComponents 第一次被挂在到 dom 上是触发的钩子,并且只会触发一次。类似 Vue 中的 mounted react 中的 useEffect(() => {}, []),componentDidMount。
disconnectedCallback : 当自定义元素与文档 DOM 断开连接时被调用。
adoptedCallback : 当自定义元素被移动到新文档时被调用。
attributeChangedCallback : 当自定义元素的被监听属性变化时被调用。上述例子中我们监听了 type 的变化,使 Button 组件呈现不同状态。
虽然 WebComponents 有三个要素,但却不是缺一不可的,WebComponents 借助 shadow dom 来实现样式隔离,借助 templates 来简化标签的操作。
在这个例子用我们使用了 slot 传入了俩个标签之间的内容,如果我们想要不使用 slot 传入标签之间的内容怎么办?
我们可以通过 innerHTML 拿到自定义组件之间的内容,然后把这段内容插入到对应节点即可。
组件通信
了解上面这些基本的概念后,我们就可以开发一些简单的组件了,但是如果我们想传入一些复杂的数据类型(对象,数组等)怎么办?我们只传入字符串还可以么?答案是肯定的!
传入复杂数据类型
使用我们上面的 Button,我们不仅要改变状态,而且要想要传入一些配置,我们可以通过传入一个 JSON 字符串
// html
// button.js
class CaiButton extends HTMLElement {
constructor() {
xxx
}
static get observedAttributes() {
return [‘type’, ‘config’] // 监听 config
}
attributeChangedCallback(name, oldValue, newValue) {
if(name === ‘config’) {
newValue = JSON.parse(newValue)
}
this[name] = newValue;
this.render();
}
render() {
}
}
window.customElements.define(‘cai-button’, CaiButton)
})()
这种方式虽然可行但却不是很优雅。
对于使用者说:我用你个组件你还要让我把所有的复杂类型都转换成字符串?
对于开发组件者来说:我为什么要每次都 JSON.parse() 一下?
HTML 中会有很长的数据。
因此我们需要换一个思路,我们上面使用的方式都是 attribute 传值,数据类型只能是字符串,那我们可以不用它传值吗?答案当然也是可以的。和 attribute 形影不离还有我们 js 中的 property,它指的是 dom 属性,是 js 对象并且支持传入复杂数据类型。
// table 组件 demo,以下为伪代码 仅展示思路
table.dataSource = [{ name: ‘xxx’, age: 19 }]
table.columns = [{ title: ”, key: ” }]
这种方式虽然解决上述问题,但是又引出了新的问题 — 自定义组件中没有办法监听到这个属性的变化,那现在我们应该怎么办?或许从一开始是我们的思路就是错的,显然对于数据的响应式变化是我们原生 js 本来就不太具备的能力,我们不应该把使用过的框架的思想过于带入,因此从组件使用的方式上我们需要做出改变,我们不应该过于依赖属性的配置来达到某种效果,因此改造方法如下。
zs 18 ls 18
我们把属于 HTML 原生的能力归还,而是不是采用配置的方式,就解决了这个问题,但是这样同时也决定了我们的组件并不支持太过复杂的能力。
状态的双向绑定
上面讲了数据的单向绑定,组件状态页面也会随之更新,那么我们怎么实现双向绑定呢?
接下来我们封装一个 input 来实现双向绑定。
// js
(function () {
const template = document.createElement(‘template’)
template.innerHTML = `
`
class CaiInput extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({
mode: ‘closed’
})
const content = template.content.cloneNode(true)
this._input = content.querySelector(‘#caiInput’)
this._input.value = this.getAttribute(‘value’)
shadow.appendChild(content)
this._input.addEventListener(“input”, ev => {
const target = ev.target;
const value = target.value;
this.value = value;
this.dispatchEvent(new CustomEvent(“change”, { detail: value }));
});
}
get value() {
return this.getAttribute(“value”);
}
set value(value) {
this.setAttribute(“value”, value);
}
}
window.customElements.define(‘cai-input’, CaiInput)
})()
这样就封装了一个简单双向绑定的 input 组件,代码中 get / set 和 observedAttributes / attributeChangedCallback 前者是监听单个,后者可以监听多个状态改变并做出处理。
这里面核心的一步是我们监听了这个表单的 input 事件,并且在每次触发 input 事件的时候触发 自定义的 change 事件 ,并且把输入的参数回传。
那我们应该怎么使用呢?以 Vue 为例子, Vue 的双向绑定 v-model 其实是一个语法糖, 我们的组件则没有办法使用这个语法糖,与 v-model 不简化写法类似 封装我们自己的组件库
设计目录结构
第一步:要有一个优雅的组价库我们首先要设计一个优雅的目录结构,设计目录结构如下
.
└── cai-ui
├── components // 自定义组件
| ├── Button
| | ├── index.js
| └── …
└── index.js. // 主入口
独立封装
独立封装我们的组件,由于我们组件库中组件的引入,蓝狮注册登陆我们肯定是需要把每个组件封装到单独文件中的。
在我们的 Button/index.js 中写入如下:
(function () {
const template = document.createElement(‘template’)
template.innerHTML = `
class CaiButton extends HTMLElement { constructor() { super() // 其余和上述一样 } static get observedAttributes() { return ['type'] } attributeChangedCallback(name, oldValue, newValue) { this[name] = newValue; this.render(); } render() { this._btn.className =
cai-button ${this._type[this.type]}`
}
}
window.customElements.define(‘cai-button’, CaiButton)
})()
封装到组件到单独的 js 文件中
全部导入和按需导入
支持全部导入,我们通过一个 js 文件全部引入组件
// index.js
import ‘./components/Button/index.js’
import ‘./components/xxx/xxx.js’
按需导入我们只需要导入组件的 js 文件即可如 import ‘cai-ui/components/Button/index.js’
自定义配置主题
支持主题色可配置 我们只需把颜色写成变量即可,改造如下:
(function () {
const template = document.createElement(‘template’)
template.innerHTML = `
`
// 后面省略…
})()
0 Comments