一文保姆式大白话讲清楚Web Component原理、使用、通信方式、应用场景等问题,如果看了不明白,请挂直接脑科

一文保姆式大白话讲清楚Web Component原理、使用、通信方式、应用场景等问题,如果看了不明白,请挂直接脑科

文章目录

一文保姆式大白话讲清楚Web Component原理、使用、通信方式、应用场景等问题,如果看了不明白,请挂直接脑科

先理解概念

  • Web Components,乍一听,可能有点陌生,不慌,拆一下,就是Web+Component
  • 先看components,这是什么,组件,那这很好理解,作为一个前端开发者,尤其是管用框架的开发者,像Vue,React这些,组件是不可或缺的东西
  • 当然组件又可分为业务组件和通用组件,业务组件一般会多少关联一些框架本身的东西,比如在vue里面,我们通过SFC来抽离复杂业务,实现业务拆分和逻辑复用
  • 而通用组件一般又可分为框架通用组件和跨框架通用组件,很好理解,框架通用组件就是只能在某个框架下使用,比如在Vue框架下,有很多的通用组件,也就是Vue的生态
  • 而跨框架通用组件实现了跟框架的解耦,不管你是什么框架都能使用,那怎么才能做到不管你是什么框架才能都能使用呢,那就得跟框架无关,那怎么跟框架无关呢,用原生,因为你不管是哪个框架,用的什么原理,所有原生的东西你一定都会兼容支持
  • 那前端原生的东西里面有组件这个东西么
  • 问得好,真有,而且就是Web Components
  • 这东西是个啥呢,别着急
  • 且听我分析
  • 我们还是回到业务组件来说,在VUE框架下,我们如何封装一个SFC,很简单,就是创建一个.vue的文件,然后处理template,script,和style三部分逻辑
  • 比如我们现在创建一个业务组件叫MyButton,封装各种场景的button样式,比如一般默认,警告,成功等
  • 上代码
// 子组件MyButton.vue <template> <button :class="type">{{ name }}</button> </template> <script> export default{ name:'MyButton', props:{ type:{ type:String, default:'info', }, name:{ type:String, default:'提示', } }, data(){ return{ } } } </script> <style scoped> .info{ /**/ } .warning{ /**/ } </style> 
  • 然后我们在父组件里面引用子组件
/*Parent.vue*/ <template> <div> <my-button type="info" name="提示"></my-button> </div> </template> <script> import MyButton from './components/MyButton.vue' export default{ name:'APP', components:{ MyButton }, data(){ return{ } } } </script> <style> </style> 
  • 到这,父组件引用了子组件MyButton,然后,父组件最终被浏览器渲染
  • 你会发现,MyButton被渲染成了button,也就是说我们在父组件里面引用了MyButton,但是最终浏览器渲染的时候还是把MyButton解体,渲染成了button
  • 为什么呢,因为浏览器只会渲染HTML存在的元素,MyButton就不是一个HTML,所以他必须经过编译,转化为button然后再渲染
  • 也就是说,我们只是在编码逻辑上存在MyButton,渲染逻辑上并不存在MyButton
  • 这你就想,那我是不是多余写MyButton,虽然代码层实现了封装复用,但是最终页面上没体现出来
  • 我现在就想让浏览器里面渲染MyButton,而不是button,怎么办呢,那就需要把MyButton变成HTML,那怎么才能变成HTML呢
  • 问得好,还真有,Web Component就能做到,怎么做到,MyButton现在不是非HTML,那我就声明他是,怎么声明,自定义
  • 怎么自定义,使用 Custom Elements

关键来了,渲染之后,我们打开F12,看看elements(元素)一栏里面,Mybutton被渲染成了啥

在这里插入图片描述

