コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

期待の新星が登場!超軽量Webフレームワーク【Piecesjs】でサクッと作る、ネイティブWebコンポーネント

Getting Started with Piecesjs: Building Native Web Components with a Lightweight Framework

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


モダンでクリエイティブなウェブサイトを構築する際、複雑なインタラクションの管理とクリーンでモジュラーなコードの維持は課題となります。そこでpiecesjsの出番です—これはネイティブWebコンポーネントを使った作業を簡素化するために設計された軽量フロントエンドフレームワークです。従来のフレームワークが持つ重い制約なしに、コンポーネントを動的に管理できる柔軟性を提供します。

コアとなる概念として、「Piece」はウェブページのどこにでも配置できるモジュラーコンポーネントです。各Pieceは独立して動作し、独自のスタイルとインタラクションをカプセル化しているため、サイト全体で容易に管理および再利用が可能です。

Piecesjsは各ページに必要なJavaScriptとCSSのみを動的にインポートし、パフォーマンスを最適化しながら柔軟性を維持します。大規模なフレームワークとは異なり、不要なコードや制限的なアーキテクチャの負担なしに、必要なものだけを構築することができます。

JavaScriptロジックを多用する—複数のステップ、状態、イベントを処理する—クリエイティブなウェブサイト向けに設計されており、高度にインタラクティブな体験を作成したい開発者向けに、合理的でスケーラブルなアプローチを提供します。

このプロジェクトはViteを使用して構築されており、高速なコンパイルと、postCSS経由のCSSを含む、容易なアセットのインポートを提供します(有効なpostCSS設定が必要です)。

piecesjsの最初のコードは2024年3月に書かれました。

リポジトリを探索したい場合は、こちらで確認できます: GitHub: https://github.com/piecesjs/piecesjs

主な機能

  • 動的JS & CSSインポート:各ページに必要なJavaScriptとCSSのみを自動的に読み込み、パフォーマンスを向上させます
  • スコープ付きイベント管理:this.on()とthis.off()メソッドを使用して、特定のコンポーネントのスコープ内でイベントを容易に管理できます
  • スコープ付きHTMLElementsへの便利なアクセス:this.$()またはthis.domAttr('slug')を使用してコンポーネント内の要素に素早くアクセスできます
  • アクティブなコンポーネント間のシームレスな通信:this.call()またはthis.emit()を使用してコンポーネント間で簡単に通信できます
  • 効率的なグローバルCSS管理:スタイルを整理するためのグローバルCSSインポートの合理的な処理
  • PiecesManager:すべてのアクティブなpieceへの一元的なアクセスを提供し、コンポーネント管理を簡素化します

なぜpiecesjsを作ったのか

Locomotiveの元フロントエンドリード兼クリエイティブデベロッパーとして、私たちは開発者間の共有ワークフローの確立を優先してきました。目標は、プロジェクトの移行や開発者間のサポートを円滑にするための、結束力のあるユニットを作ることでした。Locomotiveは常に、様々なフロントエンドやバックエンドのボイラープレート、locomotive-scrollのようなツールを通じて、その方法論を共有することに尽力してきました。私自身のツールを構築し、改良し、他者と共有を続けることは自然な流れでした。

フリーランスとしてのキャリアを始めた際、まだmodularjsに依存していたlocomotive-front-end-boilerplateを使用していました。私は、主要な概念を保持しながら、必要なコードのみを動的にインポートする、より現代的なものを作りたいと考えていました。

私は様々な理由で大規模な最新フレームワークを好んでいません。ワークフローを開発する際は、新しい標準やテクノロジーに合わせて進化させ、適応させることを好みます。個人で作業する場合でもチームで作業する場合でも、locomotive-scrollのような独自のツールを開発し、共有することは常にやりがいがありました。

どのようなインフラストラクチャで動作するか

piecesjsは非常に適応性が高く、様々なCMSプラットフォーム、構造、ワークフローで実行できます。npmパッケージとして、異なるセットアップにシームレスに統合できます。

piecesjsは以下の環境ですでにテスト済みです:

コーディングの時間

それでは、piecesjsを使い始めましょう。まず、フレームワークのコア概念に慣れていただくために、シンプルな静的プロジェクトの構築方法を説明します。その後、Astroを例として使用し、より複雑なワークフローへの組み込み方を見ていきます。

