基于 Web Component 跨框架组件解决方案
Web Component 基本介绍
Web Components 允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。
兼容性如下图所示:
Web Component Api
CustomElementRegistry.define()
customElements.define(name, constructor, options)
注册一个 custom element,该方法接受以下参数:
- name: 元素名称
- constructor: 自定义元素构造器。
- options:
可选参数,控制元素如何定义。目前有一个选项支持:- extends. 指定继承的已创建的元素。被用于创建自定义元素。
Element.attachShadow()
var shadowroot = element.attachShadow(shadowRootInit)
给指定的元素挂载一个 Shadow DOM,并且返回对 ShadowRoot 的引用:
-
shadowRootInit,包括下列字段:
-
mode 模式
-
open: 可以从 js 外部访问 shadow root 节点
element.shadowRoot // 返回一个 ShadowRoot 对象 -
closed: 拒绝从 js 外部访问关闭的 shadow root 节点
element.shadowRoot // 返回 null
-
-
delegatesFocus 焦点委托 一个布尔值,当设置为 true 时,指定减轻自定义元素的聚焦性能问题行为。 当 shadow DOM 中不可聚焦的部分被点击时,让第一个可聚焦的部分成为焦点,并且 shadow host(影子主机)将提供所有可用的 :focus 样式。
-
生命周期
connectedCallback:当 custom element 首次被插入文档 DOM 时,被调用。disconnectedCallback:当 custom element 从文档 DOM 中删除时,被调用。adoptedCallback:当 custom element 被移动到新的文档时,被调用。attributeChangedCallback: 当 custom element 增加、删除、修改自身属性时,被调用。
更多
Web Component 示例代码
示例一
<form>
<div>
<label for="cvc"
>Enter your CVC
<popup-info
img="img/alt.png"
data-text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."
></popup-info
></label>
<input type="text" id="cvc" />
</div>
</form>
// Create a class for the element
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super()
// Create a shadow root
const shadow = this.attachShadow({ mode: 'open' })
// Create spans
const wrapper = document.createElement('span')
wrapper.setAttribute('class', 'wrapper')
const icon = document.createElement('span')
icon.setAttribute('class', 'icon')
icon.setAttribute('tabindex', 0)
const info = document.createElement('span')
info.setAttribute('class', 'info')
// Take attribute content and put it inside the info span
const text = this.getAttribute('data-text')
info.textContent = text
// Insert icon
let imgUrl
if (this.hasAttribute('img')) {
imgUrl = this.getAttribute('img')
} else {
imgUrl = 'img/default.png'
}
const img = document.createElement('img')
img.src = imgUrl
icon.appendChild(img)
// Create some CSS to apply to the shadow dom
const style = document.createElement('style')
console.log(style.isConnected)
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`
// Attach the created elements to the shadow dom
shadow.appendChild(style)
console.log(style.isConnected)
shadow.appendChild(wrapper)
wrapper.appendChild(icon)
wrapper.appendChild(info)
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo)
示例二
<div>
<button class="add">Add custom-square to DOM</button>
<button class="update">Update attributes</button>
<button class="remove">Remove custom-square from DOM</button>
</div>
// Create a class for the element
class Square extends HTMLElement {
// Specify observed attributes so that
// attributeChangedCallback will work
static get observedAttributes() {
return ['c', 'l']
}
constructor() {
// Always call super first in constructor
super()
const shadow = this.attachShadow({ mode: 'open' })
const div = document.createElement('div')
const style = document.createElement('style')
shadow.appendChild(style)
shadow.appendChild(div)
}
connectedCallback() {
console.log('Custom square element added to page.')
updateStyle(this)
}
disconnectedCallback() {
console.log('Custom square element removed from page.')
}
adoptedCallback() {
console.log('Custom square element moved to new page.')
}
attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom square element attributes changed.')
updateStyle(this)
}
}
customElements.define('custom-square', Square)
function updateStyle(elem) {
const shadow = elem.shadowRoot
shadow.querySelector('style').textContent = `
div {
width: ${elem.getAttribute('l')}px;
height: ${elem.getAttribute('l')}px;
background-color: ${elem.getAttribute('c')};
}
`
}
const add = document.querySelector('.add')
const update = document.querySelector('.update')
const remove = document.querySelector('.remove')
let square
update.disabled = true
remove.disabled = true
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
add.onclick = function () {
// Create a custom square element
square = document.createElement('custom-square')
square.setAttribute('l', '100')
square.setAttribute('c', 'red')
document.body.appendChild(square)
update.disabled = false
remove.disabled = false
add.disabled = true
}
update.onclick = function () {
// Randomly update square's attributes
square.setAttribute('l', random(50, 200))
square.setAttribute(
'c',
`rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`
)
}
remove.onclick = function () {
// Remove the square
document.body.removeChild(square)
update.disabled = true
remove.disabled = true
add.disabled = false
}
更多示例
Stencil 基本介绍
Stencil.js 是一个基于 Web Components 设计的框架,由它创建的组件库,可以轻松地在可以引入 Web Component 的浏览器和框架上运行
Stencil Api
修饰器
-
@Component(opts: ComponentOptions) 声明一个 web component
export interface ComponentOptions { /** * Tag name of the web component. Ideally, the tag name must be globally unique, * so it's recommended to choose a unique prefix for all your components within the same collection. * * In addition, tag name must contain a '-' */ tag: string /** * If `true`, the component will use scoped stylesheets. Similar to shadow-dom, * but without native isolation. Defaults to `false`. */ scoped?: boolean /** * If `true`, the component will use native shadow-dom encapsulation, it will fallback to `scoped` if the browser * does not support shadow-dom natively. Defaults to `false`. * * If an object literal containing `delegatesFocus` is provided, the component will use native shadow-dom * encapsulation. When `delegatesFocus` is set to `true`, the component will have `delegatesFocus: true` added to its * shadow DOM. When `delegatesFocus` is `true` and a non-focusable part of the component is clicked: * - the first focusable part of the component is given focus * - the component receives any available `focus` styling * Setting `delegatesFocus` to `false` will not add the `delegatesFocus` property to the shadow DOM and therefore * will have the focusing behavior described for `shadow: true`. */ shadow?: boolean | { delegatesFocus: boolean } /** * Relative URL to some external stylesheet file. It should be a `.css` file unless some * external plugin is installed like `@stencil/sass`. */ styleUrl?: string /** * Similar as `styleUrl` but allows one to specify different stylesheets for different modes. */ styleUrls?: string[] | d.ModeStyles /** * String that contains inlined CSS instead of using an external stylesheet. * The performance characteristics of this feature are the same as using an external stylesheet. * * Notice, you can't use sass, or less, only `css` is allowed using `styles`, use `styleUrl` if you need more advanced features. */ styles?: string /** * Array of relative links to folders of assets required by the component. */ assetsDirs?: string[] /** * @deprecated Use `assetsDirs` instead */ assetsDir?: string } -
@Prop(options: PropOptions) 声明暴露的 property/attribute
export interface PropOptions { /** * The name of the associated DOM attribute. * Stencil uses different heuristics to determine the default name of the attribute, * but using this property, you can override the default behaviour. */ attribute?: string | null /** * A Prop is _by default_ immutable from inside the component logic. * Once a value is set by a user, the component cannot update it internally. * However, it's possible to explicitly allow a Prop to be mutated from inside the component, * by setting this `mutable` option to `true`. */ mutable?: boolean /** * In some cases it may be useful to keep a Prop in sync with an attribute. * In this case you can set the `reflect` option to `true`, since it defaults to `false`: */ reflect?: boolean } -
@State() 生命组件内部状态
-
@Watch(propName: string) 监听器
-
@Element() 声明 host element 的引用
-
@Method() 声明暴露的方法
-
@Event(opts?: EventOptions) 声明事件发射器
export interface EventOptions { /** * A string custom event name to override the default. */ eventName?: string /** * A Boolean indicating whether the event bubbles up through the DOM or not. */ bubbles?: boolean /** * A Boolean indicating whether the event is cancelable. */ cancelable?: boolean /** * A Boolean value indicating whether or not the event can bubble across the boundary between the shadow DOM and the regular DOM. */ composed?: boolean } -
@Listen(eventName: string, opts?: ListenOptions) listens for DOM events 监听 DOM 事件
export interface ListenOptions { /** * Handlers can also be registered for an event other than the host itself. * The `target` option can be used to change where the event listener is attached, * this is useful for listening to application-wide events. */ target?: ListenTargetOptions /** * Event listener attached with `@Listen` does not "capture" by default, * When a event listener is set to "capture", means the event will be dispatched * during the "capture phase". Please see * https://www.quirksmode.org/js/events_order.html for further information. */ capture?: boolean /** * By default, Stencil uses several heuristics to determine if * it must attach a `passive` event listener or not. * * Using the `passive` option can be used to change the default behaviour. * Please see https://developers.google.com/web/updates/2016/06/passive-event-listeners for further information. */ passive?: boolean }
生命周期
- connectedCallback()
- disconnectedCallback()
- componentWillLoad()
- componentDidLoad()
- componentShouldUpdate(newValue, oldValue, propName): boolean
- componentWillRender()
- componentDidRender()
- componentWillUpdate()
- componentDidUpdate()
- render()
Stencil 示例代码
目录结构
stencil-library 代码
生成初始化项目
lerna init
mkdir packages
cd packages/
npm init stencil components stencil-library
cd stencil-library
# Install dependencies
npm install
应用打包
stencil.config.ts
import { Config } from '@stencil/core'
import { vueOutputTarget } from '@stencil/vue-output-target'
import { reactOutputTarget } from '@stencil/react-output-target'
export const config: Config = {
namespace: 'stencil-library',
outputTargets: [
reactOutputTarget({
componentCorePackage: 'stencil-library',
proxiesFile: '../react-app/src/components.ts',
includeDefineCustomElements: true,
}),
vueOutputTarget({
componentCorePackage: 'stencil-library', // i.e.: stencil-library
proxiesFile: '../vue-app/src/components.ts',
}),
{
type: 'dist',
esmLoaderPath: '../loader',
},
{
type: 'dist-custom-elements',
},
{
type: 'docs-readme',
},
{
type: 'www',
serviceWorker: null, // disable service workers
},
],
}
vue 项目引用
初始化项目
# From your top-most-directory/
lerna create vue-app
# or if you are using other monorepo tools, create a new Vue library
# Follow the prompts and confirm
cd packages/vue-app
# Install Vue dependency
npm install vue@3 --save-dev
# or if you are using yarn
yarn add vue@3 --dev
# Add the stencil-library dependency
lerna add stencil-library
引用
<script setup lang="ts">
import { MyComponent } from './components'
</script>
<template>
<div>
<MyComponent first="first" middle="middle" last="last"></MyComponent>
</div>
</template>
react 项目引用
初始化项目
# From your top-most-directory/
lerna create react-app
# or if you are using other monorepo tools, create a new Vue library
# Follow the prompts and confirm
cd packages/react-app
# Install Vue dependency
npx create-react-app
引用
import { MyComponent } from './components'
function App() {
return (
<div className="App">
<MyComponent></MyComponent>
</div>
)
}
export default App
jquery 项目引用
初始化项目
lerna create js-app
cd packages/js-app
npm i -D vite
index.html
<my-component id="my-component"></my-component>
<script type="module">
import { defineCustomElements } from 'stencil-library/loader'
defineCustomElements()
</script>