T-CREATOR

Preact で埋め込みウィジェット配布:他サイトに設置できる軽量 UI の作り方

Preact で埋め込みウィジェット配布:他サイトに設置できる軽量 UI の作り方

自社サービスのウィジェットを他のサイトに簡単に埋め込んでもらいたい、そんなニーズはありませんか。チャットボット、SNS シェアボタン、レビューウィジェットなど、様々なサービスが埋め込み型の UI を提供しています。Preact を使えば、わずか 3KB という軽量さで、React 風の開発体験を保ちながら、どんな Web サイトにも設置できるウィジェットを作成できるのです。

この記事では、Preact を使った埋め込みウィジェットの作り方を、初心者の方にもわかりやすく解説していきます。実装の基礎から配布方法まで、実践的な知識が身につくでしょう。

背景

Web 埋め込みウィジェットとは

Web 埋め込みウィジェットは、外部サイトに設置できる独立した UI コンポーネントです。利用者は自分の Web サイトに数行の HTML コードを追加するだけで、高機能な UI を組み込めます。

代表的な例として、以下のようなサービスがあります。

#サービス例用途
1Google Analyticsアクセス解析
2Intercomチャットサポート
3Disqusコメントシステム
4AddThisSNS シェアボタン
5Stripe Elements決済フォーム

これらのウィジェットは、設置先のサイトのスタイルや JavaScript と干渉せず、独立して動作する必要があります。

なぜ Preact が最適なのか

React、Vue、Angular など、多くのフレームワークが存在する中で、Preact が埋め込みウィジェットに適している理由を見ていきましょう。

軽量性が最大の魅力です。ウィジェットは外部サイトに読み込まれるため、ファイルサイズが大きいとページの読み込み速度に悪影響を与えてしまいます。Preact は本体がわずか 3KB と非常に軽量で、React の約 10 分の 1 のサイズです。

以下の図で、主要フレームワークと Preact のサイズ比較を示します。

mermaidflowchart TB
    subgraph frameworks["フレームワークサイズ比較"]
        react["React<br/>約40KB"]
        vue["Vue 3<br/>約34KB"]
        preact["Preact<br/>約3KB"]
        vanilla["Vanilla JS<br/>約0KB"]
    end

    subgraph impact["ページ読み込みへの影響"]
        large["大きいサイズ<br/>読み込み遅延"]
        medium["中程度のサイズ<br/>軽微な影響"]
        small["小さいサイズ<br/>影響なし"]
    end

    react --> large
    vue --> medium
    preact --> small
    vanilla --> small

    style preact fill:#90EE90
    style small fill:#90EE90

Preact は軽量でありながら、React とほぼ同じ API を提供しているため、React 開発者であればすぐに習得できます。JSX や Hooks も使えるので、モダンな開発体験を損なうことはありません。

課題

埋め込みウィジェット開発の難しさ

埋め込みウィジェットを作成する際には、通常の Web アプリ開発とは異なる課題に直面します。これらを理解しておくことで、適切な設計ができるようになるでしょう。

スタイルの独立性確保

設置先のサイトには様々な CSS が適用されています。グローバルな CSS ルールがウィジェットに影響を与えてしまうと、意図しない見た目になってしまいます。

逆に、ウィジェットのスタイルが設置先のサイトに影響を与えることも避けなければなりません。

JavaScript の競合回避

設置先のサイトでは、jQuery やその他のライブラリが使われている可能性があります。グローバル変数の競合やイベントリスナーの干渉を防ぐ必要があるでしょう。

また、複数のバージョンの React が存在する環境でも動作する必要があります。

パフォーマンスへの配慮

ウィジェットの読み込みで、設置先のページ表示が遅くなってはいけません。非同期読み込みやコード分割など、最適化技術が求められます。

以下の図で、ウィジェット実装時の主な課題を整理します。

mermaidflowchart TD
    widget["埋め込みウィジェット"]

    widget --> style_issue["スタイル課題"]
    widget --> js_issue["JavaScript課題"]
    widget --> perf_issue["パフォーマンス課題"]

    style_issue --> style1["グローバルCSS<br/>の影響を受ける"]
    style_issue --> style2["設置先サイトの<br/>スタイルを破壊"]

    js_issue --> js1["ライブラリの<br/>競合"]
    js_issue --> js2["グローバル変数<br/>の衝突"]

    perf_issue --> perf1["読み込み時間<br/>の増加"]
    perf_issue --> perf2["メモリ使用量<br/>の増加"]

    style perf_issue fill:#FFB6C1
    style js_issue fill:#FFB6C1
    style style_issue fill:#FFB6C1

