你不知道的web component系列——改造Vite模版了解shadowDom和插槽

注: 本文属于你不知道的web component系列的前置部分, 旨在帮助开发者更好的了解web component,渐进式的深入web component这一浏览器特性!

前言

web component早在数十年前就已经作为浏览器的新特性被讨论和应用, 它旨在解决下面的开发痛点:

  1. 一个页面中不同的公用组件之间样式的侵扰,经常在我们需要开发一个页面新功能时,却被别的样式影响导致我们需要反复的修改和覆盖.
  2. 页面中其它元素绑定的事件会影响到我要开发的功能,我们需要去调查究竟是哪一点影响到了,十分影响开发进度和体验.
  3. 当不使用开发框架开发时, 我们所要考虑的问题复杂度急剧升高, 开发不规范问题会导致代码新功能的添加、错误的排查等等难度陡然升高.

本文就将会基于这些痛点, 以vite为工具,以改造vite提供的模版来实现并打包一个web component,读完本文你将会获得:

  1. 对web component的更深入的理解,真正理解web component的三大技术核心之——ShadowDom & slot+template
  2. 基于web component的OOP式开发思维

系列如下:

1. 你不知道的web component系列——改造Vite模版实现一个自定义元素

初始化模版

本文使用的版本信息如下: create-vite@5.5.2 vite/5.4.2将会基于这些版本来改动最终实现一个web component 版本的Vite template, 下面是步骤:

  1. 执行命令: npm create vite, 按照步骤选择others -> create-vite-extra
  2. Select a template这里选择 : library -> typescript
  3. 切换到改目录下后, 安装相关依赖, 然后我们就可以开始啦!

什么是Node

Node就是节点, 这里我们指的就是树的节点. 不同的节点组合在一起就构成了一个树状的数据结构, 一个树状的数据结构组成应该分为下面这几部分:

没错,正是由一个跟节点以及其它类型的节点组成了一个节点树,他们之间存在着复杂的关系.

而我们日常见到的html文档就是由一个document树编译的,它的根节点是document,然后下面有着很多的几点比如,元素节点,文本节点,属性节点等等.

他们共同构成了一个document tree,我们向document上面添加节点时可以借助浏览器的api,形如document.append(...nodes)等等

本文要介绍的shadowDOM**也是一个节点树,**与我们常见的document tree不同的地方在于它的根节点是一个ShadowRoot,它也是节点的一种,但是它特殊的地方在于它必须存在于另外一个node树上,如下图:

创建一个ShadowDOM

上一节,我们仅仅只是创建了数据我们自己的自定义元素——<vite-app>,它存在着样式被影响的问题,我们想要让我们vite-app中的样式和Vite模版本身的样式隔离起来该如何借助shadowDOM?

首先,我们先看看如何创建一个shadowDOM,如上面我们说的,我们想要创建一个shadowDOM的前提是我么需要一个shadowRoot来作为这个树的根节点,因而我们需要首先创建一个shadowRoot,然后再把它插入到我们想要插入到页面中的元素即可, 如下:

import './style.css'
import typescriptLogo from './typescript.svg'
import { setupCounter } from '../lib/main'

// 这里用的是上节我们实现的vite-app组件:
class ViteApp extends HTMLElement {
  constructor() {
    super()
  }

  connectedCallback() {
    console.log('始化了');

    // 首先创建shadow root
    const shadowroot = this.attachShadow({ mode: 'open' })

    // 然后构建我们的shadowDOM树
    // 可以用形如: 
    // shadowroot.append()等常见添加子节点方法或者直接: 

    shadowroot.innerHTML = `
      <div>
        <a href="<https://vitejs.dev>" target="_blank">
          <img src="/vite.svg" class="logo" alt="Vite logo" />
        </a>
        <a href="<https://www.typescriptlang.org/>" target="_blank">
          <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
        </a>
        <h1>Vite + TypeScript</h1>
        <div class="card">
          <button id="counter" type="button"></button>
        </div>
        <p class="read-the-docs">
          Click on the Vite and TypeScript logos to learn more
        </p>
      </div>
    `
    // 初始化逻辑
    setupCounter(shadowroot.querySelector<HTMLButtonElement>('#counter')!)
  }
}

customElements.define('vite-app', ViteApp)
const viteapp = document.createElement('vite-app')
document.querySelector<HTMLDivElement>('#app')!.append(viteapp)

效果如下:

我们可以发现, logo的样式和文字的样式已经被移除掉了,并且在右侧的devTools面板也多出来了一个特殊的节点: shadow-root (open),这就是shadowDOM,那么样式隔离我们显而易见,那么逻辑尼,如何体现?

我们尝试在代码的其它部分来解释这一特性:

...省略其余部分
class ViteApp extends HTMLElement {
	  connectedCallback() {
	    console.log('始化了');
	
	    // 首先创建shadow root
	    const shadowroot = this.attachShadow({ mode: 'open' })
	    // ...其余部分
    }
}

