SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

高速・軽量なJavaScriptフレームワーク「Svelte」の世界

作りながら学ぶ「Svelte」の構造とモダンなフロントエンド開発の考え方

高速・軽量なJavaScriptフレームワーク「Svelte」の世界 第2回

  • X ポスト
  • このエントリーをはてなブックマークに追加

配列と{#each}構文で会議の参加者一覧を作成――各参加者をコンポーネントに分割

 先程の例では、各参加者を表示するHTML要素を、人数分ベタ書きしていました。これはあまりエレガントではありません。そこで、まずは参加者を表すステートparticipantsを作成して、それを使ってすべての参加者を表示するようにしてみましょう、

<script lang="ts">
let visible = true;
let participants = [
		{ name: '参加者猫 (2)', video: true },
		{ name: '参加者猫 (3)', video: true },
		{ name: '参加者猫 (4)', video: true },
	];
</script>
...
{#each participants as participant, index}
	<div class="participant">
		<span>{participant.name}</span>
		<img src="https://placekitten.com/320/240?image={index+2}">
	</div>
{/each}
...

 各参加者の表示はコンパクトになり、participantsステートに要素を追加すれば自動的に表示が更新されるようにもなりました。{}はHTML本文だけでなく、属性値(imgタグのsrc属性)の中でも使用できることに注目です。

 さて、ここで、各参加者の名前をクリックすることで、ビデオをON/OFFできる機能を追加したいと思います。どうすれば良いでしょうか。

 最も単純な方法として、{#each}で受け取ったインデックスを使う方法が考えられます。次のようにして、配列のインデックスを使ってparicipants配列の中のvideoフラグを更新するイメージです:

{#each participants as participant, index}
  ...
  <span on:click={() => participants[index].video = !participants[index].video}>
		{participant.name}
  </span>
  ...
{/each}

 しかしながら、やっていることの単純さのわりに少々複雑に見えがちですし、今回はよりシンプルに書く方法があるので、それを試してみましょう。すなわち、コンポーネント分割です。

 ここまでApp.svelteという単一のコンポーネントだけを使ってコードを書いてきました。このコンポーネントはアプリケーション全体を表現しているので、その内部ではアプリケーションに関することすべてが同時に存在しています。何かを変更しようとすれば、別の何かがそれに影響を与える可能性も常に考えなければなりません。これでは、気軽な変更やテストは難しくなります。

 そこで、アプリケーションに関する状態のうち、「参加者1人に関すること」だけを管理するコンポーネントを作成してみます。「参加者1人に関すること」については、そのコンポーネントが管轄することにして、App.svelteの中では忘れても良いことにします。

 具体的には、App.svelteと同じフォルダにParticipant.svelteというファイルを作成することで行います。これもApp.svelteと同じくSvelteコンポーネントなので、書き方も基本的には同じです。それでは、先程「参加者1人」にあたる部分を、Participant.svelteに移しましょう。

<script lang="ts">
</script>

<div class="participant">
  <span>{participant.name}</span>
  <img src="https://placekitten.com/320/240?image={index+2}">
</div>

<style>
	.participant {
		position: relative;
		float: left;
		width: 320px;
		height: 240px;
		border: 1px solid black;
		margin: 2px;
		background: black;
	}
	.participant span {
		background: black;
		color: white;
		padding: 0 0.5rem;
		position: absolute;
		top: 0;
	}
</style>

 このコンポーネントのHTML断片の中ではparticipant変数を参照していますが、この時点ではparticipantはありません。このコードがApp.svelteの中にいた頃、participantは参加者一覧を表すparticipants配列から取得していましたが、このコンポーネントは「参加者1人」を管理するのが目的でした。参加者一覧を管理する役割は、今でもApp.svelteにまかせて良さそうです。

 ということは、App.svelteから参加者1人に関する情報を渡してもらう必要があります。こういうときに使うのがプロパティ(properties/props)です。プロパティは、基本的にはステートと同じように内部状態を表しますが、ひとつだけ特別な違いがあります。それは、コンポーネントの外部から値を設定できるということです。その性質がここで役に立つわけですね。

 Svelteでは、プロパティは次のように書きます:

export let participant;

 合わせて、Participant.svelteは次のようになります:

<script lang="ts">
  export let participant;
</script>

<div class="participant">
  <span>{participant.name}</span>
  <img src="https://placekitten.com/320/240?image=1">
</div>

<style>
	.participant {
		position: relative;
		float: left;
		width: 320px;
		height: 240px;
		border: 1px solid black;
		margin: 2px;
		background: black;
	}
	.participant span {
		background: black;
		color: white;
		padding: 0 0.5rem;
		position: absolute;
		top: 0;
	}
</style>

 前半で述べた通り、これは通常のJavaScriptにおけるexportとはまったく違った意味を持つので注意が必要です。といっても「外部に露出する」というexportという言葉の意味からはそこまで離れていないので、慣れてしまえばそれほど気になるという人はあまりいないように思います。

 このように宣言すると、コンポーネントを使う側からは次のように使うことができます:

<script lang="ts">
  import Participant from './Participant.svelte';
  let participants = [
		{ name: 'Member cat 1', video: true },
		{ name: 'Member cat 2', video: true }
	]
</script>

{#each participants as participant}
        <Participant participant={participant.name} />
{/each}

 内部状態であるはずのparticipantですが、プロパティとして宣言されていることで、呼び出し側からその値を設定できるようになっています。

 ところで、App.svelteの中では参加者1人に関する情報をまとめて管理するためにparticipantというオブジェクトにname, videoをまとめる必要がありました。が、そもそもコンポーネント自体がオブジェクトのようなものなので、二重にまとめて扱う意味はないように感じます。そこで、コンポーネントを次のように変更してみましょう。

export let name;
export let video;

 こうしてみると、videoApp.svelteから指定することはないような気がしてきます。なんといっても、App.svelteが「参加者1人に関すること」を考えなくて良くするためにこのコンポーネントを作ったのですから。videoは、プロパティではなく単なる内部状態、すなわちステートにしていましましょう。

 そして、これは本筋ではないのですが、これまで{each}構文の渡してくれるindexを猫の種類を指定するために使っていました。これもコンポーネント外部から渡す必要がありますが、良い機会なので、index(配列内の順番)であることに意味はなく、「猫の種類」を指定するためのプロパティなんだ、という意図が明確になるように、catTypeという名前をつけてあげましょう。まとめると、Participant.svelteのプロパティとステートは、次のようになります:

export let name;
export let catType;
let video;

 App.svelte側はこうなります:

<script lang="ts">
	import Participant from './Participant.svelte'
	let visible = true;
	
	let participantNames = [ '参加者猫 (2)', '参加者猫 (3)', '参加者猫 (4)'];
</script>
<div class="participant p1">
	<span>参加者猫 (1) (自分)</span>
	{#if visible}
		<img src="https://placekitten.com/320/240?image=1">
	{:else}
		<img src="https://via.placeholder.com/320x240/000000/FFFFFF/?text=Video%20OFF">
	{/if}
</div>
{#each participantNames as name, index}
	<Participant {name} catType={index} />
{/each}
<div class="list-of-participants">
	<ul>
		<li>参加者猫 (1) (自分)</li>
		{#each participantNames as name}
			<li>{name}</li>
		{/each}
	</ul>
</div>
<div class="control">
	<button>退出</button>
	<button>音声 OFF</button>
	<button on:click={() => visible = !visible}>ビデオ ON/OFF</button>
	<button>画面共有</button>
</div>

<style>
	.participant {
		position: relative;
		float: left;
		width: 320px;
		height: 240px;
		border: 1px solid black;
		margin: 2px;
		background: black;
	}
	.participant span {
		background: black;
		color: white;
		padding: 0 0.5rem;
		position: absolute;
		top: 0;
	}
	.list-of-participants {
		clear: both;
	}
</style>

 とてもシンプルになりました。{name}となっている箇所は、name={name}の省略記法です。Svelteでは、プロパティ名とそれに渡そうとする変数名が同じ場合だけ、このように省略することができます。最初は戸惑うかもしれませんが、慣れると無駄がなくて便利です。

 さて、呼び出し側を修正したら、次のは呼び出される側を仕上げましょう。といっても、舞台は整っています。単なる変数であるステートを更新するだけですべて実現できるように準備してきたので、後はとても単純です。

<script lang="ts">
  export let name;
  export let catType;
  let video;
</script>

<div class="participant">
  <span>{name}</span>
  <img src="https://placekitten.com/320/240?image={catType+2}">
</div>

<style>
	.participant {
		position: relative;
		float: left;
		width: 320px;
		height: 240px;
		border: 1px solid black;
		margin: 2px;
		background: black;
	}
	.participant span {
		background: black;
		color: white;
		padding: 0 0.5rem;
		position: absolute;
		top: 0;
	}
</style>

 これで、コンポーネント分割が完了しました。

 さて、元々の目的であった「各参加者の名前をクリックすると、それぞれのカメラをON/OFFできる機能」を作り込みましょう。

 といっても、最初に自分のカメラ画像に対して実装したのと同じ機能です。複数の参加者を同時に扱うと少し複雑そうに感じたものですが、コンポーネントに分割した今となっては、一人のことだけを考えれば良いので簡単そうです。

 videoステートの真偽に応じて画像を表示し分けるようにして、span要素にvideoステートをトグルするハンドラを書いてあげましょう。こうなるはずです:

<script lang="ts">
  export let name;
  export let catType;
  let video = true;
</script>

<div class="participant">
  <span on:click={() => video = !video}>{name}</span>
  {#if video}
    <img src="https://placekitten.com/320/240?image={catType+2}">
  {:else}
    <img src="https://via.placeholder.com/320x240/000000/FFFFFF/?text=Video%20OFF">
  {/if}
</div>

<style>
	.participant {
		position: relative;
		float: left;
		width: 320px;
		height: 240px;
		border: 1px solid black;
		margin: 2px;
		background: black;
	}
	.participant span {
		background: black;
		color: white;
		padding: 0 0.5rem;
		position: absolute;
		top: 0;
	}
</style>

 これで完成です。アプリケーションを表示してみましょう。これまで通り「ビデオON/OFF」ボタンで自分のビデオ表示を切り替えられることに加えて、各参加者の名前をクリックすることでそれぞれの参加者のビデオの表示も同様のことができるようになりました。

 1つ1つのコンポーネントを開発しているときは単純に感じていたものが、全体として表示されると、それなり複雑な機能を備えているように感じます。

 それも当然で、実際のところ、それなりに複雑な機能を開発していたからです。すべてをApp.svelteに詰め込んでいたときを思い出してみてください。それなりに複雑な機能の関係性を頭の中に配置してコードを書くのは、それなりの負担を感じるものです。

 しかし、それを小さく狭い単位に分割することで、1つ1つは単純に感じられるようにできました。コンポーネントを分割してデータの流れをシンプルにすることによる効果を感じていただけたのではないでしょうか。

次のページ
setIntervalで定期的に参加者が訪れるようにする――配列の代入更新

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
高速・軽量なJavaScriptフレームワーク「Svelte」の世界連載記事一覧
この記事の著者

濱口 恭平(ハマグチ キョウヘイ)

 1990年・三重県生まれ。ドイツに拠点を置く Web3 / Privacy Tech スタートアップ でCTO/VPoEを務める傍ら、サイドプロジェクトとして「誰でも使えるビデオ通話SDK 」"kommu" を開発している。 多くの人に快適な開発者体験を提供できるフロントエンド技術を探求する中で Svelte に出会い、開発者コミュニティでの交流をきっかけに2021年には公式イベントSvelte Summit での登壇を経験。 2022年からは国内...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/15572 2022/02/28 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング