zoukankan      html  css  js  c++  java
  • 前端未来趋势之原生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 中,然后直接在浏览中打开预览效果的。

      <my-component />
      
      <script type="module">
        import {
          defineComponent,
          reactive,
          html,
          onMounted
        } from 'https://unpkg.com/@vue/lit@0.0.2';
    
        defineComponent('my-component', () => {
          const state = reactive({
            text: 'Hello World',
          });
          
          function onClick() {
            alert('cliked!');
          }
    
          onMounted(() => {
            console.log('mounted');
          });
    
          return () => html`
            <p>
              <button @click=${onClick}>Click me</button>
              ${state.text}
            </p>
          `;
        })
      </script>
    

    源码解读

    // 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<T extends object>(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。是浏览器原生就支持的。

    示例:艺术背景图

      <script src="https://unpkg.com/css-doodle@0.8.5/css-doodle.min.js"></script>
    
      <css-doodle>
        :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;
        }
      </css-doodle>
    

    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模板)<template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

    Custom elements

    用户可以使用 customElements.define 自定义 html 元素。

    customElements.define(elementName, class[, extendElement]);
    
    • elementName: 名称不能是单个单词,必须用短横线分隔。
    • class: 用以定义元素行为的类,包含生命周期。
    • extendElement: 可选参数,一个包含 extends 属性的配置对象,指定创建元素继承哪个内置 HTML 元素

    根据定义,得出有两种 custom element:

    • Autonomous custom elements: 独立元素,不继承内建的HTML元素。和 html 元素一样使用,例如<custom-info></custom-info>
    • Customized built-in elements: 继承内建的HTML元素。使用先写出内建html元素便签,通过 is 属性指定 custom element 名称,例如<p is="custom-info"></p>

    还有生命周期:

    • connectedCallback:当 custom element首次被插入文档DOM时,被调用。
    • disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
    • adoptedCallback:当 custom element被移动到新的文档时,被调用。
    • attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。

    示例:独立元素。

      <button onclick="changeInfo()">更改内容</button>
      <custom-info text="hello world"></custom-info>
    
      <script>
        // Create a class for the element
        class CustomInfo extends HTMLElement {
          // 必须加这个属性监听,返回需要监听的属性,才能触发 attributeChangedCallback 回调
          static get observedAttributes() {
            return ['text'];
          }
    
          constructor() {
            // Always call super first in constructor
            super();
    
            // Create a shadow root
            const shadow = this.attachShadow({mode: 'open'});
            // Create p
            const info = document.createElement('p');
            info.setAttribute('class', 'info');
    
            // Create some CSS to apply to the shadow dom
            const style = document.createElement('style');
            console.log(style.isConnected);
    
            style.textContent = `
              .info {
                color: red;
              }
            `;
    
            // Attach the created elements to the shadow dom
            shadow.appendChild(style);
            console.log(style.isConnected);
            shadow.appendChild(info);
          }
    
          connectedCallback () {
            // 赋值
            this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
          }
    
          attributeChangedCallback(name, oldValue, newValue) {
            // TODO
            console.log(name, oldValue, newValue)
            this.shadowRoot.querySelector('.info').textContent = newValue
          }
        }
    
        // Define the new element
        customElements.define('custom-info', CustomInfo);
    
        function changeInfo() {
          document.querySelector('custom-info').setAttribute('text', 'custom element')
        }
      </script>
    

    示例:继承元素

    <p is="custom-info" text="hello world"></p>
    
      <script>
        // Create a class for the element,extend p element
        class CustomInfo extends HTMLParagraphElement {
          constructor() {
    
            super();
    
            const shadow = this.attachShadow({mode: 'open'});
            const info = document.createElement('span');
            info.setAttribute('class', 'info');
    
            const style = document.createElement('style');
            console.log(style.isConnected);
    
            style.textContent = `
              .info {
                color: red;
              }
            `;
    
            shadow.appendChild(style);
            console.log(style.isConnected);
            shadow.appendChild(info);
          }
    
          connectedCallback () {
            this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
          }
        }
    
        // Define the new element, extend p element
        customElements.define('custom-info', CustomInfo, {extends: 'p'});
      </script>
    

    更多,请参考:Custom elements

    Shadow DOM

    Web components 的重要功能是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,使代码更加干净、整洁。Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。

    附加到哪个元素上,和定义 custom element 时有关,如果是独立元素,附加到 document body 上;如果是继承元素,则附加到继承元素上。

    可以和操作普通 DOM 一样,利用 API 操作 Shoadow DOM。

    let shadow = elementRef.attachShadow({mode: 'open'});
    let shadow = elementRef.attachShadow({mode: 'closed'});
    

    open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,如'document.querySelector('custom-info').shadowRoot'。反之,获取不到。

    更多,请参考:Shadow DOM

    HTML templates

    template 和 slot 元素可以创建出非常灵活的 shadow DOM 模板,来填充 custom element。 对于重复使用的 html 结构,可以起到简化作用,非常有意义。

    示例

    <!-- 显示 default text -->
      <custom-info></custom-info>
    
      <!-- 显示 template info -->
      <custom-info>
        <span slot="info">template info</span>
      </custom-info>
    
      <template id="custom-info">
        <style>
          p {
            color: red;
          }
        </style>
        <p><slot name="info">default text</slot></p>
      </template>
    
      <script>
        class CustomInfo extends HTMLElement {
          constructor() {
            super();
    
            const shadowRoot = this.attachShadow({mode: 'open'});
    
            const customInfoTpCon = document.querySelector('#custom-info').content;
            
            shadowRoot.appendChild(customInfoTpCon.cloneNode(true));
          }
        }
    
        customElements.define('custom-info', CustomInfo);
      </script>
    

    更多,请参考:HTML templates and slots

    web components 示例

    web component todolist

    其他库 todolist 大比拼

    看图,结果不言而喻。

    总结

    浏览器原生能力正在变得很强大。web component 值得拥抱一下。虽然 template 还不是很完善(不支持表达式),但这也只是白板上的一个黑点。

    参考:

    1. 尤大 3 天前发在 GitHub 上的 vue-lit 是啥?
    2. Web Components
    3. web-components-todo
  • 相关阅读:
    工程结构
    生活决策
    工作原则概要与列表
    生活原则概要与列表
    在Windows2008下安装SQL Server 2005无法启动服务的解决办法
    MySQL启动提示High Severity Error解决方案
    知乎页面颜色个性化修改
    博客园 CodingLife 模板 翻页样式美化方法
    【翻译】GitHub Pages Basics 基本使用帮助【一】GitHub Pages 是什么?
    【翻译】GitHub Pages Basics 基本使用帮助【首页】
  • 原文地址:https://www.cnblogs.com/EnSnail/p/13938504.html
Copyright © 2011-2022 走看看