常州做網(wǎng)站麥策電商戶(hù)外廣告
[React]利用Webcomponent封裝React組件
為什么這么做
我個(gè)人認(rèn)為,最重要的點(diǎn)是可以很方便地跨框架掛載和卸載wc元素(至少我在項(xiàng)目里是這么玩的),此外,基于wc的css沙箱以及它的shadowRoot機(jī)制,可以提供一套隔離機(jī)制,保證每個(gè)渲染組件的邊界分明。
利用AI總結(jié)羅列了一下都有啥優(yōu)點(diǎn)…
- 封裝性:Web Components提供了一種封裝UI組件的方法,使得組件可以在不同的框架或無(wú)框架環(huán)境中重用。
- 可重用性:封裝為Web Components的React組件可以在任何支持Web Components的環(huán)境中使用,不限于React應(yīng)用。
- 封裝的樣式和行為:Web Components允許你封裝組件的HTML結(jié)構(gòu)、樣式和行為,確保樣式和行為不會(huì)泄露到父組件或全局作用域。
- 獨(dú)立性:Web Components封裝的組件具有獨(dú)立性,它們擁有自己的DOM樹(shù)和作用域,不會(huì)影響外部環(huán)境。
- 易于集成:Web Components提供了一種標(biāo)準(zhǔn)化的集成方式,可以更容易地將React組件集成到其他Web應(yīng)用中。
- 更好的性能:Web Components的自定義元素可以在不影響主線(xiàn)程的情況下進(jìn)行升級(jí)和渲染,這有助于提高應(yīng)用性能。
- 標(biāo)準(zhǔn)化:Web Components基于W3C標(biāo)準(zhǔn),這意味著它們?cè)诓煌臑g覽器和環(huán)境中具有更好的一致性和兼容性。
- 易于維護(hù):由于Web Components封裝的組件具有清晰的接口和封裝性,維護(hù)和更新組件變得更加容易。
- 樣式隔離:Web Components的Shadow DOM技術(shù)可以確保組件的樣式不會(huì)受到外部樣式的影響,同時(shí)也防止組件內(nèi)部樣式泄露到外部。
- 生命周期管理:Web Components允許你定義組件的生命周期鉤子,如
connectedCallback
、disconnectedCallback
等,這與React組件的生命周期方法類(lèi)似。 - 跨框架使用:封裝為Web Components的React組件可以被其他前端框架或庫(kù)使用,例如Vue、Angular或原生JavaScript。
- 自定義元素:Web Components允許開(kāi)發(fā)者定義自定義HTML元素,這些元素可以像標(biāo)準(zhǔn)HTML元素一樣使用。
- 易于測(cè)試:Web Components的封裝性使得測(cè)試組件變得更加簡(jiǎn)單,因?yàn)槟憧梢元?dú)立于其他組件來(lái)測(cè)試它們。
- 更好的封裝和抽象:Web Components提供了一種封裝和抽象UI組件的方式,使得組件的實(shí)現(xiàn)細(xì)節(jié)對(duì)使用者是透明的。
Webcomponent入門(mén)
先來(lái)簡(jiǎn)單地過(guò)一下webcomponent的基礎(chǔ)
官方文檔:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
示例
下面是一個(gè)最簡(jiǎn)單的示例,自定義了一種名為”simple-component“的元素,并且它沒(méi)有shadowRoot(意味著它并沒(méi)有與外界隔離樣式)。
class SimpleComponent extends HTMLElement {constructor() {super();this.innerHTML = `<p>Hello, World!</p>`;}
}customElements.define('simple-component', SimpleComponent);
下面是一個(gè)內(nèi)容更豐富一些的示例,有基礎(chǔ)的大概過(guò)一眼也知道大概了。
// 1.自定義標(biāo)簽都是用class 的形式去繼承
class myDiv extends HTMLElement {// 監(jiān)聽(tīng)static get observedAttributes() {return ['option']}constructor() {super()// 這樣我們才能夠去追加元素this.attachShadow({ mode: 'open' })}// 重要:生命周期方法 開(kāi)始connectedCallback() {console.log('connectedCallback生命周期')this.render({option: this.getAttribute('option'),})// 獲取元素console.log(this.shadowRoot.querySelector('.content'))console.log('this.shadowRoot: ', this.shadowRoot)document.addEventListener('click', e => {// 重要:冒泡的順序,通過(guò)這個(gè)可以判斷有沒(méi)有在鼠標(biāo)內(nèi)部進(jìn)行點(diǎn)擊if (e.composedPath().includes(this)) {console.log('點(diǎn)擊了里面')}})this.shadowRoot.querySelector('.content').addEventListener('click', e => {console.log('e: ', e)// window.dispatchEvent})}// 重要:生命周期方法 重新渲染 .甚至還是第一次進(jìn)行渲染,比connect還快// 會(huì)重新渲染 connectCallbackattributeChangedCallback(attr, oldValue, newValue) {if (oldValue) {switch (attr) {case 'option':this.shadowRoot.querySelector('.title').textContent = newValue}}console.log('attributeChangeCallback', 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 的樣式絕對(duì)隔離// 重要: :host選擇器可以選中根也就是my-div的樣式。外面的選擇器樣式要高于這個(gè)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}`this.shadowRoot.appendChild(nodeTemplate.content)this.shadowRoot.appendChild(nodeStyles)setTimeout(() => {this.borderAdd()}, 3000)}
}// 名字必須小寫(xiě) 駝峰必須要轉(zhuǎn)成橫線(xiàn)
customElements.define('my-div', myDiv)
shadowRoot
一個(gè)Web組件可以有且僅有一個(gè)shadowRoot
。shadowRoot
是與該組件關(guān)聯(lián)的影子DOM的根節(jié)點(diǎn)。當(dāng)使用attachShadow
方法創(chuàng)建影子DOM時(shí),它會(huì)返回一個(gè)shadowRoot
對(duì)象,這個(gè)對(duì)象是唯一的,并且與創(chuàng)建它的元素關(guān)聯(lián)。
例如:
class MyComponent extends HTMLElement {constructor() {super();this.shadow = this.attachShadow({ mode: "open" });this.shadow.innerHTML = `<p>I am in the shadow DOM!</p>`;}
}customElements.define('my-component', MyComponent);
在這個(gè)例子中:
MyComponent
類(lèi)擴(kuò)展了HTMLElement
,定義了一個(gè)Web組件。- 在構(gòu)造函數(shù)中,通過(guò)調(diào)用
this.attachShadow({ mode: "open" })
創(chuàng)建了一個(gè)shadowRoot
,并將其存儲(chǔ)在this.shadow
變量中。 - 這個(gè)
shadowRoot
是唯一的,并且與MyComponent
實(shí)例關(guān)聯(lián)。
關(guān)鍵點(diǎn):
- 唯一性:每個(gè)Web組件實(shí)例只能有一個(gè)
shadowRoot
。 - 關(guān)聯(lián)性:
shadowRoot
與創(chuàng)建它的Web組件實(shí)例是緊密關(guān)聯(lián)的,不能被其他組件實(shí)例訪(fǎng)問(wèn)。
因此,盡管可以在shadowRoot
內(nèi)創(chuàng)建多個(gè)子元素和結(jié)構(gòu),但每個(gè)Web組件實(shí)例只能有一個(gè)shadowRoot
。這有助于保持組件的封裝性和獨(dú)立性。
生命周期
connectedCallback
:- 當(dāng)自定義元素被插入到文檔DOM樹(shù)中時(shí)調(diào)用此方法。這類(lèi)似于React中的
componentDidMount
。
- 當(dāng)自定義元素被插入到文檔DOM樹(shù)中時(shí)調(diào)用此方法。這類(lèi)似于React中的
disconnectedCallback
:- 當(dāng)自定義元素從DOM樹(shù)中移除時(shí)調(diào)用此方法。類(lèi)似于React中的
componentWillUnmount
。
- 當(dāng)自定義元素從DOM樹(shù)中移除時(shí)調(diào)用此方法。類(lèi)似于React中的
attributeChangedCallback
:- 當(dāng)自定義元素的屬性被更改時(shí)調(diào)用此方法。它接收三個(gè)參數(shù):屬性名稱(chēng)、舊值和新值。這可以用于響應(yīng)屬性的變化,類(lèi)似于React中的
componentDidUpdate
,但是它是針對(duì)屬性而不是狀態(tài)。
- 當(dāng)自定義元素的屬性被更改時(shí)調(diào)用此方法。它接收三個(gè)參數(shù):屬性名稱(chēng)、舊值和新值。這可以用于響應(yīng)屬性的變化,類(lèi)似于React中的
adoptedCallback
:- 當(dāng)自定義元素被移動(dòng)到新的文檔時(shí)調(diào)用此方法。這在Web Components中是特有的,因?yàn)樽远x元素可以跨文檔使用。
主要屬性
- 觀(guān)察者模式(Observed attributes):
- 通過(guò)在自定義元素類(lèi)中定義一個(gè)靜態(tài)的
observedAttributes
屬性數(shù)組,可以指定哪些屬性的更改應(yīng)該觸發(fā)attributeChangedCallback
。
- 通過(guò)在自定義元素類(lèi)中定義一個(gè)靜態(tài)的
connected
和disconnected
屬性:- 這些屬性可以用于檢查自定義元素是否已經(jīng)連接到文檔的DOM樹(shù)中。
shadowRoot
屬性:- 每個(gè)自定義元素都有一個(gè)
shadowRoot
屬性,它是一個(gè)Shadow DOM樹(shù)的根??梢栽谶@個(gè)屬性上使用生命周期回調(diào)來(lái)管理Shadow DOM的創(chuàng)建和更新。
- 每個(gè)自定義元素都有一個(gè)
constructor
:- 雖然不是Web Components的生命周期回調(diào),但是自定義元素的構(gòu)造函數(shù)是定義元素屬性和方法的地方,并且在元素實(shí)例化時(shí)調(diào)用。
Lit框架入門(mén)
一般知道了上面的基礎(chǔ),就可以寫(xiě)wc組件了,但實(shí)際開(kāi)發(fā)中,肯定還是需要借助一些已有的開(kāi)發(fā)框架來(lái)輔助開(kāi)發(fā),而Lit就是目前最成熟且使用量最高的。
原理介紹
Web組件的更新并不是每次都進(jìn)行全量更新。Web組件的更新機(jī)制非常靈活,能夠根據(jù)組件的狀態(tài)和屬性的變化來(lái)決定是否需要更新。以下是一些關(guān)鍵點(diǎn):
-
屬性變化觸發(fā)更新:
- Web組件的更新通常是由屬性的變化觸發(fā)的。當(dāng)組件的屬性發(fā)生變化時(shí),瀏覽器會(huì)調(diào)用
attributeChangedCallback
方法來(lái)處理這些變化。
- Web組件的更新通常是由屬性的變化觸發(fā)的。當(dāng)組件的屬性發(fā)生變化時(shí),瀏覽器會(huì)調(diào)用
-
狀態(tài)變化觸發(fā)更新:
- 組件的內(nèi)部狀態(tài)變化也可能導(dǎo)致更新。例如,在LitElement中,當(dāng)使用
@property
裝飾器定義的屬性發(fā)生變化時(shí),會(huì)觸發(fā)更新。
- 組件的內(nèi)部狀態(tài)變化也可能導(dǎo)致更新。例如,在LitElement中,當(dāng)使用
-
生命周期方法:
- 組件的生命周期方法,如
connectedCallback
,disconnectedCallback
,adoptedCallback
,firstUpdated
,updated
等,都可以在特定時(shí)機(jī)觸發(fā)更新。
- 組件的生命周期方法,如
-
選擇性更新:
- 更新機(jī)制可以是選擇性的。例如,在LitElement中,可以通過(guò)使用
requestUpdate
方法來(lái)請(qǐng)求更新,而不必每次都進(jìn)行全量更新。
- 更新機(jī)制可以是選擇性的。例如,在LitElement中,可以通過(guò)使用
-
虛擬DOM:
- 一些Web組件框架(如LitElement)使用虛擬DOM技術(shù)來(lái)優(yōu)化更新過(guò)程。虛擬DOM可以比較組件的新舊狀態(tài),并只更新那些實(shí)際發(fā)生變化的部分。
-
優(yōu)化性能:
- 為了避免不必要的全量更新,Web組件通常會(huì)使用一些優(yōu)化技術(shù),例如節(jié)流(throttle)和防抖(debounce)來(lái)減少更新次數(shù)。
-
自定義渲染邏輯:
- 開(kāi)發(fā)者可以通過(guò)自定義渲染邏輯來(lái)控制組件的更新過(guò)程。例如,可以在
render
方法中手動(dòng)決定哪些部分需要重新渲染。
- 開(kāi)發(fā)者可以通過(guò)自定義渲染邏輯來(lái)控制組件的更新過(guò)程。例如,可以在
-
條件渲染:
- 組件可以通過(guò)條件渲染來(lái)決定是否需要更新某些部分。例如,只有當(dāng)特定條件滿(mǎn)足時(shí)才重新渲染某些元素。
示例
以下是一個(gè)使用LitElement的示例,展示了如何控制組件的更新:
import { LitElement, html, css, property } from 'lit';class MyComponent extends LitElement {@property({ type: String })message = '';render() {return html`<div><p>${this.message}</p></div>`;}updated(changedProperties) {super.updated(changedProperties);if (changedProperties.has('message')) {console.log('Message updated:', this.message);}}
}customElements.define('my-component', MyComponent);
在這個(gè)示例中:
message
屬性使用@property
裝飾器定義,當(dāng)其值發(fā)生變化時(shí),會(huì)觸發(fā)組件的更新。render
方法定義了組件的渲染邏輯,只有當(dāng)message
屬性發(fā)生變化時(shí),相關(guān)的部分才會(huì)重新渲染。updated
方法在組件更新后被調(diào)用,可以在這里處理更新后的邏輯。
通過(guò)這種方式,Web組件可以有效地控制更新過(guò)程,避免不必要的全量更新,從而提高性能。
增加的生命周期和內(nèi)置屬性
Lit 相對(duì)于傳統(tǒng) Web 組件規(guī)范增加的一些生命周期鉤子和特性:
-
render
方法:- 這是 Lit 的核心特性之一。
render
方法是一個(gè)返回組件模板的函數(shù),Lit 會(huì)根據(jù)這個(gè)方法的內(nèi)容來(lái)渲染組件的 UI。
- 這是 Lit 的核心特性之一。
-
update
方法:- 這個(gè)方法在組件的屬性或狀態(tài)發(fā)生變化時(shí)被調(diào)用。Lit 會(huì)調(diào)用這個(gè)方法來(lái)決定是否需要重新渲染組件。
-
shouldUpdate
方法:- 這個(gè)方法允許開(kāi)發(fā)者自定義更新邏輯,決定是否需要進(jìn)行更新。如果返回
false
,則跳過(guò)更新。
- 這個(gè)方法允許開(kāi)發(fā)者自定義更新邏輯,決定是否需要進(jìn)行更新。如果返回
-
willUpdate
方法:- 在組件更新之前被調(diào)用,可以用于執(zhí)行更新前的準(zhǔn)備工作。
-
updated
方法:- 在組件更新之后被調(diào)用,可以用于執(zhí)行更新后的邏輯處理。
-
firstUpdated
方法:- 在組件首次更新后被調(diào)用。這與 Web 組件的
connectedCallback
有些相似,但專(zhuān)門(mén)用于處理首次渲染后的邏輯。
- 在組件首次更新后被調(diào)用。這與 Web 組件的
-
connectedCallback
:- 這是 Web 組件規(guī)范中的方法,Lit 也支持。當(dāng)組件被插入到文檔中時(shí)調(diào)用。
-
disconnectedCallback
:- 這是 Web 組件規(guī)范中的方法,Lit 也支持。當(dāng)組件從文檔中移除時(shí)調(diào)用。
-
attributeChangedCallback
:- 這是 Web 組件規(guī)范中的方法,Lit 也支持。當(dāng)組件的屬性發(fā)生變化時(shí)調(diào)用。
-
adoptedCallback
:- 這是 Web 組件規(guī)范中的方法,Lit 也支持。當(dāng)組件被移動(dòng)到新文檔時(shí)調(diào)用。
-
requestUpdate
方法:- 這個(gè)方法可以被開(kāi)發(fā)者調(diào)用,以請(qǐng)求更新組件的屬性。Lit 會(huì)安排在下一個(gè)微任務(wù)中處理這些更新。
-
updateComplete
Promise:- 一個(gè) Promise,當(dāng)組件的更新完成后會(huì)解析。這可以用于在更新完成后執(zhí)行異步操作。
-
樣式管理:
- Lit 提供了
CSSResult
和unsafeCSS
等 API,用于更安全和方便地管理組件的樣式。
- Lit 提供了
-
屬性裝飾器:
- 使用
@property
裝飾器定義的屬性會(huì)觸發(fā)更新,并且可以指定屬性的類(lèi)型和是否同步到 DOM 屬性。
- 使用
-
狀態(tài)管理:
- Lit 通過(guò)
state
方法和reactive
裝飾器,提供了一種聲明式的方式來(lái)管理組件的狀態(tài)。
- Lit 通過(guò)
核心:結(jié)合Lit框架實(shí)現(xiàn)React組件封裝
那么基于以上,我們可以很容易地就實(shí)現(xiàn)利用Lit框架創(chuàng)造出一個(gè)webcomponent容器,然后用來(lái)包裹React組件。
Base基礎(chǔ)類(lèi)
import { LitElement, ReactiveElement, adoptStyles, unsafeCSS, PropertyValues } from 'lit'
import { property } from 'lit/decorators.js'type ThrottleFn = (...args: any[]) => void
type DelayFn = (fn: ThrottleFn) => voidconst throttleWith = <T extends ThrottleFn>(fn: T,delayFn: DelayFn,leading = false
): T => {let lastArgs: Parameters<T>, lastThis: unknown, isWaiting = falseconst throttledFn = (...args: Parameters<T>) => {lastArgs = args// eslint-disable-next-linelastThis = thisif (!isWaiting) {if (leading) {fn.apply(lastThis, lastArgs)}isWaiting = truedelayFn(() => {fn.apply(lastThis, lastArgs)isWaiting = false})}}return throttledFn as T
}export default class Base extends LitElement {private _wcStyle?: string@property({ attribute: 'wc-style' })get wcStyle() {return this._wcStyle}set wcStyle(val: string | undefined) {this._wcStyle = valthis.adoptStyles()}/*** 使事件不能跨越ShadowDOM邊界傳播*/@property({ type: Boolean, attribute: 'prevent-compose' })protected preventCompose = false/*** 使事件不冒泡*/@property({ type: Boolean, attribute: 'prevent-bubbles' })protected preventBubbles = false// 應(yīng)用樣式protected adoptStyles = throttleWith(() => {const apply = () => {if (this.renderRoot instanceof ShadowRoot) {const styles = (this.constructor as typeof ReactiveElement).elementStyles.slice() // 獲取原有樣式this.wcStyle && styles.push(unsafeCSS(this.wcStyle))adoptStyles(this.renderRoot, styles) // 重新應(yīng)用樣式}}this.renderRoot ? apply() : this.updateComplete.then(apply)},(fn: any) => Promise.resolve().then(fn))// 派發(fā)事件emit(eventName: string, detail?: any, options?: CustomEventInit) {let event = new CustomEvent(eventName, {detail,composed: !this.preventCompose,bubbles: !this.preventBubbles,cancelable: false,...options,})this.dispatchEvent(event)return event}// 判斷 slot 是否傳入內(nèi)容hasSlot(name?: string) {if (name && name !== 'default') {return !![...this.childNodes].find(node => node.nodeType === node.ELEMENT_NODE && (node as Element).getAttribute('slot') === name)}return [...this.childNodes].some(node => {if (node.nodeType === node.TEXT_NODE && !!node.textContent?.trim()) {return true}if (node.nodeType === node.ELEMENT_NODE) {const el = node as HTMLElementif (!el.hasAttribute('slot')) {return true}}return false})}// 各個(gè)生命周期// 掛載時(shí)connectedCallback() {super.connectedCallback()console.log('Custom element added to page.')// 第一次被插入文檔時(shí)執(zhí)行,跳過(guò)節(jié)點(diǎn)刪除后又重新插入的情形if (!this.hasUpdated) {this.setAttribute('wc-component', '')this.setAttribute('wc-pending', '')}}// 卸載時(shí)disconnectedCallback() {super.disconnectedCallback()console.log('Custom element removed from page.')}// 移動(dòng)到另一個(gè)文檔的時(shí)候adoptedCallback() {console.log('Custom element moved.')}// 元素的屬性被添加、刪除或修改時(shí)調(diào)用attributeChangedCallback(name: string, oldValue: any, newValue: any) {super.attributeChangedCallback(name, oldValue, newValue)console.log(`Attribute ${name} has changed.`)}// 或使用靜態(tài)屬性代替get方法static get observedAttributes() {// 指定要監(jiān)聽(tīng)的元素的屬性數(shù)組// 對(duì)應(yīng)的attr改變后,會(huì)觸發(fā)attributeChangedCallback// return ['name', 'date']return []}// 是否應(yīng)該更新protected shouldUpdate(_changedProperties: PropertyValues): boolean {return true}// 即將更新protected willUpdate(_changedProperties: PropertyValues): void {super.willUpdate(_changedProperties)console.log('willUpdate')}// 首次更新元素時(shí)調(diào)用。實(shí)現(xiàn)在更新后對(duì)元素執(zhí)行一次性工作protected firstUpdated(changedProperties: PropertyValues) {super.firstUpdated(changedProperties)console.log('this.hasUpdated: ', this.hasUpdated)// this.requestUpdate()// 兩幀數(shù)后執(zhí)行requestAnimationFrame(() => {requestAnimationFrame(() => {this.removeAttribute('wc-pending')})})}protected updated(_changedProperties: PropertyValues): void {super.updated(_changedProperties)this.updateComplete.then((res) => {console.log('updateComplete', res)})}
}
withProperties封裝
import type { LitElement, PropertyValues } from 'lit'type Constructor<T> = new (...args: any[]) => Texport default <T extends Constructor<LitElement>>(superClass: T) => {class WithPropertiesElement extends superClass {props: Record<string, any> = {}willUpdate(changedProperties: PropertyValues) {const obj = [...changedProperties.entries()].reduce<any>((obj, [key]) => ((obj[key] = (this as any)[key]), obj),{})this.props = { ...this.props, ...obj }super.willUpdate(changedProperties)}}return WithPropertiesElement as Constructor<{props: Record<string, any>}> & T
}
這段代碼定義了一個(gè)高階組件(Higher-Order Component,HOC),用于增強(qiáng) LitElement 組件的功能。具體來(lái)說(shuō),它的作用是:
-
創(chuàng)建一個(gè)帶有額外屬性管理功能的組件類(lèi):
- 通過(guò)擴(kuò)展傳入的基類(lèi)(比如
LitElement
或其子類(lèi)),添加一個(gè)props
屬性來(lái)存儲(chǔ)組件的屬性值。
- 通過(guò)擴(kuò)展傳入的基類(lèi)(比如
-
在組件更新前處理屬性變化:
- 重寫(xiě)
willUpdate
生命周期方法,這個(gè)方法在組件的屬性發(fā)生變化并且組件即將更新之前被調(diào)用。
- 重寫(xiě)
-
收集并存儲(chǔ)屬性變化:
- 使用
changedProperties
對(duì)象(一個(gè) Map 類(lèi)型的對(duì)象,包含屬性名和屬性變化的信息)來(lái)收集屬性的變化。 - 將變化的屬性存儲(chǔ)到
this.props
對(duì)象中,這樣可以通過(guò)props
屬性訪(fǎng)問(wèn)組件的所有屬性值。
- 使用
-
保持基類(lèi)的
willUpdate
方法的調(diào)用:- 調(diào)用
super.willUpdate(changedProperties)
以確?;?lèi)的willUpdate
方法也能正常執(zhí)行。
- 調(diào)用
代碼詳解
- 定義了一個(gè)默認(rèn)導(dǎo)出的函數(shù),它接受一個(gè)構(gòu)造函數(shù)
superClass
(應(yīng)該是LitElement
或其子類(lèi)的構(gòu)造函數(shù))。 - 創(chuàng)建一個(gè)新類(lèi)
WithPropertiesElement
,繼承自superClass
。 - 在
WithPropertiesElement
類(lèi)中定義了一個(gè)props
屬性,用于存儲(chǔ)屬性值。 - 重寫(xiě)
willUpdate
方法,在組件更新前處理屬性變化,并將變化的屬性存儲(chǔ)到this.props
中。 - 返回
WithPropertiesElement
類(lèi),并通過(guò)類(lèi)型斷言確保它具有額外的props
屬性。
使用示例
假設(shè)你有一個(gè)基礎(chǔ)的 LitElement 組件:
import { LitElement, html } from 'lit';class MyElement extends LitElement {count = 0;render() {return html`<p>Count: ${this.count}</p>`;}
}customElements.define('my-element', MyElement);
你可以使用這個(gè)高階組件來(lái)增強(qiáng)它:
import { WithPropertiesElement } from './WithPropertiesElement';
import { LitElement, html } from 'lit';const EnhancedElement = WithPropertiesElement(MyElement);customElements.define('enhanced-element', EnhancedElement);const element = new EnhancedElement();
document.body.appendChild(element);console.log(element.props); // { count: 0 }
在這個(gè)示例中,EnhancedElement
繼承自 MyElement
并添加了屬性管理功能。可以通過(guò) element.props
訪(fǎng)問(wèn)組件的所有屬性值。
這種模式在需要在組件中統(tǒng)一管理屬性或在組件更新前進(jìn)行額外處理時(shí)非常有用。
存放React組件的webcomponent基類(lèi)
重頭戲來(lái)了
import { ChildPart, html, PropertyValues } from 'lit'
import { query } from 'lit/decorators.js'
import { Fragment, createElement as h } from 'react'
import ReactDOM from 'react-dom'
import withProperties from '../mixin/withProperties'
import LightBase from './Base'type H = typeof hconst Root: React.FC<any> = props => {return h(Fragment, {...props,})
}const omit = (obj: Record<string, any>, filter: string[] = []) =>Object.fromEntries(Object.entries(obj).filter(([key]) => !filter.includes(key)))// React組件基類(lèi)
export default class extends withProperties(LightBase) {// 子類(lèi)要重寫(xiě)這個(gè)方法來(lái)渲染自己的組件protected renderReact(h: H): React.ReactNode {return null}protected customContainer(): Element | undefined {return this.$reactRoot}protected getReactProps(props: Record<string, any>) {return omit(props, ['preventCompose', 'preventBubbles', 'localeMessages'])}protected extraStyle = ''@query('.react-root')$reactRoot?: HTMLElementupdated(changed: PropertyValues) {super.updated(changed)this.doRender()}connectedCallback() {super.connectedCallback()// 節(jié)點(diǎn)刪除后重新插入的情形if (this.hasUpdated) {this.doRender()}}disconnectedCallback() {super.disconnectedCallback()this.doUnmount()}private container?: Elementprivate doRender() {const container = this.customContainer()if (!container) {this.doUnmount() // 卸載前一次渲染的內(nèi)容} else {this.container = containerReactDOM.render(h(Root, {}, this.renderReact(h)), container, () => {// hack for error: https://github.com/lit/lit/blob/f8ee010bc515e4bb319e98408d38ef3d971cc08b/packages/lit-html/src/lit-html.ts#L1122// 在React中使用此組件且非首次更新時(shí)會(huì)報(bào)錯(cuò),因?yàn)閘it默認(rèn)會(huì)在組件下創(chuàng)建一個(gè)注釋節(jié)點(diǎn),更新時(shí)會(huì)對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行操作,而React渲染時(shí)把這個(gè)注釋節(jié)點(diǎn)干掉了,這里要把他加回去const childPart = (this as any).__childPart as ChildPart | undefinedchildPart?.startNode && this.appendChild(childPart.startNode)})}}private doUnmount() {if (this.container) {ReactDOM.unmountComponentAtNode(this.container)}}render() {return html` <div class="react-root"></div> `}
}
使用Demo
import { unsafeCSS } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import ReactBase from './ReactBase'// 自己的React組件
import Component from './Component'import style from './index.less?inline'@customElement('my-diy-react-wc')
export default class DataReport extends ReactBase {static get styles() {return unsafeCSS([style])}/*** 自定義屬性*/@property()language: string = 'zh-CN'// ReactBase中用來(lái)渲染React,不要?jiǎng)h除renderReact() {return <Component language={this.language} />}
}
參考文章
https://juejin.cn/post/7296850940404580364?searchId=2024071620331848BC966F0D2051B9C533#heading-9
lit官網(wǎng):https://lit.dev/docs/components/styles/
webcomponent文檔:https://developer.mozilla.org/en-US/docs/Web/API/Web_components