Custom Elements

  • 这是啥玩意呢,一句话,这是一个可以让你注册一个自定义HTML么的方法
  • 上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title></head><body><!-- 这里使用自定义组件 --><my-button></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<button>提示</button>`}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看到没有,这时候MyButton直接被渲染了,而不是拆解为button,为了验证这一逻辑,我们在Button里面再加入些其他元素
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title></head><body><!-- 这里使用自定义组件 --><my-button></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<div> <span>我是MyButton组件的span标签</span> <button>提示</button> </div>`}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看到了吧,无论我们在MyButton内添加多少元素,他都会被MyButton封装然后整体渲染不拆解,
  • 这个就很牛逼了啊,相当于我们可以自己创建很多HTML,然后他会被当做一个整体组件直接渲染
  • 那么问题来了,既然是组件,干过Vue的都知道,组件是有生命周期的,最起码有挂载和销毁,这个自定义HTML有么
  • 别慌,真有
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<div> <span>我是MyButton组件的span标签</span> <button>提示</button> </div>`}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)this.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 可以看到,customElements有两个生命周期,connectedCallback,在组件挂载到页面上时触发,disconnectedCallback在页面上移除时触发
  • 现在又来一个问题,既然是组件,那么肯定是可以被复用的,我们尝试一下
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<div> <span>我是MyButton组件的span标签</span> <button>提示</button> </div>`}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)this.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
在这里插入图片描述
  • 可以看到,复用也没问题,观察仔细的小伙伴肯定发现了,每一次组件的复用其实都是在创建一个MyButton类的实例,所以this之前当前组件,我们就可以通过this去获得组件和组件内部的元素和属性
  • 现在又来一个问题,虽然我们现在能封装原生的组件了,但是我们要问一个问题,封装组件的目的是什么
  • 有很多,但其中有一项是为了样式隔离,防止全局污染,也就是说我们希望组件内的样式是封闭的,不受任何外部样式的渲染,不然我本来定义了一个红色的button,但由于全局样式对button定义了蓝色,最后呈现了一个蓝色的button,这不是我想要的
  • 那customElements能实现样式隔离么,不重要,上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow;}</style></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<div>/*我们在组件内设置div的背景色为红色*/ <span>我是MyButton组件的span标签</span> <button>提示</button> </div>`}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)this.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 呀哈,这么一看好像是样式隔离了昂,别高兴地太早,
  • 与其说是发生了隔离,不如说是行内样式的优先级高于style
  • 为什么这么说,上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{width:100px;height: 100px;background-color: yellow;}</style></head><body><divstyle="background-color: red;"></div><script></script></body></html>
在这里插入图片描述
  • 明白没,现在组件的背景显示红色不是因为样式发生了隔离,是他本来就是应该是红色,应为行内样式的优先级高
  • 那怎么验证呢,我们提高全局样式的优先级试试,怎么提高,!impportant
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.innerHTML=`<div>/*我们在组件内设置div的背景色为红色*/ <span>我是MyButton组件的span标签</span> <button>提示</button> </div>`}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)this.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看见没,样式完全没隔离,那这可不是我们想要的,我们想要的是样式的绝对隔离
  • 有没有办法呢
  • 有,用Shadow DOM

Shadow DOM

  • 啥是个ShadowDOm,Shadow就是影子,影子DOM,也就是说,你最终在页面上看到的只是个影子DOM,而不是真实的DOM,真实ODM去哪了呢
  • 简单点说,真实的DOM被一个透明的玻璃盒子罩住了,你在页面上只能透过玻璃盒子看到DOM,但是你操作不多,自然也就无法影响到
  • 所以我们给DOM罩一个玻璃罩
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()const shadow=this.attachShadow({mode:'open'})const component=document.createElement('div') component.style.backgroundColor='red' component.innerHTML=`/*我们在组件内设置div的背景色为红色*/ <span>我是MyButton组件的span标签</span> <button>提示</button> ` shadow.appendChild(component)}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)this.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 意思我们通过this.querySelector(‘button’)获取button时获取到的是空,啊,为啥嘞,别着急,听哥哥给你分析
  • 现在MyButton在主DOM里面,this还是指向的MyButton,这没问题
  • 但是MyButton里面的button现在不直接在MyButton里面了,它在MyButton里面的玻璃罩的里面
  • 多说无意,上代码

所以你直接通过this肯定找不到button。你得通过this.玻璃罩.button才能找到

在这里插入图片描述

看到这看似没啥问题吧,别急,再看看控制台打印了啥

在这里插入图片描述
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.shadow=this.attachShadow({mode:'open'})const component=document.createElement('div') component.style.backgroundColor='red' component.innerHTML=`/*我们在组件内设置div的背景色为红色*/ <span>我是MyButton组件的span标签</span> <button>提示</button> `this.shadow.appendChild(component)}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)/*通过shadow也就是玻璃罩去哪shadow内部的元素*/this.shadow.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.shadow.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看到了吧,找到了button,点击的时候也打印了
  • 到这,你又觉得完美的不行了
  • 不,还不完美,为啥不完美呢,你看我们上面写MyButton里面的代码是怎么写的,都是通过/``/进行的字符串拼接
  • 这样写不仅没有代码提示,还很难处理格式,就很容易混乱
  • 那有没有可能这样,就是我还是能像正常写HTML代码那样去写MyButton
  • 你真机智,还真可以,那就是template

template

  • 这个词可不陌生,vue的SFC里面不都是template么,翻译就是模板
  • 是的,就是模板,那怎么用呢
  • 别着急,上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 定义模板,不会自动渲染,需要克隆后才会渲染 --><templateid="myButtonTemplate"><style>.bg{background-color: red;}</style><divclass="bg"><span>我是MyButton组件的span标签</span><buttonclass="info">提示</button></div></template><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.shadow=this.attachShadow({mode:'open'})/*我们不再通过字符串去定义MyButton里面的内容,而是把tempalte里面的内容克隆过来*/const template=document.getElementById('myButtonTemplate')const templateContent=template.content.cloneNode(true)// const component=document.createElement('div')// component.style.backgroundColor='red'// component.innerHTML=`/*我们在组件内设置div的背景色为红色*/// <span>我是MyButton组件的span标签</span>// <button>提示</button>// `this.shadow.appendChild(templateContent)}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)/*通过shadow也就是玻璃罩去哪shadow内部的元素*/this.shadow.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.shadow.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看明白了么亲,
  • template就是允许你在页面内直接像写HTML那样,定义一个模板
  • 但是这个模板不直接被渲染,为啥呢,因为我现在只是定义啊,很多业务逻辑还没写完呢,你渲染个毛啊
  • 那要怎么渲染嗯,很简单,你需要渲染的时候就把我克隆过去,然后去渲染就行了
  • 那有人就又要问了,为什么要克隆呢,不直接使用template.content呢
  • 问得很好,我们先不说出答案,先试一下,看会怎么样
  • 上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 定义模板,不会自动渲染,需要克隆后才会渲染 --><templateid="myButtonTemplate"><style>.bg{background-color: red;}</style><divclass="bg"><span>我是MyButton组件的span标签</span><buttonclass="info">提示</button></div><script> console.log('111111111')</script></template><!-- 这里使用自定义组件 --><my-buttontype="info"></my-button><my-buttontype="warning"></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.shadow=this.attachShadow({mode:'open'})/*我们不再通过字符串去定义MyButton里面的内容,而是把tempalte里面的内容克隆过来*/const template=document.getElementById('myButtonTemplate')const templateContent=template.content//.cloneNode(true)/*不使用cloneNode*/// const component=document.createElement('div')// component.style.backgroundColor='red'// component.innerHTML=`/*我们在组件内设置div的背景色为红色*/// <span>我是MyButton组件的span标签</span>// <button>提示</button>// `this.shadow.appendChild(templateContent)}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)/*通过shadow也就是玻璃罩去哪shadow内部的元素*/this.shadow.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.shadow.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • 看出设问题没有,第一个组件my-button的实例是完成的,第二个里面的DOM丢失了
  • 有点意思吧,这是为什么呢
  • 那得说说tempalte.content的本质,
  • tempalte.content返回的其实是一个DocumentFragement,Fragement应该都知道吧,不知道的去查字典,去抽自己的脸,啥都不知道怎么成长为像我一样的架构师
  • 这玩意有三个特性,第一个很普通,可就是容器特性,那肯定么,既然是Fragment,作为容器,就可以存放HTML节点,但是他本身不会渲染到页面上,就是可以理解为我是个盒子,里面装一堆HTML元素
  • 第二个就很奇葩了,移动特性。啥意思,就是你跟我要的时候HTML元素的时候,我不是把盒子的里面的元素复制一份给你,而是直接倒出来给你,我自己就没有了,我靠,好嘛,很大方
  • 这也就是上面为什么第二个MyButton里面没东西了,因为我们创建了两个MyButton实例,执行第一个的时候,通过template.content拿到了DOM渲染,但此时template.content已经就剩一个空盒子了,第二个去拿的时候,拿了个寂寞,所以没有东西
  • 第三个,共享特性,就是所有访问template.content的代码,拿到的都是同一个盒子,也就是同一个DocuemntFragement,而不是副本,相比这有些弟弟又开始联想对象拷贝啥的了,好着了,说明知识扎实
  • 所以,需要cloneNode
  • 那cloneNode干了个啥呢,既然你是谁要就直接给自己不留,那我的作用是克隆一样的DocumenFragmen给你,我自己还留着,这样别人还可以来cloneNode
  • 至于他的参数cloneNode(true),既然是克隆,那参数肯定就是深浅的问题么,true就是深拷贝,里面的子节点统统拷贝,false就是只克隆盒子,也就是DocuemntFragment,那不是克隆了个寂寞么,所以,让他true
  • -到这,你又觉得完美了
  • 不,还不完美,为啥呢,干过vue的都知道
  • 我们在搞SFC的时候,往往会加一个slot,干啥呢,就是在引用组件的时候提搞一个插槽,让组件内容的自定义
  • 说白点就是我定义组件的时候并不定义他具体是什么样,你给我什么东西我就是什么样,这就很牛逼啊,高度自由化啊
  • 在vue里面,我们可以通过匿名插槽,具名插槽,默认插槽等传递自定义内容,这里我们可以使用slot么,乖乖,你真棒,完全可以的
  • 二话不说,上代码
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 定义模板,不会自动渲染,需要克隆后才会渲染 --><templateid="myButtonTemplate"><style>.bg{background-color: red;}</style><divclass="bg"><span>我是MyButton组件的span标签</span><buttonclass="info">提示</button><div><!-- 具名插槽 --><slotname="title"></slot></div><div><!-- 默认插槽 --><slotname="img"><span>如果不传内容,我就会显示</span></slot></div><div><!-- 匿名插槽 --><slot></slot></div></div><script> console.log('111111111')</script></template><!-- 这里使用自定义组件 --><my-buttontype="info"><spanslot="title">我是组件1传递具名插槽title过来的</span><spanslot="img">我是组件1传递默认插槽img过来的</span><span>我是组件1传递匿名插槽过来的</span></my-button><my-buttontype="warning"><spanslot="title">我是组件2传递具名插槽title过来的</span><span>我是组件2传递匿名插槽过来的</span></my-button><script>// 定义自定义组件classMyButtonextendsHTMLElement{constructor(){super()this.shadow=this.attachShadow({mode:'open'})/*我们不再通过字符串去定义MyButton里面的内容,而是把tempalte里面的内容克隆过来*/const template=document.getElementById('myButtonTemplate')const templateContent=template.content.cloneNode(true)/*不使用cloneNode*/// const component=document.createElement('div')// component.style.backgroundColor='red'// component.innerHTML=`/*我们在组件内设置div的背景色为红色*/// <span>我是MyButton组件的span标签</span>// <button>提示</button>// `this.shadow.appendChild(templateContent)}//生命周期,组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成') console.log(this)/*通过shadow也就是玻璃罩去哪shadow内部的元素*/this.shadow.querySelector('button').addEventListener('click',()=>{ console.log(`click ${this.getAttribute('type')} button`)})}//生命周期,组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除')this.shadow.querySelector('button').removeEventListener('click',null)}}//注册自定义元素,也就是把MyButton注册为my-button,这样就相当于注册了一个HTML元素,名字叫my-button,可以直接被浏览器渲染 customElements.define('my-button',MyButton)</script></body></html>
在这里插入图片描述
  • slot完美支持
  • 到这里,哥哥们,咱们讲了customElements、shadowDOM、template、slot,这时候你再问我,啥是个Web Compoents,我会告诉你,师爷,这他么的就是特么的Web Components
  • 他不是一个单一的技术,而是上述四个原生浏览器的特性组合,合理应用,形成的组件生态
  • 明白了不,那你又问了,人家vue啥的组件都是SFC,咱们都写在主页面里么了,密密麻麻一大堆,咱们能抽离SFC么
  • 这话问的,那必须行啊,咋也是组件啊

