Open26

Svelteを触りながら実務で取り入れられないか模索するスクラップ

masaksmasaks

公式ドキュメント
一旦ゴールを設定(随時更新)

  • ローカル開発環境(※PhpStormでしかやらないよ)
    • TypeScript
      • できた。めっちゃ楽
    • ESLint, Formatter
      • 現状どちらかを捨てなければいけない。自分はPrettierを取った。
    • ホットリロード
      • できる。
    • デバッグ
    • スキャフォールディングツールの調査
      • 初回のテンプレート以外見つからなかった。
        • angular-devkit-schematicsの力を再び借りるか…。
  • 実際の構文
    • コンポーネントの親子関係の作り方
      • できた。
    • レイヤリング等
      • 模索中
  • 状態管理
    • 一旦Serviceでできないか
    • デファクトスタンダートなやつ探す
      • そもそもSvelte自体に備わっていた
  • envの管理
masaksmasaks

ローカル環境構築(PhpStorm版)

masaksmasaks

ESLintとPrettierも入れてみる。
参考はこちら→ ESLint and Prettier with Svelte
大体これでいける。TypeScriptに関してはこちら→ TypeScriptのプロジェクトにESLintとPrettierを導入する方法(+VSCodeでの設定)
※Svelteのversion 3(TypeScriptが公式に対応したバージョン)が大体2020年6~7月あたりなので、参考にするなら同年7, 8月以降の記事を参考にした方が良さそう。

ただこれだと、TypeScript化した .svelte 内で型を宣言するとLinterエラーが起こる。

masaksmasaks

Svelte3 で Typescript,Pug,Sassを使える様にセットアップする#ESLintの導入
この人も同じエラーが出ている。(投稿日見るとほぼ同時期)

いったん .svelte ファイルはJSのまま進めて、JSDocで型付けをするようにしてみようかな。
lang=ts でやるとJSDocの import() ができない。PrettierのフォーマットとESLintどちらをとるかと言ったら…。

Prettierかなぁ…。lang=ts 使いたいし…。

masaksmasaks

実際の構文

masaksmasaks

配列、オブジェクトのReactivityは 新しい配列やオブジェクトを割り当てない限り更新されない
これが公式のチュートリアルで明言されているのは非常に良い。

masaksmasaks

子コンポーネントへの値の渡し方

  • 子コンポーネント側のプロパティに export をつける。(Angularの @Input() )
Child.svelte
<script>
    export let answer;
</script>

<p>The answer is {answer}</p>
  • 親側は export されているプロパティに値を渡すだけ
parent.svelte
<script>
    import Child from './Childe.svelte'
</script>

<Child answer={42}/>
masaksmasaks

(Deepl翻訳)

リストから最初の項目を削除しても、Thing1 は変わらず、それに関連付けられたキー(この場合はインデックス)は変わらないので、Thing1 のままです。以前は Thing2 に送られていたプロップが Thing1 に送られるようになりました。これはチェーン全体で起こります。しかし、要素が1つ減ったので、Thing5はDOMから削除されます。 キー "0" (Thing1) に関連付けられたコンポーネント Thing のインスタンスは、最初のアイテムが削除されても破棄されません。これは、キーが同じままであるために起こります(新しい配列はインデックス 0 にもアイテムを持っています)。Thing1に送られているプロップだけが変更され、id: 1の元のアイテムの色を持つ初期変数を残しています。

each に対応する子コンポーネントのインスタンスが残ってしまうために、このようなことが起こるようだ。

  1. each でそれぞれの要素に対応する子コンポーネントインスタンスが生成、描画される。この各要素は配列のインデックスに紐づけられる。
  2. はじめの要素を消すと配列内では0番目の要素が削除されるが、DOM上は1番目の要素は残ったままで5番目の要素が削除される。
  3. 本来(DOM上の)1番目の要素は、2.で「はじめの要素を削除した」ため2番目のPropsが反映されて描画されるはずだが、その1番目の要素には破棄されていない「配列で0番目の要素をPropsとする子コンポーネントインスタンス」が残ってしまっている。
  4. 故に initial だけ残ってしまったということ。
masaksmasaks

それの対策として今回の「keyed-each-blocks」が存在する。