Pieceのライフサイクル

piecesjsでは、各「Piece」は明確に定義されたライフサイクルに従います。すべてのpieceをリストアップして読み込むと(その方法は後述します)、フレームワークは各pieceがDOMに追加された際に自動的に処理を行います。PieceがDOMに挿入されると、自動的にpremount()とmount()関数がトリガーされ、正しく初期化され、インタラクションの準備が整います。

premount(firstHit = true){}
render(){} // JavaScriptでレンダリングを行う場合
mount(firstHit = true){} // firstHitパラメータは、更新後または内容が変更された場合にfalseに設定されます
update(){} // 属性が変更された場合に呼び出されます。その後、unmount()、premount()、mount()が呼び出されます
unmount(update = false){} // updateは属性変更後にunmount()が呼び出された場合はtrueです

最初のPieceの作成

ヘッダー、2つのカウンター、およびリセットボタンを持つシンプルなページを構築してみましょう。これら3つの部分はそれぞれ別個の「Piece」となります。このセクションでは、pieceの作成と読み込み、それらの間の通信の有効化、ライフサイクルの観察、およびカスタムイベントの実験について説明します。

インストール

npm i piecesjs --save

Header.js – HTMLでレンダリングされるシンプルなPiece

PieceはHTMLで直接レンダリングするか、JavaScriptを使用してリアクティブコンポーネントとして作成するかを選択できます—これは完全にあなたの好みによります。この例では、HTMLで直接レンダリングされるヘッダーPieceを作成します。

index.htmlにて:

<!-- log属性はPieceのライフサイクルをログ出力するのに便利です -->
<c-header log class="c-header">
    <h1>piecesjs</h1>
</c-header>

/assets/js/componentsフォルダにHeader.jsファイルを作成:

import { Piece } from 'piecesjs';

class Header extends Piece {
  constructor() {
    // 2番目の引数は、特定のCSSが不要な場合はオプションです
    super('Header', {
      stylesheets: [() => import('/assets/css/components/header.css')],
    });
  }
}

// カスタム要素を登録
customElements.define('c-header', Header);

/assets/css/components/header.cssにCSSファイルを作成:

.c-header {
  display: block;
  padding: 20px;
}

次に、app.jsファイルでPieceを読み込みます:

import { load } from 'piecesjs';

//
// コンポーネントのインポート
//
// ------------------------------------------------------------
load('c-header', () => import('/assets/js/components/Header.js'));
// ------------------------------------------------------------

最後に、HTMLファイルにapp.jsを読み込むスクリプトタグを追加します。おめでとうございます🎉—最初のPieceが作成できました!これで、独自のスタイルとともに動的にページに読み込まれます。フルウェブサイトで作業している場合、特定のページでヘッダーが表示されない場合、そのスタイルシート(header.css)は不要に読み込まれることはなく、サイトの効率を保ちます。

Counter.js – リアクティブなPiece

次に、より複雑なPiece—リアクティブな値を持つカウンターを作成しましょう。この例では2つのカウンターを作ります。

index.htmlにて:

<!-- 
1つ目には、特定の通信を可能にするためにcid属性を定義します
-->
<c-counter cid="firstCounter" class="c-counter" value="2"></c-counter>
<c-counter class="c-counter" value="0"></c-counter>

/assets/js/componentsフォルダにCounter.jsファイルを作成して新しいPieceを作ります:

import { Piece } from 'piecesjs';

class Counter extends Piece {
  constructor() {
    super('Counter', {
      stylesheets: [() => import('/assets/css/components/counter.css')],
    });
  }
  
  // カスタム要素に"value"属性があるので、
  // 簡単にアクセスできるようにgetterとsetterを初期化します
  get value() {
    return this.getAttribute('value');
  }
 
  set value(value) {
    return this.setAttribute('value', value);
  }
}

customElements.define('c-counter', Counter);

/assets/css/components/counter.cssにスタイルを追加:

.c-counter {
    display: block;
    border: 1px solid black;
    padding: 20px;
    margin: 10px 0;
    border-radius: 12px;
}

Counter.jsでrender関数を使用してPiece内のHTMLをレンダリングします:

