本記事は Riot.js Advent Calendar 2019 の第24日目の記事になります!(遅刻組です…)
Riot.js
(以下、riot)は非常にシンプルかつ軽量で入門の敷居も低く、とても書きやすいコンポーネント指向のUIライブラリです。今回は, Svelte というライブラリの 公式チュートリアル を参考に, riot との書き方の比較を見ていきたいと思います.
結構長いので, 適当に流しながら見ていくと良いかもしれません.
※1 数が多いので, チュートリアルの全てを書いているわけではありません
※2 随時更新するかもしれません
前提
本記事に対する riot に関する前提を書いていきます.
1点目: html の設定
ベースとなる index.html は以下のように
-
app
タグを設定していること(必要な子コンポーネントも同様とする) -
mount
メソッドで初期パラメータは渡さないこと
とします.
<body>
<app></app>
<script data-src="src/app.riot" type="riot"></script>
<script>
riot
.compile()
.then(() => {
riot.mount("app")
})
.catch(e => {
console.error(e)
})
</script>
</body>
2点目: カスタムコンポーネント
カスタムコンポーネントの書き方は基本的に
-
export default{}
に集約する - 開始と終了の
app
タグは省略
するものとします.
一応以下のような書き方もできますが, 本記事では割愛します.
<h1>Hello { title }!</h1>
<script>
const title = 'Riot.js'
</script>
3点目: 書き方
svelte はバンドラを使った書き方をしているようですので, 基本的に riot もそれに則って行こうと思いますが, 一部インブラウザコンパイルの書き方をしています.(1点目の書き方がまさにそれ)
基本文法
ではここから実際に書いていきます. 基本的には svelte 側の書き方をベースに, それを riot4 で書いていきますのでご注意ください.
拡張子
- svelte →
.svelte
- riot4 →
.riot
or.riot.html
Hello world!
svelte
<script>
let name = 'world';
</script>
<h1>Hello {name.toUpperCase()}!</h1>
riot
<h1>Hello { title }!</h1>
<script>
export default {
title: 'Riot.js'
}
</script>
Dynamic attributes
svelte
<script>
let src = 'tutorial/image.gif';
let name = 'Rick Astley';
</script>
<img {src} alt="{name} dances.">
riot
<img {src} alt="{title} dances.">
<script>
// name は予約語のため title に変更
export default {
src: 'tutorial/image.gif',
title: 'Rick Astley'
}
</script>
Styling
<p>This is a paragraph.</p>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
もし riot4 で Scoped CSS
を指定したい場合は :host {}
を使ってください.
参考: Scoped CSS
Nested components
svelte
<script>
// 読み込むだけで良い
import Nested from './Nested.svelte';
</script>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is a paragraph.</p>
<Nested/>
riot
<Nested/>
<script>
import Nested from './nested.riot'
export default {
// components オブジェクトとして設定
components: {
Nested
}
}
</script>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is another paragraph.</p>
HTML tags
svelte
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>
<p>{@html string}</p>
riot
<p>{string}</p>
<script>
export default {
string: 'this string contains some <strong>HTML!!!</strong>',
onMounted() {
this.$('p').innerHTML = this.string
}
}
</script>
riot4 には HTML のバインディング機能はありません…
Reactivity
ここからは少しアプリケーションっぽいものを見ていきます.
Assignments
svelte
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
riot
<button onclick={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<script>
export default {
count: 0,
handleClick() {
this.count += 1
this.update()
}
}
</script>
やっぱり riot は明示的に update()
メソッドを実行しないといけないのが面倒に見えるかもしれませんね
Declarations
svelte
<script>
let count = 0;
$: doubled = count * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>
riot
<button onclick={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>{count} doubled is {doubled}</p>
<script>
export default {
count: 0,
onBeforeMount() {
this.doubled = this.count * 2
},
handleClick() {
this.count += 1
this.doubled = this.count * 2
this.update()
}
}
</script>
$: hoge
で自動で監視する変数を増やせるのは便利ですねー. 実は riot ですと, もう一つ doubled
という変数を用意しなくても, this.doubled
と書けば内部的に doubled
という変数を持ってくれます.
また svelte のチュートリアルにも記載があるように, {count * 2}
と書いても同じことができますね.
Statements
svelte
<script>
let count = 0;
$: if (count >= 10) {
alert(`count is dangerously high!`);
count = 9;
}
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
riot
<button onclick={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<script>
export default {
count: 0,
handleClick() {
this.count += 1
if (this.count >= 10) {
alert('count is dangerously high!');
}
this.update()
}
}
</script>
$: ***
と書いても, ***
の中で count
変数をコントロールできるというのが本題ですが, 今回の題材ですと, if
文を handleclick()
メソッドの中で書いても動作すれば良いのでちょっと例が微妙でしたね.
Updated arrays and objects
svelte
<script>
let numbers = [1, 2, 3, 4];
function addNumber() {
numbers = [...numbers, numbers.length + 1];
}
$: sum = numbers.reduce((t, n) => t + n, 0);
</script>
<p>{numbers.join(' + ')} = {sum}</p>
<button on:click={addNumber}>
Add a number
</button>
riot
<button onclick={addNumber}>
Add a number
</button>
<p>{numbers.join(' + ')} = {sum}</p>
<script>
export default {
numbers: [1, 2, 3, 4],
onBeforeMount() {
this.sum = this.numbers.reduce((t, n) => t + n, 0)
},
addNumber() {
this.numbers = [...this.numbers, this.numbers.length + 1]
this.sum = this.numbers.reduce((t, n) => t + n, 0)
this.update()
}
}
</script>
何度か使ってきていますが, riot ですと初期化時点で何かしらの処理をしたい場合, onBeforeMount()
メソッド内で明示的に処理を書かないといけません. 本当はこのメソッド内の処理を省略したいですね💦重複して書いていますし.
Props
ここからはコンポーネントへの値の渡し方, いわゆる props
について見ていきます.
Declaring props
svelte
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
<script>
export let answer;
</script>
<p>The answer is {answer}</p>
riot
<Nested answer={42}/>
<script>
import Nested from './nested.riot'
export default {
// components オブジェクトとして設定
components: {
Nested
}
}
</script>
<p>The answer is {props.answer}</p>
<script>
// インブラウザコンパイルでは, この script が丸っと不要
import Nested from './Nested.riot'
export default {
components: {
Nested
}
}
</script>
<!-- or -->
<p>The answer is {answer}</p>
<script>
import Nested from './nested.riot'
export default {
components: {
Nested
},
// 本当は answer: props.answer と書きたい
answer: '',
onBeforeMount(props) {
this.answer = props.answer
}
}
</script>
riot の子コンポーネントでは, props.answer
でも扱えますが, 上記のコメントにあるように export default の変数定義では使えないのが残念です.
Default values
親(main)コンポーネントは先程の Declaring props
と同じなので省略します.
svelte
<script>
// 初期値を設定
export let answer = 'a mystery';
</script>
<p>The answer is {answer}</p>
riot
<p>The answer is {props.answer || answer}</p>
<script>
export default {
answer: 'a mystery'
}
</script>
<!-- or -->
<p>The answer is {answer}</p>
<script>
export default {
answer: 'a mystery',
onBeforeMount(props) {
this.answer = props.answer || this.answer
}
}
</script>
こちらも riot は明示的に props の値かコンポーネント内の変数かを提示しないとダメなようです(ここは svelte 楽ですね)
Spread props
svelte
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg}/>
<script>
export let name;
export let version;
export let speed;
export let website;
</script>
<p>
The <code>{name}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>
riot
<Info {...pkg} />
<script>
import Info from './info.riot';
export default {
components: {
Info
},
pkg: {
name: 'riot',
version: 4,
speed: 'blazing',
website: 'https://riot.js.org'
}
}
</script>
<p>
The <code>{props.name}</code> package is {props.speed} fast.
Download version {props.version} from <a href="https://www.npmjs.com/package/{props.name}">npm</a>
and <a href={props.website}>learn more here</a>
</p>
<!-- or -->
<p>
The <code>{title}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>
<script>
export default {
onBeforeMount(props) {
this.title = props.name
this.version = props.version
this.speed = props.speed
this.website = props.website
}
}
</script>
何度か書いて思いましたが, 直接 {props.name}
と HTML に書いたほうが楽ですね.
Logic
ここからは条件分岐などのロジックについて書いていきます.
IF-Else blocks
svelte
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
riot
<button if={user.loggedIn} onclick={toggle}>
Log out
</button>
<button if={!user.loggedIn} onclick={toggle}>
Log in
</button>
<script>
export default {
// ここは必須
user: {},
onBeforeMopunt() {
this.user = { loggedIn: false }
},
toggle() {
this.user.loggedIn = !this.user.loggedIn
this.update()
}
}
</script>
2点あります.
-
else
の機能がない(else-if
も) -
user: {}
が必要
特に二つ目が意外で, object ではなくプリミティブな変数であればこれが不要なんですよね…
Each blocks
svelte
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<h1>The Famous Cats of YouTube</h1>
<ul>
{#each cats as { id, name }, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a></li>
{/each}
</ul>
riot
<h1>The Famous Cats of YouTube</h1>
<ul each={(cat, i) in cats}>
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{i + 1}: {cat.name}
</a></li>
</ul>
<script>
export default {
cats: [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
]
}
</script>
svelte は {#each cats as { id, name }, i}
のように, each ループ内で object のそれぞれの key 毎に指定して取得できるのが良いですね
Keyed each blocks
svelte
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each things as thing}
<Thing current={thing.color}/>
{/each}
<script>
// `current` is updated whenever the prop value changes...
export let current;
// ...but `initial` is fixed upon initialisation
const initial = current;
</script>
<p>
<span style="background-color: {initial}">initial</span>
<span style="background-color: {current}">current</span>
</p>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>
riot
<button onclick={handleClick}>
Remove first thing
</button>
<Thing each={thing in things} current={thing.color} />
<script>
import Thing from './thing.riot'
export default {
components: {
Thing
},
things: [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
],
handleClick() {
this.things = this.things.slice(1);
this.update()
}
}
</script>
<p>
<span style="background-color: {initial}">initial</span>
<span style="background-color: {current}">current</span>
</p>
<script>
export default {
onBeforeMount(props) {
// `current` is updated whenever the prop value changes...
this.current = props.current
// ...but `initial` is fixed upon initialisation
this.initial = this.current
}
}
</script>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>
riot でもカスタムコンポーネントに直接 each
ディレクティブを使って複数の子コンポーネントをレンダリングすることができます.
Await blocks
svelte
<script>
let promise = getRandomNumber();
async function getRandomNumber() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>
generate random number
</button>
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
riot
riot には現在 async/await に対応する機能が存在しないため, 自前で Promise
を使って実装するしかないです…これも今後実装されてほしいですね
おわりに
さすがに全部は書いていないですが, svelte のチュートリアル見た感じ, かなりライトなライブラリでとても書きやすい印象を受けました. riot の新しいライバル感が…w npm でのダウンロード数も伸びてきており, 世界的にも注目され始めているようです.
軽量な分, やはり小規模向けでサクッと作るときの選択肢としてはかなり良さげなのではないかなと思います!もしご興味ある方は触ってみてください!もちろん riot も👍
ではでは(=゚ω゚)ノ