自从大二初次接触前端以来,一直都有记markdown笔记的习惯.又因为掘金最近有一个活动.所以开了这个专栏。我会写一些业务相关的小技巧和前端知识中的重点内容之类的整理一下发上来。难免有点地方不够细致。欢迎大家指教
这篇文章 是我 学习 webcomponet 组件至今的一些 知识点,包括生命周期,属性传递,事件触发,shadow选择器之类。下面的两张截图是 我开发的 基于webcomponent 的 组件库。
有空的朋友可以给我的代码点点star之类的嘿嘿。
废话少说,我们直接开始吧。
1.0 web component
1.0.0 最基本的webcomponent
typescript
// removeNode(button);
import styles from "../css/normal.css" assert {type : 'css'};
class myDiv extends HTMLElement {
// 监听
static get observedAttributes(){
return ["option","name"]
}
constructor() {
super();
this.shadowRoot!
// 这样我们才能够去追加元素
this.attachShadow({ mode: 'open' })
}
// 重要:生命周期方法 开始
connectedCallback() {
console.log("connectedCallback生命周期")
}
attributeChangedCallback(attr:string,oldValue:string,newValue:string){
}
render() {
let nodeTemplate = document.createElement("template")
nodeTemplate.innerHTML = `
<div class="content" >
<div class="title">组件 </div>
<slot name="container"></slot>
</div>
`
this.shadowRoot!.appendChild(nodeTemplate.content)
this.shadowRoot!.adoptedStyleSheets =[styles]
}
}
// 名字必须小写 必须有横线
customElements.define("my-div", myDiv)
1.0.0 生命周期
scss
1.shadow dom
专门操作自定义元素
2.自定义元素
js中 继承 HTMLElement 获得的
有四个生命周期:
connectedCallback() // 挂载的时候
disconnectedCallback() // 卸载的时候
adoptedCallback() // 移动的时候
attributeChangeCallback() // 属性变化时
3.temple
方便定义插槽
1.0.1 helloworld | 生命周期 | shadow dom | host选择器(这个非常重要) | 样式继承
w3c中 有一些 东西可以继承,有一些东西继承不了。如果需要不继承所有属性
可以设置
xml
:host {
all:initial;
}
<!-- 什么需要设置全局 就写在:host里面 -->
:host {
xx
}
<!-- 属性中包含primary 就会生效 -->
:host([theme~='primary']) {
}
xml
html 中
<!-- 重要,最好给自定义元素一个display为block 不然没有宽高 -->
<script src="main.js"></script>
<my-div option="你好">
啊啊啊
</my-div>
javascript
// 1.自定义标签都是用class 的形式去继承
class myDiv extends HTMLElement {
constructor() {
super();
// 这样我们才能够去追加元素
this.attachShadow({ mode: 'open' })
}
// 重要:生命周期方法 开始
connectedCallback() {
console.log("connectedCallback生命周期")
this.render({
option:this.getAttribute("option")
})
}
render(data) {
let { option } = data
let nodeTemplate = document.createElement("template")
nodeTemplate.innerHTML = `
<div class="content" >
<div class="title">${option} </div>
<slot></slot>
</div>
`
let nodeStyles = document.createElement("style")
// shadow dom 的样式绝对隔离
// 重要: :host选择器可以选中根也就是my-div的样式。外面的选择器样式要高于这个
// :host(.active) .content 这一行代码指的是 这个最外层时 active 样式 ,然后得到其中的 .content 的样式
nodeStyles.innerHTML = `
:host(.active) .content{
margin-top:20px;
background:rgba(0,0,0,30%);
}
:host{
display:block
}
.content{
width:100px;
height:100px;
background:rgba(0,0,0,20%)
}
`
this.shadowRoot.appendChild(nodeTemplate.content)
this.shadowRoot.appendChild(nodeStyles)
}
}
// 名字必须小写 必须有横线
customElements.define("my-div", myDiv)
1.0.2 shadowRoot | 选择shadowdom里面元素
dart
// 重要:选择shadow 的 元素 :二次selector+shadowRoot
// console.log(document.querySelector("my-div").shadowRoot.querySelector(".content"))
// 重要:向着下面找。调用父方法 selector.方法
// 子方法就 二次selector+shadowRoot.方法
// 往回找 方法 可以 在子元素上 子元素.getRootNode().host.方法. 单单子元素.getRootNode() 那么就是拿到了shadow 元素
console.log(document.querySelector("my-div").borderAdd())
document.querySelector("my-div").setAttribute("option","改编")
1.0.3 调用方法 | 元素.方法
scala
class myDiv extends HTMLElement 里面添加这个方法,然后只要选中这个元素就可以调用这个方法
类似 docuemnt.query("").borderAdd()
类似 docuemnt.query("").shadowdom.querySelector().borderAdd()
borderAdd(){
console.log("borderadd")
this.shadowRoot.querySelector(".content").style.border="3px solid green"
}
1.0.4 attributeChangedCallback | observedAttributes| 监听属性变化
scala
class myDiv extends HTMLElement 里面
static get observedAttributes(){
return ["option"]
}
attributeChangedCallback(attr,oldValue,newValue){
if(oldValue){
switch (attr){
case "option":
this.shadowRoot.querySelector(".title").textContent = newValue
}
}
console.log("arrributeChangeCallback",attr,oldValue,newValue)
}
然后在html上面
<my-div option="你好">
啊啊啊
</my-div>
1.0.4 slot 插槽 | ::slotted 选择器 | assignedElements
这玩意外部样式能够影响里面
ini
--1.html 调用
<my-div option="你好">
<div slot="tab">
tab
</div>
</my-div>
ini
--2.js里面
this.shadowRoot.innerHTML =`
<div>
<slot name="tab"></slot>
</div>
`
css
--3.如果要选择里面的元素 selectorAll 不起作用
那么 this.tabs = this.shadowRoot.querySelector(`slot[name="tab"]`).assignedElements({flatten:true})
this.tabs = this.shadowRoot.querySelector(`slot[name="tab"]`).assignedElements({flatten:true})
--4. css 选择器可以 像这样 选择
::slotted([slot="container"]){
display:none
}
::slotted(.active){
display:block
}
1.0.5 composedPath | 判断点击位置
javascript
document.addEventListener("click",(e)=>{
// 重要:冒泡的顺序,通过这个可以判断有没有在鼠标内部进行点击
if(e.composedPath().includes(this)){
console.log("点击了里面")
}
})
1.0.6 继承
css
一般来说影子属性的值是继承不到的,因此我们可以用
:host{
--border-color: #6C63FF
}
--border-color: #6C63FF
然后 var(--border-color)
不想基层
1.0.7 :not(:defined) | 防止闪烁
scss
/* 直到影子dom的dom结构被添加之后才会出现,非常有用 */
:not(:defined){
display: block;
opacity: 0;
transition: all .3s ease;
}
这个选择器确实不得了 js中 居然能够延后执行
setTimeout(()=>{
this.shadowRoot.appendChild(nodeTemplate.content)
this.shadowRoot.appendChild(nodeStyles)
},2000)
scss
完整示例
// 1.自定义标签都是用class 的形式去继承
class myDiv extends HTMLElement {
// 监听
static get observedAttributes(){
return ["option"]
}
constructor() {
super();
// 这样我们才能够去追加元素
this.attachShadow({ mode: 'open' })
}
// 重要:生命周期方法 开始
connectedCallback() {
console.log("connectedCallback生命周期")
// 获取元素
// console.log(this.shadowRoot.querySelector(".content"))
// 获取属性
// console.log( this.getAttribute("data-option"))
this.render({
option:this.getAttribute("option")
})
document.addEventListener("click",(e)=>{
// 重要:冒泡的顺序,通过这个可以判断有没有在鼠标内部进行点击
if(e.composedPath().includes(this)){
console.log("点击了里面")
}
})
}
// 重要:生命周期方法 重新渲染 .甚至还是第一次进行渲染,比connect还快
// 会重新渲染 connectCallback
attributeChangedCallback(attr,oldValue,newValue){
if(oldValue){
switch (attr){
case "option":
this.shadowRoot.querySelector(".title").textContent = newValue
}
}
console.log("arrributeChangeCallback",attr,oldValue,newValue)
}
borderAdd(){
console.log("borderadd")
this.shadowRoot.querySelector(".content").style.border="3px solid green"
}
render(data) {
let { option } = data
// console.log()
let nodeTemplate = document.createElement("template")
nodeTemplate.innerHTML = `
<div class="content" >
<div class="title">${option} </div>
<slot name="container"></slot>
</div>
`
let nodeStyles = document.createElement("style")
// shadow dom 的样式绝对隔离
// 重要: :host选择器可以选中根也就是my-div的样式。外面的选择器样式要高于这个
nodeStyles.innerHTML = `
:host(.active) .content{
margin-top:20px;
background:rgba(0,0,0,30%);
}
:host{
display:block
}
.content{
width:100px;
height:100px;
background:rgba(0,0,0,20%)
}
::slotted([slot="container"]){
display:none
}
::slotted(.active){
display:block
}
`
setTimeout(()=>{
this.shadowRoot.appendChild(nodeTemplate.content)
this.shadowRoot.appendChild(nodeStyles)
},1500)
}
}
// 名字必须小写 必须有横线
customElements.define("my-div", myDiv)
1.0.8 组件传值
javascript
目前最好的方法应该是通过customEvent 来做
window.addEventListener("test",(e)=>{
console.log("出现吧",e.detail)
})
let event = new CustomEvent('test',{
detail:{
title:"我是标题哦"
}
})
// 最后还跟着是否能够冒泡和是否阻止默认操作
window.dispatchEvent(
event,true,false
)
1.0.9 组件化css示例
css
html 中
<webzen-button theme="primary"></webzen-button>
~= 的意思是含有
css
:host([theme~='primary']) button{
color:red
}
:host([theme~='primary'][theme~='small']) button{
color:red
}
1.0.10 get | set | 修改监听属性值
scala
class ElfinTransition extends HTMLElement {
get name() {
return this.getAttribute('name');
}
set name(value) {
if (value) {
this.setAttribute('name', value);
} else {
this.removeAttribute('value');
}
}
}
getAttribute 和 setAttribute 触发
1.0.12 css modules
python
slot.js 中
import styles from "./slot.css" assert {type : 'css'}
this.shadowRoot.adoptedStyleSheets =[styles]
1.0.13 元素动画和元素移除
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
你好
<script>
console.log(top)
let input = document.createElement("input")
input.setAttribute("type", "text")
document.body.after(input)
function slide(elem, direction) {
const keyframe = [
{ transform: 'translateX(-50%)', opacity: 0 },
{ transform: 'translateX(0%)', opacity: 1 }
]
return elem.animate(keyframe,{
fill:"forwards",
duration:300,
easing:"ease-in-out"
})
}
let event= slide(input)
// 这里可以移除掉这个元素
event.onfinish=(e)=>{
console.log(e,this)
}
</script>
</body>
</html>
1.0.14 基类
javascript
const randomID = () => {
return Math.random().toString(36).substring(2, 8);
};
export default class wz_base extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.id = randomID();
if (typeof globalThis.components !== 'object') {
globalThis.components = {};
}
Object.defineProperty(globalThis.components, this.id, {
value: this,
configurable: true,
});
}
disconnectedCallback() {
delete globalThis.components[this.id];
}
}
javascript
使用
// import events from "inquirer/lib/utils/events";
import base from '../base/index.js';
import styles from './index.css' assert { type: 'css' };
class TimePicker extends base {
constructor() {
super();
}
static get observedAttributes() {
return ["onValue"];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, oldValue, newValue);
switch (name){
case "onValue":
console.log(new Function(newValue))
}
}
connectedCallback() {
console.log("组件id:",this.id,globalThis.components[this.id])
console.log(this.getAttribute("onValue"))
console.log(new Function(this.getAttribute("onValue"))())
let nodeTemplate = document.createElement('template');
nodeTemplate.innerHTML = `
<label for="time-picker">选择时间:</label>
<input type="time" id="time-picker">
`;
const template = nodeTemplate.content;
this.shadowRoot.appendChild(template.cloneNode(true));
this.input = this.shadowRoot.querySelector('#time-picker');
this.input.addEventListener('change', this.handleChange.bind(this));
// this.input.addEventListener('change', eval(this.getAttribute("onValue")(event)));
this.shadowRoot.adoptedStyleSheets = [styles];
}
handleChange(event) {
globalThis.eventbus.emit('测试', event.target.value);
console.log(
this.getAttribute('onValue')
);
this.dispatchEvent(
new CustomEvent('timeChanged', { detail: event.target.value })
);
}
}
customElements.define('time-picker', TimePicker);
1.0.?? 完整示例
html
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="slot.js"></script>
<style>
#test{
background: red;
}
/* 直到影子dom的dom结构被添加之后才会出现,非常有用 */
:not(:defined){
display: block;
opacity: 0;
transition: all .3s ease;
}
</style>
</head>
<body>
<my-div option="测试">
<div slot="container" id="test" >
hello
</div>
<div slot="container" id="test" class="active">
tab1
</div>
<div slot="container" id="test" class="active">
tab2
</div>
<div slot="container" id="test" class="active">
tab3
</div>
</my-div>
<script>
customElements.whenDefined('my-div').then(() => {
console.log("开始define")
});
setTimeout(() => {
console.log(document.querySelector("my-div").shadowRoot.querySelector(`slot[name="container"]`).assignedElements({ flatten: true }))
}, 3000);
</script>
</body>
</html>
slot.js
scss
// 1.自定义标签都是用class 的形式去继承
class myDiv extends HTMLElement {
// 监听
static get observedAttributes(){
return ["option"]
}
constructor() {
super();
// 这样我们才能够去追加元素
this.attachShadow({ mode: 'open' })
}
// 重要:生命周期方法 开始
connectedCallback() {
console.log("connectedCallback生命周期")
// 获取元素
// console.log(this.shadowRoot.querySelector(".content"))
// 获取属性
// console.log( this.getAttribute("data-option"))
this.render({
option:this.getAttribute("option")
})
document.addEventListener("click",(e)=>{
// 重要:冒泡的顺序,通过这个可以判断有没有在鼠标内部进行点击
if(e.composedPath().includes(this)){
console.log("点击了里面")
}
})
// this.shadowRoot.querySelector(".content").addEventListener(("click"),()=>{
// // window.dispatchEvent
// })
}
// 重要:生命周期方法 重新渲染 .甚至还是第一次进行渲染,比connect还快
// 会重新渲染 connectCallback
attributeChangedCallback(attr,oldValue,newValue){
if(oldValue){
switch (attr){
case "option":
this.shadowRoot.querySelector(".title").textContent = newValue
}
}
console.log("arrributeChangeCallback",attr,oldValue,newValue)
}
borderAdd(){
console.log("borderadd")
this.shadowRoot.querySelector(".content").style.border="3px solid green"
}
render(data) {
let { option } = data
// console.log()
let nodeTemplate = document.createElement("template")
nodeTemplate.innerHTML = `
<div class="content" >
<div class="title">${option} </div>
<slot name="container"></slot>
</div>
`
let nodeStyles = document.createElement("style")
// shadow dom 的样式绝对隔离
// 重要: :host选择器可以选中根也就是my-div的样式。外面的选择器样式要高于这个
nodeStyles.innerHTML = `
:host(.active) .content{
margin-top:20px;
background:rgba(0,0,0,30%);
}
:host{
display:block
}
.content{
width:100px;
height:100px;
background:rgba(0,0,0,20%)
}
::slotted([slot="container"]){
display:none
}
::slotted(.active){
display:block
}
`
setTimeout(()=>{
this.shadowRoot.appendChild(nodeTemplate.content)
this.shadowRoot.appendChild(nodeStyles)
},1500)
}
}
// 名字必须小写 必须有横线
customElements.define("my-div", myDiv)
最后的一些絮叨
其实我在公司有一段时间一直在做 webcomponet之类的框架。因为公司业务做的是b端的业务所以一直没有问题。但是c端上面webcompoent 没有 corejs 一样 的 垫片机制。导致很多东西 只能用 react 或者 vue 在包装一层。。。因此目前来说,我们暂时舍弃掉了webcompoent