初期化とクリーンアップ

ウィジェットはページ読み込み後に動的に初期化され、場合によっては動的に削除されることもあります。適切なライフサイクル管理が必要でしょう。

解決策

Preact による埋め込みウィジェットの設計方針

上記の課題を解決するため、以下の設計方針でウィジェットを構築していきます。

Shadow DOM でのカプセル化

Shadow DOM を使用することで、スタイルと DOM を完全に独立させられます。設置先の CSS の影響を受けず、逆に影響も与えません。

名前空間の活用

グローバルスコープを汚染しないよう、すべての機能を 1 つの名前空間内に収める設計にします。

遅延読み込みの実装

ウィジェットの初期化を非同期で行い、ページ表示をブロックしないようにします。

以下の図で、Preact ウィジェットのアーキテクチャを示します。

mermaidflowchart TB
    subgraph host["設置先サイト"]
        html["HTML<br/>【script タグ】"]
        dom["既存のDOM"]
    end

    html -->|非同期読み込み| loader["ローダースクリプト"]

    loader -->|初期化| widget["ウィジェット本体"]

    subgraph widget_internal["ウィジェット内部"]
        shadow["Shadow DOM<br/>【独立した空間】"]
        preact["Preact<br/>コンポーネント"]
        styles["スコープ付き<br/>CSS"]
    end

    widget --> shadow
    shadow --> preact
    shadow --> styles

    dom -.独立.-> shadow

    style shadow fill:#E6F3FF
    style widget_internal fill:#F0F0F0

この設計により、スタイルの独立性、JavaScript の競合回避、パフォーマンスの最適化をすべて実現できます。

実装の全体像

ウィジェット実装は、以下の 3 つのパーツで構成されます。

#パーツ名役割
1ローダースクリプトウィジェットの初期化を担当
2Preact コンポーネントUI ロジックの実装
3ビルド設定配布可能な形式への変換

それぞれを順番に実装していきましょう。

具体例

プロジェクトのセットアップ

まずは、Preact プロジェクトの初期化から始めます。Yarn を使ってプロジェクトを作成しましょう。

必要なパッケージのインストール

新しいディレクトリを作成し、必要なパッケージをインストールします。

bashmkdir preact-widget
cd preact-widget
yarn init -y

次に、Preact と開発に必要なツールをインストールします。

bashyarn add preact
yarn add -D vite @preact/preset-vite

ここでは、高速なビルドツールである Vite を使用します。Vite は開発サーバーの起動が速く、HMR(Hot Module Replacement)も優れているため、開発効率が向上するでしょう。

プロジェクト構造の作成

以下のようなディレクトリ構造でファイルを配置します。

bashpreact-widget/
├── src/
│   ├── widget.tsx        # Preactコンポーネント
│   ├── loader.ts         # ローダースクリプト
│   └── styles.css        # ウィジェットのスタイル
├── package.json
└── vite.config.ts        # Viteの設定

Preact コンポーネントの実装

それでは、実際のウィジェットコンポーネントを作成していきます。シンプルなチャットウィジェットを例に解説します。

基本的なコンポーネント構造

src​/​widget.tsx ファイルを作成し、基本となるコンポーネントを実装します。

typescriptimport { h } from 'preact';
import { useState } from 'preact/hooks';

まず必要なモジュールをインポートします。h 関数は JSX を変換するために必要で、useState はコンポーネントの状態管理に使用します。

typescriptinterface WidgetProps {
  position?: 'bottom-right' | 'bottom-left';
  primaryColor?: string;
}

ウィジェットの設定を Props として受け取れるよう、TypeScript のインターフェースを定義します。これにより、利用者が表示位置や色をカスタマイズできるようになります。

typescriptexport function ChatWidget({
  position = 'bottom-right',
  primaryColor = '#0066FF',
}: WidgetProps) {
  // ウィジェットの開閉状態を管理
  const [isOpen, setIsOpen] = useState(false);
  // メッセージの入力内容を管理
  const [message, setMessage] = useState('');

  return (
    <div class={`widget-container ${position}`}>
      {/* コンテンツは次のステップで実装 */}
    </div>
  );
}