render() {
  return `
    <h2>${this.name} component</h2>
    <p>Value: ${this.value}</p>
    <button class="c-button">Increment</button>
  `;
}

⚠️ app.jsに新しいPieceを追加して読み込むことを忘れないでください:

import { load } from 'piecesjs';

//
// コンポーネントのインポート
//
// ------------------------------------------------------------
load('c-header', () => import('/assets/js/components/Header.js'));
load('c-counter', () => import('/assets/js/components/Counter.js'));
// ------------------------------------------------------------

結果は以下のようになります。

次に、ボタンにクリックイベントを追加して値をインクリメントできるようにし、カウンターをインタラクティブにします。これを実現するには、value属性の変更を監視し、更新時に再レンダリングをトリガーするために、static get observedAttributes()関数を追加する必要があります。

Counter.jsにて:

mount() {
  // this.$でクエリ
  this.$button = this.$('button')[0];

  // イベントリスナー
  this.on('click', this.$button, this.increment);
}

increment() {
  this.value = parseInt(this.value) + 1;
}

unmount() {
  // ここでリスナーを削除することが重要です
  this.off('click', this.$button, this.increment);
}

// 属性が変更された場合に自動的にupdate関数を呼び出し、
// 新しいレンダリングをトリガーするために重要
static get observedAttributes() {
  return ['value'];
}

できました!🎉 最初のリアクティブなPieceが完成しました!

Reset.js – Piece間の通信

次に、カウンターと通信してその値をリセットするResetPieceを作成します。すべてのカウンターをリセットするものと、最初のカウンターのみをリセットするものの2つのリセットコンポーネントをHTMLに追加します。これを実現するために、counterToReset属性を使用し、リセットしたいカウンターのcidと同じ値を割り当てます。

<c-reset class="c-button">Reset counters</c-reset>
<c-reset class="c-button" counterToReset="firstCounter">Reset first counter</c-reset>

componentsフォルダにReset.jsを作成できます:

import { Piece } from 'piecesjs';

class Reset extends Piece {
  constructor() {
    super('Reset');
  }

  mount() {
    // イベント名、ターゲット、関数、パラメータ(オプション)
    this.on('click', this, this.click);
  }

  click(e) {
    // 関数名、パラメータ、Piece名、pieceのcid(オプション)
    this.call('reset', {}, 'Counter', this.counterToReset);
  }

  unmount() {
    this.off('click', this, this.click);
  }

  // counterToReset属性に"this.counterToReset"で
  // 簡単にアクセスするためのgetterとsetter
  get counterToReset() {
    return this.getAttribute('counterToReset');
  }

  set counterToReset(value) {
    return this.setAttribute('counterToReset', value);
  }
}

// カスタム要素を登録
customElements.define('c-reset', Reset);

click関数を詳しく見てみましょう:call()メソッドはCounter.jsPieceで定義されたreset関数をトリガーします。call()メソッドの最後の引数はオプションで、Pieceのcidを参照します。この引数が省略された場合、reset関数は存在するすべてのCounterコンポーネントに適用されます。

⚠️ この新しいPieceをapp.jsに読み込むことを忘れないでください。

まだ触れていない便利なメソッドとしてemit()メソッドがあります。例えば、アクションを他のすべてのPieceに通知するためにカスタムイベントをディスパッチしたい場合、Piece内で以下のように実行できます:

this.emit('something', document, {
  value: 'Something is happened',
});

デフォルトでは、イベントはdocumentでトリガーされますが、特定のHTMLElementにスコープを限定することもできます。

その後、任意のPieceから以下のようにしてこのイベントをリッスンできます:

mount() {
  this.on('something', document, this.somethingIsHappened);
}

somethingIsHappened(e) {
  console.log(e.detail) // {value: 'Something is happened'}
}

// イベントリスナーの削除を忘れずに
unmount() {
  this.off('something', document, this.somethingIsHappened);
}

以上です!この第一部を楽しんでいただけ、piecesjsでさらに探求したくなる inspiration を得ていただけたことを願っています。次は、piecesjsをAstroで素早く実装する方法を見ていきますが、その前に、いくつかの有用な詳細情報を共有させてください:

メモ:メソッド、プロパティ、属性のリスト

