從原生web組件到框架組件源碼(一)

  • 2020 年 11 月 1 日
  • 筆記

溫馨提醒,當你覺得看我寫的很亂的時候,就對了,那是因為我查閱了大量的資料提取出來的,因為有點東西不太理解,所以你會感覺有的部分重複了,也不是重複,只是後面對前面的內容進行梳理了一些,需要耐心的看到最後

自定義元素

我們發現自定義元素總是有破折號的Q,<my-component><bacon-cheese>

因為瀏覽器供應商已承諾不創建其名稱中包含短劃線的新內置元素,以防止衝突

<app-element></app-element>
<element></element>

  const appElement = document.querySelector('app-element');
  console.log(appElement.constructor.name);
  //  HTMLElement類型的
  const element=document.querySelector('element')
  console.log(element.constructor.name);
  // HTMLUnknownElement

上面兩個自定義元素,我們通過constructor.name 知道HTML 元素類型

  • <app-element> 實際上是一個自定義元素, 他基於HTMLElement 上標記的基本數據類型
  • <element> 數據類型HTMLUnknownElement, 是一個無效的HTML元素,瀏覽器並不知道它是什麼元素
class MyComponent extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<h1>Hello world</h1>`;
  }
}
    
customElements.define('my-component', MyComponent);

<my-component></my-component>

customElements

引用customElements ,將返回瀏覽器載入自定義元素的全局記錄,類似於註冊表

方法 描述
customElement.define(``name,class(function)) 在頁面上定義一個自定義元素
customElement.get(``name) 獲取已定義的自定義元素的類。
customElement.whenDefined(``name) 帶回 定義自定義元素時。
customElement.upgrade(``node) 允許您手動更新自定義元素

我們通過customElements.define() 定義自定義元素

獲取自定義元素

class AppElement extends HTMLElement {
  /* ... */
}
customElements.define("app-element", AppElement);

customElements.get("app-element") === AppElement; // true

.get() 獲得所請求的自定義元素的類

特定的操作

  customElements.define('my-counter', MyCounter);
  customElements.whenDefined('my-counter').then(()=>{
    console.log('xxx');
  })

簡單的理解,我們在自定義元素 初始化後,進行的一些操作

更新操作

customElements.upgrade

// 創建一個自定義元素
const element = document.createElement("app-element");
// 我們把這個自定義元素定義好
class AppElement extends HTMLElement { /* ... */ }
customElements.define("app-element", AppElement);
console.log(element.constructor === HTMLElement); // true
  //我們更新下這個元素,他已經從 HTMLElement=>AppElement
  customElements.upgrade(element)
  ae.constructor === HTMLElement;   // false
  ae.constructor === AppElement;    // true

我們在.createElement() 定義前,他是HTMLElment類型,但是upgrade更新後,他就是AppElement ,

所以有必要進行手動更新

自定義元素的生命周期

connectedCallback() 是從元素的分離constructor 出來的

connectedCallback通過用於講內容添加到元素

影子DOM

影子dom的特點

<div class="element">
  #shadow-root
    <div class="inner-element">
      ...
    </div>
</div>

shadowRootInit

element.attachShadow(shadowRootInit);

shadowRootInit設置

{mode:'open'}
element.shadowRoot // 返回一個ShadownRoot對象

root元素可以從js外部訪問根節點

{mode:'closed'}
element.shadowRoot // null

拒絕js外部返回關閉的shadow

<slot> 包含文檔內容的內容

<div id="example">我是本來的元素,</div>
<script>
  let example = document.getElementById('example');
  let shadowRoots = example.attachShadow({mode: 'open'});
  shadowRoots.innerHTML = `<style>
