この記事の概要
svelte.jsで間違い探しゲームをつくってみました。いろいろとハマったポイントもあったので忘れないようにメモしていきます。
作ったモノの概要
ピクトグラムを使った間違い探しゲームです。
チュートリアルも実装されています。
公開先
-
ソース
-
公開先URL(実際に遊べます)
使ったフレームワーク
- svelte.js (JavaScript全体のフレームワーク)
- driver.js (チュートリアル用のライブラリ)
- bulma (CSSのフレームワーク)
svelte.jsを使って困ったこと
npm installで他のパッケージが上手くインストールできない
webpackあたりでエラーが出てインストールできなかったモノがあったので、一旦はindex.htmlにCDNで利用するようにしました。ここらへんはもう少し調査が必要そうです。今回は作ることを優先したので一旦CDNで利用する形を取りました。
配列を追加しても再描画されない
公式のドキュメントにも書いていますが配列を追加しただけだと画面上のオブジェクトが更新されません。
ログで出力するとちゃんと変数に入っているので、結構ココでハマるポイントかもしれません。
それじゃどうすればいいのかというと、変数として再代入することで解決します。svelte.jsの仕様上、変数に再代入されたときだけちゃんと再描画として認識するようです。vue.jsで慣れていると「ナンデダヨ!!」っていう部分です。
- 参考サイト
-
https://svelte.dev/tutorial/updating-arrays-and-objects
- 以下のnumberにプッシュしてからの再代入がなんとも・・・
- numbers.push(numbers.length + 1);
- numbers = numbers;
-
https://svelte.dev/tutorial/updating-arrays-and-objects
ちなみに今回のソースで該当の部分は以下のとおり
<script>
export const changeImage = () => {
setAnimation();
imgArray = [];
const appearRnd = Math.ceil(Math.random() * appearImg) - 1;
const imageRnd = Math.ceil(Math.random() * imageCount);
for (let step = 0; step < appearImg; step++) {
if (appearRnd == step) {
// imgArray.push(`pict_img/${imageRnd}-A.png`);
// ※pushの場合は変数に再代入する必要がある
// imgArray = imgArray;
imgArray = [...imgArray, `pict_img/${imageRnd}-A.png`];
} else {
// imgArray.push(`pict_img/${imageRnd}-B.png`);
imgArray = [...imgArray, `pict_img/${imageRnd}-B.png`];
}
}
};
</script>
{#if isVisible}
<div transition:fly={{ y: 100 }} class="container has-text-centered">
{#each imgArray as img}
<BtnPict imagePath={img} clickFunc={clickAction} />
{/each}
</div>
{/if}
使う機能をimportする必要がある
vue.jsだと初期で読み込むcreatedやmountedなどはimportせずに利用できます。svelte.jsの場合は以下のように、その都度読み込む必要があるため面倒です。もしかすると上手くやる方法があるかもしれません。
<script>
// onMountを使うなら毎回宣言する必要がある
import { onMount } from "svelte";
</script>
動的にon:clickに対して関数を設定するときに注意が必要
on:clickに関数を設定するときにそのまま設定すると死にます。引数がない場合は()なしで設定すればよいのですが、引数付きの関数の場合は気をつける必要があります。
以下はダメパターン。
<script>
export let clickFunc = (msg) => console.log(msg);
</script>
<p on:click={clickFunc ('コンソールログがタイヘンなことに・・・')}>TEST</p>
以下はイイパターン。
<script>
export let clickFunc = (msg) => console.log(msg);
</script>
<p on:click={() => {clickFunc ('ヤッホー')} }>TEST</p>
svelte.jsを使ってよかったこと
変数の扱いがわかりやすい
vue.jsに比べてそのままJavaScriptの変数を扱うような感じです。具体的にいうとvue.jsの場合はテンプレートに渡す変数はpropsとして渡す必要があります。そしてpropsから渡ってきた変数はそのまま再代入するとwarningで怒られます。dataで変数を宣言しなおして再代入することが正しいわけです。
vue.jsですが以下はwarningで怒られます。
export default {
props: {
propsValue: Number
},
methods: {
editValue() {
this.propsValue = 20
}
}
vue.jsでは以下が正しい姿
export default {
props: {
propsValue: Number
},
data() {
// プロパティの値を設定
dataValue: this.propsValue
},
methods: {
editValue() {
// データに定義した値を変更
this.dataValue = 20
}
}
ここまでvue.jsのことを書いてきましたが、propsやdata()でそれぞれの変数の扱いがありますが、svelte.jsだとそういった扱いの違いがなく直感的に扱えるのは良い点だと思いました。
svelte.jsで違うコンポーネントに渡したい場合は・・・
■ App.svelte
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
exportをつけるだけで渡せます。
■ Nested.svelte
<script>
export let answer;
</script>
<p>The answer is {answer}</p>
読み取り専用のストアの変数を設定できる
上記のようにコンポネントで変数を渡し合うことでどこで何の変数が使われているか把握しづらくなります。1対1で渡すだけならいいのですが、コンポネントに渡してその先のコンポネントに渡してのバケツリレーになったりすると悲惨です。
そのためにアプリケーションで扱うデータセットをstoreと呼ばれる領域で一元管理することで、各コンポーネントはstoreにアクセスすれば常に共通の値を参照する仕組みがあります。これはvue.jsでも同様でいろいろなコンポネントから値を参照する場合に有効だったりします。
ここでもvue.jsの話になりますが、vue.jsの場合はstoreにgetterやsetterを記述して、storeの変数を参照したいときはgetterを使い、storeの変数を更新したいときはsetterを使うわけです。
svelte.jsでもstoreを使えるわけですがよりわかりやすく使用できます。
■store.js
import { writable } from 'svelte/store';
export const ST_score = writable(0);
■App.js
<script>
import { ST_score, ST_gamestart } from "./stores.js";
import { get } from "svelte/store";
//更新
ST_score.set(100);
//他のコンポネントから更新されたか検知したい場合はsubscribeを利用
let score = 0;
ST_score.subscribe((value) => {
score = value;
});
//ちなみにsvelte/storeのgetでも取得はできる
//(ただし他での更新は検知されない)
//初期値を代入するときに利用できそう
console.log(get(ST_score));
</script>
ただし扱いやすくなる反面、使われたくないタイミングで使われるケースもあるわけです。例えば、変なところで変数を更新されたくないため、読み取り専用にしたいな~というケースもあるわけです。そこでsvelte.jsではJavaとかでいうところのカプセル化のような隠蔽化するような仕組みもあります。
今回の作ってみたモノではハイスコアを読み取り専用にしてスコアの状態からハイスコアを更新するようにしています。ちなみにハイスコアはローカルストレージに保存するようにしています。
import { readable, writable } from 'svelte/store';
import { get } from 'svelte/store';
const lsKey_hiScore = "hiscore"
let lsHiScore = localStorage.getItem(lsKey_hiScore);
lsHiScore = lsHiScore ? lsHiScore : 0;
export const ST_score = writable(0);
export const ST_hiScore = readable(null, function start(set) {
//readableで初期値をセットできる
set(lsHiScore);
//この中だけで値の更新ができる
//スコアの変更を検知して条件によって更新する
ST_score.subscribe(value => {
console.log('hi-score: ' + get(ST_score));
if (lsHiScore < value) {
set(value);
localStorage.setItem(lsKey_hiScore, value);
}
});
});
最初から使えるトランジションが便利(効果や演出がラク)
フェードインやスクロールインのアニメーションなどカンタンに使えます。
時間の設定なども細かくできるので他のCSSのライブラリとか使わずアニメーションを設定出来るのは便利かなと。
ただし、使うトランジションごとにインポートは必要で、コンポネント各所で利用すると思ったとおりの演出にならないこともあるので、イイカンジにお付き合いする必要があります。
実際にトランジションを使う場合は、ifなどで切り替わることでトランジションを発生させることができるようです。もしも初回の表示でトランジションを使いたい場合は、onMountなどで切り替えるとよさそうです。
<script>
import { onMount } from "svelte";
import { fly } from "svelte/transition";
let isVisible = false;
onMount(() => {
let isVisible = true;
});
</script>
{#if isVisible}
<p transition:fly={{ y: 100 }}>
下から上にせり上がる感じのやーつ
</p>
{/if}
総評
最初こそはいろいろとvue.jsとの違いで文句があったりしましたが、あとになればなるほどサクサク書けるようになりました。
storeによるreadableですが設定することで他のstoreの変数をトリガーにする形にしているので面倒くさいことになったかなと。
あとは1つのコンポネントが大きくなってきたら細かく分けたり、ここらへんはvue.jsと同様のセンスが必要だったりします。
あとはチュートリアルのライブラリがかゆいところに手が届かなくて試行錯誤したのが苦労したところかなと思います。

