NimでJavaScriptターゲットの開発をする方法
NimではC言語にトランスパイルして実行可能バイナリを作る以外にもJavaScriptを出力することもできます。
これが非常にテクニックが必要なので今回はわかりやすく網羅的に解説していきたいと思います。
JSターゲットの基礎
コンパイル
echo "hoge"
nim js js_sample.nim
nim js
コマンドでjs_sample.js
を出力します。
もし実行環境にNodeJSが入っていれば、そのまま実行することもできます。
nim js -r js_sample.nim
hoge
ライブラリ
JavaScript向けの標準ライブラリがあり、便利に使うことができます。
lib | 説明 |
---|---|
asyncjs | JSの非同期処理のasync/awaitを使うことができます。NimのFuture[T] がJSのPromise<T> になります。 |
dom | ブラウザが持っているdocument やwindow などDOM操作をするためのライブラリです。 |
jsbigints | JSのBitInt型を扱います。 |
jsconsole |
conoel.log() などを呼び出せるようになります。 |
jscore | JSのMath 、JSON 、Date などのライブラリを提供しますが標準ライブラリを使ったほうが安全です。 |
jsffi | NimとJSの間で型を相互に変換するライブラリです。 |
jsfetch | JSからAPIアクセスするためのHTTPクライアントです。 |
jsheaders | jsfetchと共に使うHTTPヘッダーを扱うライブラリです。 |
jsformdata | jsfetchと共に使うHTTPフォームデータを扱うライブラリです。 |
jsre | JSでの正規表現を扱うライブラリです。 |
jsutils | JSでの型を扱う便利機能を提供するライブラリです。 |
また3rdパーティライブラリとしてはnodejs
というラッパーライブラリがあります。かなり巨大です。
型の扱い
Nimの型はJSに出力されるとどうなるか見ていきましょう。
import std/jsffi
import std/times
let i = 0
let j = 0.0
let str = "string"
let cstr:cstring = "cstring"
let date = now()
コンパイルするとJSファイルが出力されます。
function makeNimstrLit(c_33556801) {
var result = [];
for (var i = 0; i < c_33556801.length; ++i) {
result[i] = c_33556801.charCodeAt(i);
}
return result;
}
function getTime_922747872() {
var result_922747873 = ({seconds: 0, nanosecond: 0});
var millis_922747874 = new Date().getTime();
var seconds_922747880 = convert_922747358(2, 3, millis_922747874);
var nanos_922747891 = convert_922747358(2, 0, modInt(millis_922747874, convert_922747358(3, 2, 1)));
result_922747873 = nimCopy(result_922747873, initTime_922747806(seconds_922747880, chckRange(nanos_922747891, 0, 999999999)), NTI922746910);
return result_922747873;
}
function now_922748331() {
var result_922748332 = ({m_type: NTI922746911, nanosecond: 0, second: 0, minute: 0, hour: 0, monthdayZero: 0, monthZero: 0, year: 0, weekday: 0, yearday: 0, isDst: false, timezone: null, utcOffset: 0});
result_922748332 = nimCopy(result_922748332, local_922748328(getTime_922747872()), NTI922746911);
return result_922748332;
}
var i_469762051 = 0;
var f_469762052 = 0.0;
var str_469762053 = makeNimstrLit("string");
var cstr_469762054 = "cstring";
var date_469762055 = now_922748331();
JSの世界で素の文字列として扱うには、cstring
を使う必要があります。
配列の扱い
JSの世界の動的配列を扱うためのJsObject型が用意されています。
JsObject = ref object of JsRoot
Dynamically typed wrapper around a JavaScript object.
import std/jsconsole
import std/jsffi
proc func1() =
let dyArr = newJsObject()
dyArr["id"] = 1
dyArr["name"] = "Alice".cstring
dyArr["status"] = true
console.log(dyArr)
console.log(jsTypeOf(dyArr))
func1()
{ id: 1, name: 'Alice', status: true }
object
Nimの構造体を定義するとJSの世界ではobjectとして扱われます。
to
とtoJs
の関数を使って、JsObjectと構造体の相互変換ができます。
JsObjectを使うとコンパイル時の静的な型チェックが行われなくなるので、なるべくロジックは構造体とそのメソッドを使ったほうが良いでしょう。
proc to(x: JsObject; T: typedesc): T:type {.importjs: "(#)"}
Converts a JsObject x to type T.
proc toJs[T](val: T): JsObject {.importjs: "(#)"}
Converts a value of any type to type JsObject.
type Person = object
id:int
name:cstring
status:bool
proc new(_:type Person, id:int, name:string, status:bool):Person =
return Person(id:id, name:name.cstring, status:status)
proc func1() =
let dyArr = newJsObject()
dyArr["id"] = 1
dyArr["name"] = "Alice".cstring
dyArr["status"] = true
console.log(dyArr)
console.log(jsTypeOf(dyArr))
let person = dyArr.to(Person)
console.log(person)
let person2 = Person.new(2, "Bob", false)
console.log(person2)
func1()
{ id: 1, name: 'Alice', status: true }
object
{ id: 1, name: 'Alice', status: true }
{ id: 2, name: 'Bob', status: false }
DOM操作
HTMLのinputタグから入力した文字をリアルタイムでpタグに表示させてみましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script defer type="module" src="app.js"></script>
<title>Document</title>
</head>
<body>
<input type="text" id="input">
<p id="content"></p>
</body>
</html>
import dom
proc onInput(e:Event) =
let content = document.getElementById("content")
content.innerText = e.target.value
let input = document.getElementById("input")
input.addEventListener("input", onInput)
JSファイルが出力されます。
function onInput_469762050(e_469762051) {
var content_469762052 = document.getElementById("content");
content_469762052.innerText = e_469762051.target.value;
}
var input_469762062 = document.getElementById("input");
input_469762062.addEventListener("input", onInput_469762050, false);
domライブラリを使うことで、NimからEvent
、document
、getElementById
などが使えるようになります。
APIアクセス
フロントエンドの開発をするにはAPIアクセスは欠かせません。
NimにはJSターゲットでAPIアクセスをするためのjsfetch
ライブラリが用意されています。
import std/asyncjs
import std/jsfetch
import std/jsconsole
proc apiAccess() {.async.} =
let url:cstring = "https://api.coindesk.com/v1/bpi/currentprice.json"
let resp = await fetch(url)
let json = await resp.json()
console.log(json)
discard apiAccess()
JSファイルが出力されます。
async function apiAccess_469762071() {
var result_469762073 = null;
BeforeRet: do {
var url_469762079 = "https://api.coindesk.com/v1/bpi/currentprice.json";
var resp_469762087 = (await fetch(url_469762079));
var json_469762092 = (await resp_469762087.json());
console.log(json_469762092);
result_469762073 = undefined;
break BeforeRet;
} while (false);
return result_469762073;
}
var _ = apiAccess_469762071();
プラグマについて
NimではJSターゲットの開発を行う時にはプラグマをよく使う必要があります。
プラグマとは他の言語であるアノテーションのようなもので、コンパイラに対してコンパイル時に指示を出すことができます。
exportc
これまで見てきた出力されたJSファイルを見ると、変数名や関数名にsuffixがついていました。exportc
を使うことで、suffixを付けるのを禁止できます。
import std/jsconsole
import std/jsffi
proc hello(arg: cstring){.exportc.} =
let arg {.exportc.} = arg
console.log("hello " & arg)
let name {.exportc.}: cstring = "Alice"
hello(name)
function hello(arg_469762052) {
var arg = arg_469762052;
console.log(("hello " + arg));
}
var name = "Alice";
hello(name);
emit
emitを使うと、その中で書いた処理がそのまま出力されるJSファイルに入れられます。
JSターゲットの開発を行う時にはその中で素のJSの処理を定義することができます。
{.emit:"""
function hello(arg){
console.log("hello " + arg)
}
""".}
function hello(arg){
console.log("hello " + arg)
}
importjs
JSの関数とNimの関数をマッピングし、Nimの世界からJSの関数を呼べるようにするために使います。
#
を使うと引数が前から順番に、@
を使うと後ろ全部がその位置に挿入されます。
import std/jsffi
{.emit:"""
function add(a, b){
console.log(a + b)
}
""".}
proc add(a, b:int) {.importjs:"add(#, #)".}
add(2, 3)
function add(a, b){
console.log(a + b)
}
add(2, 3);
実践的な開発を行う
ではこれまで見てきたことを踏まえて、Preactという軽量なReact風ライブラリをNimから呼び出して使ってみましょう。
ここで使うHTMLファイルはこのようにします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script defer type="module" src="app.js"></script>
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
Preactの処理をNimから呼ぶ
emit
を使ってCDNからライブラリをインポートし、importjs
を使ってライブラリの関数とNimの関数をマッピングします。
import std/dom
import std/jsffi
# ==================== Preactの定義 ====================
{.emit: """
import { h, render } from 'https://cdn.jsdelivr.net/npm/preact@10.11.3/+esm';
import htm from 'https://cdn.jsdelivr.net/npm/htm@3.1.1/+esm';
const html = htm.bind(h);
""".}
type Component* = JsObject
proc html*(arg:cstring):Component {.importjs:"eval('html`' + # + '`')".}
template html*(arg:string):Component = html(arg.cstring)
{.emit: """
function renderApp(component, dom){
render(html``<${component} />``, dom)
}
""".}
proc renderApp*(component: proc():Component, dom: Element) {.importjs: "renderApp(#, #)".}
# ================== hooks ==================
{.emit:"""
import { useState, useEffect } from 'https://cdn.jsdelivr.net/npm/preact@10.11.3/hooks/+esm';
""".}
type IntStateSetter = proc(arg: int)
proc intUseState(arg: int): JsObject {.importjs: "useState(#)".}
proc useState*(arg: int): (int, IntStateSetter) =
let state = intUseState(arg)
let value = to(state[0], int)
let setter = to(state[1], IntStateSetter)
return (value, setter)
type StrStateSetter = proc(arg: cstring)
proc strUseState(arg: cstring): JsObject {.importjs: "useState(#)".}
proc useState*(arg: cstring): (cstring, StrStateSetter) =
let state = strUseState(arg)
let value = to(state[0], cstring)
let setter = to(state[1], StrStateSetter)
return (value, setter)
type States* = cstring|int|float|bool
proc useEffect*(cb: proc(), dependency: array) {.importjs: "useEffect(#, [])".}
proc useEffect*(cb: proc(), dependency: seq[States]) {.importjs: "useEffect(#, #)".}
ライブラリの呼び出し側はこのようにします。
JSXの部分はJSが解釈する文字列であり、その中で呼び出したい変数や関数はそこに書いたとおりの変数名で呼ばれることを期待するため、{.exportc.}
を使ってsuffixが付かないようにします。
import std/jsffi
import std/dom
import ./lib
proc App():Component {.exportc.} =
let (message {.exportc.}, setMessage) = useState("")
let (msgLen {.exportc.}, setMsgLen) = useState(0)
proc setMsg(e:Event) {.exportc.} =
setMessage(e.target.value)
useEffect(proc() =
setMsgLen(message.len)
, @[message])
return html("""
<input type="text" oninput=${setMsg} />
<p>${message}</p>
<p>message length...${msgLen}</p>
""")
renderApp(App, document.getElementById("app"))
このように動きます。
JavaScriptの静的型付けとしてのNim
let (message {.exportc.}, setMessage) = useState("")
ここでのsetMessage
はcstring型しか引数として受け付けない関数であるStrStateSetter
です。
lib.nim
でこのように定義しているからです。
type StrStateSetter = proc(arg: cstring)
proc strUseState(arg: cstring): JsObject {.importjs: "useState(#)".}
proc useState*(arg: cstring): (cstring, StrStateSetter) =
let state = strUseState(arg)
let value = to(state[0], cstring)
let setter = to(state[1], StrStateSetter)
return (value, setter)
もしここでint型を入れようとするとどうなるでしょうか
proc setMsg(e:Event) {.exportc.} =
# setMessage(e.target.value)
setMessage(1)
もちろんコンパイルエラーになります。
/projects/nimjs/app.nim(11, 15) Error: type mismatch: got <int literal(1)>
but expected one of:
StrStateSetter = proc (arg: cstring){.closure.}
終わりに
NimでJavaScriptターゲットの開発をするテクニックについて紹介しました。
このようにNodeJSの環境を使うことなく、非常に簡単にJSの資産を使ってNimでReact風SPAを静的な型安全に作れることがわかりました。
今回紹介したことをベースにして、Nim製フロントエンドフレームワークの開発を進めていきます。応援してくれたら嬉しいです。
またJSをラップしたNimのライブラリの資産が増えていくことも願っています。
Discussion