SFC化

  • 很简单,我们组件的流程单独封装为一个js模块
  • 直接上代码,新建一个MyButton.js,把逻辑抽离一下
/*MyButton.js*/// 1. 修正变量名拼写错误:tempalte → templateconst template =` <style> .bg{ background-color: red; } .info { cursor: pointer; } </style> <div> <span>我是MyButton组件的span标签</span> <button>提示</button> <div> <!-- 具名插槽 --> <slot name="title"></slot> </div> <div> <!-- 默认插槽 --> <slot name="img"> <span>如果不传内容,我就会显示</span> </slot> </div> <div> <!-- 匿名插槽 --> <slot></slot> </div> </div> <!-- 移除无效的script标签:template内的script不会执行 --> `;classMyButtonextendsHTMLElement{constructor(){super();this.shadow =this.attachShadow({mode:'open'});// 2. 创建template元素并赋值模板内容const el = document.createElement('template'); el.innerHTML = template;// 用修正后的变量名 console.log(el)// 3. 确保content存在(template元素的content永远是DocumentFragment,不会undefined)const templateContent = el.content.cloneNode(true);this.shadow.appendChild(templateContent);// 4. 抽离事件函数(方便移除事件)this.handleButtonClick=()=>{ console.log(`click ${this.getAttribute('type')} button`);};}// 生命周期:组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成'); console.log(this);// 5. 增加判空逻辑,避免找不到button时报错const btn =this.shadow.querySelector('button');if(btn){ btn.addEventListener('click',this.handleButtonClick);}}// 生命周期:组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除');// 6. 修正removeEventListener:传入相同的函数引用,且判空const btn =this.shadow.querySelector('button');if(btn){ btn.removeEventListener('click',this.handleButtonClick);}}}// 注册自定义元素 customElements.define('my-button', MyButton);exportdefault MyButton 
  • 在主页面内引用
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- 这里使用自定义组件 --><my-buttontype="info"><spanslot="title">我是组件1传递具名插槽title过来的</span><spanslot="img">我是组件1传递默认插槽img过来的</span><span>我是组件1传递匿名插槽过来的</span></my-button><my-buttontype="warning"><spanslot="title">我是组件2传递具名插槽title过来的</span><span>我是组件2传递匿名插槽过来的</span></my-button><scripttype="module"src="./MyButton.js"></script></body></html>
  • 这时候会发现报下面错误,原因是我们直接双击打开的html文件,所以跟js文件之间产生了跨域
