EY-Office ブログ

Svelteに入門してみた(React/Vueとの違い)

Svelteというフロントエンド・フレームワークが話題になっているようです。

世界最大のJavaScript関連のサーべー The State of JavaScript Surveyの Front-end Frameworks を見ると利用や便利性では4位以下ですが、満足や興味では1位になっています!

svelte

特徴

Svelteのホームページには以下のような特徴が上げられています。

  • Write less code
  • No virtual DOM
  • Truly reactive

まとめると、少ないコードで書ける、Virtual DOM(仮想DOM)を使ってないが、宣言的UI(reactive)のフロントエンド・フレームワークです。 またホームページには以下のようにも書かれています。

Svelte は、ユーザーインターフェイスを構築するための根本的な新しいアプローチです。ReactやVueのような従来のフレームワークがその作業の大部分をブラウザ上で行うのに対し、Svelteはその作業をアプリをビルドする際に行われるコンパイルステップに移行させます。DeepLで翻訳

ReactやVueは仮想DOMという技術を使い、 メモリー上で変化のあったコンポーネントの変化に対応する仮想DOMを作り、現状の仮想DOMと比較し本当に変更しなくてはいけない部分のみ画面のDOMを書き換える事で、現実的な性能の宣言的UIを実現しています。

それに対し、Svelteではコンパイル(Svelteのコードからブラウザーで実行できるJavaScriptに変換する処理)の際にデータ等の変更に対応してDOMを書き換えるコードを自動生成する事で宣言的UIを実現しています。
皆さんが、昔jQuery等を使いデータ変更時のDOM変更処理を書いていた部分が自動化されているのです!

Svelteのコード

Vue3に入門してみた(TypeScriptを使ったReactとVue3の比較)で書いた、毎度おなじみのReactのジャンケン・アプリのコードをSvelteで書いてみました。

Jyanken.svelte

Vueのように、JavaScript, HTML, CSSが分かれています。

しかしJavaScript(TypeScript)の部分はReactに似てるかもしれません。ただし、Stateはありません、通常の変数です(今回は使っていませんがRedux/Vuexのようなステート管理ライブラリーが用意されています)。

HTMLはVueと同様に普通のHTMLを書きます、ただし特別な属性ではなくReact同様{式}でJavaScriptと連携しています。

import type { ScoreType, Jjudgment } ・・・ の部分はimport { ScoreType, Jjudgment } ・・・ ではエラーになってしまいました(今回 import type 記法をはじめて知りました)。

<script lang="ts">
  import ScoreBox from './ScoreBox.svelte'
  import type { ScoreType, Jjudgment } from './ScoreBox.svelte'
  import JyankenBox, { Te } from './JyankenBox.svelte'

  let scores: ScoreType[] = []

  const pon = (human: Te) => {
    console.log(scores)
    const computer: Te = Math.floor(Math.random() * 3)
    const judgment: Jjudgment = (computer - human + 3) % 3
    const score = {human: human, computer: computer, judgment: judgment}
    scores = [score, ...scores]
  }

</script>

<main>
  <h1>じゃんけん ポン!</h1>
  <JyankenBox actionPon={te => pon(te)} />
  <ScoreBox scores={scores} />
</main>

JyankenBox.svelte

コンポーネントにはReact/Vue同様に引数Propsがありますが、コード上ではexport let プロパティ名前で宣言します。ただしコンポーネントがexportする値や型は<script context="module">の中に書きます。
イベントハンドルはon:click=で指定します、Vue寄りでしょうかね。

Vueと同様にCSSが素直に書けるのは良いですね。

<script context="module" lang="ts">
  export enum Te { Guu = 0, Choki, Paa}
</script>

<script lang="ts">
  export let actionPon: (te: number) => void
</script>

<div>
  <button on:click={() => actionPon(Te.Guu)}>グー</button>
  <button on:click={() => actionPon(Te.Choki)}>チョキ</button>
  <button on:click={() => actionPon(Te.Paa)}>パー</button>
</div>

<style>
  div {margin: 0 20px}
  button {margin: 0 10px; padding: 3px 10px; font-size: 14px}
</style>

ScoreBox.svelte

