import ReadMore from '~/components/ReadMore.astro'

標準的なHTMLの<script>タグを使用すれば、React・Svelte・Vue等のUIフレームワークを利用せずにインタラクティブ性をAstroコンポーネントへ追加できます。これによってブラウザ上で実行されるJavaScriptを送信してAstroコンポーネントに機能を追加できるようになります。

クライアントサイドスクリプト

スクリプトは、イベントリスナーの追加やアナリティクスデータの送信、アニメーションの再生など、JavaScriptがウェブ上でできることすべてに使用できます。

astro
<!-- src/components/ConfettiButton.astro --><button data-confetti-button>Celebrate!</button><script>  // npmモジュールをインポートする。  import confetti from 'canvas-confetti';  // ページ上のコンポーネントのDOMを探す。  const buttons = document.querySelectorAll('[data-confetti-button]');  // ボタンがクリックされたときにconfettiを発火するイベントリスナーを追加する。  buttons.forEach((button) => {    button.addEventListener('click', () => confetti());  });</script>

デフォルトでは、Astroは<script>タグを処理してバンドルし、npmモジュールのインポートやTypeScriptの記述などをサポートします。

Astroで<script>を使用する

.astroファイル内に単数(もしくは複数)の<script>タグを追加することによってクライアントサイドJavascriptを追加できます。

下の例では、<Hello />コンポーネントをページに追加することでブラウザコンソール上にログメッセージを出力します。

astro
<h1>Welcome, world!</h1><script>  console.log('Welcome, browser console!');</script>

スクリプトの処理

デフォルトで<script>タグはAstroで処理されます。

  • すべてのインポートはバンドルされ、ローカルファイルやNodeモジュールをインポートできます。
  • 処理されたスクリプトは、type="module"としてページの<head>に挿入されます。
  • TypeScriptを対応しており、TypeScriptファイルもインポートできます
  • コンポーネントがページに複数回使われている場合、スクリプトは一度だけ含まれます。
astro
<script>  // 処理される! バンドルされる! TypeScriptサポート!  // ローカルスクリプトやNodeモジュールのインポートも動作します。</script>

type="module"属性は、ブラウザにスクリプトをJavaScriptモジュールとして扱わせます。これにはいくつかのパフォーマンス上のメリットがあります。

  • レンダリングがブロックされません。モジュールスクリプトとその依存関係がロードされる間、ブラウザは残りのHTMLの処理を続けます。
  • ブラウザはモジュールスクリプトを実行する前に、HTMLが処理されるのを待ちます。"load"イベントをリッスンする必要はありません。
  • asyncdefer属性は不要です。モジュールスクリプトは常に延期されます。

:::note async属性はレンダリングのブロックを防ぐため、通常のスクリプトにとって価値があります。しかし、モジュールスクリプトでは既にこの動作が含まれています。モジュールスクリプトにasyncを追加すると、ページが完全に読み込まれる前にモジュールスクリプトが実行されます。おそらくこれはあなたが望んでいることではありません。 :::

処理の対象外にする

Astroがスクリプトを処理しないようにするには、is:inlineディレクティブを追加します。

astro
<script is:inline>  // 書かれたとおりにレンダリングされます。  // ローカルインポートが解決されずに動作しません。  // コンポーネント内の場合、コンポーネントが利用されている箇所全てに繰り返しレンダリングされます。</script>

:::note Astroは、状況によってはスクリプトタグを処理しません。特に、<script>タグにtype="module"src以外の属性を追加すると、Astroはそのタグをis:inlineディレクティブがあるかのように扱います。スクリプトがJSX式で記述されている場合も同様です。 :::

<script> タグで利用可能なディレクティブについてもっと知りたい場合は、ディレクティブリファレンスページを参照してください。

ページにJavaScriptファイルを含める

スクリプトを.js.tsファイルのように分離して書きたい場合や、他のサーバーから外部スクリプトを参照したい場合には<script>タグのsrc属性の参照を使えます。

ローカルスクリプトのインポート

これを使うケース: スクリプトがsrcに存在している時。

Astroはスクリプトの処理のルールに従って、ビルド、最適化、そしてページ内にスクリプトを追加します。

astro
<!-- `src/scripts/local.js`への相対パス --><script src="../scripts/local.js"></script><!-- このローカルのTypeScriptファイルも動作します。 --><script src="./script-with-types.ts"></script>

外部スクリプトをロードする

これを使うケース: JavaScriptファイルがpublicディレクトリに存在しているか、CDNの時。

プロジェクトのsrcフォルダ以外のスクリプトをロードするには、is:inlineディレクティブを含めます。このアプローチでは、上記のようにスクリプトをインポートする際にAstroが提供しているJavaScriptの処理・バンドル・最適化はスキップされます

astro
<!-- `public/my-script.js`スクリプトへの絶対パス --><script is:inline src="/my-script.js"></script><!-- リモートサーバー上にあるスクリプトへの完全なURL --><script is:inline src="https://my-analytics.com/script.js"></script>

