NimのフレームワークKaraxを試してみる

公開:2020/10/08
更新:2020/10/08
6 min読了の目安(約6100字TECH技術記事

概要

初めてzennに記事を投稿させていただきます
初心者ゆえ至らない所も数々あると思いますので、ご指摘いただけると幸いです

NimのSPAフレームワークであるKaraxを使って、簡単な三目並べを作成します
Karaxのリポジトリ

要点

  • NimのKaraxというフレームワークを使えば、Reactのような仮想DOMを用いたSPAが作れる
  • Nimの簡潔な記法で書けるので楽
  • コンポーネント間の情報の受け渡しを気にしなくていいので、(開発に向いているかは別として)楽に書くことができる

動作環境

Nim Compiler Version 1.0.6
nimble v0.11.0
Karax 1.1.3

Karaxをインストールする

前提

  • OSにNimがインストールされている
  • Nimに付属のパッケージマネージャーNimbleがインストールされている

手順

  • 適当なディレクトリを用意する。この時のディレクトリ名がプロジェクトの名前になる
  • そのディレクトリに移動する
$ nimble init
  • nimble init はnimのプロジェクトを作成するコマンド

・karaxをインストールする。

$ nimble install karax
  • ホームディレクトリ下の.nimbleにパッケージをインストールする

Karaxを動かしてみる

index.html

  • src内に、以下のようなhtmlファイルを作成する。ファイル名は何でもいいので、とりあえずindex.htmlとしておく

index.html

<!DOCTYPE html>

<html>
<head>
    <title>HelloWorld</title>

    <link rel="stylesheet" href="./index.css">
    <link rel="icon" href="/favicon.ico">
</head>

<body>
    <div id="ROOT" />
    <script type="text/javascript" src="./helloworld.js"></script>
</body>
</html>
  • bodyタグ内の#ROOTに、nimファイル中Karaxを用いて記述した仮想DOMが追加される
  • scriptには、helloworld.nimからトランスパイルされるjsのファイルを指定する

helloworld.nim

・Karaxライブラリを用いてHello Worldと表示するためのコードを書く
・index.htmlと同じく、src内にhelloworld.nimを作る

helloworld.nim

include karax/prelude

proc createDom(): VNode =
  return buildHtml(tdiv):
    text "Hello World!"

setRenderer createDom
  • helloworld.nimをJavaScriptにトランスパイル。src内にhelloworld.jsというファイルが出力される
$ nim js src/helloworld.nim

後は……

  • ブラウザでindex.htmlを開く

  • Hello World! と表示されていたら成功!

三目並べを作ってみる

Reactのチュートリアルなどでおなじみの三目並べを作ってみましょう
今回作るのは次の機能を揃えたものです

  • ボタンを押すとxかoが表示される
  • ボタンを押すたびに、次に入力されるのがxかoかが入れ替わる
  • 次にxが入力されるのか、oが入力されるのかを表示する
  • 一直線にxかoか並んだらそれ以上入力を受け付けなくする
  • 一直線に並んだ時に、勝者を表示する

tictactoe.nim

完成形のコードは次のようになります

tictactoe.nim

include karax/prelude
import algorithm

var 
  status: array[0..8,kstring]
  xIsNext: bool = true
  winner: kstring = kstring""

status.fill(kstring"")

proc calculateWinner(): kstring =
  const lines = @[
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
  ]
  for i in lines:
    var a, b, c: int
    (a, b, c) = i
    if status[a] != kstring"" and status[a] == status[b] and status[a] == status[c]:
      return status[a]
  return ""

proc square(m: int): VNode =
  proc squareOnClick(a: int): proc() = 
    return proc() =
      if winner != kstring"": return
      status[a] = if xIsNext: kstring"x" else:kstring"o"
      xIsNext = not xIsNext
      winner = calculateWinner()

  return buildHTML(tdiv):
    button(class = "square",onclick = squareOnClick(m)):
        text status[m]

proc board(): VNode =
  return buildHtml(tdiv):
    for i in 0..2:
      tdiv(class = "board-row"):
        for j in 0..2:
          square(i*3+j)

proc createDom(): VNode =
  return buildHtml(tdiv):
    board()
    tdiv(class = "container"):
      tdiv:
        if winner == "":
          text "Next: " & (if xIsNext:"x" else:"o")
        else:
          text "Winner: " & winner


setRenderer createDom

NimとKaraxに独特の記法について軽く言及していきましょう

変数宣言

var 
  status: array[0..8,kstring]
  xIsNext: bool = true
  winner: kstring = kstring""

status.fill(kstring"")
  • VueやReactのように、コンポーネントごとに状態を管理する変数を用意する必要がない
  • グローバルスコープで定義した変数は、どの仮想DOMからもアクセスできるので、要素間の値の受け渡しが楽
  • 変数宣言を、varのコードブロックを作りまとめて行えるのはNimの機能
  • Nimで他の言語にトランスパイルするときに、string型ではなくてkstring型を用いる必要がある

board

proc board(): VNode =
  return buildHtml(tdiv):
    for i in 0..2:
      tdiv(class = "board-row"):
        for j in 0..2:
          square(i*3+j)
  • ある程度まとまった部分は、関数型コンポーネントのように実装することになる
  • 仮想DOMはVNodeという型を持つ
  • buildHtml:のブロックに、HTMLタグに対応したブロックを作っていき、仮想DOMを作る
  • divがNimの組み込みにあるため、divタグはtdivと表記される[1]
  • <タグ名>(属性=値,属性=値,属性=値,...):という形で属性を設定することができる
  • タグに文章を設定するのは、text stringtext(string) でも同じ)という風に行う
  • Htmlタグのブロック中で、for文を使うことができる
  • これらの構文はNimのマクロ機能で実装されていて、トランスパイル前に書き換えが行われる