// 我们尝试去使用js模拟在别的地方直接获取shadow root中的内容:

console.log('card', document.querySelector('.card'))
console.log('counter', document.querySelector('#counter'))

// 结果如下:
// card null
// counter null

直接获取时是获取不到的,但是我们可以换种方法,如下:

...省略其余部分
class ViteApp extends HTMLElement {
	  connectedCallback() {
	    console.log('始化了');
	
	    // 首先创建shadow root
	    const shadowroot = this.attachShadow({ mode: 'open' })
	    // ...其余部分
    }
}

// 我们尝试去使用js模拟在别的地方直接获取shadow root中的内容:

console.log('card', document.querySelector('vite-app')?.shadowRoot?.querySelector('.card'))
console.log('counter', document.querySelector('vite-app')?.shadowRoot?.querySelector('#counter'))

// 结果如下:
// 选中了我们想要的元素

因而,只有当我们的逻辑访问到shadow root时,才可以获得到我们想要拿到的元素,这几乎可以使得我们的代码实现逻辑同页面中其余元素的隔离,并且我们还可以通过切换shadow root的模式为closed让它无法使用js来选择其中的元素,实现彻底的隔离!

...省略其余部分
class ViteApp extends HTMLElement {
	  connectedCallback() {
	    console.log('始化了');
	
	    // 首先创建shadow root
	    const shadowroot = this.attachShadow({ mode: 'closed' })
	    // ...其余部分
    }
}

总结:

在这里, 我们近乎了解到了整个web component的核心概念: 自定义元素 & shadow dom,我们了解到了浏览器为了实现样式隔离和逻辑代码隔离所提供给我们的这些接口和概念,但是一个真正的组件应该还涉及到下面这些必须要考虑到问题,也是本文及本系列后续将会围绕这些点来介绍:

  1. 我们的组件在引入shadow root后似乎无法完成一些嵌套, 比如vite-app如果是一个父组件时,我们向其直接添加别的内容, 它并不能被展示,如何解决?
  2. 如何在不同的场景下应用,目前我们编写的vite-app组件是只能运行在CSR条件下的,如果我们的网页有着很强的SEO要求,如何实现它的SSR尼(后续我们会介绍declarative shadow dom来实现)?
  3. 状态可以说是前端最重要的东西, 我们的组件如何处理状态的变化, 有什么方法可以使用,它的优缺点是什么(后续文章会提到…)?
  4. 我们在编写一个web component组件时, 不能够获得良好的html,css语法提示,因为我们的代码是位于字符串内的,有没有什么办法可以解决这种麻烦?是否可以编写一个compiler,让我们可以在jsx文件中直接写html以及css(后续文章会介绍)?
  5. 当我们的项目变的复杂起来或者我们决定在业务中使用web component, 有没有一些best pratice?生态系统是否可以支持我们完成快速开发和维护(敬请期待…)?

让我们的组件更完整——引入模版和插槽

目前,当我们的vitea-app元素被定义好之后, 我们还想要给它的下面加一句话”hello world !”,此时我们通过:


customElements.define('vite-app', ViteApp)
const viteapp = document.createElement('vite-app')
document.querySelector<HTMLDivElement>('#app')!.append(viteapp)

// 新增: 
document.querySelector('vite-app')?.append('hello world!')

但是在页面中,我们却并没有发现这个元素:

这是因为,当浏览器渲染的时候发现是一个shadow DOM时, 是会忽略去渲染我们除了shadow root的其它类型节点, 只会遍历shadow root上包含的所有节点并渲染.

这时候我们可以通过插槽来解决这一问题, 如下:

   // ...省略其它内容
    shadowroot.innerHTML = `
      <div>
        <a href="<https://vitejs.dev>" target="_blank">
          <img src="/vite.svg" class="logo" alt="Vite logo" />
        </a>
        <a href="<https://www.typescriptlang.org/>" target="_blank">
          <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
        </a>
        <h1>Vite + TypeScript</h1>
        <div class="card">
          <button id="counter" type="button"></button>
        </div>
        <p class="read-the-docs">
          Click on the Vite and TypeScript logos to learn more
        </p>
        <h1>
          <slot>
            默认内容
          </slot>
        </h1>
      </div>
    `
    // ...省略其它内容
    
document.querySelector('vite-app')?.append('hello world!')

现在, 我们会发现我们的页面已经可以正常的展示我们所插入到这个组件中的元素了, 并且它具有默认展示的内容

并且除了默认的插槽外, 我们还可以通过具名插槽来控制我们传入组件的内容该渲染到组件的哪个位置,更多更详细的内容参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_templates_and_slots#更深入的示例

本文参考

DOM Standard

https://developer.mozilla.org/zhCN/docs/Web/API/Web_components/Using_shadow_DOM

https://github.com/WICG/webcomponents?tab=readme-ov-filehttps://software.hixie.ch/utilities/js/live-dom-viewer/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值