在这里插入图片描述
  • 我们安装个本地http-server
  • npm install -g http-server
  • 这下你是不是觉得又完美了,组件实现了模块化封装,不,还不够完美
  • 哪里不够完美呢,既然是组件,也实现了组件的模块化封装
  • 那组件之间着呢么通信呢
  • 我们在vue里面,组件之间的通信可以有props,emit,EventBus,vuex等等
  • web Components组件之间又该怎么通信呢

然后点击就可以运行了

在这里插入图片描述

切换到html目录下,输入http-server启动服务

在这里插入图片描述

web components组件之间的通信

  • 既然现在是组件了,那就涉及到组件之间的通讯,而组建之间的通讯无非是三类,
  • 隔代类:父子,爷孙,一直再往下
  • 平辈类:兄弟
  • 无亲类:无关联组件
  • 就这三类,再没了,而这三类又分别适用于什么样的通信方式呢,别着急
  • 我们得先了解他有什么样的通信方式是不是

组件通信之一——CustomEvent,自定义事件

  • 这个很好理解,就跟我在vue里面在子组件通过emit去触发一个自定义事件,父组件可以捕获到自定义事件,拿到参数,完成业务处理一样
  • web components也支持我们自定义一个事件,并在其中一个组件内触发,另外一个组件内捕获,并携带参数
  • 通过new CustomEvent(name,options)去自定义一个事件
  • 然后通过document.dispatchEvent(eventName)派发
  • 最后通过document.addEventListener(eventName)捕获到事件,拿到参数,处理业务逻辑
  • 话不多说,还是上代码
  • 我们把在MyButton基础上,再创建一个组件MyCount,通过点击MyButton里面的button,给MyCount里面的计数器进行+1操作,实现MyButton和MyCount通讯
