이 기사의 내용은 기본 HTML 구성 요소가 무엇인지 아십니까?에 관한 것입니다. 네이티브 HTML 구성요소의 도입에는 특정 참고 가치가 있습니다. 도움이 필요한 친구가 참고할 수 있기를 바랍니다.
안녕하세요! 지난 몇 년간을 보면 웹 프론트엔드의 발전 속도가 너무 빨라요!
몇 년 전만 해도 HTML은 프론트엔드 개발자의 기본 기술이었고, 다양한 태그를 통해 사용 가능한 웹사이트를 구축할 수 있었고, 기본적인 상호작용도 문제가 되지 않았습니다. CSS가 좀 더 있으면 황금색이고 바삭하고 맛있습니다. 이때 믹스에 JavaScript를 몇 개 더 추가하는 것은 중독성이 있습니다.
수요가 증가함에 따라 HTML의 구조는 점점 더 복잡해지고, 반복되는 코드가 많아 페이지를 수정하기가 극도로 어려워졌습니다. 이로 인해 공개 부분을 추출하고 전환할 수 있는 템플릿 도구도 부화되었습니다. 공개 구성 요소로 변환합니다. 나중에 JavaScript, JavaScript 성능 개선 그 위상은 점점 높아지고 있으며, 더 이상 단순한 반찬이 아닙니다. 프런트엔드 렌더링의 등장으로 인해 서버는 템플릿을 구문 분석해야 하는 부담이 줄어들었습니다. 서버는 정적 파일과 API만 제공하면 됩니다. 인터페이스만 하면 됩니다. 그러다가 프런트엔드 렌더링 도구가 다시 서버로 옮겨지면서 백엔드 렌더링이 나타났습니다(검은색 물음표???)
컴포넌트화는 간단히 말해서 복잡한 프런트엔드 구조를 명확하게 하고, 각 부분이 독립적이게 되면서, 높은 응집력과 낮은 Coupling으로 유지관리 비용을 대폭 절감합니다.
그렇다면 네이티브 HTML 구성요소에 대해 들어보신 적이 있나요?
네 가지 주요 웹 구성 요소 표준
기본 HTML 구성 요소에 대해 이야기하기 전에 먼저 네 가지 주요 웹 구성 요소 표준을 간략하게 소개해야 합니다. 템플릿, Shadow DOM, 맞춤 요소 및 HTML 수입. 실제로 그중 하나가 버려져 '빅3'가 됐다.
HTML 템플릿 간단히 말해서 HTML5의 태그입니다. 일반적인 상황에서는 무색, 무취이며 그 아래의 img에서도 그 존재를 느낄 수 없습니다. 다운로드되지 않으며 스크립트가 실행되지 않습니다. 이름처럼 그냥 템플릿일 뿐이고, 사용해야만 의미를 갖게 됩니다.
Shadow DOM은 구성 요소 간의 독립성을 달성할 수 있는 기본 구성 요소 캡슐화를 위한 기본 도구입니다.
Custom Elements는 기본 구성 요소를 패키징하는 데 사용되는 컨테이너로, 이를 통해 완전한 구성 요소를 얻으려면 태그만 작성하면 됩니다.
HTML Imports는 HTML의 ES6 모듈과 유사합니다. 다른 HTML 파일을 직접 가져온 다음 사용할 수 있습니다. DOM 노드. 그러나 HTML Imports와 ES6 모듈은 Chrome을 제외하고 매우 유사하기 때문에 다른 브라우저에서는 이를 구현하려고 하지 않으므로 더 이상 사용되지 않으며 권장되지 않습니다. 향후에는 ES6 모듈을 사용하여 교체할 예정이지만 새 버전에서는 아직 교체 계획이 없는 것으로 보입니다. 이 기능은 Chrome에서 제거되었으며 사용 시 콘솔에 경고가 표시됩니다. 대신 ES 모듈을 사용하라는 경고가 표시되지만 테스트했을 때 Chrome 71의 ES 모듈은 감지된 파일의 MIME 유형을 JavaScript 유형으로 강제 설정합니다. 이는 아직 지원되지 않을 수 있습니다.
Shadow DOM
기본 HTML 구성 요소에 대해 이야기하려면 먼저 Shadow DOM이 무엇인지 이야기해야 합니다.
DOM은 HTML의 가장 기본적인 골격으로 존재하며 트리의 모든 노드는 HTML의 일부입니다. 트리로서 DOM은 상위 레벨과 하위 레벨 사이에 계층적 관계를 가지고 있습니다. 우리는 이를 설명하기 위해 일반적으로 "부모 노드", "자식 노드", "형제 노드" 등을 사용합니다(물론 일부 사람들은 이러한 용어가 성별을 강조한다고 생각합니다). 그래서 그들은 또한 성 중립적인 제목을 만듭니다). 하위 노드는 상위 노드로부터 일부 항목을 상속받으며 형제 노드로부터도 특정 영향을 받습니다. 가장 분명한 점은 CSS 스타일을 적용할 때 하위 노드가 상위 노드로부터 일부 스타일을 상속한다는 것입니다. .
그리고 Shadow DOM도 DOM의 일종이므로 DOM 트리에서 자라는 특별한 보라색 고구마라는 점만 빼면 트리이기도 합니다. 아뇨, 하위 트리입니다.
뭐? DOM 자체가 하나씩 하위 트리로 구성되어 있지 않나요? 이 Shadow DOM에 특별한 것이 있나요?
Shadow DOM의 특별한 점은 상대적으로 독립적인 공간을 만드는 데 전념한다는 것입니다. DOM 트리에서도 성장하지만 환경은 외부 세계와 격리되어 있습니다. 격리된 공간에서는 DOM 트리의 상위 노드에서 일부 속성을 선택적으로 상속하거나 DOM 트리를 상속할 수도 있습니다.
Shadow DOM의 격리를 사용하여 기본 HTML 구성 요소를 만들 수 있습니다.
사실 브라우저는 이미 Shadow DOM을 통해 일부 구성 요소를 구현했지만 우리는 이를 인식하지 못한 채 사용했습니다. 이것이 Shadow DOM으로 캡슐화된 구성 요소의 매력이기도 합니다. HTML 태그만 작성하고 나머지는 저에게 맡기면 됩니다. (조금 React의 JSX와 비슷하죠?)
Shadow DOM을 사용하여 브라우저에서 구현한 예, 즉 비디오 태그를 살펴보겠습니다.
<video></video>
브라우저 렌더링 결과를 살펴보겠습니다.
잠깐만요! Shadow DOM을 말하는 것이 아닌가요? 이것이 일반 DOM과 어떻게 다릅니까? ? ?
Chrome에서 Elements는 기본적으로 내부적으로 구현된 Shadow DOM 노드를 표시하지 않습니다.
참고: 브라우저는 기본적으로 자체 Shadow DOM 구현을 숨깁니다. 사용자가 스크립트를 사용합니다. 생성된 Shadow DOM은 숨겨지지 않습니다.그러면 비디오 태그의 진짜 모습을 볼 수 있습니다.
여기서 일반 DOM을 디버깅하는 것처럼 Shadow DOM의 콘텐츠를 완전히 조정할 수 있습니다(어쨌든 일반 DOM과 동일합니다) , 새로 고침 및 복원).
위의 Shadow DOM에 있는 대부분의 노드에는 의사 속성이 있음을 알 수 있습니다. 이 속성에 따라 CSS 스타일을 외부에 작성하여 해당 노드 스타일을 제어할 수 있습니다. 예를 들어, pseudo="-webkit-media-controls-overlay-play-button" 위에 있는 입력 버튼의 배경색을 주황색으로 변경합니다.
video::-webkit-media-controls-overlay-play-button { background-color: orange; }
Shadow DOM은 실제로 DOM의 한 유형이므로 위와 마찬가지로 Shadow DOM에 Shadow DOM을 계속 중첩할 수 있습니다.
브라우저에는 ,
Shadow DOM의 격리로 인해 외부에 div { background-color: red !important } 스타일을 작성하더라도 Shadow DOM 내부의 div는 어떤 방식으로든 영향을 받지 않습니다.
즉, When; 스타일링, id를 사용해야 할 경우 id를 사용하고, class를 사용해야 할 경우 class를 사용합니다. 버튼의 클래스를 .button으로 작성해야 하는 경우 .button을 작성하면 됩니다. 현재 구성 요소의 ID와 클래스가 다른 구성 요소와 충돌할 수 있다는 점을 고려할 필요가 없습니다. 구성 요소 내에 충돌이 없는지 확인하기만 하면 됩니다. 이는 쉽습니다.
이것은 오늘날 대부분의 구성 요소 기반 프레임워크가 직면한 문제인 요소의 클래스(className)를 작성하는 방법을 해결합니다. 접두사가 붙은 네임스페이스를 사용하면 다음과 같이 클래스 이름이 너무 길어집니다. 일부 CSS-in-JS 도구를 사용하면 다음과 같은 고유한 클래스 이름을 만들 수 있습니다. : .Nav__welcomeWrapper___lKXTg, 이 이름은 여전히 약간 길고 중복된 정보를 포함하고 있습니다.
ShadowRoot
ShadowRoot는 DOM에서
처럼 처리할 수 있지만 가 아니므로 그것은 노드가 아닙니다.ShadowRoot 아래의appendChild 및 querySelectorAll과 같은 속성이나 메소드를 통해 전체 Shadow DOM 트리를 작동할 수 있습니다.
와 같은 일반 요소의 경우, attachmentShadow 메소드를 호출하여 ShadowRoot를 생성할 수 있습니다(오래되어 권장되지 않는 createShadowRoot 메소드도 있습니다). { mode : 'open' }, 이 객체에는 'open'과 'closed'라는 두 가지 값을 갖는 모드 속성이 있습니다. 이 속성은 ShadowRoot를 생성할 때 초기화되어야 하며 ShadowRoot 속성을 생성한 후에는 읽기 전용 속성이 됩니다.
mode: 'open' 和 mode: 'closed' 有什么区别呢?在调用 attachShadow 创建 ShadowRoot 之后,attachShdow 方法会返回 ShadowRoot 对象实例,你可以通过这个返回值去构造整个 Shadow DOM。当 mode 为 'open' 时,在用于创建 ShadowRoot 的外部普通节点(比如
)上,会有一个 shadowRoot 属性,这个属性也就是创造出来的那个 ShadowRoot,也就是说,在创建 ShadowRoot 之后,还是可以在任何地方通过这个属性再得到 ShadowRoot,继续对其进行改造;而当 mode 为 'closed' 时,你将不能再得到这个属性,这个属性会被设置为 null,也就是说,你只能在 attachShadow 之后得到 ShadowRoot 对象,用于构造整个 Shadow DOM,一旦你失去对这个对象的引用,你就无法再对 Shadow DOM 进行改造了。
可以从上面 Shadow DOM 的截图中看到 #shadow-root (user-agent) 的字样,这就是 ShadowRoot 对象了,而括号中的 user-agent 表示这是浏览器内部实现的 Shadow DOM,如果使用通过脚本自己创建的 ShadowRoot,括号中会显示为 open 或 closed 表示 Shadow DOM 的 mode。
浏览器内部实现的 user-agent 的 mode 为 closed,所以你不能通过节点的 ShadowRoot 属性去获得其 ShadowRoot 对象,也就意味着你不能通过脚本对这些浏览器内部实现的 Shadow DOM 进行改造。
HTML Template
有了 ShadowRoot 对象,我们可以通过代码来创建内部结构了,对于简单的结构,也许我们可以直接通过 document.createElement 来创建,但是稍微复杂一些的结构,如果全部都这样来创建不仅麻烦,而且代码可读性也很差。当然也可以通过 ES6 提供的反引号字符串(const template = `......`;)配合 innerHTML 来构造结构,利用反引号字符串中可以任意换行,并且 HTML 对缩进并不敏感的特性来实现模版,但是这样也是不够优雅,毕竟代码里大段大段的 HTML 字符串并不美观,即便是单独抽出一个常量文件也是一样。
这个时候就可以请 HTML Template 出场了。我们可以在 html 文档中编写 DOM 结构,然后在 ShadowRoot 中加载过来即可。
HTML Template 实际上就是在 html 中的一个 标签,正常情况下,这个标签下的内容是不会被渲染的,包括标签下的 img、style、script 等都是不会被加载或执行的。你可以在脚本中使用 getElementById 之类的方法得到 标签对应的节点,但是却无法直接访问到其内部的节点,因为默认他们只是模版,在浏览器中表现为 #document-fragment,字面意思就是“文档片段”,可以通过节点对象的 content 属性来访问到这个 document-fragment 对象。
通过 document-fragment 对象,就可以访问到 template 内部的节点了,通过 document.importNode 方法,可以将 document-fragment 对象创建一份副本,然后可以使用一切 DOM 属性方法替换副本中的模版内容,最终将其插入到 DOM 或是 Shadow DOM 中。
<div></div> <template> <div></div> </template>
const template = document.getElementById('temp'); const copy = document.importNode(template.content, true); copy.getElementById('title').innerHTML = 'Hello World!'; const div = document.getElementById('div'); const shadowRoot = div.attachShadow({ mode: 'closed' }); shadowRoot.appendChild(copy);
HTML Imports
有了 HTML Template,我们已经可以方便地创造封闭的 Web 组件了,但是目前还有一些不完美的地方:我们必须要在 html 中定义一大批的 ,每个组件都要定义一个 。
此时,我们就可以用到已经被废弃的 HTML Imports 了。虽然它已经被废弃了,但是未来会通过 ES6 Modules 的形式再进行支持,所以理论上也只是换个加载形式而已。
通过 HTML Imports,我们可以将 定义在其他的 html 文档中,然后再在需要的 html 文档中进行导入(当然也可以通过脚本按需导入),导入后,我们就可以直接使用其中定义的模版节点了。
已经废弃的 HTML Imports 通过 标签实现,只要指定 rel="import" 就可以了,就像这样:,它可以接受 onload 和 onerror 事件以指示它已经加载完成。当然也可以通过脚本来创建 link 节点,然后指定 rel 和 href 来按需加载。Import 成功后,在 link 节点上有一个 import 属性,这个属性中存储的就是 import 进来的 DOM 树啦,可以 querySelector 之类的,并通过 cloneNode 或 document.importNode 方法创建副本后使用。
未来新的 HTML Imports 将会以 ES6 Module 的形式提供,可以在 JavaScript 中直接 import * as template from './template.html';,也可以按需 import,像这样:const template = await import('./template.html');。不过目前虽然浏览器都已经支持 ES6 Modules,但是在 import 其他模块时会检查服务端返回文件的 MIME 类型必须为 JavaScript 的 MIME 类型,否则不允许加载。
Custom Elements
有了上面的三个组件标准,我们实际上只是对 HTML 进行拆分而已,将一个大的 DOM 树拆成一个个相互隔离的小 DOM 树,这还不是真正的组件。
要实现一个真正的组件,我们就需要用到 Custom Elements 了,就如它的名字一样,它是用来定义原生组件的。
Custom Elements 的核心,实际上就是利用 JavaScript 中的对象继承,去继承 HTML 原生的 HTMLElement 类(或是具体的某个原生 Element 类,比如 HTMLButtonElement),然后自己编写相关的生命周期函数,处理成员属性以及用户交互的事件。
看起来这和现在的 React 很像,在 React 中,你可以这样创造一个组件:class MyElement extends React.Component { ... },而使用原生 Custom Elements,你需要这样写:class MyElement extends HTMLElement { ... }。
Custom Elements 的生命周期函数并不多,但是足够使用。这里我将 Custom Elements 的生命周期函数与 React 进行一个简单的对比:
constructor(): 构造函数,用于初始化 state、创建 Shadow DOM、监听事件之类。
对应 React 中 Mounting 阶段的大半部分,包括:constructor(props)、static getDerivedStateFromProps(props, state) 和 render()。
在 Custom Elements 中,constructor() 构造函数就是其原本的含义:初始化,和 React 的初始化类似,但它没有像 React 中那样将其拆分为多个部分。在这个阶段,组件仅仅是被创建出来(比如通过 document.createElement()),但是还没有插入到 DOM 树中。
connectedCallback(): 组件实例已被插入到 DOM 树中,用于进行一些展示相关的初始化操作。
对应 React 中 Mounting 阶段的最后一个生命周期:componentDidMount()。
在这个阶段,组件已经被插入到 DOM 树中了,或是其本身就在 html 文件中写好在 DOM 树上了,这个阶段一般是进行一些展示相关的初始化,比如加载数据、图片、音频或视频之类并进行展示。
attributeChangedCallback(attrName, oldVal, newVal): 组件属性发生变化,用于更新组件的状态。
对应 React 中的 Updating 阶段:static getDerivedStateFromProps(props, state)、shouldComponentUpdate(nextProps, nextState)、render()、getSnapshotBeforeUpdate(prevProps, prevState) 和 componentDidUpdate(prevProps, prevState, snapshot)。
当组件的属性(React 中的 props)发生变化时触发这个生命周期,但是并不是所有属性变化都会触发,比如组件的 class、style 之类的属性发生变化一般是不会产生特殊交互的,如果所有属性发生变化都触发这个生命周期的话,会使得性能造成较大的影响。所以 Custom Elements 要求开发者提供一个属性列表,只有当属性列表中的属性发生变化时才会触发这个生命周期函数。
这个属性列表通过组件类上的一个静态只读属性来声明,在 ES6 Class 中使用一个 getter 函数来实现,只实现 getter 而不实现 setter,getter 返回一个常量,这样就是只读的了。像这样:
class AwesomeElement extends HTMLElement { static get observedAttributes() { return ['awesome']; } }
disconnectedCallback(): 组件被从 DOM 树中移除,用于进行一些清理操作。
对应 React 中的 Unmounting 阶段:componentWillUnmount()。
adoptedCallback(): 组件实例从一个文档被移动到另一个文档。
这个生命周期是原生组件独有的,React 中没有类似的生命周期。这个生命周期函数也并不常用到,一般在操作多个 document 的时候会遇到,调用 document.adoptNode() 函数转移节点所属 document 时会触发这个生命周期。
在定义了自定义组件后,我们需要将它注册到 HTML 标签列表中,通过 window.customElements.define() 函数即可实现,这个函数接受两个必须参数和一个可选参数。第一个参数是注册的标签名,为了避免和 HTML 自身的标签冲突,Custom Elements 要求用户自定义的组件名必须至少包含一个短杠 -,并且不能以短杠开头,比如 my-element、awesome-button 之类都是可以的。第二个参数是注册的组件的 class,直接将继承的子类类名传入即可,当然也可以直接写一个匿名类:
window.customElements.define('my-element', class extends HTMLElement { ... });
注册之后,我们就可以使用了,可以直接在 html 文档中写对应的标签,比如:
或是
),但是只有规定的少数几个标签允许自关闭,所以,在 html 中写 Custom Elements 的节点时必须带上关闭标签。
由于 Custom Elements 是通过 JavaScript 来定义的,而一般 js 文件都是通过