web components
什么是 Web Components
2011年 谷歌就提出 web components
2015 年 web components 才开始能用,只有Chrome浏览器支持
web components 浏览器原生组件化,不依赖框架
safari 2017 年实现一部分
Firfox 2018 年实现
Vue cli 已经实现编译成 web components 的功能,实际上 vue 在很大程度上参照了 web components 的实现,vue 可以说是进阶版的 web components
web components 包含技术
Web Components包含三种主要技术:
HTML Templates
熟悉 vue 的开发者对 template 模板这个概念会很熟悉了,模板、组件方便重用
<template>
和 <slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用,和vue很像
自定义组件
<template>
<span>
<!-- slot 用法也是和vue的slot用法一样 -->
<slot></slot>
</span>
</template>
<script>
const dom = document.currentScript.ownerDocument.querySelector('template')
customElements.define('my-span', class extends HTMLElement {
constructor() { // 初始化阶段
super()
// 相当于vue的setup
this.appendChild(dom)
}
})
</script>
<style>
span {
color: green;
}
</style>
在html页面中使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- link引用组件,可能会不生效,因为 HTML Imports 🈲 被废弃 -->
<link rel="import" href="./span.html" />
</head>
<body>
<!-- 在页面中使用自定义的标签 -->
<my-span>123</my-span>
<!-- 还可以用is属性,和vue里的类似 -->
<span is="my-span">456</span>
</body>
</html>
使用 HTML Modules
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import MySpan from './span.html'
</script>
<!-- 在页面中使用自定义的标签 -->
<my-span>123</my-span>
<!-- 还可以用is属性,和vue里的类似 -->
<span is="my-span">456</span>
</body>
</html>
出现报错,目前还未支持html的 module scripts
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
Custom Elements
自定义元素,开发者可以创建自己的DOM元素,扩展 HTMLElement 接口,继承自 HTMLElement, 并自定义一些 JS 逻辑。
一般继承自 HTMLElement 但是如果想要扩展某个元素,则应该继承该元素,例如想要自定义 input,则需要继承 input
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web-components</title>
</head>
<body>
<script>
// 创建一个 <a-b-c> 的元素,名为 el
const el = document.createElement('a-b-c')
// 定义一个名为 <a-b-c> 的组件
customElements.define('a-b-c', class extends HTMLElement {})
// 获取 <a-b-c> 组件的构造函数
customElements.get('a-b-c')
// 升级我们之前创建的 el 元素
customElements.upgrade(el)
// 当 <a-b-c> 组件定义后
customElements.whenDefined('a-b-c').then(() => { /* 当 <a-b-c> 组件定义后的回掉函数 */ })
</script>
</body>
</html>
Shadow DOM
一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
HTML Imports (废弃)
之前 HTML imports 也是其中的一部分,但对于 HTML Modules (ES6),HTML Imports 已经被废弃
生命周期
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<iframe src="./iframe.html"></iframe>
<life-cycle color="purple"></life-cycle>
<script>
const iframe = document.querySelector('iframe')
iframe.onload = () => {
const h1 = iframe.contentDocument.querySelector('h1')
document.body.append(document.adoptNode(h1))
}
customElements.define('life-cycle', class extends HTMLElement {
getColor() {
return this.getAttribute('color')
}
setColor(value) {
this.setAttribute('color', value)
}
static observedAttributes = ['color']
constructor() { // 初始化阶段
super()
// 相当于vue的setup
this.innerHTML = '<h1>组件</h1>'
}
connectedCallback() { // 挂载阶段
// 相当于vue的mouted
}
disconnectCallback() { // 卸载阶段
// 相当于vue的unmounted
}
adoptedCallback() { // 收养阶段
// this.innerHTML = '<h1>来自iframe的组件</h1>'
}
attributeChangedCallback(name, oldValue, newValue) { // 属性改变后的阶段
// attribute 特性
// proporty 属性
if (name === 'color') {
this.style.color = newValue
}
}
})
</script>
</body>
</html>
使用
web components 是浏览器原生组件,所以是不依赖框架,可以在原生、vue、react等都可以使用,在react中可直接使用,vue、vite中需要声明是自定义元素
react组件都是大写开头驼峰命名的组件,原生组件是必须小写且以-中划线连接,所以不需要做识别处理,原生组件不会被识别为react组件,vue 则需要指定哪些是自定义的原生组件,否则会根据vue的组件编译规则来查找,出现找不到的情况
fancy-components 是用 web components 实现的组件,使用以此为例
原生
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web-components</title>
</head>
<body>
<fc-3d-btn></fc-3d-btn>
<script type="module">
import { Fc3DBtn } from 'https://unpkg.zhimg.com/fancy-components'
// new 就相当于全局注册了这个组件,相当于 Vue 的 Vue.component('Fc3DBtn', 组件)
new Fc3DBtn()
</script>
</body>
</html>
react
src/index.js 入口文件里注册,使用就直接用对应的标签
import {
FcTypingInput
} from 'fancy-components'
new FcTypingInput()
vue
main.js 入口文件里注册,使用就直接用对应的标签,注册使用同上
vue.config.js 添加额外配置
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = {
...(options.compilerOptions || {}),
isCustomElement: tag => tag.startsWith('fc-') // 指定以fc开头的是自定义元素(原生组件),不是vue的组件
}
return options
})
}
}
vite
main.js 入口文件里注册,使用就直接用对应的标签,注册使用同上
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('fc-')
}
}
})]
})
兼容
由于 web components 各个浏览器的支持还不是特别好,如果想要使用,还是需要polyfills来做兼容
@webcomponents/webcomponentsjs 这个库可以用来做兼容