/** * MyButton.js */// 1. 修正变量名拼写错误:tempalte → templateconst template =` <style> .bg{ background-color: red; } .info { cursor: pointer; } </style> <div> <span>我是MyButton组件的span标签</span> <button>提示</button> <div> <!-- 具名插槽 --> <slot name="title"></slot> </div> <div> <!-- 默认插槽 --> <slot name="img"> <span>如果不传内容,我就会显示</span> </slot> </div> <div> <!-- 匿名插槽 --> <slot></slot> </div> </div> <!-- 移除无效的script标签:template内的script不会执行 --> `;classMyButtonextendsHTMLElement{constructor(){super();this.shadow =this.attachShadow({mode:'open'});// 2. 创建template元素并赋值模板内容const el = document.createElement('template'); el.innerHTML = template;// 用修正后的变量名 console.log(el)// 3. 确保content存在(template元素的content永远是DocumentFragment,不会undefined)const templateContent = el.content.cloneNode(true);this.shadow.appendChild(templateContent);// // 4. 抽离事件函数(方便移除事件)// this.handleButtonClick = () => {// console.log(`click ${this.getAttribute('type')} button`);// };}// 生命周期:组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成'); console.log(this);// 5. 增加判空逻辑,避免找不到button时报错const btn =this.shadow.querySelector('button');if(btn){//创建自定义事件,通过detail传递参数 btn.addEventListener('click',()=>{/** * 创建自定义事件并进行派发-start */const incrementEvent =newCustomEvent('count-increment',{bubbles:true,// 允许事件冒泡(可选,事件总线可省略)cancelable:true,detail:{step:1}// 传递的参数:每次增加1});// 全局派发事件(事件总线核心:基于document) document.dispatchEvent(incrementEvent);/** * -end */ console.log('按钮点击,触发计数增加事件')});}}// 生命周期:组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除');// 6. 修正removeEventListener:传入相同的函数引用,且判空const btn =this.shadow.querySelector('button');if(btn){ btn.removeEventListener('click',this.handleButtonClick);}}}// 注册自定义元素 customElements.define('my-button', MyButton);exportdefault MyButton 
  • MyCount组件
/** * MyCount.js */// MyCount.js console.log('222222')classMyCountextendsHTMLElement{constructor(){super(); console.log('myCOunt')// 1. 初始化计数(私有属性,避免全局污染)this.count =0;// 2. 创建 Shadow DOMthis.shadow =this.attachShadow({mode:'open'});// 3. 组件模板(显示计数)this.render();// 初始化渲染}// 核心:渲染/更新视图的方法render(){const templateStr =` <style> .count-box { margin: 16px 0; padding: 16px; border: 1px solid #eee; border-radius: 4px; font-size: 18px; } .count-num { color: #42b983; font-weight: bold; } </style> <div> 当前计数:<span>${this.count}</span> </div> `;// 清空原有内容,重新渲染this.shadow.innerHTML ='';const tpl = document.createElement('template'); tpl.innerHTML = templateStr;this.shadow.appendChild(tpl.content.cloneNode(true));}// 组件挂载到 DOM 时触发:监听全局事件connectedCallback(){// 监听 MyButton 派发的 count-increment 事件 console.log('444')this.countHandler=(e)=>{// 从事件 detail 中获取步长,更新计数this.count += e.detail.step;// 重新渲染视图this.render(); console.log('计数更新:',this.count);};// 绑定全局事件监听 document.addEventListener('count-increment',this.countHandler);}// 组件移除时:销毁事件监听(关键!避免内存泄漏)disconnectedCallback(){ document.removeEventListener('count-increment',this.countHandler);}}// 注册自定义元素 customElements.define('my-count', MyCount);exportdefault MyCount 
  • 主页面内,分别引用两个组件
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><my-count></my-count><!-- 这里使用自定义组件 --><my-buttontype="info"><spanslot="title">我是组件1传递具名插槽title过来的</span><spanslot="img">我是组件1传递默认插槽img过来的</span><span>我是组件1传递匿名插槽过来的</span></my-button><scripttype="module"src='./MyButton.js'></script><scripttype="module"src="./MyCount.js"></script></body></html>
  • 成功实现,这说明利用CustomEvent通是没有任何问题的,当然我们现在演示的两个非关联组件的通讯,这种方式也可用于隔代组件通讯和兄弟组件通讯,也就是说自定义事件通讯方式是万能的