square

proc square(m: int): VNode =
  proc squareOnClick(a: int): proc() = 
    return proc() =
      if winner != kstring"": return
      status[a] = if xIsNext: kstring"x" else:kstring"o"
      xIsNext = not xIsNext
      winner = calculateWinner()

  return buildHTML(tdiv):
    button(class = "square",onclick = squareOnClick(m)):
        text status[m]
  • 引数を取るプロシージャで仮想DOMを返す
  • イベント名 = proc{...}の形でイベントを登録することができる
  • 引数とEventを紐づけたい場合は、間にプロシージャを返すプロシージャを噛ます必要がある
    • 例えば、 button(class="square",onclick = proc() = status[a] = if xIsNext: kstring"x" else:kstring"o"):としても上手く動いてくれない

createDom + Setrenderer

proc createDom(): VNode =
  return buildHtml(tdiv):
    board()
    tdiv(class = "container"):
      tdiv:
        if winner == "":
          text "Next: " & (if xIsNext:"x" else:"o")
        else:
          text "Winner: " & winner

setRenderer createDom
  • for文が使えるので、やはりif文も使える
  • 最期にsetRenderer 仮想DOMしてあげる必要がある

実行

$ nim js src/tictactoe.nim

でjsにトランスパイルしたら、src内にtictactoe.jsというファイルができるはずですので、それに合わせてindex.htmlを書き換えます。また、cssも追加してあげましょう。

index.html

<!DOCTYPE html>

<html>
<head>
    <title>○×ゲーム</title>

    <link rel="stylesheet" href="./index.css">
    <link rel="icon" href="/favicon.ico">
</head>
<body id="body">

<div id="ROOT" />
<script type="text/javascript" src="./tictactoe.js"></script>

</body>
</html>

index.css

.board-row{
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}
.square{
    width:50px;
    height:50px;
    margin:5px;
    border:1px solid #00CCFF;
    font-size:30px;
}

これでindex.htmlファイルをブラウザで開いてあげましょう。

これで完成です!

まとめ(再掲)

  • NimのKaraxというフレームワークを使えば、Reactのような仮想DOMを用いたSPAが作れる
  • Nimの簡潔な記法で書けるので楽
  • コンポーネント間の情報の受け渡しを気にしなくていいので、(開発に向いているかは別として)楽に書くことができる

参考

Karaxのdocument
Reactのチュートリアル

おまけ

Google検索でtictactoeと調べると……

脚注
  1. 同じような例として、iタグはitalic、bタグはbold、 uタグはunderlinedと書く必要がある ↩︎