はじめに
執筆現在、「なんだかよくわからんけどすごいフレームワークだ」みたいに巷で噂になっているnue.jsというフレームワークがどんだけすごいんかを動かしながら試してみます。
なんだか Vue やら React やらのJavascript系フレームワークを全て食ってかかってお釣りが出るようなくらいすごいフレームワークだと聞いています。知らんけど。
なお、読み方はよくわかりません。「ヌエ」と書いてある記事もあったりするが、公式にはドイツ語のneue
から来ているそうで「ノイエ」と読むのかもしれません。ノイエというと何かの精神を具現化した素晴らしい兵器とかソロモンに帰ってきたあの人を彷彿とさせますがきっと気のせいでしょう。
ネイティブの発音を調べてみると「ノイエ」というよりは「ノイヤ」が近いかなと思います。
追記 2023.10.01
公式のFAQでは以下のような記載になっています。(なお初版当初と内容が変わっています)
この記載の様に、nueの読み方は「ニュー」でよさそうです。
@tristar さん、コメントありがとうございました!
なお、ドイツ語のneu
の読みは「ノイ」らしいです。ア○ベルさん何も関係なかった。
検証環境
MacBook Air M1(2020) Mem16GB
OSX Venture 13.4.1
Node v19.5
早速動かしてみる
公式ドキュメントのGetting Startedをなぞりながらサンプルを実装してみます。
nue.js自体のインストール
GitHubからのインストールとnpmを使ったインストール方法が紹介されていますが、安定版志向ということでnpmからのインストールを試してみます。
haruyan@haruyan-mac nue-demo % npm install nuejs-core
npm WARN cli npm v10.1.0 does not support Node.js v19.5.0. This version of npm supports the following node versions: `^18.17.0 || >=20.5.0`. You can find the latest version at https://nodejs.org/.
added 7 packages in 1s
6 packages are looking for funding
run `npm fund` for details
npmのバージョン警告が出ましたがnue自体のインストールは問題ないようです。
コンポーネントの説明
Component Basicsの章ではnueで作成するコンポーネントの基本説明があります。この章はただの説明だけで実際に動作するものは次の章からになるようです。
まずは動作するものを作ってみたいので次の章から順に読み進め、必要があればコンポーネントの説明に戻りつつ作業していきましょう。
Server components
Server componentsより。
まずは以下のようなjsファイルを作ります。
import { render } from 'nue'
// define a component
const component = `
<div class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p>{ desc }</p>
</aside>
</div>
`
// render the component with some data
const html = render(component, {
title: 'Media object',
desc: 'One object to rule them all',
img: 'img/art.jpg',
type: 'banner',
})
console.info(html)
node render.js
をターミナルで実行します。
haruyan@haruyan-mac nue-demo % node render.js
(node:48683) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/haruyan/repos/nue-demo/render.js:1
import { render } from 'nue'
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:73:18)
at wrapSafe (node:internal/modules/cjs/loader:1166:20)
at Module._compile (node:internal/modules/cjs/loader:1210:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1300:10)
at Module.load (node:internal/modules/cjs/loader:1103:32)
at Module._load (node:internal/modules/cjs/loader:942:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
at node:internal/main/run_main_module:23:47
おい動かんぞ!なんでや!
これは警告メッセージ、Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
に書いてある通り、package.jsonに "type": "module"
を指定しておかないとnueのモジュールがロードされないらしいです。ドキュメントに書いておいてほしい...
指示通りpackage.jsonに必要事項を記載してもう一度実行します。
haruyan@haruyan-mac nue-demo % node render.js
node:internal/errors:490
ErrorCaptureStackTrace(err);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'nue' imported from /Users/haruyan/repos/nue-demo/render.js
at new NodeError (node:internal/errors:399:5)
at packageResolve (node:internal/modules/esm/resolve:794:9)
at moduleResolve (node:internal/modules/esm/resolve:843:20)
at defaultResolve (node:internal/modules/esm/resolve:1058:11)
at nextResolve (node:internal/modules/esm/loader:163:28)
at ESMLoader.resolve (node:internal/modules/esm/loader:837:30)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:418:18)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:40)
at link (node:internal/modules/esm/module_job:75:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
なんでや!また動かんぞ!
これもエラーメッセージ通りnue
パッケージがインストールされていないから、なのですがさっきインストールしたやろ。しかし先ほどnpmでインストールしたパッケージはnuejs-core
なので、パッケージ名を修正します。
import { render } from 'nuejs-core'
これでようやくエラーなく実行できました。
haruyan@haruyan-mac nue-demo % node render.js
<div class="banner">
<img src="img/art.jpg">
<aside>
<h3>Media object</h3>
<p>One object to rule them all</p>
</aside>
</div>
これはrender
関数を使い、パラメータを埋めてレンダリングするだけのサンプルのようです。
ドキュメントではこのほかにrenderFile
関数を使って他のファイルからレンダリングをすることができるとあります。
早速やってみます。
<div class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p>{ desc }</p>
</aside>
</div>
import { renderFile } from 'nuejs-core'
// render the component with template file
(async () => {
const html = await renderFile('templates/component.nue', {
title: 'Media object',
desc: 'One object to rule them all',
img: 'img/art.jpg',
type: 'banner',
})
console.info(html)
})()
renderFile
はローカルのファイルを扱うのでPromise
を返します。ドキュメントには特に注意書きがないので、非同期処理まわりをしっかりわかっていないとチュートリアルでつまづきそうです。
jsファイルも用意できたので実行してみましょう。node render_file.js
haruyan@haruyan-mac nue-demo % node render_file.js
file:///Users/haruyan/repos/nue-demo/node_modules/nuejs-core/ssr/render.js:286
return comps[0].render(data, comps)
^
TypeError: Cannot read properties of undefined (reading 'render')
at render (file:///Users/haruyan/repos/nue-demo/node_modules/nuejs-core/ssr/render.js:286:19)
at renderFile (file:///Users/haruyan/repos/nue-demo/node_modules/nuejs-core/ssr/render.js:296:10)
at async file:///Users/haruyan/repos/nue-demo/render_file.js:5:16
また動かんやんけ!責任者出てこい!
エラーメッセージを見るといわゆるnull参照でエラー終了している模様です。nue.js側のソースコードを見てみたら読み込んだファイルの中身ではなくファイルパスをrenderするという至極単純なバグが見つかりました。修正すれば動作するようになりましたが、まだメジャーバージョン0とはいえこの程度の品質で世に出してよいものか、心配になります。
とりあえずrenderFile
は現状使用できない、ということで次の章へ進むことにします。
Reactive components
Reactive componentsを進めてみます。Reactive componentsはブラウザ上で動的なレンダリングを可能にするコンポーネントで、ReactやVueのコンポーネントに相当するものです。
今までと同様にドキュメントに書かれているコードがそのまま動作するか試してみましょう。
<section @name="image-gallery" class="gallery">
<div>
<a class="seek prev" @click="index--" :if="index"></a>
<img src="{ basedir }/{ images[index] }">
<a class="seek next" @click="index++"
:if="images.length - index > 1"></a>
</div>
<nav>
<a :for="src, i in images"
class="{ current: i == index }"
@click="index = i"></a>
</nav>
<script>
index = 0
</script>
</section>
コンポーネントの構造はVueに近いです。イベントハンドラは@
から始まる属性で書かれているところは同じ。
:
で始まる制御用の属性があることがわかります。このコードでは:if
と:for
ですね。なんとなくRubyのシンボルに近いイメージ。Ruby好きにはたまらないでのでしょうか。
説明では「これでJSXなんて書かなくてもいいんだよ!」というようなことが書いてあるみたいですが、Vueも同じだしなぁ。
続いてこのコンポーネントをコンパイルする方法です。
import { compileFile } from 'nuejs-core'
await compileFile('templates/gallery.nue', 'www/lib/gallery.js')
ドキュメントとはコンポーネントのファイルパスとimportモジュールを変えてあります。
実行してみます。
haruyan@haruyan-mac nue-demo % node compile.js
haruyan@haruyan-mac nue-demo %
何事もなく終了しました。www/lib/gallery.js
ファイルが生成されています。
export const lib = [
{
name: 'image-gallery',
tagName: 'section',
tmpl: '<section class="gallery"> <div> <a class="seek prev" :if="1" @click="0"></a> <img :src="2"> <a class="seek next" :if="4" @click="3"></a> </div> <nav> <a :for="5" :class="6" @click="7"></a> </nav> </section>',
Impl: class {
index = 0
},
fns: [
(_,e) => { _.index-- },
_ => _.index,
_ => [_.basedir,'/',_.images[_.index]],
(_,e) => { _.index++ },
_ => _.images.length - _.index > 1,
_ => ['src', _.images, 'i'],
_ => [_.i == _.index && 'current '],
(_,e) => { _.index = _.i }
]
}]
export default lib[0]
renderFile
はうまくいかなかったけどcompileFile
はうまくいくのね。
次にこのコンポーネントを実際のWebページにマウント(描画)するコードが紹介されています。
// import createApp method from Nue
import createApp from './nue.js'
// import our compiled gallery component (the default export)
import Gallery from './lib/gallery.js'
// create a gallery app and feed it with data
const gallery = createApp(Gallery, {
images: ['lemons.jpg', 'peas.jpg', 'popcorn.jpg', 'tomatoes.jpg'],
basedir: '/images/fruits'
})
// select a root node for the component
const root = document.querySelector('#gallery')
// mount the instance on the page
gallery.mount(root)
何も説明がないけど、これはhtml側で読み込むjsファイルだよね?document
を参照しているし。
またここで突然nue.js
というファイルが出てくるけど、このファイルはどこから取得するのでしょうか。
配置場所は先ほど作ったwww
フォルダっぽいです。
.node_modules/nuejs-core
内のファイルか、nueのリポジトリにあるjsファイル一式をそのままコピーすれば良いのでしょうか。
たぶんそうだろう、ということでこのようにjsファイルを配置して試してみます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="gallery"></div>
<script type="module" src="app.js"></script>
</body>
</html>
画像ファイルもwww/images/fruit/
フォルダの中に配置します。今回はいらすとやから失敬いたしました。
www
フォルダをrootにしてWebサーバを起動し、表示を確認します。
webサーバの実行はVSCodeのプラグインLive server
を使うのが楽ちんです。
コンポーネント自体は読み込まれているようで、レモンの画像が表示されました。しかしナビゲーションなどのリアクティブコンポーネントは表示されていません。
これはコンポーネントのナビゲーションリンク等に相当するa要素の中身が何もなくてレンダリングされていないように見えるだけのようです。a要素の中に適当に文字列を入れてみて再度コンパイルしてみます。
<section @name="image-gallery" class="gallery">
<div>
<a class="seek prev" @click="index--" :if="index">Seek prev</a>
<img src="{ basedir }/{ images[index] }">
<a class="seek next" @click="index++"
:if="images.length - index > 1">Seek next</a>
</div>
<nav>
<a :for="src, i in images"
class="{ current: i == index }"
@click="index = i">●</a>
</nav>
<script>
index = 0
</script>
</section>
表示結果です。
全くスタイリングしていないので表示はちょっとアレですが、prev, nextのリンクや下部ナビゲーションはしっかりと動作しました。リアクティブなコンポーネントとして動作しているようです。
コンポーネントのライフサイクル
Vueと同じように、コンポーネントのライフサイクルのイベント発生時に処理を行うことができるようです。
現状対応しているのは、constructor
,mounted
,updated
,unmounted
の4種類のようです。
gellery.js のscript要素の部分に以下のようなライフサイクルイベントを追加してみます。
<script>
index = -1
constructor(data) {
console.debug('コンストラクタ')
this.index = 1
}
mounted() {
console.debug('マウントしました')
}
updated() {
console.debug('更新しました')
}
unmounted() {
console.debug('アンマウントしました')
}
</script>
コンパイルして実際に表示させるとしっかりライフサイクルイベントが発生しているようです。
$refs
オブジェクト
コンポーネント内のタグにref
属性を加えると当該コンポーネントをthis.$refs
オブジェクトから参照できるようになります。
試しにimgタグにref属性を付与し、updated
時に画像のURLをコンソールに出すようにしてみます。
<section @name="image-gallery" class="gallery">
<div>
<a class="seek prev" @click="index--" :if="index">Seek prev</a>
<img ref="image" src="{ basedir }/{ images[index] }">
<a class="seek next" @click="index++"
:if="images.length - index > 1">Seek next</a>
</div>
<nav>
<a :for="src, i in images"
class="{ current: i == index }"
@click="index = i">●</a>
</nav>
<script>
index = 1
updated() {
console.debug(`画像変更: ${this.$refs.image.src}`)
}
</script>
</section>
しっかり動作しました。
この参照用オブジェクトはほかにも$el
や$parent
などがあるようです。
- $el : このコンポーネントそのもののHTMLElementオブジェクト
- $parent : このコンポーネントの親コンポーネントのHTMLElementオブジェクト
Sharing code between components
コンポーネントの最上段にscriptタグを書いて他のjsファイルをimportすると、そのファイルのコードをコンポーネント間で共有できる、らしい。
ドキュメントではECシステムの例として、カートのオブジェクトとカートに商品を追加する関数を共有できるという。
<!-- shared code -->
<script>
! import { shopping_cart, addToCart } from './cart.js'
</script>
このサンプルには1つのファイルに複数のコンポーネントを定義しています(nue.jsの基本機能として1つのファイルに複数のコンポーネントを定義できるとされています)。しかし2023/09時点の実装では親コンポーネントから子コンポーネントを呼び出すことはまだできないようで、この機能が本当に動作するのかは確認できていません。
まだ未実装の機能が多い、ということがわかったので次の章へ進むことにします。
Isomorphic components
[Isomorphic components](Isomorphic components)ではnue.jsのコンポーネントの特徴として、サーバー側でもクライアント側でも同じコードで同じレンダリングできるIsomorphic components
(日本語にすると同型コンポーネント?)の存在があります。
Isomorphic componentsは2つの種類があるようです。
- ユニバーサルコンポーネント: サーバーサイドでもクライアントサイドでも同じように動作する
- ハイブリッドコンポーネント: サーバーサイトとクライアントサイドでレンダリングする箇所が分かれている
Universal Components
ドキュメントの説明を読んでみましたが、クライアントでレンダリングする際に投入するモデルオブジェクトが違うだけなのでは?と思いました。サンプルで表示レイアウトが異なっているのはCSSで切り替えているのようですし...
それ以上のことはドキュメントからはわかりませんでした。
Hybrid components
ドキュメントを読んでみましたが、どういった機能で、どのような利点があるのかがいまいち読み込めませんでした。
技術的な要素としては、以下のような動作になるらしいです。
- サーバーサイドでレンダリングする際、importされていないコンポーネントで標準のHTML5(今はHTMLは廃止されHTML Living Standardに移行しているはずですがドキュメント的にこの説明でいいのだろうか)にないタグが現れた場合、
nue-island
タグに置き換え、属性として指定された値がscript
タグで挿入されるようになります。 - クライアントサイドのnue.jsが
nue-island
タグを認識すると、その属性値や内部のscript
タグに書かれた内容を見て適切なコンポーネントにレンダリングします。 - クローリングクライアントなどjavascriptを実行しないクライアントが当該ドキュメントを取得した際、このカスタムタグの中身は単なるスクリプトなので無視され、クローラーの負荷軽減、ネットワークトラヒックの軽減に役立つということのようです。
所感
2023/09現在のnue.jsを試してみた感想としては、まだ実用とは程遠いです。v0.1.1というバージョンからも分かるようにまだアルファ版以下の実装進捗であり、ドキュメントは「将来的に実装したいこと」がこれ見よがしにかかれているだけ。Examplesもありますが現状の実装でできる範囲だけしか紹介されていないようです。
またドキュメントの至る所で「(VueやReactと比べて)軽量(ライブラリの容量が少ないという意味)である」ことが強調されていますが、軽いことが必ずしも最良であるとは限りません。レンダリング速度が重要なアプリであれば間違いなく軽いことが大切ですが、生産性、特に0->1の実装が容易かどうか、メンテナンスがしやすいかどうかは現在の実装からは推し測れません。経験的に軽量フレームワークはできることが限定されているか、軽い代償に使い方がピーキーになるかデプロイできるまでの学習コストが高くなる傾向にあると思います。
githubのstarがまだ公開2週間で2.6kもあるのですが、これはバージョンが上がるたびに動作速度が落ちていくVueやReactのオルタナティブとして期待されていると同時に、現時点では過大な期待をかけられていると言えるのではないかと思います。
果たして「軽量」のままビジネスとしての実用レベルまで持っていけるかどうか、それは現在のコントリビューターの尽力とサポーターの支えにかかっていると思います。PRで貢献したいのであればnue.jsのグランドコンセプトをしっかり理解した上で目的を損なわないような貢献が求められるのではないかと思います。
現場からは 「早くスターターキットを作ってください」の一言に尽きます。スターターのない状態で一からサンプルを検証するのは大変でした。 以上です。
追記
スターターキットについて、 create-nueリポジトリがスターターキットの扱いになっているようです。cloneしてREADMEの記載通りにコマンドを打つとスターターページが表示されます。コンパイルやminify(nodeよりもbunの使用をnue.js的には推奨しているようです)のベーススクリプトも予め用意されています。また現状ではコンポーネントのレンダリングは全てサーバーサイドで行なっていて、リアクティブな動作だけクライアント側で実施されているようです。
さらに追記
つづきを書きました
https://qiita.com/haruyan_hopemucci/items/f5d1605baf24e01b8306