前端笔记:使用Web Components进行原生组件化开发

目的

前端开发中组件化开发是一种趋势,方便界面中各种自定义组件的管理与复用。现在流行的 React 和 Vue 等框架都是组件化开发的。目前前端原生也支持一定程度上的组件化开发,这个称为Web Components,这篇文章将对相关内容做个说明。

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

基础说明

原生的HTML标准中提供很多的组件,比如 p 、div 、button 这些,这些元素的默认样式和功能都朴素简单了,通常使用时我们需要根据功能需求再编写很多样式和脚本,大多数时候我们甚至需要将一些空间嵌套组合使用。这样的开发对于这些自己设计的组件的管理、维护、复用等工作都是比较麻烦的。

比较方便的一种方式是所有组件相关的html和css代码都封装到js中,这样每个组件就是一份js代码了,使用时只要调用相关函数将传教组件插入到DOM中就行。目前前端原生的组件化开发最根本的也是用js,再此基础上针对组件化需求添加了一些新特性。这整一个被称为Web Components。

Web Components主要有 四部分 三部分 组成:

  • Custom elements(自定义元素)
  • Shadow DOM(影子DOM)
  • HTML templates(HTML模板)
  • HTML Imports(HTML导入)
    前面几部分编写的代码放一个html文件中就成了一个单文件的组件,可以使用HTML Imports功能在正式页面中导入组件的html文件来使用组件;这个东西现在被废弃了,目前还没有非常好的代替这个功能的东西;

技术点介绍

Web Components 中涉及的一些技术可以在MDN上找到说明:
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

Custom elements

这东西就是组件化开发的核心了,用来创建自定义组件。这个其实很简答,创建一个继承自HTMLElement的类就行:

class YourComponent extends HTMLElement {
  constructor() {
    super();
    // 组件的功能代码写在这里
  }
}

接下来我们需要使用 CustomElementRegistry.define() 方法将上面的类和一个自定义的标签绑定,这样你就可以在页面中直接使用这里的标签来使用自定义的组件了:

customElements.define('your-component', YourComponent);
// 官方规定自定义标签必须大于一个单词,单词间用-连接

将两者结合一下就是下面的样子:

customElements.define('your-component',
  class extends HTMLElement {
    constructor() {
      super();
      // 组件的功能代码写在这里
    }
  }
);

知道上面两点内容后就可以创建自己的组件来使用了:

在这里插入图片描述

上图演示中用上了自定义的标签元素,这个内部元素内部其实就是在声明的类中用js代码创建的。上面演示中自定义组件内部的内容是在类中写死的,实际上我们希望属性是可以在使用时自由设置的。可以通过下面方式来自由设置属性:

在这里插入图片描述

除了上面的基础用法Custom elements还有一些生命周期相关的回调函数可用:

customElements.define('your-component',
  class extends HTMLElement {
    static get observedAttributes() { return ['要监听的属性列表'] } // 配合下面attributeChangedCallback()使用
    constructor() {
      super();
    }
    connectedCallback() {} // 当组件首次被插入文档DOM时被调用
    disconnectedCallback() {} // 当组件从文档DOM中删除时被调用
    adoptedCallback() {} // 当组件被移动到新的文档时被调用
    attributeChangedCallback(name, oldValue, newValue) {} // 当组件增加、删除、修改自身属性时被调用
  }
);

Shadow DOM

这东西主要用于让组件内的各个元素和DOM隔离,这样自定义的组件才能真正成为独立的组件,不会被页面中其它样式等的污染。

在上一节其实我们已经实现了自定义组件,但是这里还存在一个比较大的问题。上面的自定义组件内部的各种元素对外都是可见的,这会引起很多问题,比如内部的元素会被外部样式所修改。这时候就要用到Shadow DOM了:

在这里插入图片描述

上面的 attachShadow() 中mode参数可以设置为 closedopen ,区别在于页面中JS对shadow DOM内部元素的访问性:

在这里插入图片描述

HTML templates

前面内容中创建组件用的都是JS,但只用JS来创建组件的话组件的结构和样式稍微复杂点就变得很麻烦了,这个时候要用上HTML templates了,它可以让你用原生html、css、js语言来编写组件。HTML templates结合前面的内容使用时差不多是下面这样的结构:

  <template id='your-component-template'>
	<!-- 这里编写组件的结构、样式、脚本等 -->
  </template>
  <script>
    customElements.define('your-component',
      class extends HTMLElement {
        constructor() {
          super();
          let template = document.getElementById('your-component-template');
          let templateContent = template.content;
          const shadowRoot = this.attachShadow({ mode: 'closed' });
          shadowRoot.appendChild(templateContent.cloneNode(true)); // 克隆template内部内容添加到ShadowDOM中
        }
      }
    );
  </script>

在这里插入图片描述
上面的演示中可以看到有了HTML templates之后编写复杂的自定义组件就方便多了。另外template中的样式中可以使用 :defined :host :host() :host-context(),比如下面这样:

:defined {
  /* 匹配任何已定义的元素,包括内置元素和使用CustomElementRegistry.define()定义的自定义元素 */
}

:host {
  /* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS( containing the CSS it is used inside ) */
}

:host(xxx) {
  /* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS (这样您可以从 shadow DOM 内部选择自定义元素)— 但只匹配给定方法的选择器的 shadow host 元素 */
}

:host-context(xxx) {
  /* 选择 shadow DOM 的 shadow host ,内容是它内部使用的 CSS (这样您可以从 shadow DOM 内部选择自定义元素)— 但只匹配给定方法的选择器匹配元素的子 shadow host 元素 */
}

除了上面的基础使用外HTML templates还提供了一个更进一步的功能slot。这东西可以让你在templates的html结构中插入一个插槽,这样你在使用自定义组件的时候可以向这个插槽中插入各种各样的东西:

