blog.euxn.me

Stencil.js で CustomElements を実装する

2020-12-06 Sun.

Stencil.js とは

Stencil.js は、マルチプラットフォーム向け UI システムである Ionic の開発チームが主導して開発が進められています。

https://stenciljs.com/

執筆時点でのバージョンは v2.3.0 が提供されており、1 度のメジャーアップデートが実施されています。 また、 Ionic Framework の v4.x 以降は Stencil によって実装されています。 Stencil.js は、再利用可能なコンポーネントを、 CustomElements として定義するためのツールチェイン・コンパイラです。(公式でフレームワークではないと名言しています。) TypeScript ファーストであり、 Decorator (TC39 proposal にあるものではなく、 TS 実装の legacy なものなので、将来的に変更になる可能性はあります)と React に依存しない JSX でコンポーネントを記述します。 最近のバージョンで SSG にも対応しましたが、ここでは CustomElements を作ることにフォーカスして紹介します。

始め方

公式の Getting Started をベースに、かいつまんで説明します。

https://stenciljs.com/docs/getting-started

任意のディレクトリで $ npm init stencil を実行することで、実行時プロンプトで聞かれるプロジェクト名のディレクトリが作成されます。 ionic-pwa / app / component から選択するプロンプトが表示されますが、ここでは component を選択します。 src ディレクトリ以下に stencil に必要なファイル軍とサンプルコードが生成されます。不要であれば my-componentutils は削除してしまいましょう。 components.d.ts はビルドのたびに自動生成されます。プロジェクトにもよりますが、複数のトピックブランチで別のコンポーネントを作成した際に順序のコンフリクトが発生するため、 gitignore に追加してもよいかもしれません。

既存プロジェクトに stencil を追加するコマンドは無いようですが、 tsconfig.jsonstencil.config.ts があれば開始することができます。 なお tsconfig.json は stencil のために珍しい構成をしているため、既存プロジェクトとは別に管理し、自動生成されたものをベースに使用するのが好ましいと思います。 その後は npx stencil g component-name コマンドでコンポーネントの雛形を作成することができます。

コンポーネントの実装

ためしに$ npx stencil g first-component を実行すると、以下のようなファイルが生成されます。(テストファイルについては省略します。)

src/components/first-component/first-component.tsx
1import { Component, Host, h } from '@stencil/core';
2
3@Component({
4 tag: 'first-component',
5 styleUrl: 'first-component.css',
6 shadow: true,
7})
8export class FirstComponent {
9
10 render() {
11 return (
12 <Host>
13 <slot></slot>
14 </Host>
15 );
16 }
17
18}

css の方はこのようになっています。

src/components/first-component/first-component.css
1:host {
2 display: block;
3}

@Component() デコレータでコンポーネントクラスのメタ情報を定義します。 Angular を触ったことのある方なら馴染みやすいかと思います。 デフォルトで shadow: true であり ShadowDOM が有効化されていますが、 stenicl は ShadowDOM をオフにすることもできます。 (が、非常に推奨しません。ライフサイクルは ShadowDOM ではなく stencil 独自の実装により実行されるため、他フレームワークとの組み合わせでは対象 DOM Node が見つからない、といったバグが容易に引き起こされます。) <Host /> は ShadowDOM のルート要素、 <slot /> は子として渡された要素を表しています。

css は ShadowDOM により scoped になっています。(ShadowDOM を off にした場合は global になります。) また、scss を使用することもできます。

ためしに以下のように MyButton コンポーネントを作ってみます。