piecesjsで利用可能なメソッド、プロパティ、属性の包括的なリストについては、公式ドキュメントを参照してください:

GitHub: piecesjs Memo

大規模プロジェクトのためのヒント

ページ遷移

プロジェクトでページ遷移を扱う場合、コンポーネントのload()呼び出しを再トリガーすることが重要です。この関数は更新されたDOMをスキャンし、まだ読み込まれていないコンポーネントを初期化します。

load('c-header', () => import('/assets/js/components/Header.js'));

このプロセスを効率化するために、load()呼び出しをユーティリティ関数にまとめ、新しいコンテナがDOMに配置された後の各ページ遷移後に呼び出すことを検討してください。

unmount()関数は、Piece(またはカスタム要素)がDOMから削除されると自動的に呼び出されます。つまり、ページ遷移中にどのPieceをアンマウントする必要があるかを手動でチェックする必要はありません。piecesjsはネイティブWebコンポーネントで構築されているため、このクリーンアップは自動的に処理されます。

グローバルスタイル

piecesjsでは、変数、ユーティリティなどを管理するためのグローバルまたは共有スタイルも含めることができます。Viteを使用すると、フォルダ全体をインポートすることも可能で、非常に効率的です🔥

例えば、styles.jsファイルを作成し、HTMLにインポートして、以下のようにCSSファイルを読み込むことができます:

// フォルダをインポート
import.meta.glob('../css/settings/*.css', { eager: true });
import.meta.glob('../css/common/*.css', { eager: true });

// ファイルをインポート
import '../css/document.css';

Astroでの実装例

では、これまでに構築したpiecesjsをAstroプロジェクトに統合する方法を見ていきましょう。これは主にプロジェクト構造とファイルパスの調整を伴いますが、プロセスは非常に簡単です。

始めるには、ターミナルで以下のコマンドを実行します:

npm create astro@latest

2番目のプロンプトで「Include sample files」を選択します。その後、TypeScriptには「No」を、依存関係のインストールには「Yes」を選択します。

その後、以下のコマンドを実行してpiecesjsをインストールできます:

npm i piecesjs --save

pages/index.astroでファイルをクリーンアップし、これまでのコードを貼り付けます:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="Welcome to Astro and piecesjs">
    <c-header log class="c-header">
      <h1>piecesjs</h1>
    </c-header>

    <c-counter cid="firstCounter" class="c-counter" value="2"></c-counter>
    <c-counter class="c-counter" value="0"></c-counter>

    <c-reset class="c-button"> Reset counters </c-reset>
    <c-reset class="c-button" counterToReset="firstCounter">Reset first counter</c-reset>
</Layout>

Layout.astroに、このシンプルな例を貼り付けることができます。実装を容易にするために、pieceと共通スタイルを読み込むために必要なすべてのものをこのファイルに直接含めます:

---
const { title } = Astro.props;
---

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content="Astro description" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content={Astro.generator} />
    <title>{title}</title>
  </head>
  <body>
    <slot />

    <script>
      import { load } from 'piecesjs';

      load('c-header', () => import('../components/Header.js'));
      load('c-counter', () => import('../components/Counter.js'));
      load('c-reset', () => import('../components/Reset.js'));

      // 共通スタイル
      // フォルダをインポート
      import.meta.glob('../styles/reset/*.css', { eager: true });
      import.meta.glob('../styles/common/*.css', { eager: true });

      // ファイルをインポート
      import '../styles/global.css';
    </script>
  </body>
</html>

これで、CSSファイルとフォルダを/src/stylesに、pieceを/src/componentsに配置し、新しい構造に合わせて各Pieceのスタイルシートのパスを更新します。例えば、Header.jsでは以下のように更新します:

class Header extends Piece {
  constructor() {
    super('Header', {
      stylesheets: [() => import('/src/styles/components/header.css')],
    });
  }
}

// カスタム要素を登録
customElements.define('c-header', Header);

これで完了です!これであなたはpiecesjsを使用してAstroのエコシステム全体を探索し、CMSとの接続やその他あなたが想像できることすべてをカスタマイズできます。

piecesjsを使用して素晴らしいプロジェクトを作成する inspiration になれば幸いです!

©コハム