button {
  background: tomato;
  color: white;
}
</style>
<button id="button"><slot></slot> 我是添加的內容</button>`;
</script>

HMTL模板

template 元素是HTML流中可以標記重複使用的程式碼模組,但是這些模組不能立即呈現

<template id="books">
  <li><span class="title"></span> &mdash; <span class="author"></span></li>
</template>
<ul id="contents"></ul>
<script>
 const books = [
    { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' },
    { title: 'A Farewell to Arms', author: 'Ernest Hemingway' },
    { title: 'Catch 22', author: 'Joseph Heller' }
  ];
  const fragment=document.querySelector('#books')
  const contents = document.querySelector('#contents');
  books.forEach(book=>{
    // 創建內容實例
    const instance=document.importNode(fragment.content,true)
    instance.querySelector('.title').innerText=book.title;
    instance.querySelector('.author').innerText=book.author;
    // 添加到dom上
    contents.appendChild(instance)
  })
</script>

我們發現我們使用模板的時候,我們需要把javascript

// 拿到 <template></template> 標籤
const template = document.querySelector('template');
const node = document.importNode(template.content, true);
document.body.appendChild(node);

使用document.importNode 允許我們在多個位置重用相同模板內容的實例

webComponent 在項目的使用

新建一個最小的基點

class AppElement extends HTMLElement {

  constructor() {
    super();
  }

}

customElements.define("app-element", AppElement);

我們命令的時候要養成一個良好的習慣,文件通過與類命名AppElement.js

在文檔中載入組件javascript 文件

<script src="/components/AppElement.js"></script>

這樣我們就可以在html添加這個組件

<app-element></app-element>
或者我們用js的形式添加
const appElement = document.createElement("app-element");
document.body.appendChild(appElement);

或者我們放在一個根文件中

<script type="module" src="/js/index.js"></script>

這樣我們就可以在index.js中使用

import "./components/AppElement.js";

組件屬性

我們可以在constructor 添加屬性或者成員

class AppElement extends HTMLElement {
    #role='devel'
  constructor() {
    super();
    this.name = "Manz";
    this.life = 5;
  	this.#role='js Devel'
  }
	test(){
    
	}
	#provateTest(){
    
	}
}

也可以添加私有屬性和方法

執行方法

上面我們在自定義元素內部寫了一些方法

<app-element onClick="this.test()"></app-element>

我們發現他會執行public公共類型的方法,私有方法只能在類內部執行

對於自身而且創建就執行靜態方法,默認的情況的其實this可以不寫因為默認調用的就是內部的方法

生命周期

特性 描述
constructor() 已創建一個特定的自定義元素,該元素已在註冊表中定義。
connectedCallback() 自定義元素已連接到HTML文檔的DOM。
disconnectedCallback() 自定義元素已從HTML文檔的DOM斷開。
adoptedCallback() 自定義元素被移動到一個新文件(常見於iframes)。
attributeChangedCallback() 自定義元素的觀察屬性已被修改。

我們可以通過document.createElementnew AppElement() 手動創建元素

不要忘記寫super(),因為我要擴展到HTMLElement

     // 調用dom時候執行
    connectedCallback() {
      this.textContent='ddd'
    }
    // 刪除dom時候執行
    disconnectedCallback(){
      console.log(333);
    }

我們發現在操作dom的時候connectedCallback一些方法

在刪除了dom的時候,會調用disconnectedCallback

adoptedCallback() 自定義元素移動一個新文件(這個我暫時不清楚),不太清楚現實的用意在哪

變更檢測

可以使用HTML元素的屬性

方法 描述 返回值
.hasAttributes() 元素有屬性嗎? boolean
.getAttributeNames() 返回一個array屬性的小寫屬性值 Array
.hasAttribute(name) 查詢某個name是否存在 boolean
.getAttribute(name) 返回name的屬性值,不存在返回null string
.removeAttribute(name) 刪除屬性name
.setAttribute(name,value) 將屬性設置name-value
.toggleAttribute(name,[boolean]) 如果存在則刪除,不存在則添加 boolean
特性 描述
static get observedAttributes() 觀察屬性以通知更改。
attributeChangedCallback(``name,``old,``now) 它會關閉,當他們改變。
class AppElement extends HTMLElement {

  static get observedAttributes() {
    return ["value", "isEnabled"];
  }

    attributeChangedCallback(name, old, now) {
      console.log(` ${name} ------ ${old} ---- ${now}.`);
    }
}

static getter observedAttributes()返回我們要觀察的屬性名稱

每當我們的屬性修改的時候,都會調用attributeChangedCallback() 方法

屬性名稱name,之前的old 值和當前的值now

每當屬性的修改都會調用這個函數

寫一個類似vue的完整版實例

<div id="templates"></div>
<template id="templateOne">
  <style>
    .aaa{
      color:red;
      font-size: 12px;
    }
  </style>
  <div class="aaa">12211212</div>
  <button onClick="clickDown()">Click</button>
  <script>
    function clickDown(){
      alert(1)
    }
  </script>
</template>

<script>
  let template=document.querySelector('#templateOne')
  let content=document.querySelector('#templates')
  content.appendChild(
    document.importNode(template.content,true)
  )
</script>

自定義組件的完整樣例

<my-counter></my-counter>
<script>
  const template = document.createElement('template');
  template.innerHTML = `
  <style>
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 4rem;
      height: 4rem;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  </style>
  <button id="dec">-</button>
  <span id="count"></span>
  <button id="inc">+</button>`;

  class MyCounter extends HTMLElement {
    constructor() {
      super();
      this.count = 0;
      this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
      this.shadowRoot.appendChild(template.content.cloneNode(true));
      this.shadowRoot.getElementById('inc').onclick = () => this.inc();
      this.shadowRoot.getElementById('dec').onclick = () => this.dec();
      this.update(this.count);
    }

    inc() {
      this.update(++this.count);
    }

    dec() {
      this.update(--this.count);
    }

    update(count) {
      this.shadowRoot.getElementById('count').innerHTML = count;
    }
  }

  customElements.define('my-counter', MyCounter);
</script>