1import { Component, ComponentInterface, h, Prop } from "@stencil/core";
2
3@Component({
4 tag: "my-button",
5 styleUrl: "button.scss",
6 shadow: false,
7})
8export class Button implements ComponentInterface {
9 @Prop() theme?: string;
10
11 @Prop() disabled?: boolean;
12
13 render(): h.JSX.IntrinsicElements {
14 const theme = this.theme ?? "secondary";
15
16 return (
17 <button
18 class={{
19 [theme]: true,
20 disabled: this.disabled,
21 }}
22 disabled={this.disabled}
23 >
24 <slot></slot>
25 </button>
26 );
27 }
28}
1:host {
2 button {
3 display: block;
4 width: 100%;
5 padding: 0.875rem;
6
7 font-weight: 700;
8 line-height: 1rem;
9
10 border-radius: 8px;
11 border: none;
12 user-select: none;
13
14 /* default is secondary */
15 &.secondary {
16 background: #d1d5db;
17 color: #6ee7b7;
18
19 &:hover {
20 background: #9ca3af;
21 }
22
23 &:active {
24 background: #6b7280;
25 color: $#10B981;
26 }
27
28 &.disabled {
29 background: #d1d5db;
30 color: #d1d5db;
31 }
32 }
33
34 &.primary {
35 background: #10b981;
36 color: white;
37
38 &:hover {
39 background: #059669;
40 }
41
42 &:active {
43 background: #047857;
44 }
45
46 &.disabled {
47 background: #d1fae5;
48 }
49 }
50 }
51}

class の表記には React でいう classnames や clsx のように Object 記法を使うことができます。 また、<Host /> 要素は省略することができます。

ライフサイクル

Stencil.js にも他のフレームワーク同様、コンポーネントのライフサイクルが存在します。

https://stenciljs.com/docs/component-lifecycle

公式ドキュメントより引用

これらのライフサイクルメソッドはコンポーネントの interface に定義されており、 class に実装して使用します。 (Angular の ngOnInit 等に近いです。)

表示時にはコンポーネントが DOM Node と接続された際に呼び出される connectedCallback および初回のみの componentWillLoad 、もしくはコンポーネントがレンダリングされる直前に呼び出される componentWillRender で表示に必要な計算処理を行います。 connectedCallback は主に 1 度のみの初期化(Props や関連要素のデフォルト値埋めなど)や関連しているコンポーネント(先祖/子孫要素)の探索などを行うのに適しています。

更新時には @WatchcomponentShouldUpdatecomponentWillUpdate と明確に順序づけて分かれている関数を使用します。

更新後に呼び出される componentDidRendercomponentDidLoadcomponentDidUpdate は、他の Component にレンダリング完了を通知するイベントを発火する用途で使うことが多いでしょう。

別のフレームワークと組み合わせて使用する場合は、それぞれのライフサイクルのタイミングの違いに注意する必要があります。

終わりに

本記事では Stencil.js による CustomElements の実装方法を紹介しました。 Stencil.js とは独立した CustomElements として出力されているため、 React や Angular のアプリケーションにもそのまま使用できます。 複数のフレームワーク間で共通して使用できるコンポーネントを実装するのに、 lit-element では表現力が物足りない場合には Stencil.js は選択肢に挙がるでしょう。

しかし、複数のフレームワークで共用するということは、それぞれのフレームワーク向けに都度実装するのが大変であったり、適切なライブラリが存在しない場合である可能性が高いです。つまり、再利用性を目的に CustomElements を実装するということは、再利用する意義のある複雑なコンポーネントになりがちということです。 デザインシステムの実装とした場合でも、入力要素の制御等、複雑な制御を行う場合、各アプリケーションのフレームワーク側で実装するか、 CutsomElements として実装するか、は慎重に判断する必要があります。

また、 CustomElements の提供する独自のイベントに対するハンドラ(例: fillfulled イベントを定義した場合の onFilfulled ハンドラなど)は CustomElements の責務の範囲外でもあり、自動で実装はされません。 また、 JSX として使用する場合、型情報が JSX で無くなってしまいます。 Stencil.js では、メジャーなフレームワーク向けにハンドラや型定義等を提供する Proxy を提供しています(バージョン的に、 まだ experimental なようですが、安定して動作しています。) Proxy の詳細についてはまた別の投稿で紹介するので、是非チェックしてください。

この記事は Japan Digital Design Advent Calendar 2020 の 6 日目の記事でした。