HTML内で、条件分岐や繰り返しは{#if ...}{#each ...}のような特殊な文法で書きます。 懐かしい感じのテンプレート言語ですね。{#await ...} が用意されているのが今風ですね 😅

<script context="module" lang="ts">
  export enum Jjudgment { Draw = 0, Win, Lose }
  export type ScoreType = {
    human: number;
    computer: number;
    judgment: Jjudgment;
  }
</script>

<script lang="ts">
 export let scores: ScoreType[]

  const teString = ["グー","チョキ", "パー"]
  const judgmentString = ["引き分け","勝ち", "負け"]
</script>

<table>
  <thead>
    <tr>
      <th>あなた</th>
      <th>コンピュター</th>
      <th>勝敗</th>
    </tr>
  </thead>
  <tbody>
    {#each scores as scrore}
      <tr>
        <td>{teString[scrore.human]}</td>
        <td>{teString[scrore.computer]}</td>
        <td>{judgmentString[scrore.judgment]}</td>
      </tr>
    {/each}
  </tbody>
</table>

<style>
  table {margin-top: 20px; border-collapse: collapse}
  th {border: solid 1px #888; padding: 3px 15px}
  td {border: solid 1px #888; padding: 3px 15px; text-align: center}
</style>

まとめ

コードをReact/Vueと見比べるとシンプルで短い事がわかると思います。また記法はReact/Vueの良いとこ取りのようですね。😀

SvelteはReact/Vueとはまったく異なる技術をベースとしているので、今後の発展がとても気になります。まだSvelte用のライブラリーなどのエコシステムは発展途上ですが、すでにSvelte Summit 2020のようなカンファレンスも実施されているんですね!

また情報は少ないですが、SvelteホームページのTutorialが素晴らしいので興味を持った人はこのチュートリアルで学んでください。

参考:React/Vueのコード

Jyanken

  • React
import React, { useState } from 'react'
import JyankenBox, { Te } from './JyankenBox'
import ScoreBox, { ScoreType, Jjudgment } from './ScoreBox'

const Jyanken: React.FC = () => {
  const [scores, setScrores] = useState<ScoreType[]>([])

  const pon = (human: Te) => {
    const computer: Te = Math.floor(Math.random() * 3)
    const judgment: Jjudgment = (computer - human + 3) % 3
    const score = {human: human, computer: computer, judgment: judgment}
    setScrores([score, ...scores])
  }

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
      <ScoreBox scores={scores} />
    </>
   )
}

export default Jyanken
  • Vue3
<template>
  <div>
    <h1>じゃんけん ポン!</h1>
    <JyankenBox v-bind:actionPon="pon"/>
    <ScoreBox v-bind:scores="state.scores"/>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue'
import JyankenBox, { ActonPonType, Te } from './JyankenBox.vue'
import ScoreBox, { ScoreType, Jjudgment } from './ScoreBox.vue'

type State = {
  scores: ScoreType[];
}

export default defineComponent({
  name: 'Jyanken',
  components: {
    JyankenBox, ScoreBox
  },
  setup () {
    const state = reactive<State>({ scores: [] })
    const pon: ActonPonType = (human: Te) => {
      const computer: Te = Math.floor(Math.random() * 3)
      const judgment: Jjudgment = (computer - human + 3) % 3
      const score = { human: human, computer: computer, judgment: judgment }
      state.scores = [score, ...state.scores]
    }
    return { state, pon }
  }
})
</script>

JyankenBox

  • React
import React from 'react'

export enum Te { Guu = 0, Choki, Paa}

type JyankenBoxProps = {
  actionPon: (te: number) => void
}
const JyankenBox: React.FC<JyankenBoxProps> = ({actionPon}) => {
  const divStyle: React.CSSProperties = {margin: "0 20px"}
  const buttonStyle: React.CSSProperties = {margin: "0 10px",
     padding: "3px 10px", fontSize: 14}
  return (
    <div style={divStyle}>
      <button onClick={() => actionPon(Te.Guu)} style={buttonStyle}>グー</button>
      <button onClick={() => actionPon(Te.Choki)} style={buttonStyle}>チョキ</button>
      <button onClick={() => actionPon(Te.Paa)} style={buttonStyle}>パー</button>
    </div>
  )
}

export default JyankenBox
  • Vue3
<template>
  <div>
    <button v-on:click="actionPon(Te.Guu)">グー</button>
    <button v-on:click="actionPon(Te.Choki)">チョキ</button>
    <button v-on:click="actionPon(Te.Paa)">パー</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'

export enum Te { Guu = 0, Choki, Paa}
export type ActonPonType = (te: number) => void

export default defineComponent({
  props: {
    actionPon: Function as PropType<ActonPonType>
  },
  setup () {
    return { Te }
  }
})
</script>

<style scoped>
div {
  margin: 0 20px;
}
button {
  margin: 0 10px;
  padding: 3px 10px;
  font-size: 14px;
}
</style>

ScoreBox

  • React
import React from 'react'

export enum Jjudgment { Draw = 0, Win, Lose }
export type ScoreType = {
  human: number;
  computer: number;
  judgment: Jjudgment;
}

type ScoreListProps = {
  scores: ScoreType[]
}
const ScoreBox: React.FC<ScoreListProps> = ({scores}) => {
  const teString = ["グー","チョキ", "パー"]
  const judgmentString = ["引き分け","勝ち", "負け"]

  const tableStyle: React.CSSProperties = {marginTop: 20, borderCollapse: "collapse"}
  const thStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px"}
  const tdStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px",
      textAlign: "center"}
  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={thStyle}>あなた</th>
          <th style={thStyle}>コンピュター</th>
          <th style={thStyle}>勝敗</th>
        </tr>
      </thead>
      <tbody>
        {scores.map((scrore, ix) =>
          <tr key={ix}>
            <td style={tdStyle}>{teString[scrore.human]}</td>
            <td style={tdStyle}>{teString[scrore.computer]}</td>
            <td style={tdStyle}>{judgmentString[scrore.judgment]}</td>
          </tr>
        )}
      </tbody>
    </table>
  )
}

export default ScoreBox
  • Vue3
<template>
  <table>
    <thead>
      <th>あなた</th>
      <th>コンピュター</th>
      <th>勝敗</th>
    </thead>
    <tbody>
      <tr v-for="(scrore, index) in scores" :key="index">
      <td>{{ teString[scrore.human] }}</td>
      <td>{{ teString[scrore.computer] }}</td>
      <td>{{ judgmentString[scrore.judgment] }}</td>
      </tr>
    </tbody>
  </table>
</template>

<script lang="ts">
import { defineComponent, ref, PropType } from 'vue'

export enum Jjudgment { Draw = 0, Win, Lose }
export type ScoreType = {
  human: number;
  computer: number;
  judgment: Jjudgment;
}

export default defineComponent({
  props: {
    scores: Array as PropType<ScoreType[]>
  },
  setup () {
    const teString = ref<string[]>(['グー', 'チョキ', 'パー'])
    const judgmentString = ref<string[]>(['引き分け', '勝ち', '負け'])
    return { teString, judgmentString }
  }
})
</script>

<style scoped>
table {
  margin-top: 20px;
  border-collapse: collapse;
}
td, th {
  border: solid 1px #888;
  padding: 3px 15px;
  text-align: center;
}
</style>

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