Web components原生组件快速实战(一)

238 阅读4分钟

前言

Web Components 允许开发者创建可复用的自定义元素,由于是浏览器的原生组件,能够无缝对接到各种前端框架。 这个系列将介绍如何使用原生html+js+css开发 Web Components原生组件并作为组件库的一员使用 。

1.目录准备

我们的项目使用以下文件目录结构来组织UI组件库

ui-component/
├── component1/
│   └── component1.html
│   └── index.js
├── component2/
│   └── component2.html
│   └── index.js
└── index.js

其中,ui-component 是 UI 组件的根目录,下面的 component1component2 和 component3 是不同的 UI 组件目录,每个组件目录下的 index.js 文件包含该组件的js实现代码, html文件则是该组件的html模板。ui-component目录下的index.js 文件则是UI组件的导出文件。 如果是vue项目,可以在main.js中直接引入ui-component目录下的index.js,也可以打包成插件使用。

2. 第一个自定义组件--Input组件

第一个自定义组件就用input吧,最常的form元素之一。

ui-component下新建对应目录,userInput.html建立模板内容。(虽然简单的组件的html部分可以在js文件中直接拼接进去,但是复杂一点的组件需要一定的html和css,完全采用js中拼接字符串比较麻烦,所以抽取专门的模板文件)。

<style>
 .user-input{
     border: 0;
     border-bottom: 1px solid #D7D7D7;
     min-width: 30px;
     width: 100%;
     height: 26px;
     line-height: 26px;
     font-size: 14px;
 }
 .user-input:focus{
     outline: none;
     border-bottom: 1px solid #1296db;
 }
 </style>

<div><input type="text" class="user-input"/></div>

userInput.js下自定义组件,将userInput.html引入:

import templatePanel from './userInput.html'

const template = document.createElement('template')
template.innerHTML = templatePanel
export default class InputNumber extends HTMLElement {

static observedAttributes = ['name','value'];
constructor() {
  super();

    // 将userInput.html作为模板内容引入到自定义组件中
    this._shadowRoot.appendChild(template.content.cloneNode(true));
 }

static get observedAttributes() {
  return ['name','value'];
 }
    
attributeChangedCallback(name, oldValue, newValue) {
    
}

connectedCallback() {
    
 }
}
    
if (!customElements.get('user-input')) {
  // Register
  customElements.define('user-input', UserInput);
}

记得在index.js中导出userInput

import './userInput/userInput.js'

此时可以像正常html标签一样直接使用,如<user-Input></user-Input>,也可以在js文件中通过new userInput()创建一个自定义元素,然后通过dom方法append到对应元素下。

3.进一步扩展

此时一个简单的input组件已经可以使用了,但是作为组件库的一员,它还需要继续扩展如下功能:

  • 监听属性的变化,同步更新内容
  • 将元素标记为一个表单关联的自定义元素,能在form中像原生input一样使用
  • 对外抛出的事件

3.1监听属性变化

在 Web Components 里,我们可以借助 `observedAttributes` 静态方法与 `attributeChangedCallback` 生命周期方法来监听属性的变化。

observedAttributes :此方法返回一个数组,数组中的元素为需要监听的属性名。在上述示例中,return['name','value'] 表示要监听 'name','value' 属性的变化。

attributeChangedCallback : 当被监听的属性发生变化时,该方法会被调用。它接收三个参数:name(发生变化的属性名)、oldValue(属性的旧值)和 newValue(属性的新值)。比如当value值从外部改变时,需要修改对应dom中的值,这里为了避免每次都刷新dom的消耗,处理为值跟上一次不一样才更新dom。

attributeChangedCallback(name, oldValue, newValue) {
 // 当观察的属性发变化时调用
  if (newValue === oldValue && newValue !== 'undefined') {
    return;
  }
 if (name === 'value) {
   this.value= (newValue === "undefined" || newValue === undefined) ? "" : newValue;
   this._shadowRoot.querySelector(".user-input").value = this.value;
   }

}

3.2将元素标记为一个表单关联的自定义元素

代码如下

static formAssociated = true;
constructor() {
  ...
  this.value;
  this.value_;
  // 获得访问内部表单控件 API 的能力
  this.internals_ = this.attachInternals();
  this._shadowRoot = this.attachShadow({mode: 'open'});
  this._shadowRoot.appendChild(template.content.cloneNode(true));
}

// 表单控件通常暴露一个“value”属性
get value() {
  return this.value_;
}
set value(v) {
  this.value_ = v;
}

// 提供它们有助于确保与浏览器提供的控件保持一致。
get form() {
  return this.internals_.form;
}

3.3对外抛出的事件

当input组件的值变化时需要通知外面,这里我们正常监听input元素的input事件,blur事件和enter事件。 connectedCallback() 是在 custom element 首次被插入文档 DOM 时被调用的。这个回调函数通常用于执行一些初始化操作,比如添加事件监听器、请求数据等等。在这个时候,元素已经被添加到了文档中,可以访问到 DOM 和其他元素。

connectedCallback() {
  // 当元素被插入到DOM时调用
 
  // 监听输入值,失去焦点时触发handleChange
  this._shadowRoot.querySelector(".user-input").addEventListener('blur', this.handleChange);

  // 输入回车触发handleChange
  this._shadowRoot.querySelector(".user-input").addEventListener('keyup',this.handleEnter);

  this._shadowRoot.querySelector(".user-input").addEventListener('input', this.handleChange);
}
    
handleChange =(e) => {
this.value = e.target.value;
this.internals_.setFormValue(this.value_);
this.dispatchEvent(new UIEvent('change'));

}

此时<user-Input></user-Input>就可以像一个原生input一样使用,也可以在form表单中和原生的input一样获取值。 当然还可以继续扩展,比如表单中的验证规则,不同类型的input等等...这里就不继续展开了