15
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

巷で話題のnue.jsを試してみた(2) v0.1.1でのコンポーネントレンダリング

Last updated at Posted at 2023-09-24

先日書いた 巷で話題のnue.jsを試してみた の続きです。

create-nueのソースコードを読んで、前回の記事で完成できなかった複数コンポーネントのレンダリングができるようになったので後悔、もとい公開しようと思います。

TodoリストとTodo入力を作ってみる

実装例としてよくあるTodoリストを作ってみます。

Todoデータ管理コンポーネント

Sharing code between componentsの章に記載されているように、コンポーネント間でコードの共有を使った方法を試してみます。

以下のようなTodoデータ管理コンポーネントを作成します。

todo.js
class TodoItem {
  constructor(title) {
    this.title = title
    this.isDone = false
  }
}

export const todoItems = [new TodoItem('sample todo')]

export function addTodoItem(title) {
  todoItems.push(new TodoItem(title))
}

Todoの中身を保持するtodoItemsという変数と、Todoを追加するaddTodoItem関数を外部公開するコンポーネントです。TodoItemクラスは内部でしか使用しません。
この実装はチュートリアルのカート実装に内容を合わせています。

Todoコンポーネント

上記データコンポーネントを共有するようにした、レンダリング用のコンポーネントを作成します。

templates/TodoComponents.nue.html
<script>
  import { todoItems, addTodoItem } from "./todo.js";
</script>

<div @name="todo-list">
  <p :for="item in items">
    <span>{ item.title }</span>
    <spna>{ item.isDone ? '完了' : '進行中' }</spna>
  </p>
  <script>
    constructor(){
      this.items = todoItems
      console.debug('constractor todo-list')
    }
  </script>
</div>

<div @name="input-todo">
  <input type="text" name="title" ref="inputTodo" />
  <button @click="onClickInputTodo">Add Todo</button>
  <script>
    constructor(){
      console.debug('constractor input-todo')
    }
    onClickInputTodo() {
      addTodoItem(this.$refs.inputTodo.value);
    }
  </script>
</div>

ファイルの先頭にscriptタグを書き、コード共有したいjsファイルをimportします。

続いて2つのコンポーネント todo-listinput-todoを定義しています。

コンポーネント名はv0.1.1現在ではケバブケースまたはスネークケースでしか記載できないようです。キャメルケース、パスカルケースでは記載できません。なぜなら後段のレンダリングでタグ名は全部lowercase変換されてしまうからです。前回の記事ではそこでハマっていました。

コード共有でimportした変数は、一旦コンポーネントのthisにバインドしないと:forディレクティブでアクセスできないようです。要注意点。

Todo入力コンポーネントで新しいTodoを追加する処理では、$refsを利用してinput要素の中身を取得しています。refの使い方はReactよりも易しそうです。

ページの作成

コンポーネントを配置するページを作成します。

templates/AppTodo.nue.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <main @name="app-todo">
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <input-todo />
      <h2>List</h2>
      <todo-list />
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>

Todoコンポーネントファイルにて、@nameで定義したinput-todotodo-listをタグにして単純に配置しているだけです。
また、app_todo.jsというjsファイルはクライアントレンダリング用のスクリプトです。
中身はあとで紹介します。

render.jsの作成

サーバーサイドでページファイルをレンダリングするためのrender.jsを作成します。
中身はcreate-nueを丸ごとパクって固有名詞だけ変更しています。

render.js
import { parse, render } from 'nuejs-core'
import { promises as fs } from 'node:fs'

const read = async (name, dir = 'templates') => await fs.readFile(dir + '/' + name, 'utf-8')
// read dependencies (server-side components)
const page = await read('AppTodo.nue.html')

const html = render(page)

// write index.html
await fs.writeFile('./www/todo.html', html)

console.log('wrote', 'www/todo.html')

AppTodo.nue.htmlをレンダリングしてwww/todo.htmlに書き出しています。
renderFile使ってないなぁ。
レンダリング対象が増えてきたらフォルダをglabして出力するrender.jsを書いた方が良さそう。
なおrender.jscreate-nueではnpmのコマンドに組み込まれています。

コンポーネントのコンパイル

Todoコンポーネントのコンパイルを行うcompile.jsを用意します。

compile.js
import { compileFile } from 'nuejs-core'

await compileFile('templates/TodoComponents.nue.html','www/lib/TodoComponents.js')

compile.jscreate-nueではnpmのコマンドに組み込まれていました。npmから実行可能です。

クライアントレンダリング用スクリプトの作成

ページファイルを作成したときに最後に出てきたapp_todo.jsを作成します。

www/app_todo.js
import createApp from './nue.js'
import { lib } from './lib/TodoComponents.js'