然后点击MyButton组件的提示按钮,看看MyCount里面的计数器是否+1

在这里插入图片描述

这时候我们看到MyButton组件和MyCount都正常显示

在这里插入图片描述

组件通信之一——属性监听

  • 啥意思,既然是属性监听,意思就是属性变化了,能知道
  • 那问题来了,谁改的,改了谁,改了什么,又是谁知道了,怎么知道的
  • 问的好,这些问题清楚了,也就明白属性监听是什么回事了
  • 谁改的,父组件
  • 改了谁,子组件
  • 改了什么,子组件属性
  • 谁知道了,子组件
  • 怎么知道的,通过observedAttributes声明监听,然后通过attributeChangedCallback触发
  • 啥意思,就是子组件在实例化时通过observedAttributes进行声明一些需要监听的属性,比如name等,如果父组件改变了对应的子组件的name,那就会触发子组件的attributeChangedCallback,同时携带新的name值回来
  • 这不就实现了父组件向子组件传递值么
  • 我们来验证
  • 我们改造一下MyButton,设置两种类型的按钮,除了现在的提示info,在定义一个警示warning,然后在主页面里面增加一个button,通过点击button,改变子组件的type属性,切换子组件的按钮状态
  • 话不多说,上代码
/** * MyButton.js */// 1. 修正变量名拼写错误:tempalte → templateconst template =` <style> .bg{ } .info { background-color: #EFEFEF; cursor: pointer; color:#540008 } .warning{ background-color: red; cursor: pointer; color:white } </style> <div> <span>我是MyButton组件的span标签</span> <button>提示</button> <div> <!-- 具名插槽 --> <slot name="title"></slot> </div> <div> <!-- 默认插槽 --> <slot name="img"> <span>如果不传内容,我就会显示</span> </slot> </div> <div> <!-- 匿名插槽 --> <slot></slot> </div> </div> <!-- 移除无效的script标签:template内的script不会执行 --> `;classMyButtonextendsHTMLElement{/** * 1.声明需要监听的属性,进监听已经声明的属性,没声明的不监听 * 这是一个静态方法 */staticgetobservedAttributes(){return['type']//监听type}constructor(){super();this.shadow =this.attachShadow({mode:'open'});// 2. 创建template元素并赋值模板内容const el = document.createElement('template'); el.innerHTML = template;// 用修正后的变量名 console.log(el)// 3. 确保content存在(template元素的content永远是DocumentFragment,不会undefined)const templateContent = el.content.cloneNode(true);this.shadow.appendChild(templateContent);// // 4. 抽离事件函数(方便移除事件)// this.handleButtonClick = () => {// console.log(`click ${this.getAttribute('type')} button`);// };}/** * 属性变化的回调函数 * 父组件修改子组件额type属性时触发 */attributeChangedCallback(attrName,oldVal,newVal){if(oldVal === newVal)returnthis.shadow.querySelector('button').className=newVal }// 生命周期:组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成'); console.log(this);// 5. 增加判空逻辑,避免找不到button时报错const btn =this.shadow.querySelector('button');if(btn){//创建自定义事件,通过detail传递参数 btn.addEventListener('click',()=>{const incrementEvent =newCustomEvent('count-increment',{bubbles:true,// 允许事件冒泡(可选,事件总线可省略)cancelable:true,detail:{step:1}// 传递的参数:每次增加1});// 全局派发事件(事件总线核心:基于document) document.dispatchEvent(incrementEvent); console.log('按钮点击,触发计数增加事件')});}}// 生命周期:组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除');// 6. 修正removeEventListener:传入相同的函数引用,且判空const btn =this.shadow.querySelector('button');if(btn){ btn.removeEventListener('click',this.handleButtonClick);}}}// 注册自定义元素 customElements.define('my-button', MyButton);exportdefault MyButton 
  • 主页面
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- <my-count></my-count> --><!-- 这里使用自定义组件 --><div><buttononclick="changeType()">改变MyButton的type为warning</button></div><my-buttontype="info"><spanslot="title">我是组件1传递具名插槽title过来的</span><spanslot="img">我是组件1传递默认插槽img过来的</span><span>我是组件1传递匿名插槽过来的</span></my-button><scripttype="module"src='./MyButton.js'></script><!-- <script type="module" src="./MyCount.js"></script> --><script>functionchangeType(){ document.querySelector('my-button').setAttribute('type','warning') console.log(document.querySelector('my-button'))}</script></body></html>
在这里插入图片描述
  • 如此,就完成了父组件向子组件传值
  • 那如果子组件需要向父组件传值呢,谢谢,请用自定义事件
  • 当然,上面父组件向子组件传值的时候,有没有可能还有这么一种方法
  • 子组件在父组件里面吧,那么父组件是不是可以拿到子组件的实例,那是不是可以调用子组件的方法,那是不是也可以进行传值交互,你真是个聪明的孩子,是的