状態管理のための Hooks を設定します。isOpen でチャットウィンドウの開閉、message で入力中のテキストを管理しましょう。

UI の実装

チャットウィジェットの UI を段階的に構築していきます。

typescript// トグルボタンの実装
const toggleButton = (
  <button
    class='widget-toggle'
    onClick={() => setIsOpen(!isOpen)}
    style={{ backgroundColor: primaryColor }}
    aria-label='チャットを開く'
  >
    {isOpen ? '✕' : '💬'}
  </button>
);

ウィジェットを開閉するためのボタンを作成します。アクセシビリティのため、aria-label 属性も設定しています。

typescript// チャットウィンドウの実装
const chatWindow = isOpen && (
  <div class='widget-window'>
    <div
      class='widget-header'
      style={{ backgroundColor: primaryColor }}
    >
      <h3>チャットサポート</h3>
    </div>
    <div class='widget-body'>
      <div class='widget-messages'>
        <div class='message message-bot'>
          こんにちは!何かお困りですか?
        </div>
      </div>
    </div>
    <div class='widget-footer'>
      {/* フッターは次のステップで実装 */}
    </div>
  </div>
);

チャットウィンドウの基本構造を作成します。ヘッダー、メッセージ表示エリア、入力エリアの 3 つのセクションで構成されます。

typescript// 入力フォームの実装
const inputForm = (
  <form
    onSubmit={(e) => {
      e.preventDefault();
      console.log('送信:', message);
      setMessage('');
    }}
  >
    <input
      type='text'
      value={message}
      onInput={(e) => setMessage(e.currentTarget.value)}
      placeholder='メッセージを入力...'
      class='widget-input'
    />
    <button type='submit' class='widget-send'>
      送信
    </button>
  </form>
);

メッセージ入力フォームを実装します。送信時の処理は実際の API と連携させることもできるでしょう。

完全なコンポーネント

すべてのパーツを組み合わせた完全なコンポーネントは以下のようになります。

typescriptexport function ChatWidget({
  position = 'bottom-right',
  primaryColor = '#0066FF',
}: WidgetProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [message, setMessage] = useState('');

  return (
    <div class={`widget-container ${position}`}>
      <button
        class='widget-toggle'
        onClick={() => setIsOpen(!isOpen)}
        style={{ backgroundColor: primaryColor }}
        aria-label={
          isOpen ? 'チャットを閉じる' : 'チャットを開く'
        }
      >
        {isOpen ? '✕' : '💬'}
      </button>

      {isOpen && (
        <div class='widget-window'>
          <div
            class='widget-header'
            style={{ backgroundColor: primaryColor }}
          >
            <h3>チャットサポート</h3>
          </div>
          <div class='widget-body'>
            <div class='widget-messages'>
              <div class='message message-bot'>
                こんにちは!何かお困りですか?
              </div>
            </div>
          </div>
          <div class='widget-footer'>
            <form
              onSubmit={(e) => {
                e.preventDefault();
                console.log('送信:', message);
                setMessage('');
              }}
            >
              <input
                type='text'
                value={message}
                onInput={(e) =>
                  setMessage(e.currentTarget.value)
                }
                placeholder='メッセージを入力...'
                class='widget-input'
              />
              <button type='submit' class='widget-send'>
                送信
              </button>
            </form>
          </div>
        </div>
      )}
    </div>
  );
}

スタイルの実装

ウィジェットのスタイルを src​/​styles.css ファイルで定義します。Shadow DOM 内で適用されるため、外部のスタイルと競合しません。

ベーススタイル

css/* リセットとベーススタイル */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.widget-container {
  position: fixed;
  z-index: 999999;
  font-family: -apple-system, BlinkMacSystemFont,
    'Segoe UI', sans-serif;
}

最初にリセット CSS を適用し、ウィジェットを固定位置に配置します。z-index を高く設定することで、他の要素より前面に表示されます。

位置指定のスタイル

css/* 表示位置の設定 */
.widget-container.bottom-right {
  bottom: 20px;
  right: 20px;
}

.widget-container.bottom-left {
  bottom: 20px;
  left: 20px;
}

利用者が選択した位置にウィジェットを配置するためのスタイルです。

トグルボタンのスタイル

css/* トグルボタン */
.widget-toggle {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  border: none;
  color: white;
  font-size: 24px;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transition: transform 0.2s, box-shadow 0.2s;
}