共通スクリプトパターン

onclickや他のイベントをハンドリングする

いくつかのUIフレームワークではonclick={...}(React/Preact)や@click="..."(Vue)のように独自の構文でイベントを処理しています。Astroは標準的なHTMLにより近く、イベントのために独自の構文は利用しません。

その代わりに、ユーザーのインタラクションをハンドリングするために<script>タグ内にaddEventListenerを利用します。

astro
<button class="alert">Click me!</button><script>  // ページ内から`alert`クラスを持つすべてのボタンを探す。  const buttons = document.querySelectorAll('button.alert');  // 各ボタンがクリックされたときのハンドリング  buttons.forEach((button) => {    button.addEventListener('click', () => {      alert('Button was clicked!');    });  });</script>

:::note ページに複数の<AlertButton />コンポーネントが存在する場合でも、Astroは複数回スクリプトを実行しません。スクリプトはページに1つしか含まれないようにバンドルされます。querySelectorAll を利用すると、このスクリプトはページ上でalertクラスを持つすべてのボタンにイベントリスナーをアタッチします。 :::

カスタム要素を持つWebコンポーネント

Webコンポーネント標準を使うことで独自の動作をするHTML要素を作成できます。.astroコンポーネントでカスタム要素を定義するとUIフレームワークライブラリを利用しなくてもインタラクティブなコンポーネントを作ることができます。

下の例では、ハートボタンがクリックされた回数を記録し、その最新のカウント数を<span>に更新する<astro-heart>HTML要素を新しく定義しています。

astro
<!-- カスタム要素 "astro-heart" でコンポーネント要素を囲みます。 --><astro-heart>  <button aria-label="Heart">💜</button> × <span>0</span></astro-heart><script>  // 新しいタイプのHTML要素の動作を定義する  class AstroHeart extends HTMLElement {    constructor() {			super();      let count = 0;      const heartButton = this.querySelector('button');      const countSpan = this.querySelector('span');      // ボタンがクリックされるごとにカウントを更新する。			heartButton.addEventListener('click', () => {        count++;        countSpan.textContent = count.toString();      });		}  }  // <astro-heart>要素としてAstroHeartクラスを利用することをブラウザに教える  customElements.define('astro-heart', AstroHeart);</script>

ここでカスタム要素を利用するメリットは2つあります。

  1. document.querySelector()を使って全ページを検索する変わりに、this.querySelector()を使えば検索範囲が現在のカスタム要素のインスタンスに限られます。これにより、一回で一つのコンポーネントだけを操作しやすくなります。
  2. <script>は一度だけしか実行されませんが、ブラウザはページ上の<astro-heart>を見つける度にカスタム要素のconstructor()メソッドを実行します。これにより、ページで複数回コンポーネントを使用する場合でも一つのコンポーネントを安全に記述できます。

web.devの再利用可能なウェブコンポーネントガイドMDNのカスタム要素の紹介で、カスタム要素について詳しく学べます。

フロントマター変数をスクリプトに渡す

Astroコンポーネントでは、---フェンスの間にあるフロントマターのコードはサーバー上で実行され、ブラウザでは利用できません。サーバーからクライアントへ変数を渡すには、変数を保存しておきJavaScriptがブラウザで実行されたときに読み込む必要があります。

これを実現するための1つの手段はdata-* attributesを利用してHTML出力に変数の値を保存する方法です。カスタム要素を含むスクリプトはブラウザ上にHTMLがロードされると要素のdatasetを利用することで、この属性を読み込むことができます。

下の例では、message propをdata-message属性に保存されているので、カスタム要素がthis.dataset.messageを読み込みブラウザ上で値を取得しています。

astro
---const { message = 'Welcome, world!' } = Astro.props;---<!-- message propをdata属性に保存する --><astro-greet data-message={message}>  <button>Say hi!</button></astro-greet><script>  class AstroGreet extends HTMLElement {    constructor() {			super();      // data属性からmessageを読み込む      const message = this.dataset.message;      const button = this.querySelector('button');      button.addEventListener('click', () => {        alert(message);      });		}  }  customElements.define('astro-greet', AstroGreet);</script>

これでコンポーネントは何度でも利用でき、異なるmessageを表示できます

astro
---import AstroGreet from '../components/AstroGreet.astro';---<!-- messageの初期値「Welcome, world!」を利用する --><AstroGreet /><!-- propsとしてカスタムmessageを渡す --><AstroGreet message="Lovely day to build components!" /><AstroGreet message="Glad you made it! 👋" />

:::tip[知っていましたか?] この方法はReactなどのUIフレームワークを用いて書かれたコンポーネントにpropsを渡す際にAstroが裏で行っていることです!client:*ディレクティブを持つコンポーネントに対して、Astroは<astro-island>カスタム要素にprops属性を作成してサーバーサイドのpropsをHTML出力結果に保存します。 :::