在这里插入图片描述
如果templates中只有一个slot,那就可以不用指定name,你在网页上使用自定义标签之间的所有内容都会放到这个slot间。

整合成独立组件文件

上面演示中组件和页面都是在同一个文件中的,实际使用中我们通常是希望组件可以封装在独立的文件中的。

在HTML Imports弃用之前我们可以把组件相关的代码(比如template、script)这些写到一个html文件中,然后在真实页面头部中使用 <link rel="import" href="your-component.html"> 方式引用组件。

在HTML Imports已经被废弃并且ES Module还无法import html文件的现在我们只能把template部分代码作为字符串嵌入到js代码中来使用了。最终组件就是一个独立的js文件,比如下面这样:

在这里插入图片描述

示例演示与说明

需要注意的是下面演示中JS使用了ES module的方式,该方式必须使用服务器,最简单的比如VS Code中安装Live Server扩展来处理。

示例一

export default class YourComponent extends HTMLElement {

  static get observedAttributes() { return ['color'] }

  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
    <style>
    :host {
      color: ${this.color};
    }
    </style>
    <p id='text'><slot></slot></p>
    `;
  }

  get color() {
    return this.getAttribute('color') || 'blue'; // 如果没有设置颜色则返回bule作为默认颜色
  }

  set color(value) {
    this.setAttribute('color', value);
  }

  connectedCallback() {
    this.text = this.shadowRoot.getElementById('text');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name == 'color' && this.text) {
      this.text.style.color = newValue;
    }
  }

}

if (!customElements.get('your-component')) {
  customElements.define('your-component', YourComponent);
}

在这里插入图片描述
上面是个简单的组件,仅仅只是控制了下文字的颜色,不过基本上展示出了原生单文件组件的一些用法,真正应用的时候更多的只是拓展组件结构,处理更多的属性。

上面演示中对于color这个属性,可以通过组件标签中添加属性来设置,也可以在页面上用CSS进行设置,另外虽然上面没有演示,其实也可以通过js使用 color(value) 或 setAttribute(name,value) 方法来设置。

示例二

export default class YourComponent extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
    <style>
    :host>.btn {
      width: 5rem;
      height: 2rem;
      line-height: 2rem;
      margin: 0.2rem;
      font-size: 1.25rem;
      text-align: center;
    }
    :host(:not([type="ok"]):not([type="error"]))>.btn {
      color: blue; 
    }
    :host([type="ok"])>.btn { 
      color: white; 
      background-color: green;
    }
    :host([type="error"])>.btn { 
      color: white; 
      background-color: red;
    }
    :host>.btn:hover {
      opacity: 0.6; 
    }
    :host>.btn:active {
      opacity: 0.2; 
    }
    :host-context(.round)>.btn {
      border-radius: 0.5rem;
    }
    </style>
    <button class='btn'><slot></slot></button>
    `;
  }
}

if (!customElements.get('your-component')) {
  customElements.define('your-component', YourComponent);
}

在这里插入图片描述
上面的示例主要使用了 :host() 和 :host-context() 这两个选择器,很多时候纯css也可以实现很多功能。

es6-string-html

上面把模板的代码当作字符串来处理最终用是能用了,但是代码编写、修改的时候就比较纠结了。好在在VScode中可以安装es6-string-html扩展在一定程度上改善这个问题:
在这里插入图片描述

安装该插件后只要在字符串前面加上 /*html*/ 就可以以Html形式渲染字符串内容,编写和修改上也一般的代码编写差不多。目前来说唯一没法实现的就是格式化操作。

总结

Web Components 使用总体来说并不复杂。就我个人而言比起Vue、React这类需要编译的框架,我更加喜欢浏览器原生就能解析使用的开发方式。

更多内容可以参考MDN相关的例程:
https://github.com/mdn/web-components-examples

另外也可以参考第三方的原生组件库:
https://github.com/XboxYan/xy-ui

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
您提到的错误是 `eslint-plugin-vue` 的一个警告,它确实提醒了使用v-html指令可能导致XSS攻击的风险。这是因为v-html指令会直接将变量中的HTML代码插入到DOM中,如果HTML代码未经过适当的验证和过滤,可能会导致安全漏洞。 为了遵循最佳实践,您可以考虑以下替代方案来解决这个问题: 1. 使用Vue.js的文本插值(Double Mustache)来显示变量中的内容,而不是使用v-html指令。这样可以确保HTML代码被转义,避免XSS攻击的风险。例如: ```html <q-input v-model="inputValue">{{ inputValue }}</q-input> ``` 2. 如果您确实需要渲染一些富文本内容,并且可以信任输入的HTML代码,那么您可以考虑使用第三方库,如`DOMPurify`来过滤和验证HTML代码。`DOMPurify`可以帮助您防止XSS攻击,并确保渲染的HTML代码是安全的。以下是一个使用`DOMPurify`的示例: 首先,在项目中安装`DOMPurify`: ```bash npm install dompurify ``` 然后,在代码中引入并使用`DOMPurify`: ```html <template> <q-input v-model="inputValue" :value="sanitizedInputValue"></q-input> </template> <script> import DOMPurify from 'dompurify'; export default { data() { return { inputValue: '<b>Hello World!</b>', }; }, computed: { sanitizedInputValue() { return DOMPurify.sanitize(this.inputValue); }, }, }; </script> ``` 在上面的示例中,我们使用`DOMPurify.sanitize`方法过滤和验证输入的HTML代码,然后将过滤后的内容绑定到QInput组件的:value属性上。 通过使用这些替代方案,您可以避免直接使用v-html指令带来的潜在安全风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naisu Xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值