什么是 Web Components
开发项目的时候为什么不手写原生 JS,而是要用现如今非常流行的前端框架,原因有很多,例如:
- 良好的生态
- 数据驱动试图
- 模块化
- 组件化
- 等
Web Components 就是为了解决“组件化”而诞生的,它是浏览器原生支持的组件化,不依赖任何库、依赖和打包工具就可以在浏览器中运行。
Vue、React 的组件化并不是真正的组件化,虽然写代码时写的是确实的组件化代码,但是编译后就不再是组件化了。
例如用 Vue + ElementUI 开发的应用,ElementUI 的组件都是 el
开头的,如 <el-button>
,但编译后显示在页面上的就不再是 <el-button>
标签了。
这有点类似于 CSS 预处理器(如 Sass、Less),那些在开发阶段编译的变量(如 $color: red;
)其实并不是真正的变量,而是伪变量。在编译过后就没有变量的概念了,所以很难和 JS 通信。
例如有一个需求,在页面上给用户提供一个输入框,用户输入什么颜色(如 red
、#ff00000
),网站就会变成相应颜色的主题色,可是在我们获取到用户输入后,却没有变法将它们赋值给 Sass 变量上去。因为 Sass 代码在编译后已经变成了 CSS 代码,没有 Sass 变量了,例如 color: $color;
编译为 color: red;
。
所以此时就需要一个浏览器原生就支持的,不需要编译就能够运行的变量,于是 CSS 变量就出现了(--color: red;
、color: var(--color)
),可以非常方便地与 JS 进行通信,因为它是浏览器级别地原生变量。
同理,框架的组件化也不是真正的标准,每家都用自己的组件化标准,这就导致了生态的分裂,而且这些框架的组件化也都是靠编译才能实现的,并且非常依赖于这个框架,是一种共生的关系,就像使用 Vue 时,后缀以 .vue
结尾的文件,根本没有办法在浏览器中运行,必须下载 Node、Webpack、vue-loader 等工具进行打包,但还是无法在脱离 Vue 这个框架的安装包的情况下进行运行。
通常来说,浏览器厂商会吸收一些前端非常流行框架之中的可取之处,然后推动其成为标准,并在浏览器中原生实现这些功能,最经典的莫过于 jQuery 的 $()
选择器。
“都 21 世纪了,还提 jQuery?”
尽管这几年风生水起的 Vue 和 React 加剧了 jQuery 的没落,但全世界仍有超过 6600 万个网站在使用 jQuery,同时 jQuery 也给业界留下了产生深远影响的遗产,W3C 就仿照 $()
函数实现了 querySelector()
和 querySelectorAll()
方法。
而讽刺的是,也正是这两个原生方法的出现,大大加快了 jQuery 的没落,因为它们取代了 jQuery 最常用的功能之一:快捷的选择 DOM 元素。
那么浏览器原生支持的组件化会取代现在所流行的库或框架么?
还记得当 document.querySelector 最开始被广泛的被浏览器支持并且结束了无处不在的JQuery。这最终给我们提供了一个原生的方法,虽然JQuery已经提供了很久。我觉得这同样将会发生在像Angular和React这的前端框架身上。
这些框架可以帮助我们去做一些做不到的事情,比如创建可以复用的前端组件,但是这样需要付出复杂度、专属语法、性能消耗的代价。 但是这些将会得到改变。
现代浏览器的API已经更新到你不需要使用一个框架就可以去创建一个可服用的组件。Custom Element和Shadow DOM都可以让你去创造可复用的组件。
最早在2011年,Web Components就已经是一个只需要使用HTML、CSS、JavaScript就可以创建可复用的组件被介绍给大家。这也意味着你可以不使用类似React和Angular的框架就可以创造组件。甚至,这些组件可以无缝的接入到这些框架中。
那么事实真的是这样么,其实不然。
Web Components 与如今非常流行的 MVVM 框架是一种共存的关系,而不是一种互斥的关系,就像 Sass 变量和 CSS 变量,两者可以非常完美的互补,而不是说用了 CSS 变量就不能用 Sass 变量。
再者来说,我们用那些 MVVM 框架也并不仅仅只是为了它们的组件化功能,虽然组件化是其中非常重要的一项功能,但是还有页面路由、数据绑定、模块化、CSS 预处理器、虚拟 DOM、Diff 算法,以及各种庞大的生态等功能。
Web Components 要解决的仅仅只是组件化的这么一项功能。
React 和 Web Components 为了解决不同的问题而生。Web Components 为可复用组件提供了强大的封装,而 React 则提供了声明式的解决方案,使 DOM 与数据保持同步。两者旨在互补。作为开发人员,可以自由选择在 Web Components 中使用 React,或者在 React 中使用 Web Components,或者两者共存。
我们认为 Vue 和 Web Components 主要是互补的技术。Vue 为使用和创建定制元素提供了出色的支持。无论你是将自定义元素集成到现有的 Vue 应用程序中,还是使用 Vue 来构建和分发自定义元素都很方便。
从 Vue 和 React 官网可以看到它们都对 Web Components 有很好的支持。
可以在 Custom Elements Everywhere 查看“自定义元素”(Web Components 的功能)在各个框架中的互操作性得分。
Making sure frameworks and custom elements can be BFFs
翻译:确保框架和自定义元素可以成为永远的最好的朋友(Best Friend Forever)。
既然 WebComponents 不会取代前端框架,那为什么还要学习?
一方面,浏览器原生支持的组件化我们肯定是需要了解的,这是为了跟上时代的步伐。
另一方面,那些流行框架想要顺应时代的趋势,不被大浪淘沙的淘汰掉,也必须要符合浏览器的标准,才能够在前端领域成为常青树。
而且我们无法保证现在流行的框架在几年后依然流行,当初用 jQuery 的大部分人也觉得有生之年它不会被替代。
Web Components 最重要的一点,就是组件化的概念其实是相通的,并不是一个全新的很复杂的概念,所以如果你用过 MVVM 框架,Web Components 其实是非常易于理解的,并不像学习一门全新框架那么耗费精力。而且如果你用过 Vue,那么 Web Components 简直就是手到擒来,因为尤雨溪在创建 Vue 的时候就大量参考了 Web Components 的语法,导致 Web Components 现在的写法有一部分和 Vue 长得几乎一摸一样。
Web Components 还有一个难能可贵的一点,就是它并不是一门单一的技术,它总共有三种规范:
- HTML Template:HTML 模板
- Custom Elements:自定义元素
- Shadow DOM:影子 DOM
第四个规范 HTML Imports 已被弃用,原因是该规范从未在任何浏览器中实现,已经被 ES Modules 取代。
参考:
其中有 Custom Elements 和 Shadow DOM 都是可以在不依赖 Web Components 的情况下用在其他地方上去的,也就是说只要你的想象力足够丰富,脑洞够大,就可以利用它们把 DOM 玩出花来。
下面挑选了市面上既好玩,颜值又高的组件库来体验以下 Web Components:
- css-doodle:直译 - css 涂鸦
- fancy-components:直译 - 花式组件库,可惜没有官方文档
css-doodle
官方网站:css-doodle.com
<!DOCTYPE html>
<html lang="en">
<head>
<title>css-doodle 静态效果</title>
<script src="https://unpkg.com/css-doodle"></script>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<css-doodle>
:doodle {
@grid: 20 / 100vmax;
background: #12152f;
}
::after {
content: '\@hex(@rand(0x2500, 0x257f))';
font-size: 5vmax;
color: hsla(@rand(360), 70%, 70%, @rand(.9));
}
</css-doodle>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>css-doodle 动态效果</title>
<script src="https://unpkg.com/css-doodle"></script>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
background: #011627;
display: grid;
place-items: center;
}
</style>
</head>
<body>
<css-doodle>
:doodle {
@grid: 1x1x100 / 100vmax;
animation: r 23s linear infinite;
}
@size: 100% 50%;
position: absolute;
top: 25%;
transform: rotate(@r(360deg));
perspective: @r(100px, 200px);
::after {
content: '';
position: absolute;
@size: @r(.5vmin, 5vmin);
color: @p(#fdffc2, #2ec4b6, #e71d36, #ff9f1c);
background: currentColor;
box-shadow: @m2(0 0 1.2vmin currentColor);
animation: cycle @r(2s) linear infinite;
--trans: scaleX(@r(1, 5)) translateZ(@r(10vmin, 20vmin));
transform: rotateY(0) @var(--trans);
}
:empty::after { display: none; }
@keyframes cycle {
to {
transform: rotateY(@p(-1turn, 1turn)) @var(--trans)
}
}
@keyframes r {
to {
transform: rotate(1turn)
}
}
</css-doodle>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>css-doodle 水波效果</title>
<script src="https://unpkg.com/css-doodle"></script>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
background: radial-gradient(#459dc1, #070729);
display: grid;
place-items: center;
}
</style>
</head>
<body>
<!-- 这个效果比较复杂,只能运行在 Google 内核的浏览器,Firefox 或 Safari 看不到效果 -->
<css-doodle>
:doodle {
@grid: 80x1 / 100vw 100vh;
@min-size: 100px;
filter: url(#filter);
animation: r 23s linear infinite;
}
@size: 100% 50%;
position: absolute;
top: 25%;
transform: rotate(@r(360deg));
perspective: 130px;
::after {
content: '';
position: absolute;
@size: @r(10px);
background: #fff;
box-shadow: @m3(0 0 calc(.5vmin + 5px) #fff);
animation: cycle @r(2s, 8s) linear infinite;
animation-delay: -@r(100s);
--trans: scaleX(@r(.1, 5)) translateZ(105px);
transform: rotateY(0) @var(--trans);
}
@keyframes cycle {
to {
transform: rotateY(@p(-1turn, 1turn)) @var(--trans)
}
}
@keyframes r {
to {
transform: rotate(@p(-1turn, 1turn))
}
}
</css-doodle>
<svg style="width: 0;height: 0">
<filter id="filter">
<feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur"></feGaussianBlur>
<feColorMatrix in="blur" mode="matrix" values="
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 18 -7
"
result="goo"
></feColorMatrix>
<feBlend in="SourceGraphic" in2="goo"></feBlend>
</filter>
</svg>
</body>
</html>
css-doodle 是用 Web Components 技术做出来的优秀案例,不过由于在 css-doodle 中写的代码并不是 Web Components 的语法,而是 css-doodle 通过 Web Components 技术拿到代码内容,然后用正则表达式解析出来的,属于它的自创语法。开发者可以自行去官网学习这些与 Web Components 无关的语法。
css-doodle 适合做背景,虽然 css-doodle 很炫,但它依然需要开发者自己写样式,而且与我们平时开发时所需要的组件(按钮、轮播图、文本输入框等)相差较大。
fancy-components 就是一款可以提供这些常用组件,可以用于日常开发的组件库。
fancy-components
官方网站:https://fancy-components.gitee.io/home-page/
<!DOCTYPE html>
<html lang="en">
<head>
<title>fancy-components</title>
<style>
* {
padding: 0; margin: 0; }
html, body {
height: 100%; }
body {
display: grid;
place-items: center;
background: #3f2766;
}
fc-3d-btn {
--color: #6e50a6;
--shadow-color: rgba(255, 255, 255, .4);
--inset-shadow-color: #315;
--inset-shadow-color-active: rgba(49, 23, 7, .9);
--cover-color: rgba(0, 0, 0, .4);
}
</style>
</head>
<body>
<div>
<!-- html 只支持小写标签,不支持驼峰命名法 -->
<fc-input white placeholder="Username"></fc-input>
<br />
<fc-input white disabled value="fancy components" placeholder="Username"></fc-input>
<br />
<fc-btn>fancy-components</fc-btn>
<br />
<fc-warp-btn></fc-warp-btn>
<br />
<fc-3d-btn></fc-3d-btn>
<br />
<fc-underline-btn></fc-underline-btn>
<br />
<fc-pixel-btn></fc-pixel-btn>
<br />
<fc-parentheses-btn></fc-parentheses-btn>
<br />
<fc-round-btn></fc-round-btn>
<br />
<fc-arrow-btn></fc-arrow-btn>
<br />
<fc-bubbles click>
<fc-parentheses-btn>撒花</fc-parentheses-btn>
</fc-bubbles>
</div>
<script type="module">
import {
FcTypingInput } from 'http://unpkg.com/fancy-components'
import {
FcDblWarpBtn } from 'http://unpkg.com/fancy-components'
import {
FcWarpBtn } from 'http://unpkg.com/fancy-components'
import {
Fc3DBtn } from 'http://unpkg.com/fancy-components'
import {
FcUnderlineBtn } from 'http://unpkg.com/fancy-components'
import {
FcPixelBtn } from 'http://unpkg.com/fancy-components'
import {
FcParenthesesBtn } from 'http://unpkg.com/fancy-components'
import {
FcRoundBtn } from 'http://unpkg.com/fancy-components'
import {
FcArrowBtn } from 'http://unpkg.com/fancy-components'
import {
FcBubbles } from 'http://unpkg.com/fancy-components'
// 注册组件
// 可以传递一个重命名组件名的字符串,必须是小写,且用 `-` 连接
// 不传参数默认组件名就是 fc-typing-input
new FcTypingInput('fc-input')
new FcDblWarpBtn('fc-btn')
new FcWarpBtn()
new Fc3DBtn()
new FcUnderlineBtn()
new FcPixelBtn()
new FcParenthesesBtn()
new FcRoundBtn()
new FcArrowBtn()
new FcBubbles()
</script>
</body>
</html>
在脚手架中使用 Web Components 组件库
Vue 2 中使用
npm i -g @vue/cli
vue create vue2-app
cd vue2-app
npm i fancy-components
npm run serve
// src\main.js
import Vue from 'vue'
import App from './App.vue'
import {
FcBubbles } from 'fancy-components'
// 禁用 no-new 校验规则
/* eslint-disable no-new */
new FcBubbles()
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
<!-- src\App.vue -->
<template>
<div id="app">
<fc-bubbles click><img alt="Vue logo" src="./assets/logo.png"></fc-bubbles>
<!-- <FcBubbles click><img alt="Vue logo" src="./assets/logo.png"></FcBubbles> -->
</div>
</template>
...
Web Components 原生组件的地位和 HTML 标签的地位是相同的,大写的驼峰命名组件会被当做 Vue 组件,原生组件要和 HTML 标签一样,不要写成驼峰命名。React 框架中也一样。
Vue CLI 旧版本中使用 Web Components 控制台可能会发出警告,原因是 Vue 将 原生组件当作 Vue 组件去判断,警告组件没有注册,解决办法就是配置
ignoredElements
让 Vue 忽略原生组件:Vue.config.ignoredElements = [ // 正则匹配 /^fc-/, // 或字符串 'css-coodle' ]
Vue 3 中使用
vue create vue3-app
cd vue3-app
npm i fancy-components
npm run serve
// src\main.js
import {
createApp } from 'vue'
import App from './App.vue'
import {
FcBubbles } from 'fancy-components'
/* eslint-disable no-new */
new FcBubbles()
createApp(App).mount('#app')
<!-- src\App.vue -->
<template>
<fc-bubbles click><img alt="Vue logo" src="./assets/logo.png"></fc-bubbles>
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
使用上如 Vue2 一样,但实际上会报错:
[Vue warn]: Failed to resolve component: fc-bubbles
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
原因与 Vue CLI 旧版本创建的 Vue2 应用一样,解决办法依然是配置忽略原生组件(自定义元素),参考:Vue 与 Web Components
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => ({
...options,
compilerOptions: {
// 将所有 fc- 开头的标签名都视为自定义元素
isCustomElement: tag => tag.startsWith('fc-')
}
}))
}
}
重启应用,警告已经不见了,但是点击仍然没有生效,打开 Element 面板发现组件的 click
属性并没有添加上,而其他属性如 click1
可以添加,这可能是因为 Vue3 认为 click
是一个不能直接添加的关键字,测试发现只需将 click
改成大写 Click
即可添加上。
<fc-bubbles Click><img alt="Vue logo" src="./assets/logo.png"></fc-bubbles>
在 Vite 中使用 Web Components 组件库
# npm 6.x
npm create vite@latest vite-vue-app
√ Select a framework: » vue
√ Select a variant: » vue
cd vite-vue-app
npm install
npm i fancy-components
npm run dev
// src/main.js
import {
createApp } from 'vue'
import App from './App.vue'
import {
FcBubbles } from 'fancy-components'
new FcBubbles()
createApp(App).mount('#app')
<!-- src\App.vue -->
<!-- 注意 Click 大写 -->
<fc-bubbles Click><img alt="Vue logo" src="./assets/logo.png" /></fc-bubbles>
还要配置忽略的自定义元素:
// vite.config.js
import {
defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 将所有 fc- 开头的标签名都视为自定义元素
isCustomElement: tag => tag.startsWith('fc-')
}
}
})
]
})
不需要重启即可生效。