App.svelte
{#each cats as cat (cat.id)}
    <li>...</li>
{/each}

とインデックスとなる値を each で指定することで、そのその指定された値とDOM上の要素をそれぞれ紐づける。
ちなみにそもそもそのIteratableな値のインデックスを紐付け対象とすることもできる。

App.svelte
{#each cats as cat (cat)}
    <li>...</li>
{/each}

ただ公式としては、こう言ったIteratableな値というのは大抵APIを介して取得したデータのためなるべくStringやNumberの値を利用してとのこと。

Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server.

masaksmasaks

子コンポーネント→親コンポーネントのイベント通知

  • 子コンポーネント側で createEventDispatcher を使用する。
Child.svelte
<script>
    import { createEventDispatcher } from 'svelte';
	
    const dispatch = createEventDispatcher();
	
    function sayHello() {
        dispatch('message', {
            text: 'Hello!'
        });
    }
</script>
...
  • 親側での通知の受け取りは、 dispatch() の第一引数に指定された文字列をイベント名として受け取る。
Parent.svelte
<script>
    import Child from './Child.svelte';

    function handleMessage(event) {
        alert(event.detail.text);
    }
</script>

<Child on:message={handleMessage}/>

またコンポーネント階層が複数の場合、一番下の子コンポーネントのイベントを一番上の親に通知するには中間のコンポーネントが createEventDispatcher() を転送する必要がある。その際中間コンポーネントも createEventDispatcher() を生成して dispatch() しなければいけないと思うが、実はそんなめんどくさいことはしなくて良い。値の無い on:~ を実装するだけで上に転送するものと判断してくれる。ちなみにブラウザ標準の click と言ったイベントも同じように on:click とするだけで自動で転送してくれる。

Intermediate.svelte
<script>
    import Child from './Child.svelte';
</script>

<Child on:message/>
masaksmasaks

子コンポーネントのインスタンスを取得するには bind:this を使用する。
ただし readonly である。

Example.svelte
<script>
    import Child from './Child.svelte';
    let childComponentInstance;

    onMount(() => {
        console.log(childComponentInstance);
    })
</script>

<Child bind:this={childComponentInstance}/>
masaksmasaks

Components Bindingということもできる。
子コンポーネントで export された値は、親コンポーネント側でBindingすることができる。
※いろいろ端折ってある

Parent.svelte
<script>
let num;
</script>
<Child bind:numberHoge={num}/>
Child.svelte
<script>
export let numberHoge;
</script>

ただし、親コンポーネント子コンポーネントのどちらからも変更ができてしまうため、自分だったら基本使わないしメンバーには禁止させる。

masaksmasaks

Content Projectionをする機能が備わっている。
Svelteの場合は <slot></slot> を定義することで Content Projection を実現する。
もちろん名前を付けて複数の <slot></slot> を定義できる。

Child.svelte
<div>
    <slot name="hoge"></slot>
</div>
Parent.svelte
<Child>
    <p slot="hoge">
        Hello world!
    </p>
</Child>

$$slots を使うことで、対応するSlotsに要素が渡されてない時はレンダリングしないといったこともできる。

masaksmasaks

Slots Props という子から Content Projection する親に値を渡せる仕組みがあるが…。
ちょっと慣れるまでわかりづらい…。

<Hoverable let:hovering={active}>

この構文で let:hovering が子から渡される値で、親側はそれを active という名前で使用する感じ。
確かに書く量は減るが、個人的には Container/Presentational Component の原則的にイベントで値を子から親に通知したい…。

masaksmasaks

Svelte-Component という要素を使えば if でコンポーネントの表示を分岐させなくても、動的にコンポーネントを描画できる。
※ただし、使用するコンポーネントを import しておく必要がある。IDEによってはもしかすると「使用していない変数」ということで Warning が表示されるかもしれない(特にPhpStorm)

masaksmasaks

状態管理

masaksmasaks

Svelteには状態の更新とそれを特定のコンポーネントに通知するための仕組みを備えている。
それが Store と呼ばれるもの。
RxJSに似ているかも…?

masaksmasaks

RxJSの .next() にあたるものが set() , update() .
コールバック関数を使用して更新するのが update() で、値で更新するのが set() かな?
→実際に update() にプリミティブな値を渡したら not a function と怒られたのでそうだろう。

masaksmasaks

またコンポーネントが複数の Store を使用すると subscribe() の実装が増えてしまう。
→そうならないために Store となっている変数名に $ のprefixをつけることで自動で subscribe する。ちなみにこのauto subscribeはコンポーネント破棄時に自動で unsubscribe してくれる。

App.svelte
<script>
    import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>
masaksmasaks

Store は WritableReadable を作成できる。
derived を使用することで他の Store に基づいた別の Store を作成できる。

export const time = readable(new Date(), function start(set) {
    const interval = setInterval(() => {
        set(new Date());
    }, 1000);

    return function stop() {
        clearInterval(interval);
    };
});

export const elapsed = derived(
    time,
    $time => {~~~}
);

また Writable な Store であればそれ自体を Binding することも可能。

<script>
	import { name, greeting } from './stores.js';
</script>

<h1>{$greeting}</h1>
<input bind:value={$name}>

ただどうだろう…。個人的には後述の Custom Stores のメソッド経由で更新したい派。
と思ったが、結局 関連する Store のデータは一つのファイルにまとめる ので別に問題無いかも?
derived 使えば簡潔にできそうだし。どちらにせよ、複雑な Store は作りたく無い。

masaksmasaks

Custom Stores
上記のようにすることで、Stores 固有の update , set 等のメソッドを隠蔽して、宣言的な Stores を使用できる。