然后我们点击按钮,改变子组件的type为warning

在这里插入图片描述

组件通信之一——实例方法

  • 很简单,就是拿到组件的实例,然后调用实例的方法,传参,交互
  • 我们改一下MyButton,写一个changeTyppe(type)方法,供实例调用,调用后改变button状态;然后父组件获取MyButton实例,然后调用该方法
/** * MyButton.js */// 1. 修正变量名拼写错误:tempalte → templateconst template =` <style> .bg{ } .info { background-color: #EFEFEF; cursor: pointer; color:#540008 } .warning{ background-color: red; cursor: pointer; color:white } </style> <div> <span>我是MyButton组件的span标签</span> <button>提示</button> <div> <!-- 具名插槽 --> <slot name="title"></slot> </div> <div> <!-- 默认插槽 --> <slot name="img"> <span>如果不传内容,我就会显示</span> </slot> </div> <div> <!-- 匿名插槽 --> <slot></slot> </div> </div> <!-- 移除无效的script标签:template内的script不会执行 --> `;classMyButtonextendsHTMLElement{/** * 1.声明需要监听的属性,进监听已经声明的属性,没声明的不监听 * 这是一个静态方法 */// static get observedAttributes(){// return ['type']//监听type// }constructor(){super();this.shadow =this.attachShadow({mode:'open'});// 2. 创建template元素并赋值模板内容const el = document.createElement('template'); el.innerHTML = template;// 用修正后的变量名 console.log(el)// 3. 确保content存在(template元素的content永远是DocumentFragment,不会undefined)const templateContent = el.content.cloneNode(true);this.shadow.appendChild(templateContent);// // 4. 抽离事件函数(方便移除事件)// this.handleButtonClick = () => {// console.log(`click ${this.getAttribute('type')} button`);// };}/** * 属性变化的回调函数 * 父组件修改子组件额type属性时触发 */// attributeChangedCallback(attrName,oldVal,newVal){// if(oldVal === newVal) return// this.shadow.querySelector('button').className=newVal// }changeType(type){this.shadow.querySelector('button').className=type }// 生命周期:组件挂载到DOM时触发connectedCallback(){ console.log('my-button挂载完成'); console.log(this);// 5. 增加判空逻辑,避免找不到button时报错const btn =this.shadow.querySelector('button');if(btn){//创建自定义事件,通过detail传递参数 btn.addEventListener('click',()=>{const incrementEvent =newCustomEvent('count-increment',{bubbles:true,// 允许事件冒泡(可选,事件总线可省略)cancelable:true,detail:{step:1}// 传递的参数:每次增加1});// 全局派发事件(事件总线核心:基于document) document.dispatchEvent(incrementEvent); console.log('按钮点击,触发计数增加事件')});}}// 生命周期:组件从DOM移除时触发disconnectedCallback(){ console.log('my-button 已移除');// 6. 修正removeEventListener:传入相同的函数引用,且判空const btn =this.shadow.querySelector('button');if(btn){ btn.removeEventListener('click',this.handleButtonClick);}}}// 注册自定义元素 customElements.define('my-button', MyButton);exportdefault MyButton 
  • 主页面
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title></title><style>/*设置divde全局样式*/div{background-color: yellow !important;/*提高全局样式的优先级*/}</style></head><body><!-- <my-count></my-count> --><!-- 这里使用自定义组件 --><div><buttononclick="changeType()">改变MyButton的type为warning</button></div><my-buttontype="info"><spanslot="title">我是组件1传递具名插槽title过来的</span><spanslot="img">我是组件1传递默认插槽img过来的</span><span>我是组件1传递匿名插槽过来的</span></my-button><scripttype="module"src='./MyButton.js'></script><!-- <script type="module" src="./MyCount.js"></script> --><script>functionchangeType(){/** * 获取MyButton实例,然后调用实例的方法,进行传值交互 */ document.querySelector('my-button').changeType('warning') console.log(document.querySelector('my-button'))}</script></body></html>

组件通信之一——其他

  • 还有很多通信方式,
  • 比如广播Broadcast Channel
  • Shared Worker
  • LocalStorage/SessionStorage
  • 全局状态
  • 等等
  • 篇幅问题,就不一一赘述了,大家可自行写一下
  • 好,那么问题来了,现在web component搞清楚了,怎么通讯也搞清楚了,问题来了,在哪用呢,场景是啥呢
  • 问得好

web components应用场景

