写完本篇文章之后我曾在掘金上搜过类似的文章。感觉我的文章和别人写的没啥两样。所以我就删掉从不同的角度重写了一遍,解决了一个很重要的问题,怎么才能像使用React组件那样使用WebComponents组件?
为什么很重要呢?举个例子:
如果使用普通的WebComponents组件,应该怎样添加事件呢?就像这样:
...
counterDOM.addEventListener('myChange',function(e){
console.log("App -> e", e.detail.value);
})
return (<my-counter max={10} min={-10} ref={counterDOM} ></my-counter>)
...
说句实话,大家不觉得这样的写法很丑陋吗? 那这样的组件还有谁会去用呢?
那如果是这样呢?是不是就好多了。
<Counter
onMyChange={(e) => {
console.log("App -> e", e.detail.value);
}}
max={10}
min={-10}
></Counter>
下面的动图展示了实际的效果:
接下来我们就实现这个组件。
my-counter 的实现
我没有使用原生的WebComponents的写法,而是使用了Lit 这个WebComponents库。Lit 建立在 Web Components 标准之上,压缩后的大小只有5KB,并且渲染速度非常快,因为 Lit 在更新时只接触 UI 的动态部分——无需重建虚拟树并将其与 DOM 进行比较。而且每个 Lit 组件都是原生 Web 组件。
下载Lit:
npm i lit
接下来书写我们的 my-counter 组件:
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { emit } from "./tool";
import styles from "./counter.style";
@customElement("my-counter")
export default class MyCounter extends LitElement {
static styles = styles;
@property({ type: Number })
value = 0;
@property({ type: Number })
min: number = -Infinity;
@property({ type: Number })
max: number = Infinity;
handleIncrease() {
this.value = Math.min(this.value + 1, this.max);
this.requestUpdate();
emit(this, "myChange", {
detail: {
value: this.value,
},
});
}
handleReduce() {
this.value = Math.max(this.value - 1, this.min);
this.requestUpdate();
emit(this, "myChange", {
detail: {
value: this.value,
},
});
}
render() {
return html`
<div class="counter-container">
<button class="button-left" @click=${this.handleReduce}>-</button>
<p class="value-show">${this.value}</p>
<button class="button-right" @click=${this.handleIncrease}>+</button>
</div>
`;
}
}
declare global {
namespace JSX {
interface IntrinsicElements {
"my-counter": any;
}
}
}
编写样式文件:
import { css } from "lit";
export default css`
.counter-container {
box-sizing: border-box;
width: 120px;
height: 32px;
opacity: 1;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
}
.button-left,
.button-right {
outline: none;
border: 0;
width: 31px;
height: 30px;
cursor: pointer;
background-color: white;
}
.button-left {
border-right: 1px solid #ccc;
}
.button-right {
border-left: 1px solid #ccc;
}
.value-show {
margin: 0;
width: 50px;
text-align: center;
}
`;
现在我们已经写完了一个WebComponents了,现在可以使用它了:
const App = ()=>(<my-counter></my-counter>)
接下来我们就看看怎样像使用React组件一样使用它。
转换为React组件
我们先写一个转换函数:
// 保存事件函数的MAP
const listenedEvents = new WeakMap();
const addOrUpdateEventListener = (node, event, listener) => {
// 根据DOM获取事件对象
let events = listenedEvents.get(node);
// 如果没有则设置新的
if (events === undefined) {
listenedEvents.set(node, (events = new Map()));
}
// 根据传递进来的对象获取函数
let handler = events.get(event);
if (listener !== undefined) {
// 有传递函数
if (handler === undefined) {
// 没有获取到函数 添加新的函数
events.set(event, (handler = { handleEvent: listener }));
node.addEventListener(event, handler);
} else {
// 更新函数
handler.handleEvent = listener;
}
} else if (handler !== undefined) {
// 删除
events.delete(event);
node.removeEventListener(event, handler);
}
};
const setRef = (ref, value) => {
if (typeof ref === "function") {
ref(value);
} else {
ref.current = value;
}
};
const setProperty = (node, name, value, old, events) => {
// 获取事件函数
const event =
events === null || events === undefined ? undefined : events[name];
if (event !== undefined) {
// 函数不一样添加监听
if (value !== old) {
addOrUpdateEventListener(node, event, value);
}
} else {
node[name] = value;
}
};
const conversionComponents = (React, tagName, elementClass, events) => {
const { Component, createElement } = React;
// 获取传递给WebComponents的Props,包括事件
const classProps = new Set(
Object.keys(events !== null && events !== void 0 ? events : {})
);
// 获取传递给WebComponents的Props,包括事件
for (const p in elementClass.prototype) {
if (!(p in HTMLElement.prototype)) {
classProps.add(p);
}
}
class ReactComponent extends Component {
constructor(props) {
super(props);
// 保存DOM的引用
this.DOM = null;
}
_updateElement(oldProps) {
if (this.DOM === null) {
return;
}
// 将新的Props值传递给自定义元素,并且设置事件的监听
for (const prop in this.DOMProps) {
setProperty(
this.DOM,
prop,
this.props[prop],
oldProps ? oldProps[prop] : undefined,
events
);
}
}
componentDidMount() {
this._updateElement();
}
componentDidUpdate(old) {
this._updateElement(old);
}
render() {
// 获取WebComponents组件的DOM
const userRef = this.props.__forwardedRef;
if (this._ref === undefined || this._userRef !== userRef) {
this._ref = (value) => {
if (this.DOM === null) {
this.DOM = value;
}
if (userRef !== null) {
setRef(userRef, value);
}
this._userRef = userRef;
};
}
// 设置最新的Props
const props = { ref: this._ref };
this.DOMProps = {};
for (const [k, v] of Object.entries(this.props)) {
if (classProps.has(k)) {
this.DOMProps[k] = v;
}
}
return createElement(tagName, props);
}
}
// 转发Ref
return React.forwardRef((props, ref) =>
createElement(
ReactComponent,
{ ...props, __forwardedRef: ref },
props === null || props === undefined ? undefined : props.children
)
);
};
export default conversionComponents;
这转换函数主要的思路就是将我们自定元素经过createElement函数转化为React元素,然后将给这个元素添加的Props进行转换编辑,如果传递的是事件函数,那我们就将这函数添加到自定义元素的DOM上即可。这样就不用写之前那也丑陋的写法了。
接下来就是转换我们的WebComponents组件:
import React, { FC } from "react";
import conversionComponents from "./conversionComponents";
import MyCounter from "./counter";
interface PropCounter {
max?: number;
min?: number;
onMyChange?: (e: { detail: { value: number } }) => void;
}
const Counter: FC<PropCounter> = conversionComponents(
React,
"my-counter",
MyCounter,
{
onMyChange: "myChange",
}
);
export { Counter };
使用:
import React, { useState } from "react";
import { Counter } from "./components";
function App() {
const [num, setNum] = useState<number>(0);
return (
<div>
<h1>Destiny__ {num}</h1>
<Counter
onMyChange={(e) => {
console.log("App -> e", e.detail.value);
setNum(e.detail.value);
}}
max={10}
min={-10}
></Counter>
</div>
);
}
export default App;
经过这个转换函数之后,我们就可以像使用正常的组件一样使用WebComponents组件了!!
感兴趣的同学可是尝试一下。遇到问题欢迎下方留言!!随时解答。 另外如果你不想使用Lit这个库,原生也是可以实现这个功能的!只不过要改写转换函数而已!!