.widget-toggle:hover {
  transform: scale(1.05);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}

円形のトグルボタンに、ホバー時のアニメーションを追加します。視覚的なフィードバックがあることで、ユーザー体験が向上するでしょう。

チャットウィンドウのスタイル

css/* チャットウィンドウ */
.widget-window {
  position: absolute;
  bottom: 80px;
  width: 360px;
  height: 500px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

チャットウィンドウをトグルボタンの上に表示し、角丸とシャドウで洗練された見た目にします。

css/* ヘッダー */
.widget-header {
  padding: 16px;
  color: white;
}

.widget-header h3 {
  font-size: 18px;
  font-weight: 600;
}

ヘッダー部分のスタイルを定義します。プライマリーカラーが背景色として適用されます。

css/* メッセージエリア */
.widget-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  background: #f7f7f7;
}

.widget-messages {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.message {
  padding: 12px;
  border-radius: 8px;
  max-width: 80%;
}

.message-bot {
  background: white;
  align-self: flex-start;
}

メッセージ表示エリアのスタイルです。スクロール可能にし、メッセージごとに適切な間隔を設定します。

入力フォームのスタイル

css/* フッター(入力エリア) */
.widget-footer {
  padding: 16px;
  background: white;
  border-top: 1px solid #e0e0e0;
}

.widget-footer form {
  display: flex;
  gap: 8px;
}

.widget-input {
  flex: 1;
  padding: 10px 12px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  font-size: 14px;
  outline: none;
}

.widget-input:focus {
  border-color: #0066ff;
}

.widget-send {
  padding: 10px 20px;
  background: #0066ff;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 600;
  transition: background 0.2s;
}

.widget-send:hover {
  background: #0052cc;
}

入力フォームと送信ボタンのスタイルを定義します。フォーカス時の視覚的フィードバックも忘れずに実装しましょう。

ローダースクリプトの実装

ローダースクリプトは、ウィジェットを初期化し、Shadow DOM に描画する役割を担います。

基本的な初期化関数

src​/​loader.ts ファイルを作成し、初期化ロジックを実装します。

typescriptimport { render, h } from 'preact';
import { ChatWidget } from './widget';
import styles from './styles.css?inline';

必要なモジュールとスタイルをインポートします。Vite の ?inline クエリを使うことで、CSS を文字列として読み込めます。

typescriptinterface WidgetConfig {
  position?: 'bottom-right' | 'bottom-left';
  primaryColor?: string;
  containerId?: string;
}

ウィジェットの設定を定義するインターフェースを用意します。

typescriptfunction initWidget(config: WidgetConfig = {}) {
  // デフォルト値の設定
  const {
    position = 'bottom-right',
    primaryColor = '#0066FF',
    containerId = 'preact-chat-widget',
  } = config;

  // 次のステップで実装
}

初期化関数のベースを作成します。利用者が設定を省略した場合のデフォルト値も定義しておきましょう。

Shadow DOM の作成と初期化

typescriptfunction initWidget(config: WidgetConfig = {}) {
  const {
    position = 'bottom-right',
    primaryColor = '#0066FF',
    containerId = 'preact-chat-widget',
  } = config;

  // コンテナ要素の作成
  const container = document.createElement('div');
  container.id = containerId;
  document.body.appendChild(container);

  // 次のステップで実装
}

ウィジェットを配置するためのコンテナ要素を作成し、body に追加します。

typescript// Shadow DOMの作成
const shadowRoot = container.attachShadow({ mode: 'open' });

// Shadow DOM内にスタイルを注入
const styleElement = document.createElement('style');
styleElement.textContent = styles;
shadowRoot.appendChild(styleElement);

Shadow DOM を作成し、スタイルを注入します。これにより、外部の CSS から完全に独立した環境が作られます。

typescript// Preactコンポーネントのレンダリング用要素を作成
const mountPoint = document.createElement('div');
shadowRoot.appendChild(mountPoint);

// Preactコンポーネントをレンダリング
render(
  h(ChatWidget, { position, primaryColor }),
  mountPoint
);

Preact コンポーネントをレンダリングします。h 関数を使って JSX を構築し、render 関数で Shadow DOM 内にマウントします。

グローバル API の公開

typescript// グローバルスコープにAPIを公開
declare global {
  interface Window {
    PreactChatWidget: {
      init: (config?: WidgetConfig) => void;
    };
  }
}

window.PreactChatWidget = {
  init: initWidget,
};

利用者がスクリプト読み込み後にウィジェットを初期化できるよう、グローバル API を公開します。名前空間を使うことで、他のスクリプトとの競合を避けられるでしょう。

typescript// 自動初期化のサポート
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    initWidget();
  });
} else {
  initWidget();
}