通用UI组件库

  • 首当其冲是通用UI组件库,为啥呢,还是从web components的组合特性来说,原生支持,样式隔离,无依赖性,跨框架,标准化
  • 所以他开发出来的UI组件在哪都能用,跟项目本身不会有任何耦合,简直完美

低代码平台

  • 低代码平台需要即插即用,跨环境兼容的组件,而web component简直就是量身定做的,典型的像阿里宜搭,腾讯微搭登都是web components实现的

第三放嵌入式组件开发

  • 第三方组件(广告、支付按钮、统计图表、地图插件)需要嵌入到任意网站,需避免样式 / JS 冲突,Web Components 的隔离性可确保不影响宿主页面。

微前端

  • 这个就不多说了吧,web component好像简直就是wield微前端实现的

Read more

深入解读 AI 编程工具 — Cursor

在 AI 工具爆发的时代,各类辅助编程产品层出不穷。而其中 Cursor 因其独特的设计与对开发者真实问题的深度关注,正在成为开发者群体热议的焦点。 本文将带你清晰了解:什么是 Cursor?它如何工作?真正解决了哪些痛点?为何能成为行业快速增长的工具?  一、Cursor 的起源与快速成长 Cursor 背后的初创公司 Anysphere 成立于 2022 年,而 Cursor 的首个版本在 2023 年 3 月推出。仅仅两年时间,Anysphere 就完成了 9 亿美元的 C 轮融资,公司估值高达 99 亿美元!更令人惊讶的是,Cursor 的年收入已经突破 5 亿美元,这在开发工具领域几乎前所未有——据我所知,没有其他公司能在推出第一款产品后的两年内达到这样的规模。 Cursor 的快速普及也得益于企业级市场的认可:

前端Canvas:让你的网站更具视觉冲击力

前端Canvas:让你的网站更具视觉冲击力 毒舌时刻 前端Canvas?这不是游戏开发才用的吗? "Canvas性能差,我不用"——结果错过了丰富的视觉效果, "Canvas太复杂了,我学不会"——结果只能用静态图片, "我用CSS就够了,要Canvas干嘛"——结果无法实现复杂的动画效果。 醒醒吧,Canvas不是游戏开发的专利,前端也可以用它来创建丰富的视觉效果! 为什么你需要这个? * 丰富的视觉效果:创建动态图形、动画和游戏 * 高性能:直接操作像素,性能优异 * 交互性:支持鼠标、触摸等交互 * 数据可视化:绘制图表、仪表盘等 * 跨平台:在所有现代浏览器中运行 反面教材 // 反面教材:简单的Canvas绘制 function drawCircle() { const canvas = document.getElementById('canvas'

手把手教你用ESP32-S3开发板打造小智AI语音助手(含DeepSeek/Qwen接入指南)

手把手教你用ESP32-S3开发板打造小智AI语音助手(含DeepSeek/Qwen接入指南) 几年前,当我第一次把一块小小的ESP32开发板连接到电脑上,看着它闪烁的LED灯时,我完全没想到,今天我会用它来构建一个能听懂我说话、能和我智能对话的AI伙伴。硬件开发曾经是那么遥不可及,需要复杂的电路知识、昂贵的设备和漫长的学习曲线。但现在,一切都变了。 ESP32-S3这颗芯片,以其强大的处理能力、丰富的接口和亲民的价格,正在重新定义AI硬件开发的门槛。结合开源的语音识别框架和如今触手可及的大语言模型,我们每个人都能在自家的工作台上,亲手打造一个属于自己的智能语音助手。这不再是科技巨头的专利,而是每个硬件爱好者和AI初学者都能实现的梦想。 这篇文章,就是为你准备的实战指南。无论你是第一次接触ESP32的硬件新手,还是对AI应用充满好奇的开发者,我都会带你一步步走完整个流程——从硬件选型到固件烧录,从网络配置到模型接入,最后实现一个真正能用的、支持离线/在线混合模式的智能语音交互系统。我们不仅会使用现成的方案,更会深入探讨如何实现本地化部署,让你对自己的AI助手有完全的控制权。

A2UI与AG-UI深度对比:两大AI界面协议的异同与选择

A2UI与AG-UI深度对比:两大AI界面协议的异同与选择

名字相似,目标不同:一个让AI画界面,一个让AI连接应用 开篇:一个容易混淆的问题 如果你最近在关注AI智能体技术,可能会遇到两个名字非常相似的协议:A2UI和AG-UI。 第一次看到这两个名字,很多人都会困惑: * 这是同一个东西吗? * 如果不是,它们有什么区别? * 我应该用哪一个? 这种困惑是完全可以理解的。毕竟,它们的名字只差一个字母,都和"AI"、"UI"有关,而且都是2024-2025年才出现的新协议。 但实际上,A2UI和AG-UI是两个完全不同的协议,解决的是不同层面的问题。 让我用一个简单的比喻来说明: * A2UI就像是一种"UI设计语言",告诉前端"应该画一个什么样的界面" * AG-UI就像是一条"通信管道",负责在智能体和前端应用之间传递各种信息 一个关注"画什么",一个关注"