この記事の内容は、ネイティブ HTML コンポーネントとは何か知っていますか?というものです。ネイティブ HTML コンポーネントの紹介は、参考になると思います。 #########おい!ここ数年を見てみると、Web フロントエンドの開発は非常に速いです。
#4 つの Web コンポーネント標準
ネイティブ HTML コンポーネントについて説明する前に、その 4 つについて簡単に紹介します。主要な Web コンポーネント標準は次のとおりです。 HTML テンプレート、Shadow DOM、カスタム要素、HTML 輸入品。実際にはそのうちの1つが放棄されたため、それが「ビッグ3」になりました。 HTML テンプレート 聞いたことがある人も多いと思いますが、HTML5 の タグは、通常の状態では、その存在すら認識されません。その下の img はダウンロードされず、スクリプトも実行されません。 その名の通り、これは単なるテンプレートであり、使用して初めて意味を持ちます。
Shadow DOM は、ネイティブ コンポーネントのカプセル化のための基本ツールであり、コンポーネント間の独立性を実現できます。
Custom Elements は、ネイティブ コンポーネントをパッケージ化するために使用されるコンテナーであり、タグを記述するだけで完全なコンポーネントを取得できます。
HTML Imports は HTML の ES6 モジュールに似たもので、別の HTML ファイルを直接インポートして使用できます。 DOM ノード。ただし、HTML インポートと ES6 モジュールは Chrome を除いて非常に似ているため、 他のブラウザーはこれを実装しようとしないため、非推奨となり、推奨されません。将来的にはES6 Moduleを使用して置き換える予定ですが、新しいバージョンでの置き換え計画はまだないようです。 この機能は 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 を作成した場合は非表示になりません。これで、video タグの本当の顔がわかります:
ここでは、通常のデバッグと同じように、自由に調整できます。 DOM Shadow DOM の内容 (いずれにせよ、通常の DOM と同じであり、リフレッシュ後に復元されます)。
上記のシャドウ 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 はまったく影響を受けません。 ##つまり、スタイルを記述するときは、id を使用する必要がある場合は id を使用し、クラスを使用する必要がある場合はクラスを使用します。ボタンのクラスを .button として記述する必要がある場合は、.button として記述する必要があります。現在のコンポーネントの ID とクラスが他のコンポーネントと競合する可能性があることを考慮する必要はありません。コンポーネント内で競合がないことを確認するだけで済みます。これは簡単です。
これにより、現在ほとんどのコンポーネントベースのフレームワークが直面している問題、つまり「Element のクラス (className) をどのように記述するか?」が解決されます。接頭辞付きの名前空間を使用すると、次のようにクラス名が長すぎます。 .header-nav-list-sublist-button-icon; いくつかの CSS-in-JS ツールを使用すると、次のような一意のクラス名を作成できます。 : .Nav__welcomeWrapper___lKXTg、この名前はまだ少し長く、冗長な情報が含まれています。
ShadowRootShadowRoot は Shadow DOM のルートです。DOM では
のように扱うことができます。つまり、 ではありません。 では、ノードではない場合でも一部のプロパティを使用できません。ShadowRoot の appendChild や querySelectorAll などの属性またはメソッドを使用して、Shadow DOM ツリー全体を操作できます。
などの通常の要素の場合、attachShadow メソッドを呼び出すことで ShadowRoot を作成できます (古くなっているため、オブジェクトを受け入れる createShadowRoot メソッドもあります)。初期化: { mode: 'open' }、このオブジェクトには 2 つの値を持つ mode 属性があります: '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 文件都是通过