DOM の読み込み完了後に自動的に初期化する機能を追加します。これにより、利用者は何も設定せずに使い始めることもできます。

ビルド設定

Vite を使って、配布可能な形式にビルドする設定を行います。

Vite の設定ファイル

vite.config.ts ファイルを作成し、ビルドオプションを設定します。

typescriptimport { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

Vite と preact のプリセットをインポートします。

typescriptexport default defineConfig({
  plugins: [preact()],
  build: {
    lib: {
      entry: 'src/loader.ts',
      name: 'PreactChatWidget',
      fileName: 'widget',
      formats: ['iife'],
    },
    rollupOptions: {
      output: {
        inlineDynamicImports: true,
      },
    },
  },
});

ライブラリモードでビルドし、IIFE(即時実行関数式)形式で出力します。すべてのコードを 1 つのファイルにバンドルすることで、利用者は 1 つの script タグを追加するだけで済みます。

以下の図で、ビルドプロセスを視覚化します。

mermaidflowchart LR
    subgraph source["ソースコード"]
        tsx["widget.tsx<br/>【Preact コンポーネント】"]
        loader["loader.ts<br/>【初期化スクリプト】"]
        css["styles.css<br/>【スタイル】"]
    end

    subgraph build["ビルドプロセス"]
        vite["Vite"]
        bundle["バンドル処理"]
        minify["最小化"]
    end

    subgraph output["出力ファイル"]
        iife["widget.iife.js<br/>【配布用ファイル】"]
    end

    tsx --> vite
    loader --> vite
    css --> vite

    vite --> bundle
    bundle --> minify
    minify --> iife

    style iife fill:#90EE90

package.json のスクリプト設定

package.json にビルドコマンドを追加します。

json{
  "name": "preact-chat-widget",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "preact": "^10.19.3"
  },
  "devDependencies": {
    "@preact/preset-vite": "^2.8.1",
    "typescript": "^5.3.3",
    "vite": "^5.0.10"
  }
}

これで開発、ビルド、プレビューのコマンドが使えるようになります。

ビルドと配布

それでは実際にビルドを実行し、配布用ファイルを生成しましょう。

bashyarn build

ビルドが完了すると、dist ディレクトリに以下のファイルが生成されます。

pythondist/
├── widget.iife.js      # 本番用(最小化済み)
└── widget.iife.js.map  # ソースマップ

この widget.iife.js ファイルを CDN や Web サーバーにアップロードすれば、配布準備は完了です。

利用者への実装方法

ウィジェットを配布した後、利用者は以下の HTML コードを自分の Web サイトに追加するだけで使えます。

基本的な設置方法

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>ウィジェット設置例</title>
  </head>
  <body>
    <h1>私のWebサイト</h1>
    <p>ここに通常のコンテンツが表示されます。</p>

    <!-- ウィジェットの読み込み -->
    <script src="https://your-cdn.com/widget.iife.js"></script>
  </body>
</html>

たったこれだけで、チャットウィジェットが表示されます。自動初期化が有効なため、追加の設定は不要です。

カスタマイズ設定

利用者が表示位置や色をカスタマイズしたい場合は、以下のように設定できます。

html<script src="https://your-cdn.com/widget.iife.js"></script>
<script>
  // 自動初期化を無効にしたい場合は、初期化前に設定を変更
  window.PreactChatWidget.init({
    position: 'bottom-left',
    primaryColor: '#FF5722',
  });
</script>

このように、シンプルな API で柔軟なカスタマイズが可能になっています。

開発時のデバッグ

開発中は、Vite の開発サーバーを使うと効率的です。

bashyarn dev

開発サーバーが起動したら、index.html ファイルを作成してテストできます。

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>ウィジェット開発</title>
  </head>
  <body>
    <h1>開発用ページ</h1>
    <script type="module" src="/src/loader.ts"></script>
  </body>
</html>

ファイルを保存すると即座にブラウザに反映されるため、快適に開発を進められるでしょう。

エラー処理とデバッグ

実際の運用では、エラー処理も重要です。以下のようなエラーハンドリングを追加することをお勧めします。

