Nue.js和訳&create-nueの解説
[鵺]Nue.jsがフロントエンドを永遠に塗り替えるらしいを見て興味が沸いたのでお試し
TOEIC800程度の英語力&フロントエンド半年程度のエンジニアなので怪しいかも
Introduction和訳
Nue JS はzipで最小2.3kbとかなりサイズが小さいUI作成用JSライブラリです。Nue Toolset[1]のコアとなる部分です。Vue/React/Svelteといったものに似ていますが,使っていくにあたって以下のようなものがありません:hooks, effects, props, portals, watchers, injects, suspensions,その他あまり見かけないようなアブストラクト
基本的なHTML,CSS,JSを学んでいれば問題ありません。
少ないコードでUIを作成
Nueの最大の利点はより少ないコードで同じことが出来るという点です。必要なコード数に2-10倍の差が付くことは珍しいことではありません。例として,Nueで書いたList boxコンポーネントを置いておきます(以下コンポーネント)
Reactは2500行のJSです。Nueでは5-8割の力で約1/10に減らしています。
"It's just HTML"
NueはHTMLに基づくtemplate syntaxを使っています。
<div @name="media-object" class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p :if="desc">{ desc }</p>
<slot/>
</aside>
</div>
Reactが"Just JavaScript"と言われているならば,Nueは"Just HTML"と考えられるべきでしょう。つまりインタラクティブデザイン,アクセシビリティ,UXに重きを置いているUX開発者にとってピッタリと言えます。
React,Vue,Svelte,Tailwind,Astroとの比較
スケーラビリティ
Nueがフロントエンド開発に新レベルのスケーラビリティをもたらす3つの理由:
- 関心の分離[2], easy-to-readはスパゲッティよりもスケールしやすい
- ミニマリズム, 数百行のコードは数千行のコードよりスケールしやすい
- 才の分離,UX開発者がフロントエンドのフロントを作ってJS/TS開発者がフロントエンドのバックを作ればチームスキルが最適化されます。
スタイルの分離
Nueは依存度の高さからscoped CSS,tailwind,その他CSS-in-JSなフレームワークを使うことを推奨していません。styleはlayout及びstructureからは分離されるべきでしょう。なぜなら:
- 再利用可能なコード: スタイルがコンポーネントにハードコーティングされてないならば,同じコンポーネントをページやコンテクストによって見た目を変えるということが出来るでしょう。
- No spaghetti code: 純粋なHTML及び純粋なCSSはそれらの混ざったスパゲッティよりも簡単に読めます。
- より高速なページ読み込み: 分離されたスタイルならば主となるCSSを抽出しやすく,HTMLページをcritical 14kb limit[3]以下に保ちやすいです。
4種類のコンポーネント
Nueには4種類のコンポーネントモデルがあり,どんなアプリケーションをも作れるようになっています:
- Serverコンポーネントはサーバーでrenderされます。jsなしで読みこみを高速化し検索エンジンにクローリングされるようなコンテンツメインのWebサイトを作るのに役立ちます。
- Reactiveコンポーネントはクライアントでrenderされます。動的なアイランドやSPAを作るのに役立ちます。
- Hybridコンポーネントは部分的にそれぞれサーバー,クライアントでrenderされます。動画のタグや画像ギャラリーのような,リアクティブでSEO対策するコンポーネントを作るのに役立ちます。
- Universalコンポーネントはサーバークライアント両方で同時に利用されます。
UIライブラリファイル
Nueでは1ファイルで複数のコンポーネントを定義できます。このことは関連するコンポーネントをまとめ依存関係の管理をすっきりさせてくれる良い方法です。
<!-- 共有する変数及びメソッド -->
<script>
import { someMethod } from './util.js'
</script>
<!-- first component -->
<article @name="todo">
...
</article>
<!-- second component -->
<div @name="todo-item">
...
</div>
<!-- third component -->
<time @name="cute-date">
...
</time>
ライブラリファイルを使うと,ディレクトリ構造がよりすっきりとし,要素を結合するのにより少ないコピペコード(boilerplate)で済むでしょう。他の開発者のためにライブラリをパッケージ化するのにも役立ちます。
シンプルなツール
Nuejsは単純なサーバーサイドレンダリングのためのrender
関数と,ブラウザでコンポーネントを生成するためのcompile
関数からなっています。開発環境を整えるためにWebpackやViteのような巨大なバンドラーは必要ありません。Nueをプロジェクトにインポートすれば良いのです。
NOTE バンドラーは数百数千のnpm依存関係のあるようなビジネスモデルレイヤーでは理に適っています。Bunやesbuildは高性能で選択肢の一つです。
Use cases
NueJSはサーバー/クライアントサイド両方をサポートし,コンテンツメインのWebサイトもリアクティブなSPAも作ることが出来る万能なツールです。
- UIライブラリ開発 リアクティブなフロントエンドなりサーバー側でコンテンツを生成するなり出来る再利用可能なコンポーネントを作る。
- プログレッシブエンハンスメント NueJSはコンテンツメインのWebサイトを動的コンポーネント/アイランドで強化するのに必要十分なライブラリです。
- 静的サイトジェネレータ プロジェクトにインポートすればrender出来ます。何もバンドラーは要りません。
- シングルページアプリ 今後追加するNue MVCと共によりシンプルでよりスケーラブルなアプリが作れます。
- Templating NueはWebページやHTMLメールを生成する汎用ツールです。
実際に使ってみた
導入方法
Nuxtなどとは違い,今のところはgit cloneから始めるようです。
BunかNodeをインストールしCLIに慣れた状態で
# clone the repository
git clone https://github.com/nuejs/create-nue.git
# cd to your newly created app
cd create-nue
# install dependencies
npm install
# Build demo site and start a HTTP server
npm run start
# ブラウザで以下にアクセスしてみる
open "http://localhost:8080"
.
をカレントディレクトリだと読みこんでくれないので,npm run start
時に./scripts/minify.js
が見つからないと言われる
PowerShellだとUbuntu LTSにNodejsとnpm入れてそちらでコマンド入れれば問題ない エンジニアやるならMac使えってことなんだろうかなあ
Win固有の問題に少し苦戦しましたが無事起動
分析
nueを使ってページを作ろうと思った場合,作成するのは``www/css''内のcssと,src内の.nue及び.dataファイルです。
cssファイル
cssファイルには前述の通り,primary.css
とその他のcssがあります。
primary.css
render.js
内で読みこまれ,生成されるHTML内に直接記述されるcssです。こんな感じで<style>タグ内に出てきます。
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="generator" content="Nue (nuejs.org)">
<meta name="date.updated" content="2023-09-22T05:47:00.620Z">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="img/stack.svg">
<title>A barebones Nue app</title>
<style>/** * The contents of this file is included on the generated * HTML file for quicker page load */ /* Normalize */ * { box-sizing: border-box; margin: 0; } html { overflow-x: hidden; } a { cursor: pointer; } /* Layout */ body > * { max-width: 920px; margin: 0 auto; padding: 2em; } main { text-align: center; } main > header { margin-bottom: 2em; } /* Typography */ body { font-family: -apple-system, BlinkMacSystemFont, Avenir Next, Segoe UI, Roboto, Noto Sans, Helvetica Neue; -webkit-font-smoothing: antialiased; line-height: 1.5; color: #4a4f53; } h1, h2, h3 { color: black; line-height: .9; } h1 { font-size: 5.5em; font-weight: 900; margin-bottom: .1em; } h3 { margin-bottom: .2em; } h1 + p { font-size: 1.6em; } /* Master navigation */ .mastnav { justify-content: space-between; margin-bottom: 2.5em; align-items: center; margin-bottom: 1em; display: flex; } .mastnav a { display: inline-block; transition: transform .1s; } .mastnav a:hover { transform: scale(1.05); } .mastnav a:active { transform: scale(.99); } .icon { width: 1.4em; margin-left: .5em; } .logo { max-height: 2em; } /* Project list */ .grid { list-style: none; display: grid; grid-template-columns: 50% 50%; font-size: 1.08em; gap: 1.5em; padding: 0; } .grid li { box-shadow: 0 1px 2px #0001, .1em .4em 1em rgba(0,0,0,0.08); border-radius: 2px; padding: 1.2em 1.6em; text-align: left; } /* Footer */ footer { font-size: 1.8em; padding: 3em; } /* Mobile layout */ @media (max-width: 900px) { main > header { font-size: 80%; } h1 { font-size: 4.3em; } h1 + p { max-width: 14em; margin: 0 auto 2em; } .grid { grid-template-columns: 100%; max-width: 440px; margin: 0 auto; font-size: .95em; } }</style>
secondary_css
htmlファイル下部に生成される<link rel="stylesheet">
内のリンクから読まれるcssファイル群です。
secondary.css
というファイルではなく,後述するcontent.data
内に以下のように登録します。
secondary_css:
- css/themes.css
- css/dialog.css
するとこれもまた後述するlayout.nue
内で呼び出すことが出来ます。どういう挙動をするかは:for
で察することが出来るはず
<link :for="href in secondary_css" :href="href" rel="stylesheet" fetchpriority="low">
secondary.cssはHTMLが生成されてから読みこまれるので,先程の14KBルールを守りやすくなります。
srcフォルダ
content.data
というYAML形式のファイルと,components.nue
,islands.nue
,layout.nue
の3つのnueファイルが入っています。
content.data
定数が書かれたYAML形式のファイルです。
# Website content and settings (in YAML format)
# Parsed and given as argument to SSR render() function
# check: scripts/render.js
title: A barebones Nue app
heading: Frontend Troublesolver
description: Nue is a set of tools to make web developer's life easier
github: nuejs/create-nue
favicon: img/stack.svg
logo: img/logo.svg
projects:
- Nue JS: Build user interfaces with 10x less code
- Nue MVC: Bring back the power of cascaded styling
- Nue CSS: Build intuitive single-page apps that scale
- Nue UI: Reusable components for rapid UI development
- Nuemark: Markdown flavor for rich/interactive content
- Nuekit: Build websites and webapps with 10x less code
themes:
- name: Light
color: fff
- name: Dark
color: 21262d
- name: Vibrant
class: dark vibrant
color: 0098c8
secondary_css:
- css/themes.css
- css/dialog.css
定義された値は,Vue.jsっぽく取り出せます。
<html>
<meta charset="utf-8">
<meta name="generator" content="Nue (nuejs.org)">
<meta name="date.updated" content="{ timestamp.toISOString() }">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="{ favicon }">
<title>{ title }</title> <!-- vueとは違い1つの{}で良い -->
<style>{ primary_css }</style>
<!-- header component (server-side) -->
<site-header :logo="logo" :github="github"/> <!--propsっぽい渡し方の書き方は同じ -->
layout.nue
生成するHTMLのコアとなる部分で,1layout.vue = 1htmlファイルとなります。
以下のようにrenderされています。
// read page layout
const page = await read('layout.nue') //ここで読んでいる
// set extra, dynamic properties to data
data.primary_css = primary_css.replace(/\s+/g, ' ')
data.timestamp = new Date()
// generate HTML with the render() method
const html = '<!DOCTYPE html>\n\n' + render(page, data, lib) //ここでnuejs-core内のrender関数に渡してrenderしてる
// write index.html
await fs.writeFile('./www/index.html', html) //今のところハードコーディングして生成してる
console.log('wrote', 'www/index.html')
中身に関しては,freeCodecampなんかで学ぶような純粋なHTMLに,若干のReact/Vueっぽいシンタックスが混ざってる感じです。
<html>
<meta charset="utf-8">
<meta name="generator" content="Nue (nuejs.org)">
<meta name="date.updated" content="{ timestamp.toISOString() }">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="{ favicon }">
<title>{ title }</title>
<style>{ primary_css }</style>
<!-- header component (server-side) -->
<site-header :logo="logo" :github="github"/> <!--後述するcomponents.nue内で定義されたコンポーネントです -->
<main>
<header>
<h1>{ heading }</h1>
<p>{ description }</p>
</header>
<article>
<item-list :items="projects" class="grid"/>
</article>
<footer>
<img :src="favicon" class="icon">
</footer>
</main>
<!-- theme selector dialog (client-side component)-->
<theme-selector :themes="themes" id="themes"/>
<!-- setup all client-side/reactive components このsetup.jsから,npm run start時に生成されるnue.js及びisland.jsが読みこまれる -->
<script type="module" src="setup.js"></script>
<!-- secondary CSS -->
<link :for="href in secondary_css" :href="href" rel="stylesheet" fetchpriority="low">
</html>
ルーターがまだない(Nue MVCやNuekitとして追加予定)のでlayout.nueの単一ページのみ作れる形となります。
components.nue
静的なコンポーネントを定義するファイルです。 @name
でコンポーネントを定義しています。
<!--
Library of server-side components used by layout.nue
See: scripts/render.js
-->
<!-- site header -->
<header @name="site-header" class="mastnav">
<a href="https://nuejs.org/"><img class="logo" :src="logo"></a>
<nav>
<dialog-opener key="themes"/>
<a href="https://github.com/{ github }">
<img src="img/github.svg" class="icon"></a>
</nav>
</header>
<!-- generic/re-usable list component -->
<ul @name="item-list">
<li :for="el in data">
<h3>{ el.name }</h3><p>{ el.desc }</p></li>
<script>
constructor({ items }) {
this.data = items.map(el => {
const [name, desc] = Object.entries(el)[0]
return { name, desc }
})
}
</script>
</ul>
全静的コンポーネントを1ファイルで定義しているのと,nue特有の書き方もあって,どれが1コンポーネントなのかや,コンポーネントの開始タグの種類なんかが一目で分かって良いと思います。
islands.nue
クライアント側で生成される動的なコンポーネントa.k.aアイランドを定義するファイルで,基本はcomponents.nueと変わりません。
動的コンポーネントなので,@click
や@change
,ここにはないですが@submit.prevent
に@keyup.enter
(エンターキーを押した時)などイベントハンドラーを用意できます。
<!--
Small library of client-side reactive components
https://nuejs.org/docs/nuejs/reactive-components.html
-->
<a @name="dialog-opener" class="dialog-opener" @click="open">
<img loading="lazy" src="img/settings.svg" class="icon">
<script>
open() {
const key = this.root.getAttribute('key')
const dialog = window[key]
if (dialog) dialog.showModal()
}
</script>
</a>
<dialog @name="theme-selector">
<h2>Select theme</h2>
<section class="theme-options">
<label :for="el, i in themes" :style="background-color: #{el.color}" class="theme">
<input name="theme" type="radio" :checked="!i" @change="change(el, $event)">
<h4>{ el.name }</h4>
</label>
</section>
<script>
change(el, e) {
document.body.className = el.class || el.name.toLowerCase()
themes.close()
}
</script>
</dialog>
この中身はislands.js
として生成され,html生成後に読みこまれます。
export const lib = [
{
name: 'dialog-opener',
tagName: 'a',
tmpl: '<a class="dialog-opener" @click="0"> <img loading="lazy" src="img/settings.svg" class="icon"> </a>',
Impl: class {
open() {
const key = this.root.getAttribute('key')
const dialog = window[key]
if (dialog) dialog.showModal()
}
},
fns: [
(_,e) => { _.open.call(_, e) }
]
},{
name: 'theme-selector',
tagName: 'dialog',
tmpl: '<dialog> <h2>Select theme</h2> <section class="theme-options"> <label :for="0" :style="1" class="theme"> <input name="theme" type="radio" $checked="2" @change="3"> <h4>:4:</h4> </label> </section> </dialog>',
Impl: class {
change(el, e) {
document.body.className = el.class || el.name.toLowerCase()
themes.close()
}
},
fns: [
_ => ['el', _.themes, 'i'],
_ => ['background-color: #',_.el.color],
_ => !_.i,
(_,e) => { _.change(_.el, e) },
_ => [_.el.name]
]
}]
export default lib[0]
出来ないこと,不便なこと
- ルーターがないので,複数ページを持つWebサーバは立てられません。
- 拡張機能等が整備されてないので,コードがカラフルにならず,今のところはVueよりは作りにくいと思います。
- 実務経験がないので憶測ですが,考えがフロントエンドのフロント(UXデザイナー)とバック(JS/TSデベロッパー)という構造を前提としているため,日本のフロントエンドエンジニアの職務と合うかが分からない,システムエンジニアなら尚更
終わりに
アピールポイントであるように相当の行数の削減している上,ファイルの責務が職能別組織っぽいため,全部の仕様を把握しきれている個人製作だと分離が面倒かなと感じる一方で,大人数で作業するならばモジュールの結合がスムーズに行きそうで便利だなと思います。半年程度の浅いエンジニアの自分でも全体を「完全に理解する」程度ならば1時間もかからず出来るくらいにはハードルが低くされていて,今後の発展次第ではよりスピーディーで参入障壁の低い開発が出来るようになるのかなあと思います。
Discussion