前端未来趋势之原生API:Web Components


声明:未经允许,不得转载。

Web Components 现世很久了,所以你可能听说过,甚至学习过,非常了解了。但是没关系,可以再重温一下,温故知新。

浏览器原生能力越来越强。

js

曾经的 JQuery,是前端入门必学的技能,是前端项目必用的一个库。它的强大之处在于简化了 dom 操作(强大的选择器) 和 ajax(异步) 操作。

现在原生 api querySelector()querySelectorAll()classList 等的出现已经大大的弱化了 dom 操作, fetch、基于 promiseaxios 已经完全替代了 ajax, 甚至更好用了,async-await 是真的好用。

You-Dont-Need-jQuery

css

css 预处理器(如 scssless) 是项目工程化处理 css 的不二选择。它的强大之处是支持变量样式规则嵌套函数

现在 css 已经支持变量(--var)了, 样式规则嵌套也在计划之中,函数嘛 calc() 也非常强大,还支持 attr() 的使用,还有 css-module 模块化。

不用预编译,CSS直接写嵌套的日子就要到了

w3c样式规则嵌套 css-nesting-module

以前要制作酷炫复杂的 css 样式及动画,必须借助 css 预处理器的变量、函数或者js才行,现在用 (css-doodle)[https://css-doodle.com/] 技术,实现的更酷、更炫。

css-doodle作品集

web components 组件化

Web Components 可以创建可复用的组件,未来的某一天抛弃现在所谓的框架和库,直接使用原生 API 或者是使用基于 Web Components 标准的框架和库进行开发,你觉得可能吗?我觉得是可能的。

vue-lit

vue-lit,描述如下:

Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.

描述用到了 custom elements,而且浏览器控制台 elements 的 DOM 结构中也含有 shadow-root。而 custom element 和 shadow DOM 是 web components 的重要组成。具体看下面 demo,

说明:本文文档示例,都是可以直接复杂到一个 html 文档的 body 中,然后直接在浏览中打开预览效果的。

  
  
  

源码解读

// lit-html 模板,提供 html 模板(简单js表达式及事件绑定)、render 渲染能力
import { render } from 'https://unpkg.com/lit-html?module'
// reactivity 是vue3.0的核心,shallowReactive 浅响应,effect 可以理解为 watch,提供属性响应及部分生命周期处理
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

let currentInstance

export function defineComponent(name, propDefs, factory) {
  if (typeof propDefs === 'function') {
    factory = propDefs
    propDefs = []
  }
  
  // 自定义元素 custom element,原生 API
  customElements.define(
    name,
    class extends HTMLElement {
      // 设置需要监听的属性
      static get observedAttributes() {
        return propDefs
      }
      constructor() {
        super()
        // 属性接入 vue 的响应式
        const props = (this._props = shallowReactive({}))

        currentInstance = this
        // lit-html 的 html 生成的模板
        const template = factory.call(this, props)
        currentInstance = null

        // bm onBeforeMount
        this._bm && this._bm.forEach((cb) => cb())
        // shadowRoot,closed 表示不可以直接通过 js 获取到定义的 customElement 操作 shadowRoot
        const root = this.attachShadow({ mode: 'closed' })

        let isMounted = false
        effect(() => {
          if (isMounted) {
            // _bu, onBeforeUpdate
            this._bu && this._bu.forEach((cb) => cb())
          }

          // 将 template 内容挂载到 shadowRoot 上
          render(template(), root)

          if (isMounted) {
            // _u,onUpdated
            this._u && this._u.forEach((cb) => cb())
          } else {
            isMounted = true
          }
        })
      }
      // 首次挂载到 dom 上后的回调,onMounted
      connectedCallback() {
        this._m && this._m.forEach((cb) => cb())
      }
      // 卸载, onUnmounted
      disconnectedCallback() {
        this._um && this._um.forEach((cb) => cb())
      }
      // 属性监听
      attributeChangedCallback(name, oldValue, newValue) {
        this._props[name] = newValue
      }
    }
  )
}

function createLifecycleMethod(name) {
  return (cb) => {
    if (currentInstance) {
      ;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
    }
  }
}

export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')

export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

shallowReactive 源码,函数注释已经表达的很清楚了,only the root level properties are reactive。对象只有根属性响应,换言之即,浅响应,和浅拷贝类似。

/**
 * Return a shallowly-reactive copy of the original object, where only the root
 * level properties are reactive. It also does not auto-unwrap refs (even at the
 * root level).
 */
export function shallowReactive(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}

effect 源码,粗略的可以看到里面有 dep 依赖,还有 oldValue、newValue 处理。

通过分析,vue-lit 应该是将 vue3.0 的响应式和 web components 做的一个尝试。用 lit-html 的原因时因为支持模板支持简单js表达式及事件绑定(原生template目前只有slot插槽)

css-doodle

实际上,前面介绍的 css-doodle 也是一个 web component。是浏览器原生就支持的。

示例:艺术背景图。

  

  
    :doodle { 
      @grid: 1x300 / 100vw 40vmin; 
      overflow: hidden;
      background: linear-gradient(rgba(63, 81, 181, .11), #673AB7);
    }

    align-self: flex-end;
    --h: @r(10, 80, .1);
    @random(.1) { --h: @r(85, 102, .1) }

    @size: 1px calc(var(--h) * 1%);
    background: linear-gradient(transparent, rgba(255, 255, 255, .4), transparent);
    background-size: .5px 100%;
    transform-origin: center 100%;
    transform: translate(@r(-2vmin, 2vmin, .01), 10%) rotate(@r(-2deg, 2deg, .01));
    
    :after {
      content: '';
      position: absolute;
      top: 0;
      @size: calc(2px * var(--h));
      transform: translateY(-50%) scale(.14);
      background: radial-gradient(@p(#ff03929e, #673ab752, #fffa) @r(40%), transparent 50%) 50% 50% / @r(100%) @lr() no-repeat;
    }
  

dom 结构:

input、select 等内建 html 元素

input、select 也是 web component。但是是内建的,默认看不到 shadowRoot 结构,需要打开浏览器控制台的设置,勾选Show user agent shadow DOM,才可以在控制台elements中看到其结构。

设置



dom 结构

web components 组件化由 3 部分组成。

  • Custom elements(自定义元素):一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们。
  • Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板)