初期化エラーの処理

typescriptfunction initWidget(config: WidgetConfig = {}) {
  try {
    // Shadow DOMがサポートされているか確認
    if (!document.body.attachShadow) {
      console.error(
        'PreactChatWidget: Shadow DOM is not supported'
      );
      return;
    }

    // 既に初期化済みか確認
    if (
      document.getElementById(
        config.containerId || 'preact-chat-widget'
      )
    ) {
      console.warn(
        'PreactChatWidget: Widget is already initialized'
      );
      return;
    }

    // 初期化処理...
  } catch (error) {
    console.error(
      'PreactChatWidget: Initialization failed',
      error
    );
  }
}

Error: Shadow DOM is not supported

このエラーは、古いブラウザで Shadow DOM がサポートされていない場合に発生します。

発生条件

  • Internet Explorer 11 以前
  • Safari 9 以前
  • 古い Android ブラウザ

解決方法

  1. ポリフィルを導入する
  2. サポート対象ブラウザを明確にする
  3. フォールバック用の UI を用意する
typescript// ポリフィルの例
import '@webcomponents/shadydom';

パフォーマンス最適化

ウィジェットのパフォーマンスをさらに向上させるためのテクニックを紹介します。

遅延読み込みの実装

typescript// 非同期での初期化
function initWidget(config: WidgetConfig = {}) {
  // ページ読み込み完了まで待機
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      requestIdleCallback(() => {
        performInit(config);
      });
    });
  } else {
    requestIdleCallback(() => {
      performInit(config);
    });
  }
}

function performInit(config: WidgetConfig) {
  // 実際の初期化処理
}

requestIdleCallback を使うことで、ブラウザのアイドル時間を利用して初期化できます。これにより、ページの読み込みパフォーマンスへの影響を最小限に抑えられるでしょう。

コード分割

大きなウィジェットの場合、コード分割を検討しましょう。

typescript// 動的インポートの例
async function openChat() {
  const { ChatWindow } = await import(
    './components/ChatWindow'
  );
  // チャットウィンドウを表示
}

初回はトグルボタンのみを読み込み、ユーザーがクリックしたときに本体を読み込む方式にすることで、初期読み込みサイズを削減できます。

セキュリティ考慮事項

埋め込みウィジェットでは、セキュリティにも配慮が必要です。

XSS 対策

ユーザー入力を扱う場合は、必ずサニタイズを行いましょう。

typescriptimport DOMPurify from 'dompurify';

function sanitizeMessage(message: string): string {
  return DOMPurify.sanitize(message, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
    ALLOWED_ATTR: [],
  });
}

Error: XSS vulnerability detected

このエラーは、サニタイズされていないユーザー入力が表示される場合に発生する可能性があります。

発生条件

  • ユーザーが入力したテキストをそのまま表示
  • API レスポンスをそのまま HTML に挿入

解決方法

  1. DOMPurify などのライブラリでサニタイズ
  2. Preact のテキストノードとして表示(自動エスケープ)
  3. Content Security Policy の設定

CSP(Content Security Policy)対応

厳格な CSP が設定されている環境でも動作するよう、インラインスタイルを避ける設計にすることが望ましいです。

まとめ

Preact を使った埋め込みウィジェットの作り方を、セットアップから配布まで詳しく解説してきました。

重要なポイントを振り返りましょう。Preact は 3KB という軽量さで、設置先サイトのパフォーマンスに影響を与えません。Shadow DOM を使うことで、スタイルと JavaScript を完全に独立させ、設置先サイトとの競合を回避できます。Vite による効率的なビルドプロセスで、開発体験も優れています。

実装の流れは以下の通りです。

#ステップ主な作業
1セットアップPreact と Vite のインストール
2コンポーネント実装UI ロジックの作成
3スタイル実装独立した CSS の定義
4ローダー実装Shadow DOM 初期化
5ビルド設定IIFE 形式での出力
6配布CDN へのアップロード

この記事で紹介した方法を使えば、チャットサポート、通知バナー、評価ウィジェットなど、様々な埋め込み型 UI を作成できるでしょう。

さらに高度な機能として、複数のウィジェットインスタンスの管理、リアルタイム通信の実装、アクセシビリティの強化なども検討できます。あなたのサービスに最適なウィジェットを作成し、多くの Web サイトで利用してもらえるといいですね。

関連リンク