作為一名前端攻城獅,相信大家也都在關注着前端的一些新技術,近些年來前端組件化開發已為常態,我們經常把重用性高的模塊抽離成一個個的組件,來達到復用的目的,這樣減少了我們的維護成本,提高了開發的效率。但是都有一個缺點離不開框架本身,因為我們瀏覽器本身解析不了那些組件。那麼有沒有一種技術也可以達到這種效果呢?答案就是今天的主角 Web Components。
「Web Components 是一套不同的技術,允許您創建可重用的定製元素(它們的功能封裝在您的代碼之外)並且在您的 web 應用中使用它們。目前 W3C 也在積極推動,並且瀏覽器的支持情況還不錯。FireFox、Chrome、Opera 已全部支持,Safari 也大部分支持,Edge 也換成 webkit 內核了,離全面支持應該也不遠了。當然社區也有兼容的解決方案 webcomponents/polyfills。
WebComponents 三要素和生命周期Button 組件示例首先我們就從一個最簡單的 Button 組件開始,我們可以通過在組件中傳入 type 來改變按鈕的樣式,並且動態監聽了數據的變化。
//html<cai-buttontype="primary"><spanslot="btnText">按鈕</span></cai-button><templateid="caiBtn"><style>.cai-button{display:inline-block;padding:4px20px;font-size:14px;line-height:1.5715;font-weight:400;border:1pxsolid#1890ff;border-radius:2px;background-color:#1890ff;color:#fff;box-shadow:02px#00000004;}.cai-button-warning{border:1pxsolid#faad14;background-color:#faad14;}.cai-button-danger{border:1pxsolid#ff4d4f;background-color:#ff4d4f;}</style><divclass="cai-button"><slotname="btnText"></slot></div></template><script>consttemplate=document.getElementById("caiBtn");classCaiButtonextendsHTMLElement{constructor(){super()this._type={primary:'cai-button',warning:'cai-button-warning',danger:'cai-button-danger',}//開啟shadowdomconstshadow=this.attachShadow({mode:'open'})consttype=thisconstcontent=template.content.cloneNode(true)//克隆一份防止重複使用污染//把響應式數據掛到thisthis._btn=content.querySelector('.cai-button')this._btn.className+=`${this._type[type]}`shadow.appendChild(content)}staticgetobservedAttributes(){return['type']}attributeChangedCallback(name,oldValue,newValue){this[name]=newValue;this.render();}render(){this._btn.className=`cai-button${this._type[this.type]}`}}//掛載到windowwindow.customElements.define('cai-button',CaiButton)</script>三要素、生命周期和示例的解析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<cai-buttonid="btn"></cai-button><script>btn.setAttribute('config',JSON.stringify({icon:'',posi:''}))</script>//button.jsclassCaiButtonextendsHTMLElement{constructor(){xxx}staticgetobservedAttributes(){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)})()這種方式雖然可行但卻不是很優雅。