// Mount <nue-island/> nodes as a Web Component
class NueIsland extends HTMLElement {
  connectedCallback() {
    const App = lib.find(el => el.name == this.getAttribute('island'))
    if (App) createApp(App, {}, lib).mount(this)
  }
}

customElements.define('nue-island', NueIsland)

create-nuesetup.jsからほぼそのまま失敬してきました。
コンパイルで生成する./lib/TodoComponents.jsをimportし、nue-islandというカスタム要素を登録するスクリプトみたいです。これはHybrid componentsで紹介されている内容と同等のものと思われます。

レンダリングとコンパイル

いよいよレンダリングとコンパイルを実行します。

haruyan@haruyan-mac nue-demo % node render.js && node compile.js 
wrote www/todo.html

エラーなく完了しました。
レンダリングしたtodo.htmlはこんな感じに変換されています。

www/todo.html
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TodoApp</title>
  </head>
  <body>
    <main>
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <nue-island island="input-todo"></nue-island>
      <h2>List</h2>
      <nue-island island="todo-list"></nue-island>
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>

<input-todo/><todo-list/>だった箇所が<nue-island>タグに変換されています。このカスタム要素と中に書いてあるisland属性を利用してコンポーネントのレンダリング対象を判断するものと推測されます。

これで表示に必要なファイルは全て作成完了です。

実際にブラウザで表示してみる

実際の表示結果がこちらです。

image.png

Todo入力欄、Todoリスト欄がそれぞれ表示されています。todoItems初期化時に1件だけサンプルTodoを入れているのでそれがそのまま表示されています。

Todoを追加したらしっかりList側に反映されます。

image.png

異なるコンポーネント定義ファイル間で状態の共有ができるかどうか

VueでいうところのVuex、Reactでいうところのrecoilで実現されているコンポーネント間の状態共有について試してみます。
現状では狭い範囲で「状態のコンポーネント間共有」はできています。ドキュメント通り同じファイルに別々のコンポーネントを定義し、そのトップレベルにscriptタグで状態を管理するjsファイルをimportすることで共有を実現しています。

では異なるコンポーネント定義ファイルで状態の共有はできるのでしょうか?

既存のコンポーネントとは別にコンポーネントファイルを作成してみます。

templates/RichTodoComponents.nue.html
<script>
  import { todoItems, addTodoItem } from "./todo.js";
</script>

<div @name="rich-todo-list">
  <p :for="item in items">
    { item.title }  { item.isDone ? '完了' : '進行中' } です
  </p>
  <script>
    constructor(){
      this.items = todoItems
      console.debug('constractor rich-todo-list')
    }
  </script>
</div>

Todoの表示が文章になったTodoListです。どこがリッチなんだとかツッコミは受け付けません。
ファイル冒頭に先ほど作ったTodoComponentsと同様のscriptタグを設置します。

このコンポーネントをページに追加します。

templates/AppTodo.nue.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TodoApp</title>
  </head>
  <body>
    <main @name="app-todo">
      <h1>Todo</h1>
      <h2>Input Todo</h2>
      <input-todo />
      <h2>List</h2>
      <todo-list />
      <h2>Rich List</h2>
      <rich-todo-list />
      <script type="module" src="./app_todo.js"></script>
    </main>
  </body>
</html>

<rich-todo-list>を追加しました。

最後に新しく作ったコンポーネントをコンパイルするようにします。

compile.js
import { compileFile } from 'nuejs-core'
await compileFile('templates/TodoComponents.nue.html', 'www/lib/TodoComponents.js')
await compileFile('templates/RichTodoComponents.nue.html', 'www/lib/RichTodoComponents.js')

ファイルの修正が完了したら npm render.js && npm compile.jsでレンダリングとコンパイルを実行します。

初期表示はこのようになります。

image.png

Input Todo から Todoを追加するとList側にもRich List側にも追加が反映されます。

image.png

ブラ○ト、やるな!

コンポーネント間のデータ共有はVueやReactよりもかなり簡単にできそうです。簡単にできすぎて心配になるくらいです。チーム開発では闇雲に共有データを壊されないように共有データコンポーネントをしっかり設計する必要がありそうです。それに注意さえすればかなり良いんじゃないでしょうか。

終わりに

とりあえず「複数コンポーネントの描画ができました」の報告でした。
今回学んだことは、

  • 作成するコンポーネント名はケバブケースで書かないといけない。Vueみたいにパスカルケースでコンポーネント名を書くとハマるぞ!公式ドキュメントはそんなこと教えてくれないぞ!
  • 複数のコンポーネントで状態共有ができる。状態共有コンポーネントをファイルの先頭でimportするだけで済むぞ!

といったところです。

今回学習用に作成したコードをgithubに公開します。よろしければ参考にしてください。
https://github.com/haruyan-hopemucci/nue-demo

15
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?