赞
踩
我个人认为,最重要的点是可以很方便地跨框架挂载和卸载wc元素(至少我在项目里是这么玩的),此外,基于wc的css沙箱以及它的shadowRoot机制,可以提供一套隔离机制,保证每个渲染组件的边界分明。
利用AI总结罗列了一下都有啥优点…
connectedCallback
、disconnectedCallback
等,这与React组件的生命周期方法类似。先来简单地过一下webcomponent的基础
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
下面是一个最简单的示例,自定义了一种名为”simple-component“的元素,并且它没有shadowRoot(意味着它并没有与外界隔离样式)。
class SimpleComponent extends HTMLElement {
constructor() {
super();
this.innerHTML = `<p>Hello, World!</p>`;
}
}
customElements.define('simple-component', SimpleComponent);
下面是一个内容更丰富一些的示例,有基础的大概过一眼也知道大概了。
// 1.自定义标签都是用class 的形式去继承 class myDiv extends HTMLElement { // 监听 static get observedAttributes() { return ['option'] } constructor() { super() // 这样我们才能够去追加元素 this.attachShadow({ mode: 'open' }) } // 重要:生命周期方法 开始 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 => { // 重要:冒泡的顺序,通过这个可以判断有没有在鼠标内部进行点击 if (e.composedPath().includes(this)) { console.log('点击了里面') } }) this.shadowRoot.querySelector('.content').addEventListener('click', e => { console.log('e: ', e) // window.dispatchEvent }) } // 重要:生命周期方法 重新渲染 .甚至还是第一次进行渲染,比connect还快 // 会重新渲染 connectCallback attributeChangedCallback(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 的样式绝对隔离 // 重要: :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 } ` this.shadowRoot.appendChild(nodeTemplate.content) this.shadowRoot.appendChild(nodeStyles) setTimeout(() => { this.borderAdd() }, 3000) } } // 名字必须小写 驼峰必须要转成横线 customElements.define('my-div', myDiv)
一个Web组件可以有且仅有一个shadowRoot
。shadowRoot
是与该组件关联的影子DOM的根节点。当使用attachShadow
方法创建影子DOM时,它会返回一个shadowRoot
对象,这个对象是唯一的,并且与创建它的元素关联。
例如:
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);
在这个例子中:
MyComponent
类扩展了HTMLElement
,定义了一个Web组件。this.attachShadow({ mode: "open" })
创建了一个shadowRoot
,并将其存储在this.shadow
变量中。shadowRoot
是唯一的,并且与MyComponent
实例关联。关键点:
shadowRoot
。shadowRoot
与创建它的Web组件实例是紧密关联的,不能被其他组件实例访问。因此,尽管可以在shadowRoot
内创建多个子元素和结构,但每个Web组件实例只能有一个shadowRoot
。这有助于保持组件的封装性和独立性。
connectedCallback
:
componentDidMount
。disconnectedCallback
:
componentWillUnmount
。attributeChangedCallback
:
componentDidUpdate
,但是它是针对属性而不是状态。adoptedCallback
:
observedAttributes
属性数组,可以指定哪些属性的更改应该触发attributeChangedCallback
。connected
和 disconnected
属性:
shadowRoot
属性:
shadowRoot
属性,它是一个Shadow DOM树的根。可以在这个属性上使用生命周期回调来管理Shadow DOM的创建和更新。constructor
:
一般知道了上面的基础,就可以写wc组件了,但实际开发中,肯定还是需要借助一些已有的开发框架来辅助开发,而Lit就是目前最成熟且使用量最高的。
Web组件的更新并不是每次都进行全量更新。Web组件的更新机制非常灵活,能够根据组件的状态和属性的变化来决定是否需要更新。以下是一些关键点:
属性变化触发更新:
attributeChangedCallback
方法来处理这些变化。状态变化触发更新:
@property
装饰器定义的属性发生变化时,会触发更新。生命周期方法:
connectedCallback
, disconnectedCallback
, adoptedCallback
, firstUpdated
, updated
等,都可以在特定时机触发更新。选择性更新:
requestUpdate
方法来请求更新,而不必每次都进行全量更新。虚拟DOM:
优化性能:
自定义渲染逻辑:
render
方法中手动决定哪些部分需要重新渲染。条件渲染:
以下是一个使用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);
在这个示例中:
message
属性使用@property
装饰器定义,当其值发生变化时,会触发组件的更新。render
方法定义了组件的渲染逻辑,只有当message
属性发生变化时,相关的部分才会重新渲染。updated
方法在组件更新后被调用,可以在这里处理更新后的逻辑。通过这种方式,Web组件可以有效地控制更新过程,避免不必要的全量更新,从而提高性能。
Lit 相对于传统 Web 组件规范增加的一些生命周期钩子和特性:
render
方法:
render
方法是一个返回组件模板的函数,Lit 会根据这个方法的内容来渲染组件的 UI。update
方法:
shouldUpdate
方法:
false
,则跳过更新。willUpdate
方法:
updated
方法:
firstUpdated
方法:
connectedCallback
有些相似,但专门用于处理首次渲染后的逻辑。connectedCallback
:
disconnectedCallback
:
attributeChangedCallback
:
adoptedCallback
:
requestUpdate
方法:
updateComplete
Promise:
样式管理:
CSSResult
和 unsafeCSS
等 API,用于更安全和方便地管理组件的样式。属性装饰器:
@property
装饰器定义的属性会触发更新,并且可以指定属性的类型和是否同步到 DOM 属性。状态管理:
state
方法和 reactive
装饰器,提供了一种声明式的方式来管理组件的状态。那么基于以上,我们可以很容易地就实现利用Lit框架创造出一个webcomponent容器,然后用来包裹React组件。
import { LitElement, ReactiveElement, adoptStyles, unsafeCSS, PropertyValues } from 'lit' import { property } from 'lit/decorators.js' type ThrottleFn = (...args: any[]) => void type DelayFn = (fn: ThrottleFn) => void const throttleWith = <T extends ThrottleFn>( fn: T, delayFn: DelayFn, leading = false ): T => { let lastArgs: Parameters<T>, lastThis: unknown, isWaiting = false const throttledFn = (...args: Parameters<T>) => { lastArgs = args // eslint-disable-next-line lastThis = this if (!isWaiting) { if (leading) { fn.apply(lastThis, lastArgs) } isWaiting = true delayFn(() => { 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 = val this.adoptStyles() } /** * 使事件不能跨越ShadowDOM边界传播 */ @property({ type: Boolean, attribute: 'prevent-compose' }) protected preventCompose = false /** * 使事件不冒泡 */ @property({ type: Boolean, attribute: 'prevent-bubbles' }) protected preventBubbles = false // 应用样式 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) // 重新应用样式 } } this.renderRoot ? apply() : this.updateComplete.then(apply) }, (fn: any) => Promise.resolve().then(fn) ) // 派发事件 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 是否传入内容 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 HTMLElement if (!el.hasAttribute('slot')) { return true } } return false }) } // 各个生命周期 // 挂载时 connectedCallback() { super.connectedCallback() console.log('Custom element added to page.') // 第一次被插入文档时执行,跳过节点删除后又重新插入的情形 if (!this.hasUpdated) { this.setAttribute('wc-component', '') this.setAttribute('wc-pending', '') } } // 卸载时 disconnectedCallback() { super.disconnectedCallback() console.log('Custom element removed from page.') } // 移动到另一个文档的时候 adoptedCallback() { console.log('Custom element moved.') } // 元素的属性被添加、删除或修改时调用 attributeChangedCallback(name: string, oldValue: any, newValue: any) { super.attributeChangedCallback(name, oldValue, newValue) console.log(`Attribute ${name} has changed.`) } // 或使用静态属性代替get方法 static get observedAttributes() { // 指定要监听的元素的属性数组 // 对应的attr改变后,会触发attributeChangedCallback // return ['name', 'date'] return [] } // 是否应该更新 protected shouldUpdate(_changedProperties: PropertyValues): boolean { return true } // 即将更新 protected willUpdate(_changedProperties: PropertyValues): void { super.willUpdate(_changedProperties) console.log('willUpdate') } // 首次更新元素时调用。实现在更新后对元素执行一次性工作 protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties) console.log('this.hasUpdated: ', this.hasUpdated) // this.requestUpdate() // 两帧数后执行 requestAnimationFrame(() => { requestAnimationFrame(() => { this.removeAttribute('wc-pending') }) }) } protected updated(_changedProperties: PropertyValues): void { super.updated(_changedProperties) this.updateComplete.then((res) => { console.log('updateComplete', res) }) } }
import type { LitElement, PropertyValues } from 'lit' type Constructor<T> = new (...args: any[]) => T export 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 }
这段代码定义了一个高阶组件(Higher-Order Component,HOC),用于增强 LitElement 组件的功能。具体来说,它的作用是:
创建一个带有额外属性管理功能的组件类:
LitElement
或其子类),添加一个 props
属性来存储组件的属性值。在组件更新前处理属性变化:
willUpdate
生命周期方法,这个方法在组件的属性发生变化并且组件即将更新之前被调用。收集并存储属性变化:
changedProperties
对象(一个 Map 类型的对象,包含属性名和属性变化的信息)来收集属性的变化。this.props
对象中,这样可以通过 props
属性访问组件的所有属性值。保持基类的 willUpdate
方法的调用:
super.willUpdate(changedProperties)
以确保基类的 willUpdate
方法也能正常执行。代码详解
superClass
(应该是 LitElement
或其子类的构造函数)。WithPropertiesElement
,继承自 superClass
。WithPropertiesElement
类中定义了一个 props
属性,用于存储属性值。willUpdate
方法,在组件更新前处理属性变化,并将变化的属性存储到 this.props
中。WithPropertiesElement
类,并通过类型断言确保它具有额外的 props
属性。使用示例
假设你有一个基础的 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);
你可以使用这个高阶组件来增强它:
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 }
在这个示例中,EnhancedElement
继承自 MyElement
并添加了属性管理功能。可以通过 element.props
访问组件的所有属性值。
这种模式在需要在组件中统一管理属性或在组件更新前进行额外处理时非常有用。
重头戏来了
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 h const 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组件基类 export default class extends withProperties(LightBase) { // 子类要重写这个方法来渲染自己的组件 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?: HTMLElement updated(changed: PropertyValues) { super.updated(changed) this.doRender() } connectedCallback() { super.connectedCallback() // 节点删除后重新插入的情形 if (this.hasUpdated) { this.doRender() } } disconnectedCallback() { super.disconnectedCallback() this.doUnmount() } private container?: Element private doRender() { const container = this.customContainer() if (!container) { this.doUnmount() // 卸载前一次渲染的内容 } else { this.container = container ReactDOM.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中使用此组件且非首次更新时会报错,因为lit默认会在组件下创建一个注释节点,更新时会对这个节点进行操作,而React渲染时把这个注释节点干掉了,这里要把他加回去 const childPart = (this as any).__childPart as ChildPart | undefined childPart?.startNode && this.appendChild(childPart.startNode) }) } } private doUnmount() { if (this.container) { ReactDOM.unmountComponentAtNode(this.container) } } render() { return html` <div class="react-root"></div> ` } }
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中用来渲染React,不要删除 renderReact() { return <Component language={this.language} /> } }
https://juejin.cn/post/7296850940404580364?searchId=2024071620331848BC966F0D2051B9C533#heading-9
lit官网:https://lit.dev/docs/components/styles/
webcomponent文档:https://developer.mozilla.org/en-US/docs/Web/API/Web_components
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。