因此我們需要換一個思路,我們上面使用的方式都是 attribute 傳值,數據類型只能是字符串,那我們可以不用它傳值嗎?答案當然也是可以的。和 attribute 形影不離還有我們 js 中的 property,它指的是 dom 屬性,是 js 對象並且支持傳入複雜數據類型。
//table組件demo,以下為偽代碼僅展示思路<cai-tableid="table"></cai-table>table.dataSource=[{name:'xxx',age:19}]table.columns=[{title:'',key:''}]這種方式雖然解決上述問題,但是又引出了新的問題 -- 自定義組件中沒有辦法監聽到這個屬性的變化,那現在我們應該怎麼辦?或許從一開始是我們的思路就是錯的,顯然對於數據的響應式變化是我們原生 js 本來就不太具備的能力,我們不應該把使用過的框架的思想過於帶入,因此從組件使用的方式上我們需要做出改變,我們不應該過於依賴屬性的配置來達到某種效果,因此改造方法如下。
<cai-tablethead="Name|Age"><cai-tr><cai-td>zs</cai-td><cai-td>18</cai-td></cai-tr><cai-tr><cai-td>ls</cai-td><cai-td>18</cai-td></cai-tr></cai-table>我們把屬於 HTML 原生的能力歸還,而是不是採用配置的方式,就解決了這個問題,但是這樣同時也決定了我們的組件並不支持太過複雜的能力。
狀態的雙向綁定上面講了數據的單向綁定,組件狀態頁面也會隨之更新,那麼我們怎麼實現雙向綁定呢?
接下來我們封裝一個 input 來實現雙向綁定。
<cai-inputid="ipt":value="data"@change="(e)=>{data=e.detail}"></cai-input>//js(function(){consttemplate=document.createElement('template')template.innerHTML=`<style>.cai-input{}</style><inputtype="text"id="caiInput">`classCaiInputextendsHTMLElement{constructor(){super()constshadow=this.attachShadow({mode:'closed'})constcontent=template.content.cloneNode(true)this._input=content.querySelector('#caiInput')this._input.value=this.getAttribute('value')shadow.appendChild(content)this._input.addEventListener("input",ev=>{consttarget=ev.target;constvalue=target.value;this.value=value;this.dispatchEvent(newCustomEvent("change",{detail:value}));});}getvalue(){returnthis.getAttribute("value");}setvalue(value){this.setAttribute("value",value);}}window.customElements.define('cai-input',CaiInput)})()第一步:要有一個優雅的組價庫我們首先要設計一個優雅的目錄結構,設計目錄結構如下
.└── cai-ui ├── components // 自定義組件 | ├── Button | | ├── index.js | └── ... └── index.js. // 主入口獨立封裝獨立封裝我們的組件,由於我們組件庫中組件的引入,我們肯定是需要把每個組件封裝到單獨文件中的。
在我們的 Button/index.js 中寫入如下:
(function(){consttemplate=document.createElement('template')template.innerHTML=`<style>/*css和上面一樣*/</style><divclass="cai-button"><slotname="text"></slot></div>`classCaiButtonextendsHTMLElement{constructor(){super()//其餘和上述一樣}staticgetobservedAttributes(){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 文件中
全部導入和按需導入支持主題色可配置 我們只需把顏色寫成變量即可,改造如下:
(function(){consttemplate=document.createElement('template')template.innerHTML=`<style>/*多餘省略*/.cai-button{border:1pxsolidvar(--primary-color,#1890ff);background-color:var(--primary-color,#1890ff);}.cai-button-warning{border:1pxsolidvar(--warning-color,#faad14);background-color:var(--warning-color,#faad14);}.cai-button-danger{border:1pxsolidvar(--danger-color,#ff4d4f);background-color:var(--danger-color,#ff4d4f);}</style><divclass="cai-button"><slotname="text"></slot></div>`//後面省略...})()這樣我們就能在全局中修改主題色了。案例地址(https://github.com/lovelts/cai-ui)
在原生、Vue 和 React 中優雅的使用在原生 HTML 中應用:<scripttype="module">import'//cai-ui';</script><!--or--><scripttype="module"src="//cai-ui"></script><cai-buttontype="primary">點擊</cai-button><cai-inputid="caiIpt"></cai-button><script>constcaiIpt=document.getElementById('caiIpt')/*獲取輸入框的值有兩種方法*1.getAttribute*2.change事件*/caiIpt.getAttribute('value')caiIpt.addEventListener('change',function(e){console.log(e);//e.detail為表單的值})</script>在 Vue 2x 中的應用://main.jsimport'cai-ui';<template><divid="app"><cai-button:type="type"><spanslot="text">哈哈哈</span></cai-button><cai-button@click="changeType"><spanslot="text">哈哈哈</span></cai-button><cai-inputid="ipt":value="data"@change="(e)=>{data=e.detail}"></cai-input></div></template><script>exportdefault{name:"App",components:{},data(){return{type:'primary',data:'',}},methods:{changeType(){console.log(this.data);this.type='danger'}},};</script>在 Vue 3x 中的差異:在最近的 Vue3 中,Vue 對 WebComponents 有了更好的支持。Vue 在 Custom Elements Everywhere 測試中獲得了 100% 的完美分數(https://custom-elements-everywhere.com/libraries/vue/results/results.html)。但是還需要我們做出如下配置:
跳過 Vue 本身對組件的解析custom Elements 的風格和 Vue 組件很像,導致 Vue 會把自定義(非原生的 HTML 標籤)標籤解析並註冊為一個 Vue 組件,然後解析失敗才會再解析為一個自定義組件,這樣會消耗一定的性能並且會在控制台警告,因此我們需要在構建工具中跳過這個解析:
//vite.config.jsimportvuefrom'@vitejs/plugin-vue'exportdefault{plugins:[vue({template:{compilerOptions:{//將所有包含短橫線的標籤作為自定義元素處理isCustomElement:tag=>tag.includes('-')}}})]}組件的具體使用方法和 Vue 2x 類似。
在 React 中的應用importReact,{useEffect,useRef,useState}from'react';import'cai-ui'functionApp(){const[type,setType]=useState('primary');const[value,setValue]=useState();constiptRef=useRef(null)useEffect(()=>{document.getElementById('ipt').addEventListener('change',function(e){console.log(e);})},[])consthandleClick=()=>{console.log(value);setType('danger')}return(<divclassName="App"><cai-buttontype={type}><spanslot="text">哈哈哈</span></cai-button><cai-buttononClick={handleClick}><spanslot="text">點擊</span></cai-button><cai-inputid="ipt"ref={iptRef}value={value}></cai-input></div>);}exportdefaultApp;「Web Components 觸發的事件可能無法通過 React 渲染樹正確的傳遞。你需要在 React 組件中手動添加事件處理器來處理這些事件。在 React 使用有個點我們需要注意下,WebComponents 組件我們需要添加類時需要使用 claas 而不是 className
總結現階段的劣勢看完這篇文章大家肯定會覺得為什麼 WebComponents 實現了一份代碼多個框架使用,卻還沒有霸占組件庫的市場呢?我總結了以下幾點:
往期推薦
Copilot 突然收費!免費的 AI 幫忙寫代碼突然不香了?
僅用一個HTML標籤,實現帶動畫的抖音Logo
我最期待的 3 個即將推出的 CSS 特性!
CSS 穿牆術!太強了
CSS 中的簡寫到底有多少坑?以後不敢了...
小程序前景無限,還能一鍵轉換成App?
創作不易,加個點讚、在看支持一下哦!