<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>Frontend カテゴリ | フューチャー技術ブログ</title>
  <subtitle>Frontend カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/Frontend/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/Frontend/"/>
  <updated>2026-07-26T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/Frontend/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>React Server Component時代のReactの書き方(React19/Next.js16準拠)</title>
    <link href="https://future-architect.github.io/articles/20260727a/"/>
    <id>https://future-architect.github.io/articles/20260727a/</id>
    <published>2026-07-26T15:00:00.000Z</published>
    <updated>2026-07-26T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>一番Next.jsとかReactを書いていたのはhookが出たあたりで関数コンポーネントになった近辺でだいぶ前ですが、Reactのドキュメントを一通り見ていたところ、かなり書き方が変わっていそうなので、新規プロジェクトでがっつりイマドキの書き方に寄せてみようと思って学びなおしたり(以前からドキュメントの更新は追いかけてはいた)、検証したのでその備忘というか整理です。</p>
<p>本エントリーの前提としては関数コンポーネント+hooksは知っているよ、ということで進めていきます。関数コンポーネントからだとちょっと差が大きすぎてしまうのでそこは扱いません。</p>
<h2 id="React-19の考え方">React 19の考え方</h2><p>Reactの最近の変更は大まかには次のモチベーションで行われている、と考えると理解しやすいと思いました。</p>
<ul>
<li>パフォーマンスの低下原因となる余計な更新処理を減らし、応答性を上げる</li>
<li>自分で管理する状態の数とEffectを減らす</li>
</ul>
<p>いろんな新機能などを見るとこの2つの観点が混ざっているものもありますが、分解してみれば理解しやすいと思います。</p>
<p>あとは昔はなかった3つの言葉が出てきます。これを理解してから見てみないと「React変わりすぎてついていけない」となってしまうと思いました。まずはこちらの解説をします。</p>
<ul>
<li>トランジション</li>
<li>アクション</li>
<li>ストリーミング</li>
</ul>
<h3 id="トランジション">トランジション</h3><p>たぶん、一番？となりやすいところかと思います。キーボードの入力のたびに画面表示するとして、変更が重い場合に、React内部の画面の変更(レンダリング)リクエストが溜まってしまう応答性が下がってしまうことがあります。トランジションは「この変更による描画は優先度が低いから重複したら中断したり後回しにしたり破棄してもいい」というのをReactに伝えるものです。</p>
<p>状態変更はロジックで行いますが、そのロジックはReactでは主に2ヶ所に書きます。</p>
<ul>
<li>ハンドラ: ユーザーの操作が起点となるもの</li>
<li>エフェクト: 他の状態変更が起点となり、外部と同期を取るもの</li>
</ul>
<p>トランジションの中から呼ばれる関数（普通の関数もOK）がアクションと呼ばれます。</p>
<p>なお、トランジションは結果の描画を効率化して応答性を良くしますが、中で行われる通信の抑制はしません。タイプごとにコード補完のために通信する、みたいな場合の通信の間引きみたいな処理は引き続き必要です。</p>
<h3 id="アクション">アクション</h3><p>トランジションの中から呼ばれる処理です。同期も非同期も可です。サーバーに変更をポストしたりとかが主な役割になるかと思います。</p>
<p>実装方法としては単なる非同期関数とかを作ってもアクションと言えますが、それにちょっと付加価値がついたものもあります。</p>
<ul>
<li>useActionStateでラップされて作ったもの。それ単体でのステート管理の機構も持っている。<code>useState()</code>のすごい版。非同期関数対応で、アクションが実行中かどうかのフラグ管理もしてくれる</li>
<li>Server Function。以前サーバーアクションと呼ばれていたもの。サーバーで動く。APIハンドラを作らずに自動で作ってくれる</li>
</ul>
<p>組み合わせると以下の実装パターンがあります。</p>
<ul>
<li>非同期関数</li>
<li><code>useActionState</code> + 非同期関数</li>
<li>サーバー関数</li>
<li><code>useActionState</code> + サーバー関数(この場合はステートはシリアライズ可能である必要あり)</li>
</ul>
<p>アクションを呼ぶときはトランジションの中で呼ぶ必要があります。また、標準的なタグだと<code>&lt;form action=&#123;action&#125;/&gt;</code>がアクションを引数として持っています。アクションをpropsとして渡す場合は、それを受け取って実際に呼ぶ側がトランジションを開始するのがルールです。</p>
<p><code>&#39;use server&#39;</code>がついたファイルに書かれたのサーバー関数です。クライアントからも呼べるがHTTPをまたいでの実行となります。</p>
<figure class="highlight ts"><figcaption><span>サーバー関数</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use server&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">updateProfile</span>(<span class="params"><span class="attr">formData</span>: <span class="title class_">FormData</span></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> name = formData.<span class="title function_">get</span>(<span class="string">&#x27;name&#x27;</span>) <span class="keyword">as</span> <span class="built_in">string</span>;</span><br><span class="line">  <span class="keyword">const</span> email = formData.<span class="title function_">get</span>(<span class="string">&#x27;email&#x27;</span>) <span class="keyword">as</span> <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!name || name.<span class="title function_">trim</span>().<span class="property">length</span> === <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">success</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">message</span>: <span class="string">&#x27;名前を入力してください。&#x27;</span>,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> db.<span class="property">user</span>.<span class="title function_">update</span>(&#123; <span class="attr">where</span>: &#123; id &#125;, <span class="attr">data</span>: &#123; name, email &#125; &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">success</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">message</span>: <span class="string">&#x27;プロフィールを更新しました！&#x27;</span>,</span><br><span class="line">    <span class="attr">user</span>: &#123; name, email &#125;,</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="ストリーミング">ストリーミング</h3><p><code>Promise</code>のいけてる使い方です。サーバーとクライアントの間でPromiseをやり取りする仕組みです。</p>
<p>もともとはlazyのコンポーネント非同期読み込みのために<code>Promise</code>をthrowすると解決するまで待つというのが16あたりのころにありましたが、<Suspense>コンポーネントとの組み合わせで、ロード中などが表示できるようになりました。</p>
<p>ErrorBoundaryは関数コンポーネントとしては長らく提供されてきませんでしたが、サードパーティのライブラリを使うように公式ドキュメントも追加されていますね。</p>
<figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Suspense</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ErrorBoundary</span> &#125; <span class="keyword">from</span> <span class="string">&quot;react-error-boundary&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;<span class="name">ErrorBoundary</span> <span class="attr">fallbackRender</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>エラー中<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>ロード中<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;<span class="name">AsyncComponent</span> /&gt;</span> &#123;/*非同期コンポーネント*/&#125;</span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">Suspnse</span>&gt;</span></span></span><br><span class="line"><span class="language-xml"><span class="tag">&lt;/<span class="name">ErrorBoundary</span>&gt;</span></span></span><br></pre></td></tr></table></figure>

<p>このような<code>Promise</code>を扱う仕組みの上に、クライアント／サーバーの間の仕組みとして整理されたのがストリーミングです。</p>
<p>サーバーコンポーネントではデータの取得を非同期に書けますが、awaitで待つのではなく、Promiseのまま子供のコンポーネントなどに渡せるようになりました。こうすることで、ロード中の状態でサーバーサイドレンダリングが行われてクライアントで一時的な結果表示が行われ、サーバー側の処理が終わったらその後フロントエンドが更新されて表示が行われます。</p>
<figure class="highlight jsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Suspense</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; fetchUsers &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/db&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">UserList</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../components/UserList&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Page</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// ここで await しない！</span></span><br><span class="line">  <span class="keyword">const</span> usersPromise = <span class="title function_">fetchUsers</span>();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">main</span> <span class="attr">className</span>=<span class="string">&quot;p-8&quot;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Suspense</span> <span class="attr">fallback</span>=<span class="string">&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        &lt;<span class="attr">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          ⏳ ユーザーデータをロード中...</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#125;&gt;</span></span><br><span class="line"><span class="language-xml">        &#123;/* Promiseのまま子コンポーネントに渡す */&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">UserList</span> <span class="attr">usersPromise</span>=<span class="string">&#123;usersPromise&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">main</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; use &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">User</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../lib/db&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Props</span> = &#123;</span><br><span class="line">  <span class="comment">// データそのものではなく「データを取得するPromise」を受け取る</span></span><br><span class="line">  <span class="attr">usersPromise</span>: <span class="title class_">Promise</span>&lt;<span class="title class_">User</span>[]&gt;; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">UserList</span>(<span class="params">&#123; usersPromise &#125;: <span class="title class_">Props</span></span>) &#123;</span><br><span class="line">  <span class="comment">// React 19の use() で Promise を解決する</span></span><br><span class="line">  <span class="comment">// Promiseが未解決(pending)の間は自動的にサスペンドされ、親の &lt;Suspense&gt; が発火</span></span><br><span class="line">  <span class="keyword">const</span> users = <span class="title function_">use</span>(usersPromise);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;users.map((user) =&gt; (</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;user.id&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;user.name&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ))&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>今までは同様の非同期更新をしようとすると、一度フロント側がロードされて表示されてから、CSRで<code>useEffect()</code>を使って追加情報を取得し、結果を待って更新となっていましたが、初回のサーバーリクエスト時に追加リクエストまで開始するのでかなりリードタイムは更新されます。</p>
<h2 id="応答性を上げる">応答性を上げる</h2><h3 id="エフェクトロジックの無駄起動の排除">エフェクトロジックの無駄起動の排除</h3><p>エフェクトはその名の通り、他のデータなどが変わったあとの作用（副作用）として使います。で、<code>useEffect()</code>の2つめの引数にはその中で参照している変数を入れます。それらの変数が変更されたらその中のコードが呼ばれます。最近あまり使うなという風潮ですが、どうしても使うケースはあります。</p>
<p>効率を考えていくと、Effectの「変更を検知したい変数」と「使いたいデータ」が一体化しているのが問題となることがあります。次のコードは<code>category</code>と<code>id</code>と2つ使っているのでそれを後ろで渡しています。</p>
<figure class="highlight tsx"><figcaption><span>IDが変わったらデータを取得して画面を更新したい（カテゴリだけでは呼びたくない）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> [data, setData] = <span class="title function_">useState</span>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fetchData</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`https://api.example.com/data/&#123;category&#125;/&#123;id&#125;`</span>);</span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> response.<span class="title function_">json</span>();</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">fetchData</span>();</span><br><span class="line">&#125;, [category, id]);</span><br></pre></td></tr></table></figure>

<p>でも、<code>id</code>の変更は検知したいが<code>category</code>の変化だけだとまだ通信したくないとしてもそれはここでは実現できませんでした。<code>useRef</code>とか使えばできたかもしれませんが。</p>
<p>React19で追加されたuseEffectEventを使って実行したいロジックと<code>useEffect()</code>を切り離すことができます。<code>useEffectEvent()</code>は常に最新の値を使います。</p>
<figure class="highlight tsx"><figcaption><span>useEffectとuseEffectEventに分ける</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// id変更時に呼びたいロジック</span></span><br><span class="line"><span class="comment">// categoryはその場でレキシカルスコープで見える最新の変数を参照</span></span><br><span class="line"><span class="keyword">const</span> fetchData = <span class="title function_">useEffectEvent</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`https://api.example.com/data/<span class="subst">$&#123;category&#125;</span>/<span class="subst">$&#123;id&#125;</span>`</span>);</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> response.<span class="title function_">json</span>();</span><br><span class="line">  <span class="comment">// setData(result) など</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="title function_">useEffect</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// idが変わった時だけ実行される</span></span><br><span class="line">  <span class="title function_">fetchData</span>();</span><br><span class="line">&#125;, [id]); <span class="comment">// 依存配列には id だけを含める</span></span><br></pre></td></tr></table></figure>

<h3 id="画面更新を遅延させる">画面更新を遅延させる</h3><p><code>useState()</code>で状態を更新すると、変更がトリガーされて画面更新が実行されます。<br>useDefferedValueでラップしてラップした方を画面表示に使うと、遅延して表示をさせることでユーザーの操作の反映が遅れて操作がスムーズにいかない、というのを抑制します。</p>
<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useState, useDeferredValue, memo &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">SearchPage</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [query, setQuery] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// deferredQuery は query から少し遅れて更新されます</span></span><br><span class="line">  <span class="keyword">const</span> deferredQuery = <span class="title function_">useDeferredValue</span>(query);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;/* ユーザー入力は素早く画面反映 */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">value</span>=<span class="string">&#123;query&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">        <span class="attr">onChange</span>=<span class="string">&#123;(e)</span> =&gt;</span> setQuery(e.target.value)&#125;</span></span><br><span class="line"><span class="language-xml">        placeholder=&quot;検索キーワードを入力...&quot;</span></span><br><span class="line"><span class="language-xml">      /&gt;</span></span><br><span class="line"><span class="language-xml"></span></span><br><span class="line"><span class="language-xml">      &#123;/* 検索結果の表示は遅延ロードさせる */&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">HeavyList</span> <span class="attr">query</span>=<span class="string">&#123;deferredQuery&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="サーバーとの通信の作法">サーバーとの通信の作法</h2><p>サーバーからのデータ取得周りも整理されて、それにともなう余計なコード（通信前後の状態管理など）はどんどん減ってきています。</p>
<h3 id="コンポーネント初期化時のGET処理">コンポーネント初期化時のGET処理</h3><p>だいたい、ライフサイクルメソッドやEffectで<code>fetch()</code>で情報取得して、それをステートに入れて管理、あるいはSSRだとページのルートの<code>getServerSideProps()</code>などでしか状態が取れないのでそこでまとめて取得してコンテキストに入れて・・・みたいな感じでした。中央集権が必要だったからこそたくさんのデータが一か所にあって統制も大変だったのですが、今はだいぶそのあたりは緩和され、コンポーネントごとの管理でよくなってきていると感じます。</p>
<p>以前から、クライアントコンポーネントの場合、SWRなんかは受信や再読み込みが宣言的に記述できてこの通信回りの状態管理はだいぶシンプルになっていました。</p>
<figure class="highlight tsx"><figcaption><span>以前からあるSWR</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;use client&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Profile</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// これ！</span></span><br><span class="line">  <span class="keyword">const</span> &#123; data, error, isLoading &#125; = <span class="title function_">useSWR</span>(<span class="string">&#x27;/api/user&#x27;</span>, fetcher)</span><br><span class="line"> </span><br><span class="line">  <span class="keyword">if</span> (error) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>failed to load<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  <span class="keyword">if</span> (isLoading) <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>loading...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span>hello &#123;data.name&#125;!<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>サーバーコンポーネントになると、手続き的に書けて状態管理は不要、というよりも書けません。またクライアントだとクライアントのロードが終わって最初のEffectが終わってから通信開始ですが、最初のリクエスト時に取得を開始するので完了までの時間も短くなります。</p>
<figure class="highlight tsx"><figcaption><span>サーバーコンポーネントデータ取得</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">Note</span>(<span class="params">&#123;id&#125;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`/note/&#123;id&#125;`</span>);</span><br><span class="line">  <span class="keyword">const</span> note = <span class="keyword">await</span> res.<span class="title function_">json</span>();</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">Author</span> <span class="attr">id</span>=<span class="string">&#123;note.authorId&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;note&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>先ほども紹介したように、<code>await</code>を書かないで置けばストリーミングにもできます。<code>isLoading</code>相当はReactが面倒を見てくれるという感じですね。</p>
<h3 id="初期化以外の通信">初期化以外の通信</h3><p>ボタン操作やフォーム送信後のデータの更新処理は今まではハンドラの中から<code>fetch()</code>を呼び結果をstateに入れるというのが一般的でした。useSWRMutationなどもありますが。自分で書こうとすると、1つの通信でたくさんの状態の管理が必要となります。このあたりは、改善ポイントを発見する良い指標かと思います。生成AIも注意しないとこういうコード書いてきます。</p>
<figure class="highlight tsx"><figcaption><span>昔ながらの状態を全部自分で管理</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">TraditionalButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [name, setName] = <span class="title function_">useState</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">  <span class="keyword">const</span> [isLoading, setIsLoading] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="keyword">const</span> [error, setError] = useState&lt;<span class="built_in">string</span> | <span class="literal">null</span>&gt;(<span class="literal">null</span>);</span><br><span class="line">  <span class="keyword">const</span> [resultMessage, setResultMessage] = useState&lt;<span class="built_in">string</span> | <span class="literal">null</span>&gt;(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleSubmit</span> = <span class="keyword">async</span> (<span class="params"><span class="attr">e</span>: <span class="title class_">React</span>.<span class="title class_">FormEvent</span>&lt;<span class="title class_">HTMLFormElement</span>&gt;</span>) =&gt; &#123;</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">    <span class="title function_">setIsLoading</span>(<span class="literal">true</span>);</span><br><span class="line">    <span class="title function_">setError</span>(<span class="literal">null</span>);</span><br><span class="line">    <span class="title function_">setResultMessage</span>(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/api/update-user&#x27;</span>, &#123;</span><br><span class="line">        <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">        <span class="attr">body</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; name &#125;),</span><br><span class="line">      &#125;);</span><br><span class="line">      <span class="keyword">if</span> (!response.<span class="property">ok</span>) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;サーバーでエラーが発生しました&#x27;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">const</span> data = <span class="keyword">await</span> response.<span class="title function_">json</span>();</span><br><span class="line">      <span class="title function_">setResultMessage</span>(data.<span class="property">message</span> || <span class="string">&#x27;更新が完了しました&#x27;</span>);</span><br><span class="line">      <span class="title function_">setName</span>(<span class="string">&#x27;&#x27;</span>);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (<span class="attr">err</span>: <span class="built_in">any</span>) &#123;</span><br><span class="line">      <span class="title function_">setError</span>(err.<span class="property">message</span> || <span class="string">&#x27;通信エラーが発生しました&#x27;</span>);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="title function_">setIsLoading</span>(<span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">disabled</span>=<span class="string">&#123;isLoading&#125;</span> <span class="attr">onClick</span>=<span class="string">&#123;handleSubmit&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;isLoading ? &#x27;送信中...&#x27; : &#x27;送信する&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>useActionState</code>を使うとLoading状態管理は減らせます。ついでにエラー管理も<code>&lt;ErrorBoundary&gt;</code>に任せるようにしましょう。このサンプルでは省略していますが、react-error-boundaryのFallbackComponent propを使うとエラー情報を取り出して表示したりできます（他にも色々書き方があります）。</p>
<figure class="highlight tsx"><figcaption><span>React 19の書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&#x27;use client&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useActionState, startTransition &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">ErrorBoundary</span>, <span class="title class_">FallbackProps</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;react-error-boundary&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初期状態</span></span><br><span class="line"><span class="keyword">const</span> initialState = &#123;</span><br><span class="line">  <span class="attr">resultMessage</span>: <span class="literal">null</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// クライアント側で実行するアクション関数</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">updateProfileClient</span>(<span class="params">prevState, formData</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> name = formData.<span class="title function_">get</span>(<span class="string">&#x27;name&#x27;</span>) <span class="keyword">as</span> <span class="built_in">string</span>;</span><br><span class="line">  <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&#x27;/api/update-user&#x27;</span>, &#123;</span><br><span class="line">    <span class="attr">method</span>: <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;Content-Type&#x27;</span>: <span class="string">&#x27;application/json&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">body</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(&#123; name &#125;),</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="keyword">if</span> (!response.<span class="property">ok</span>) &#123;</span><br><span class="line">    <span class="comment">// 💡 throw されたエラーは React 19 が自動で捉え、 ErrorBoundary に送られる</span></span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">&#x27;サーバー通信でエラーが発生しました (Status: &#x27;</span> + response.<span class="property">status</span> + <span class="string">&#x27;)&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  <span class="keyword">const</span> data = <span class="keyword">await</span> response.<span class="title function_">json</span>();</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">resultMessage</span>: data.<span class="property">message</span> || <span class="string">&#x27;更新が完了しました&#x27;</span>,</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">ModernButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// updateProfileClientをラップしたformAction。内部で管理する状態、ロード状態取得</span></span><br><span class="line">  <span class="keyword">const</span> [state, action, isPending] = <span class="title function_">useActionState</span>(</span><br><span class="line">    updateProfileClient, </span><br><span class="line">    initialState</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="comment">// transitionでラップする</span></span><br><span class="line">  onClick = <span class="title function_">startTransition</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">    <span class="title function_">action</span>();</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="comment">// エラーハンドリング</span></span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ErrorBoundary</span> <span class="attr">fallbackRender</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>エラー発生<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">button</span> <span class="attr">onCick</span>=<span class="string">&#123;onClick&#125;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        &#123;isPending ? &#x27;送信中...&#x27; : &#x27;送信する&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ErrorBoundary</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><code>useActionState()</code>でラップしたのを再度<code>startTransition()</code>でラップするのはちょっと無駄っぽいですよね？次のフォーム送信の場合は<code>startTransition()</code>不要なのでその感覚は薄れますが、もし<code>useAcitonState()</code>の状態管理が不要ならもっと短く書けます。<code>useTransition()</code>側で<code>isPending</code>フラグが取れるので、この状態管理のためだけに<code>useActionState()</code>を書く必要はありません。</p>
<figure class="highlight ts"><figcaption><span>送信結果の管理不要</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">PostOnlyButton</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// このstartTransitionと対応したisPendingが同時に取れる</span></span><br><span class="line">  <span class="keyword">const</span> [isPending, startTransition] = <span class="title function_">useTransition</span>()</span><br><span class="line"></span><br><span class="line">  <span class="keyword">function</span> <span class="title function_">onClick</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&quot;/api/send/request&quot;</span>)</span><br><span class="line">      :</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;onClick&#125;</span> <span class="attr">diabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      &#123;isPending ? &#x27;送信中...&#x27; : &#x27;送信する&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="フォーム送信">フォーム送信</h3><p>フォームの場合は<code>startTransition()</code>は不要です。フォームのaction propsに直接入れます。 useActionStateでラップするのがクライアントの非同期関数ではなく、サーバー関数だったら、ハイドレーションが終わってなくても起動するらしい。</p>
<figure class="highlight ts"><figcaption><span>フォームならuseActionStateはそのままactionに入れられる</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">ModernForm</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="comment">// updateProfileClientをラップしたformAction。内部で管理する状態、ロード状態取得</span></span><br><span class="line">  <span class="keyword">const</span> [state, formAction, isPending] = <span class="title function_">useActionState</span>(</span><br><span class="line">    updateProfileClient, </span><br><span class="line">    initialState</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="comment">// エラーハンドリング</span></span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">ErrorBoundary</span> <span class="attr">fallbackRender</span>=<span class="string">&#123;</span>&lt;<span class="attr">div</span>&gt;</span>エラー発生<span class="tag">&lt;/<span class="name">div</span>&gt;</span>&#125;&gt;</span></span><br><span class="line"><span class="language-xml">      // onSubmit ではなく action に formAction を渡す</span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;formAction&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        :</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;&#125;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">          &#123;isPending ? &#x27;送信中...&#x27; : &#x27;送信する&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">ErrorBoundary</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>なお、Reactではなく、react-domパッケージ側の機能になりますが、こちらもアクションのラップが不要で、単に「ロード中」などの状態などが取得したい場合は <code>userFormStatus()</code> というフックがあります。ここでは<code>pending</code>を使っていますが、<code>data</code>とか<code>method</code>とか<code>action</code>の関数も取得できます。</p>
<figure class="highlight ts"><figcaption><span>状態の管理が不要ならuseFormStatus()</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; useFormStatus &#125; <span class="keyword">from</span> <span class="string">&quot;react-dom&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">Submit</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pending &#125; = <span class="title function_">useFormStatus</span>();</span><br><span class="line">  <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">disabled</span>=<span class="string">&#123;pending&#125;</span> /&gt;</span></span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">ModernForm</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; pending, data, method, action &#125; = <span class="title function_">useFormStatus</span>();</span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">form</span> <span class="attr">action</span>=<span class="string">&#123;&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">  )</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="通信時の先行UI更新">通信時の先行UI更新</h3><p>通信完了を待たずに先に値を更新しておいてユーザーの体感を良くすることがよくあります。</p>
<figure class="highlight ts"><figcaption><span>以前の方法。ロールバック用の値を持っておく</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">React</span>, &#123; useState, useRef &#125; <span class="keyword">from</span> <span class="string">&#x27;react&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">LegacyLikeCounter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">10</span>);</span><br><span class="line">  <span class="keyword">const</span> [isPending, setIsPending] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> previousCountRef = useRef&lt;<span class="built_in">number</span>&gt;(count);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleLike</span> = <span class="keyword">async</span> (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (isPending) <span class="keyword">return</span>; <span class="comment">// 連打防止</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// サーバー通信を待たずに、バックアップをとりつつ先にStateを更新する</span></span><br><span class="line">    previousCountRef.<span class="property">current</span> = count;</span><br><span class="line">    <span class="title function_">setCount</span>(<span class="function">(<span class="params">prev</span>) =&gt;</span> prev + <span class="number">1</span>);</span><br><span class="line">    <span class="title function_">setIsPending</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="title function_">updateLikeApi</span>();</span><br><span class="line">      <span class="comment">// 成功したら何もしない（すでに画面は +1 されているため）</span></span><br><span class="line">    &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">      <span class="comment">// 失敗した場合は、useRef から古い値を読み出して手動で復元する！</span></span><br><span class="line">      <span class="title function_">alert</span>(<span class="string">&#x27;失敗したため元の数値に戻します&#x27;</span>);</span><br><span class="line">      <span class="title function_">setCount</span>(previousCountRef.<span class="property">current</span>);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">      <span class="title function_">setIsPending</span>(<span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleLike&#125;</span> <span class="attr">disabled</span>=<span class="string">&#123;isPending&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ❤️ &#123;count&#125; &#123;isPending &amp;&amp; &#x27;(更新中...)&#x27;&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>何度か出てきているトランジションとセットで使う<code>useOptimistic</code>というものがあります。トランザクション中では先に<code>useOptimistic</code>で値を更新しておきます。画面は先に値が新しくなります。その後、トランジションを抜けるまでに元となる要素（これも<code>useState</code>のはず）のsetを呼ぶとOKですが、よばないと、<code>useOptimistic</code>は破棄されて元の値が表示されます。</p>
<figure class="highlight ts"><figcaption><span>楽観的な値を使う</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">LikeCounter</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> [count, setCount] = <span class="title function_">useState</span>(<span class="number">10</span>); <span class="comment">// 本番State</span></span><br><span class="line">  <span class="keyword">const</span> [, startTransition] = <span class="title function_">useTransition</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// useOptimistic(本番State, 楽観更新用の計算式)</span></span><br><span class="line">  <span class="keyword">const</span> [optimisticCount, setOptimisticCount] = <span class="title function_">useOptimistic</span>(</span><br><span class="line">    count,</span><br><span class="line">    <span class="function">(<span class="params">current</span>) =&gt;</span> current + <span class="number">1</span></span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">handleLike</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">startTransition</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="comment">// 先に画面の数字を +1 する（仮）</span></span><br><span class="line">      <span class="title function_">setOptimisticCount</span>(count + <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">updateLikeApi</span>();</span><br><span class="line">        <span class="comment">// 成功した時だけ本番Stateを更新</span></span><br><span class="line">        <span class="title function_">setCount</span>(<span class="function">(<span class="params">prev</span>) =&gt;</span> prev + <span class="number">1</span>);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">        <span class="comment">// 失敗した時は setCount を呼ばない！</span></span><br><span class="line">        <span class="comment">// ── これだけで自動的に元の数値（10）に戻ります</span></span><br><span class="line">        <span class="title function_">alert</span>(<span class="string">&#x27;失敗したため元の数値に戻します&#x27;</span>);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;handleLike&#125;</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">      ❤️ &#123;optimisticCount&#125;</span></span><br><span class="line"><span class="language-xml">    <span class="tag">&lt;/<span class="name">button</span>&gt;</span></span></span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Next-jsのクエリーパラメータを使った検索フォーム通信">Next.jsのクエリーパラメータを使った検索フォーム通信</h3><p>先ほどのフォームのsubmitではどちらかというと、フロントエンド主体でフローをコントロールし、更新結果の管理もしていました。サーバーコンポーネント主体かつ検索のようなURLのクエリーに結果を書くフォームだと動きが大幅に変わってきます。</p>
<p>フロントのタスクとしてはURLのクエリーパラメータだけがタスクとなります。その後、Next.jsがサーバーのページのコードを再実行が自動で行われ、その中でDB問い合わせもしくはバックエンドAPI呼び出しをして結果のページを作ると画面が更新される、という流れとなります。</p>
<p>生成AIに何も指示せずに作らせると、すべてクライアントコンポーネント上の処理とされてしまいますがReact Server Componentの構成を生かすフォームはこのようになるはずです。ここはバリデーションとかは不要なので<code>useActionState()</code>は使ってませんが、もちろん組み合わせも可能です。</p>
<img fetchpriority="high" src="/images/2026/20260727a/image.png" alt="image.png" width="1200" height="585">

<figure class="highlight ts"><figcaption><span>SearchPanel(クライアントコンポーネント)</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;use client&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> &#123; usePathname, useRouter &#125; <span class="keyword">from</span> <span class="string">&quot;next/navigation&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; useState &#125; <span class="keyword">from</span> <span class="string">&quot;react&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">SearchPanelProps</span> = &#123;</span><br><span class="line">	<span class="attr">initialQuery</span>: <span class="built_in">string</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">SearchPanel</span>(<span class="params">&#123; initialQuery &#125;: <span class="title class_">SearchPanelProps</span></span>) &#123;</span><br><span class="line">	<span class="keyword">const</span> router = <span class="title function_">useRouter</span>();</span><br><span class="line">	<span class="keyword">const</span> pathname = <span class="title function_">usePathname</span>();</span><br><span class="line">	<span class="keyword">const</span> [query, setQuery] = <span class="title function_">useState</span>(initialQuery);</span><br><span class="line"></span><br><span class="line">	<span class="keyword">const</span> <span class="title function_">updateUrl</span> = (<span class="params"><span class="attr">nextQuery</span>: <span class="built_in">string</span></span>) =&gt; &#123;</span><br><span class="line">		<span class="keyword">const</span> trimmedQuery = nextQuery.<span class="title function_">trim</span>();</span><br><span class="line">		<span class="keyword">const</span> nextUrl = trimmedQuery</span><br><span class="line">			? <span class="string">`<span class="subst">$&#123;pathname&#125;</span>?<span class="subst">$&#123;<span class="keyword">new</span> URLSearchParams(&#123; q: trimmedQuery &#125;).toString()&#125;</span>`</span></span><br><span class="line">			: pathname;</span><br><span class="line"></span><br><span class="line">		router.<span class="title function_">replace</span>(nextUrl, &#123; <span class="attr">scroll</span>: <span class="literal">false</span> &#125;);</span><br><span class="line">	&#125;;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> (</span><br><span class="line">		<span class="language-xml"><span class="tag">&lt;<span class="name">form</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">			<span class="attr">onSubmit</span>=<span class="string">&#123;(event)</span> =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-xml">				event.preventDefault();</span></span><br><span class="line"><span class="language-xml">				updateUrl(query);</span></span><br><span class="line"><span class="language-xml">			&#125;&#125;</span></span><br><span class="line"><span class="language-xml">		&gt;</span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">span</span>&gt;</span>検索キーワード<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">input</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">					<span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">					<span class="attr">value</span>=<span class="string">&#123;query&#125;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">					<span class="attr">onChange</span>=<span class="string">&#123;(event)</span> =&gt;</span> setQuery(event.target.value)&#125;</span></span><br><span class="line"><span class="language-xml">					placeholder=&quot;Next.js などを入力&quot;</span></span><br><span class="line"><span class="language-xml">				/&gt;</span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">		<span class="tag">&lt;/<span class="name">form</span>&gt;</span></span></span><br><span class="line">	);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight jsx"><figcaption><span>SearchPage（サーバーコンポーネント）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">SearchPanel</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@/components/SearchPanel&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> type &#123; <span class="title class_">SearchItem</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@/lib/search&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; getAppUrl &#125; <span class="keyword">from</span> <span class="string">&quot;../../lib/app-url&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ページの引数にsearchParmasを設定</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">SearchPage</span>(<span class="params">&#123; searchParams &#125;</span>) &#123;</span><br><span class="line">	<span class="keyword">const</span> &#123; q &#125; = <span class="keyword">await</span> searchParams;</span><br><span class="line">	<span class="keyword">const</span> query = q?.<span class="title function_">trim</span>() ?? <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;SearchPage searchParams:&quot;</span>, query);</span><br><span class="line">	<span class="keyword">const</span> apiSearchParams = <span class="keyword">new</span> <span class="title class_">URLSearchParams</span>();</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> (query) &#123;</span><br><span class="line">		apiSearchParams.<span class="title function_">set</span>(<span class="string">&quot;q&quot;</span>, query);</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="keyword">new</span> <span class="title function_">URL</span>(<span class="string">`/api/search?<span class="subst">$&#123;apiSearchParams.toString()&#125;</span>`</span>, <span class="title function_">getAppUrl</span>()), &#123;</span><br><span class="line">		<span class="attr">cache</span>: <span class="string">&quot;no-store&quot;</span>,</span><br><span class="line">	&#125;);</span><br><span class="line">	<span class="keyword">const</span> results = (<span class="keyword">await</span> response.<span class="title function_">json</span>()) <span class="keyword">as</span> <span class="title class_">SearchItem</span>[];</span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> (</span><br><span class="line">		<span class="language-xml"><span class="tag">&lt;<span class="name">main</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;<span class="name">section</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">p</span>&gt;</span>Search<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">h1</span>&gt;</span>本を検索する<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">p</span>&gt;</span>入力すると URL の q パラメータが更新され、結果も連動して表示されます<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">SearchPanel</span> <span class="attr">key</span>=<span class="string">&#123;query&#125;</span> <span class="attr">initialQuery</span>=<span class="string">&#123;query&#125;</span> /&gt;</span></span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;/<span class="name">section</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;<span class="name">section</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">					<span class="tag">&lt;<span class="name">h2</span>&gt;</span>&#123;query ? `「$&#123;query&#125;」の検索結果` : &quot;検索結果&quot;&#125;<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">					<span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;results.length&#125; 件<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">					&#123;results.map((item) =&gt; (</span></span><br><span class="line"><span class="language-xml">						<span class="tag">&lt;<span class="name">li</span> <span class="attr">key</span>=<span class="string">&#123;item.id&#125;</span>&gt;</span>&#123;item.title&#125;<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">					))&#125;</span></span><br><span class="line"><span class="language-xml">				<span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">					&#123;results.length === 0 &amp;&amp; (</span></span><br><span class="line"><span class="language-xml">					<span class="tag">&lt;<span class="name">p</span>&gt;</span>該当する本がありません。<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">				)&#125;</span></span><br><span class="line"><span class="language-xml">			<span class="tag">&lt;/<span class="name">section</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">		<span class="tag">&lt;/<span class="name">main</span>&gt;</span></span></span><br><span class="line">	);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="アーキテクチャの変化">アーキテクチャの変化</h2><p>これまでもPure React(Client Side Rendering: CSR)、サーバーサイドレンダリング(ISGなども含む)などを提供するNext.jsのPages Routerがありました。これにReactサーバーコンポーネント(RSC)主体のApp Routerが加わりました。</p>
<p>すでに上に登場していますが、RSCはその名の通り、サーバーで動作し仮想DOM的なJSONをフロントに送って表示します。JSロジックの代わりに、サーバーで作ったRSCペイロードを送って差分更新することでシームレスな更新と、JSロジックの削減を両立しています。旧来のReactには様々な弱点がありました。それを解決するためにRSCが導入されました:</p>
<ul>
<li>Pure ReactやPages Routerではページの全てを構成できるロジックを配る必要があるためJSサイズが大きかった。サーバー専用コードやライブラリを減らす</li>
<li>DBやファイルシステムなどのサーバー資源へアクセスするコードをシームレスに書ける</li>
<li>データ取得コードと表示を近くにする</li>
<li>Server&#x2F;Client Componentを1つのReactツリーで統合</li>
</ul>
<p>その代わり、状態を持てなかったり、イベントハンドラが書けない、ブラウザAPIが利用できないという制約があります。</p>
<p>デフォルトはRSCで、”use client”を書くとクライアントコンポーネントになります。なお、”use server”は全く別のディレクティブでサーバー関数用で非対称なのは混乱の元ですが、サーバーコンポーネント指定のためのものではありません。クライアントコンポーネントはそのロジックがすべてブラウザ上に転送されて動作します。なお、それでもNext.jsを使うと初回のレンダリングはサーバーで行われます。</p>
<p>Pure React(CSR)、Pages Router、App Routerの動きの違いを整理しました。</p>
<h3 id="Pure-React-Client-Side-Rendering">Pure React(Client Side Rendering)</h3><p>PureだったりNext.jsのstatic export、あるいはクライアントコンポーネント利用がこれにあたります。レンダリングの場合はそれを描画するReactのロジックそのものを送ります。複雑なライブラリなどを利用するとJavaScriptのファイルサイズも多くなりがちですし、それを読み込んでロジックが実行されて初めて表示が開始されるので最初の表示までの時間(FCP)がやや遅くなるといわれています。</p>
<p>なお、Next.jsのstatic exportは、App Routerでも使えて、サーバーコンポーネントもビルド時にHTMLを作成するというのはできます。</p>
<img src="/images/2026/20260727a/image_2.png" alt="image.png" width="767" height="375" loading="lazy">

<h3 id="Next-jsのPages-Router">Next.jsのPages Router</h3><p>Reactの弱点（当時）だったSEO対策の改善なども考慮されて作られたのがSSRでした。画面全体をサーバー側で一度作ってそれを送信します。その後、ハイドレーションを行ってイベントハンドラなどを注入します。最初にJSを全部読み込み、初回表示後は前述のクライアントレンダリングになります。</p>
<img src="/images/2026/20260727a/image_3.png" alt="image.png" width="759" height="362" loading="lazy">

<p>なお、事前にページ全体のコンテンツを作っておく、SSGや、データが追加されたらそのデータを使ったページを作っておくISGというのもありますが、これはサーバー側のレンダリングをユーザーリクエストの前に作っておくということです。この時代の仕組みは「ページ全体を丸ごとキャッシュする」方向での最適化を全力で行なっています。</p>
<h3 id="Next-jsのApp-Router">Next.jsのApp Router</h3><p>RSCが動くようになったのがこちらです。サーバー主体で動きますよ、となると「PHPで良いのでは？」とか思われるかもしれませんが、サーバー側で動くコードとクライアントで動くコードをモザイクのように組み合わせて最適に動くシステムが構築できる基盤というのは他になかなかない特徴です(.netのblazeはそれに近いと聞いたことがあります)。また、クライアントコンポーネントも初回はサーバーでレンダリングされてからPages Routerと同じくハイドレーションして動きます。</p>
<img src="/images/2026/20260727a/image_4.png" alt="image.png" width="754" height="354" loading="lazy">

<p>なお、変更があった部分のみの更新となっていますが、実質layout.tsxの部分は再描画しないが、page.tsxの範囲は毎回全部転送となります。そのため、1ページの中にメインコンテンツ(やリスト)が1つみたいな構成であれば無駄な転送が少ないと言えます。一方、メインのチャット欄・ちょくちょく最新情報で更新されるチャンネル一覧・DMリスト、みたいなメインコンテンツが多いものは1つの更新で他のコンテンツもサーバー側が再描画になり、すべてがRSCペイロードに乗っかってくる、みたいな形になって美味しくなさそうです。なるべくメイン以外はクライアントコンポーネントに逃がすとかが必要かと思います。あと無限スクロールを実装するのはServer ComponentとClient Component両方を組み合わせるなど従来通りのClient Componentだけでやるよりも難易度があがります。</p>
<p>なお、page.tsxを全部転送するかと言って毎回同じCPUコストがかかるかというとそうではなく、コンポーネント単位のキャッシュなどがあるため、RSCペイロードを組み立てるコストは削減可能です。ただし、その全量をクライアントには送ることになります。</p>
<p>Pages Routerはページ全体ごとのキャッシュがメインでしたが、App Routerの場合、page.tsxの外側のlayout.tsx、あるいはコンポーネント単位、API&#x2F;DBアクセス単位など細かくキャッシュさせる機能が提供されています。</p>
<h3 id="それぞれのモードやコンポーネントの動作モード">それぞれのモードやコンポーネントの動作モード</h3><p>static exportは除いていますが、なぜ今までのReactと違って色々考えることがあるのかというと、今までのNext.jsは、CSRでできることは基本全てできていて、それをサーバー側でもやる、という思想でした。</p>
<p>ただ、Server Componentは、サーバーのみで動作します。なので今までできていたことを「効率化のために捨てる」とことが必要になります。それにより、今までできていなかった最適化の殻を破ることができます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">方式</th>
<th align="left">HTML生成</th>
<th align="left">ハイドレーション</th>
<th align="left">サーバーで動作</th>
<th align="left">ブラウザで動作</th>
</tr>
</thead>
<tbody><tr>
<td align="left">CSR</td>
<td align="left">ブラウザ</td>
<td align="left">なし(初回から必要)</td>
<td align="left"></td>
<td align="left">○</td>
</tr>
<tr>
<td align="left">SSG</td>
<td align="left">ビルド時</td>
<td align="left">あり</td>
<td align="left">○</td>
<td align="left">○</td>
</tr>
<tr>
<td align="left">ISR</td>
<td align="left">ビルド時＋必要時</td>
<td align="left">あり</td>
<td align="left">○</td>
<td align="left">○</td>
</tr>
<tr>
<td align="left">SSR(Pages Router)</td>
<td align="left">リクエスト時</td>
<td align="left">あり</td>
<td align="left">○</td>
<td align="left">○</td>
</tr>
<tr>
<td align="left">App Router (Server Component)</td>
<td align="left">ビルドorリクエスト時</td>
<td align="left">なし</td>
<td align="left">○</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">App Router (Client Component)</td>
<td align="left">ビルドorリクエスト時</td>
<td align="left">あり</td>
<td align="left">○</td>
<td align="left">○</td>
</tr>
</tbody></table></div>
<p>App Routerのビルド時orリクエスト時というのは、パラメータがない、動的要素がないコンポーネントは静的に解決されちゃうという意味です。そういうところも今までできない高速化を図っています。</p>
<h2 id="今のところ考えている基本的な方針">今のところ考えている基本的な方針</h2><p>さて、ここまで見てきたところで、どのような方針でReact&#x2F;Next.jsを書いていけばいいかという場合のチーム方針はこんな感じでやろうかと思っています。このあたりは僕がこう考えているだけなので、人によっては別の方針になるかと思います。</p>
<ol>
<li>なるべくサーバーコンポーネントにする<ol>
<li>動的なUIだからクライアントコンポーネント、とあきらめずに、 daisyUIでJSレスで実現されているものはサーバーコンポーネントで実現できると信じる</li>
<li>↑で紹介した検索パネルのようにsearchParamsに状態を移せるものかどうか検討する</li>
</ol>
</li>
<li>非同期通信のawaitを調整する。時間がかかかる重い通信・データ取得などはストリーミングも検討する</li>
<li>キャッシュできそうな部分を見つけて、コンポーネントキャッシュにしていく</li>
<li>クライアントコンポーネントでも状態を減らす<ol start="4">
<li>isLoading&#x2F;isPending系を撲滅して回る</li>
<li>サーバーアクションを活用できないか検討する</li>
<li>フォーム回りのロジックを整理する</li>
</ol>
</li>
<li>クライアントコンポーネントの遅そうなハンドラをトランジションにしていく<ol start="4">
<li>一時表示系は<code>useOptimistic</code>を使っていく</li>
</ol>
</li>
</ol>
<h2 id="まとめ">まとめ</h2><p>最初に、Reactの最近の変更のモチベーションとして2つの要素がある、ということを説明しました。それに当てはめるとこんな感じに分類できるのかな、と思います。順番はこのエントリーで紹介した順です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left"></th>
<th align="center">応答性を上げる</th>
<th align="center">状態を減らす</th>
</tr>
</thead>
<tbody><tr>
<td align="left">トランジション</td>
<td align="center">〇</td>
<td align="center"></td>
</tr>
<tr>
<td align="left"><code>useActionState</code></td>
<td align="center"></td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left">サーバー関数</td>
<td align="center"></td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left">ストリーミング</td>
<td align="center">〇</td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left"><code>&lt;Suspense&gt;</code>&#x2F;<code>&lt;ErrorBoundary&gt;</code></td>
<td align="center"></td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left"><code>useEffectEvent</code></td>
<td align="center">〇</td>
<td align="center"></td>
</tr>
<tr>
<td align="left"><code>useDefferedValue</code></td>
<td align="center">〇</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">サーバーコンポーネント</td>
<td align="center">〇</td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left"><code>useTranstion</code></td>
<td align="center">〇</td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left"><code>useActionState</code></td>
<td align="center"></td>
<td align="center">〇</td>
</tr>
<tr>
<td align="left"><code>useOptimistic</code></td>
<td align="center"></td>
<td align="center">〇</td>
</tr>
</tbody></table></div>
<p>hook出始めの頃のReactのシンプルな仕組みを組み合わせて「変更をコントロールして結果が正しく表示される」というところから、Reactに任せるところは任せて状態を減らし、なおかつReactの描画をコントロールするという感じで、もう一段ギアが上がった感じがあります。非同期通信が扱いやすくなったのもうれしいですね。</p>
<p>難しくなったのか？というと、そんなことはなくてコントロールできる範囲が広がり、設計の方針がシンプルにわかりやすくなった、という感じかに思います。サーバーサイドレンダリングでどうせ作るのであればそれをそのまま活用しようとか、状態管理ピタゴラスイッチは減らしていこう、という公式のメッセージだな、というのを感じました。状態が減ればその分複雑な状態を扱う状態管理ライブラリが必要になることも減ります。通信もサーバーコンポーネントに寄せれば、ワンショットでコンポーネント表示でデータ取得するようなケースはだいぶ楽に書けます。React 18&#x2F;19で導入された数々をきちんと理解すると、「うわー、前のやり方は無駄が多かったんだ」という気持ちになり少なくとも、以前のReactの書き方に戻ろうとは思えなくなりました。</p>
<p>19で追加された<code>useSyncExternalStore</code>とかexperimentalな<code>&lt;ViewTransition&gt;</code>みたいな面白そうな機能もあったりしますが、よく使いそうなところでまとめてみました。</p>
]]></content>
    <summary type="html">一番Next.jsとかReactを書いていたのはhookが出たあたりで関数コンポーネントになった近辺でだいぶ前ですが、Reactのドキュメントを一通り見ていたところ、かなり書き方が変わっていそうなので、新規プロジェクトでがっつりイマドキの書き方に寄せてみようと思って学びなおしたり、検証したのでその備忘というか整理です。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Next.js" scheme="https://future-architect.github.io/tags/Next-js/"/>
    <category term="React" scheme="https://future-architect.github.io/tags/React/"/>
    <category term="サーバーコンポーネント" scheme="https://future-architect.github.io/tags/%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>なぜ日本語入力と相性の悪いWebサイトができてしまうのか</title>
    <link href="https://future-architect.github.io/articles/20260120a/"/>
    <id>https://future-architect.github.io/articles/20260120a/</id>
    <published>2026-01-19T15:00:00.000Z</published>
    <updated>2026-01-19T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>海外製のチャットサービスとかでIMEを確定しようとしたら勝手に送信されてしまって困った！ という経験をしたかは多いでしょう。日本語入力はIME(Input Method Editor)を通じて行われますが、イベントのハンドリングを間違うとこのような挙動になってしまいます。ウェブフロントエンドのIMEにまつわる2つのトピックを紹介します。</p>
<ul>
<li>Enterで勝手に送信しちゃう</li>
<li>IMEの最初の文字が取れない</li>
</ul>
<h2 id="Enterで勝手に送信されちゃう">Enterで勝手に送信されちゃう</h2><p><code>&lt;input&gt;</code>でテキスト入力を作り、チャットを作りたいとします。わざわざボタンをクリックしないと送信できないのは不便なのでEnterで送信できるようにしようとします。changeイベントやblurではフォーカスを一度外さないと確定にならないため、keydownイベントでEnterを拾って送信したいとします。こんな感じでしょうか？もう令和8年なのでReact Compilerを想定してシンプルに書いています。</p>
<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> [text, setText] = <span class="title function_">useState</span>(<span class="string">&quot;&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkEnter</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (e.<span class="property">key</span> === <span class="string">&quot;Enter&quot;</span>) &#123;</span><br><span class="line">        <span class="title function_">sendMessage</span>(text); <span class="comment">// 送信！</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&lt;input</span><br><span class="line">    <span class="keyword">type</span>=<span class="string">&quot;text&quot;</span></span><br><span class="line">    value=&#123;text&#125;</span><br><span class="line">    onChange=&#123;<span class="function">(<span class="params">e</span>) =&gt;</span> <span class="title function_">setText</span>(e.<span class="property">target</span>.<span class="property">value</span>)&#125;</span><br><span class="line">    onKeyDown=&#123;checkEnter&#125;</span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure>

<p>これを実際に動かしてみると、日本語確定のEnterで送信されてしまいました。</p>
<p>「はい(変換確定)」と入力し、送信用にEnterを押すと<input>には次のようなイベントが流れてきます。</p>
<ol>
<li><code>[h]</code>押下<ul>
<li>input: value&#x3D;h</li>
<li>keydown: key&#x3D;h</li>
</ul>
</li>
<li><code>[a]</code>押下<ul>
<li>input: value&#x3D;は</li>
<li>keydown: key&#x3D;a</li>
</ul>
</li>
<li><code>[i]</code>押下<ul>
<li>input: value&#x3D;はい</li>
<li>keydown: key&#x3D;i</li>
</ul>
</li>
<li><code>[Enter]</code>押下で確定<ul>
<li>input: value&#x3D;(empty)</li>
<li>input: value&#x3D;はい</li>
<li>keydown: key&#x3D;Enter →ここで送信されちゃう！</li>
</ul>
</li>
<li><code>[Enter]</code>押下で送信したかった<ul>
<li>keydown: key&#x3D;Enter</li>
<li>change: value&#x3D;はい</li>
</ul>
</li>
</ol>
<p>これを見れば、keydownイベントでとるのではなく、changeイベントで送信すれば一見良さそうです。しかしそれではうまくいきません。Slackとかもそうですが、だいたい「送信ボタン」がありつつも、ショートカットとして「Enter」送信を許容しています。</p>
<img fetchpriority="high" src="/images/2026/20260120a/スクリーンショット_2026-01-14_16.05.15.png" alt="スクリーンショット_2026-01-14_16.05.15.png" width="492" height="135">

<p>送信確定前にちょっと別のチャンネルを見て確認しておこうと別の要素にフォーカスしたりすると次のようなイベント発生されます。</p>
<ol start="6">
<li><code>[Enter]</code>を押さずに別の要素をフォーカス<ul>
<li>change: value&#x3D;はい</li>
<li>blur: value&#x3D;Enter</li>
</ul>
</li>
</ol>
<p>このchangeではまだ送りたくはないですが、送信するかどうかはこのあとにblurが起きるかどうかで判定が必要です。が・・・なんて後の時系列で起きるイベントの判定ロジックなんて実装したくはないですよね。</p>
<p>IMEに関するイベントや情報がHTMLにはあります。IMEを使った変換の開始と確定後にはcompositionstartとcompositionendイベントが発火します。また、inputイベントやkeydownイベントには<code>isComnposing</code>という属性が付きます。Reactであれば、<code>e.nativeEvent.isComposing</code>といった感じで情報が取れます。この2つも足すと以下のようなイベントが流れてきます。</p>
<ol>
<li><code>[h]</code>押下<ul>
<li>compositionstart</li>
<li>input: value&#x3D;h isComposing&#x3D;true</li>
<li>keydown: key&#x3D;h isComposing&#x3D;true</li>
</ul>
</li>
<li><code>[a]</code>押下<ul>
<li>input: value&#x3D;は isComposing&#x3D;true</li>
<li>keydown: key&#x3D;a isComposing&#x3D;true</li>
</ul>
</li>
<li><code>[i]</code>押下<ul>
<li>input: value&#x3D;はい isComposing&#x3D;true</li>
<li>keydown: key&#x3D;i isComposing&#x3D;true</li>
</ul>
</li>
<li><code>[Enter]</code>押下で確定<ul>
<li>input: value&#x3D;(empty) isComposing&#x3D;true</li>
<li>input: value&#x3D;はい isComposing&#x3D;true</li>
<li>compositionend: value&#x3D;はい</li>
<li>keydown: key&#x3D;Enter isComposing&#x3D;false &#x2F;&#x2F; ここに注目</li>
</ul>
</li>
<li><code>[Enter]</code>押下で送信<ul>
<li>keydown: key&#x3D;Enter isComposing&#x3D;false</li>
<li>change: value&#x3D;はい</li>
</ul>
</li>
</ol>
<p>残念ながら、最後の確定のEnterではisComposing&#x3D;falseなので、ステートレスに判定はできません。しかし、compositionendからすぐ後にkeydownが発生するので、最後のcompositionendイベントでタイムスタンプを取得し、そこから短時間(20mSとか)以内のEnterは除外するというロジックにすればOKです。</p>
<p><code>isComposing</code>属性関係ないじゃん！ と思われるかもしれませんが、グリッドコントロールとかで確定済み状態でのカーソルやタブでアクティブなセルを移動したい、みたいなケースはあるかと思います。Enter以外のキーであれば、isComposing&#x3D;trueの時のこれらのキーは無視する、というロジックにすればOKです。</p>
<h2 id="キー入力でラベルの変更するが変換前の文字が入ってしまう">キー入力でラベルの変更するが変換前の文字が入ってしまう</h2><p>PowerPointのオブジェクト編集機能のようなものを作りたいとします。マウスクリックなどでフォーカスした要素に対し、キーボード入力をするとそのラベルの変更ができ、確定するとそのラベルが設定されます。よくある機能なのでイメージしやすいですよね？前項の確定部分の話はちょっとここでは除外してonChangeで確定としておきます。こんな感じでしょうか？</p>
<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> divRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">const</span> [editing, setEditing] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="keyword">const</span> [label, setLabel] = <span class="title function_">useState</span>(<span class="string">&quot;ラベル&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">focus</span>(<span class="params"></span>) &#123;</span><br><span class="line">    difRef.<span class="property">current</span>?.<span class="title function_">focus</span>();</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">keydown</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!editing) &#123;</span><br><span class="line">        <span class="title function_">setLabel</span>(e.<span class="property">key</span>);</span><br><span class="line">        <span class="title function_">setEditing</span>(<span class="literal">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">fix</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="title function_">setLabel</span>(e.<span class="property">value</span>);</span><br><span class="line">    <span class="title function_">setEditing</span>(<span class="literal">false</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&lt;div ref=&#123;divRef&#125;</span><br><span class="line">    onClick=&#123;focus&#125;</span><br><span class="line">    onKeyDown=&#123;keydown&#125;</span><br><span class="line">&gt;</span><br><span class="line">    &#123;editing ? <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">value</span>=<span class="string">&#123;label&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;fix&#125;</span> /&gt;</span></span> : label&#125;</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure>

<p>これ、そのまま動かしてみると「あいうえお」と入力すると「aいうえお」となっちゃうんですよね。keydownでは変換前の文字が入ってしまうので。inputイベントにすると、あ行だけはうまくいきますが、子音を入力すると同じ結果になります。変換途中の情報はフォーカスと一緒で、controlled componentとして外から状態を与えられないので、1文字目が変換中、という状態を外から作り出すことはできません。</p>
<p>あと、ここでは毎回リセットになっていますが、カーソル移動で末尾だけ編集したいみたいなものを実現するのは大変です。</p>
<p>代わりに、選択時に非表示のinputタグを作って、全選択状態でフォーカスしておきます。そして何かしらのイベントが発生したらそのinputタグを表示するというやり方にします。IMEが関連するテキスト入力は1つのinputタグが責任を持って最初の文字から全て受ける必要があります。クリックやカーソルでの適切な位置のカーソル移動もinputタグ任せにできるため、最小工数で自然な入力が実現できるでしょう。</p>
<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> inputRef = <span class="title function_">useRef</span>(<span class="literal">null</span>);</span><br><span class="line"><span class="keyword">const</span> [select, setSelect] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="keyword">const</span> [editing, setEditing] = <span class="title function_">useState</span>(<span class="literal">false</span>);</span><br><span class="line"><span class="keyword">const</span> [label, setLabel] = <span class="title function_">useState</span>(<span class="string">&quot;ラベル&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">focus</span>(<span class="params"></span>) &#123;</span><br><span class="line">    inputRef.<span class="property">current</span>?.<span class="title function_">focus</span>();</span><br><span class="line">    inputRef.<span class="property">current</span>?.<span class="title function_">setSelectionRange</span>(label.<span class="property">length</span>, label.<span class="property">length</span>);</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">show</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (!editing) &#123;</span><br><span class="line">        <span class="title function_">setEditing</span>(<span class="literal">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    e.<span class="title function_">preventDefault</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">fix</span>(<span class="params">e</span>) &#123;</span><br><span class="line">    <span class="title function_">setLabel</span>(e.<span class="property">value</span>);</span><br><span class="line">    <span class="title function_">setSelect</span>(<span class="literal">false</span>);</span><br><span class="line">    <span class="title function_">setEditing</span>(<span class="literal">false</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> inputStyle = <span class="string">&quot;&quot;</span>;</span><br><span class="line"><span class="keyword">if</span> (editing) &#123;</span><br><span class="line">   inputStyle = <span class="string">&quot;&quot;</span>;</span><br><span class="line">&#125; <span class="keyword">else</span> <span class="keyword">if</span> (select) &#123;</span><br><span class="line">   inputStyle = <span class="string">&quot;position: absolute; left: -9999px;&quot;</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">   inputStyle = <span class="string">&quot;display: hidden;&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&lt;div ref=&#123;divRef&#125;</span><br><span class="line">    onClick=&#123;focus&#125;</span><br><span class="line">    onKeyDown=&#123;keydown&#125;</span><br><span class="line">&gt;</span><br><span class="line">    <span class="language-xml"><span class="tag">&lt;<span class="name">input</span> <span class="attr">ref</span>=<span class="string">&#123;inputRef&#125;</span> <span class="attr">style</span>=<span class="string">&#123;inputStyle&#125;</span> <span class="attr">value</span>=<span class="string">&#123;label&#125;</span> <span class="attr">onChange</span>=<span class="string">&#123;fix&#125;</span> <span class="attr">onKeyDown</span>=<span class="string">&#123;show&#125;</span> <span class="attr">onClick</span>=<span class="string">&#123;show&#125;</span> /&gt;</span></span></span><br><span class="line">    &#123;isEditing ? <span class="literal">undefined</span> : label&#125;</span><br><span class="line">&lt;/div&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h2 id="まとめ-キーボード入力は使い勝手に直結する">まとめ: キーボード入力は使い勝手に直結する</h2><p>どちらも明確な「送信ボタン」「編集ボタン」を用意すれば用意して、操作する人に対して１アクション余計にしてもらえば良いだけの話ですが、世の中に便利なものがあるなら、それと近づけてほしいと思うのが人情というものです。</p>
<p>試験的なプログラムでその1アクションを減らすのに、生成AIに雑に指示を投げてもなかなか問題を解決してくれなくてちょっと苦戦したのでメモとしてブログにしておきます。</p>
<h2 id="生成AIに指示するなら？">生成AIに指示するなら？</h2><p>シンプルな機能であればWhatを指示することで賢いモデルであればきちんと実装してくれるのですが、過去の経験上、このIME周りのハンドリングは必ずしもうまくいかないですね。Howで実装方法を指定してあげる必要があります。</p>
<p>前者のEnterのハンドリングを指示する場合は…</p>
<blockquote>
<p><code>compositionend</code>のタイムスタンプを取得し、keydownでのEnterの判定で変換直後の物は除外してください</p>
</blockquote>
<p>…といった感じで指示すればうまくいくでしょう。</p>
<p>後者の方はやや難しいですが…</p>
<blockquote>
<p>オブジェクトの状態には選択モード、編集モードがあります。選択モードでは見えないinputタグを作り、これまでのラベルのテキストを入れた上で全選択状態としてください。その後何かしらのキー入力で編集モードになります。編集モードではinputタグが見えるようになります</p>
</blockquote>
<p>…といった感じの指示の必要があるでしょう。前者の確定のEnter除外を組み合わせたい場合は別途追加で指示が必要です。ESCでrevertとかいろいろ機能を入れようとするとそれなりに複雑化しますが、最初のIMEハンドリングがきちんとできていればなんとかなるはずです。</p>
]]></content>
    <summary type="html">海外製のチャットサービスとかでIMEを確定しようとしたら勝手に送信されてしまって困った！という経験をしたかは多いでしょう。日本語入力はIMEを通じて行われますが、イベントのハンドリングを間違うとこのような挙動になってしまいます。ウェブフロントエンドのIMEにまつわる2つのトピックを紹介します。* Enterで勝手に送信しちゃう* IMEの最初の文字が取れない</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="HTML" scheme="https://future-architect.github.io/tags/HTML/"/>
    <category term="IME" scheme="https://future-architect.github.io/tags/IME/"/>
    <category term="Web" scheme="https://future-architect.github.io/tags/Web/"/>
  </entry>
  <entry>
    <title>プロによる本気の攻略本『JavaScript/TypeScript実力強化書』で発表しました</title>
    <link href="https://future-architect.github.io/articles/20251128b/"/>
    <id>https://future-architect.github.io/articles/20251128b/</id>
    <published>2025-11-27T15:00:01.000Z</published>
    <updated>2025-11-27T15:00:01.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2025/20251128b/top.jpg" alt="" width="400" height="565">


<br>

<p>プロによる本気の攻略本『JavaScript&#x2F;TypeScript実力強化書』 - FL#115というForkwellを運営するGroovesさんのイベントで登壇してきました。こちらが発表資料になります。</p>
<iframe src="https://www.slideshare.net/slideshow/embed_code/key/mqdKZqMe9HA0UF" width="95%" height="420" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border: var(--border-1) solid #CCC; border-width:1px; margin-bottom:5px; max-width:100%;" allowfullscreen></iframe><div style="margin-bottom:5px"><strong>javascript-typescript-2-forkwell-library</strong>from <strong>Yoshiki Shibukawa</strong></div>

<br>

<p>元の原稿が2019年でECMAScript 2018を想定していたものだったので、今回の書籍化にあたってはだいぶ現行に手を入れました、というお話でした。</p>
<p>ただ、時間的にその内容を全部紹介する時間はなかったので、更新の紹介はダイジェストにしつつ、2017ぐらいのJavaScript界隈はどんなエコシステムでどんな雰囲気を書いていたのか、という話を中心にしました。「こんなに変わったんだ」「だいぶ進化したな」とか思っていただけたのではないかと思います。おそらく、多くの人はそれなりに新しいトレンドには追従している（関数型志向なフレームワークに巻き込まれる形で）と思っていますが、きっと過去との比較を見ると「知らない間に自分もレベルアップしていた」みたいな気持ちになる人も多いと思います。</p>
<p>他の人の発表も面白かったです。クロージャの仕組みとかは当たり前に思えるけれども、特に仕様をしっかり読まないとなかなか知りえない話だったりするので。ゆるーく、有識者同士で話をするのも楽しかったです。</p>
<p>イベントは本日(11&#x2F;28)も後半の部がありますので、ご興味のある方は登録するとよいと思います。</p>
]]></content>
    <summary type="html">プロによる本気の攻略本『JavaScript/TypeScript実力強化書』 - FL#115というForkwellを運営するGroovesさんのイベントで登壇してきました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="JavaScript" scheme="https://future-architect.github.io/tags/JavaScript/"/>
    <category term="TypeScript" scheme="https://future-architect.github.io/tags/TypeScript/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>Vue Fes Japan 2025にてライトニングトークに登壇しました＆フューチャーアーキテクトがスポンサーをしました</title>
    <link href="https://future-architect.github.io/articles/20251031b/"/>
    <id>https://future-architect.github.io/articles/20251031b/</id>
    <published>2025-10-30T15:00:01.000Z</published>
    <updated>2025-10-30T15:00:01.000Z</updated>
    <author><name>永井優斗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251031b/KIMG20251025_170540455.jpeg" alt="KIMG20251025_170540455.jpeg" width="1200" height="901">

<p>写真は私の登壇の様子を参加していた別の社員に撮っていただいたものです。</p>
<h2 id="はじめに">はじめに</h2><p>FVGの永井優斗です。秋のブログ週間1本目です。</p>
<p>2025年10月25日に開催された「Vue Fes Japan 2025」のライトニングトーク（LT）に登壇いたしました。昨年のプラチナスポンサーセッション（昨年の登壇報告記事） に続き、2年連続での登壇となります。</p>
<p>今回フューチャーアーキテクト株式会社は、今回ゴールドスポンサーおよびルームネイミングライツスポンサーとして協賛しました。ネーミングライツにより、１つの部屋（トラック）は「経営とITをデザインする フューチャーアーキテクトトラック」として会場内でアナウンスされたほか、<code>#vuefes_future</code> のハッシュタグで本トラックの公演内容はX上でポストされました。</p>
<p>『React Nativeならぬ”Vue Native”が実現するかも?  新世代マルチプラットフォーム開発フレームワークの LynxとLynxのVue.js対応を追ってみよう』 というタイトルで、今年3月にOSS化されたばかりのフレームワーク「Lynx」と、そのVue.js対応の動向についてお話ししました。</p>
<p>また、登壇とは別の時間は、私は当日ボランティアスタッフとしても活動しており、ハンズオンの出欠を確認したりしておりました。</p>
<h2 id="CfP-Call-for-Proposal-の採択">CfP(Call for Proposal)の採択</h2><p>今回はスポンサーセッションではなく、公募（いわゆる「CfPを通す」）での登壇です。VueFesでは、CfPのスピーカーの採択は、完全に匿名で応募者の名前を隠したうえで登壇タイトルと詳細をみて判断をしているそうです。その話を聞いて、著名なスピーカーでなくとも、ニッチな話をもっていけば、もしかしたら面白がって採択されるかもな？と思い、ニッチなネタとして3月のLynx公開時からLynxのVue対応のネタを温めていました。</p>
<p>また、私はTSKaigi 2025にも運営スタッフとして参加しています。そのAfter Eventで知り合ったsiminoriさんと、「（上記のような）Lynxネタを温めており、VueFesのLTに応募をしようと思っている」と話したところ、意気投合し、お互いLTでCfPに応募しようと盛り上がりました。まさか、二人ともLT登壇が決まるとは思わず、スピーカーの紹介がWebサイトに載った時には大変驚きました。</p>
<p>来年はTSKaigiの登壇…かな…！</p>
<h2 id="登壇内容と資料">登壇内容と資料</h2><p>当日の登壇資料はこちらです。</p>


<p>LTなので詳細を語ることを省きつつも、Vueでモバイル開発することへの過去のチャレンジを振り返りつつ、3月発表されたLynxに着目していること、Vue対応の経緯と現状について話しました。</p>
<p>また、VueLynxについては海外の方によってまとめられた記事があるため、自分がこれを発表する意味はなんだろうと考えた結果、自分自身がLynxを触った感想も入れ込みました。それを入れようとおもったがゆえに、9月にLynx記事を仕込んだりするわけですが笑</p>
<h2 id="当日の様子と反響">当日の様子と反響</h2><p>昨年に続き、2年連続での登壇、3年連続の参加となりましたが、やはりVue Fes Japanの熱気は格別でした。<br>毎回思うことですが、「Fes」という言葉が正しいと思えるくらいにお祭り感のある、たのしいカンファレンスでした。</p>
<p>登壇後の反応としては、Lynxそのものを知らなかったという話や、LynxとLynxのVue対応、あれだけ盛り上がってたのにいまは確かにそうでもないなといったコメントをいただきました。</p>
<p><strong>＜X上での反応＞</strong></p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Lynx 初聞き<br>2025年3月に出たのねhttps://t.co/8OD0OQqLyU#vuefes_mates</p>&mdash; yuki / ほにゃにゃ (@honyanyas) October 25, 2025</blockquote> 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">確かにLynx出た直後は話題になってたけど、それ以降あんまり話題になってない感じするな#vuefes #vuefes_mates</p>&mdash; thirdlf (さどるふ) (@thirdlf1) October 25, 2025</blockquote> 

<p>また、自己紹介スライドにあった、BBQインストラクターやパエリア検定上級が気になったと声をかけてくださった人もいました笑</p>
<h2 id="スポンサーブース">スポンサーブース</h2><p>フューチャーアーキテクトは、今回ゴールドスポンサーとしてブースも出展していました。</p>
<p>今回は配布物（ノベルティ）を3種類用意し、くじ引きでどれかが当たる企画を実施しました。1等は最近発売された当社の渋川よしきが著者の1人である「JavaScript&#x2F;TypeScript実力強化書―⁠―関数・非同期処理・型システム完全攻略」に渋川のサイン入りでお渡ししました。</p>
<p>くじはVueFesだからと、Vue.jsを使ったHTMLファイルで作成しました。（GeminiさんがHTMLファイル1枚で9割方作成してくれました。生成されるまで30秒くらい。ほんとにすごいね。）</p>
<p>実際には一瞬で抽選処理は終わるんですが、ドキドキ感のある「抽選中」の演出をつけたものを作成しました。</p>
<img src="/images/2025/20251031b/vuefes_lot.gif" alt="vuefes_lot.gif" width="802" height="697" loading="lazy">

<details><summary>くじのソースコードはこちら</summary>

<figure class="highlight html"><figcaption><span>vuefeslot.html</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;ja&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>くじ引き<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://unpkg.com/vue@3&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css">        <span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-family</span>: -apple-system, BlinkMacSystemFont, <span class="string">&#x27;Segoe UI&#x27;</span>, Roboto, <span class="string">&#x27;Helvetica Neue&#x27;</span>, Arial, sans-serif;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">display</span>: flex;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">justify-content</span>: center;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">align-items</span>: center;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">min-height</span>: <span class="number">100vh</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">background-color</span>: <span class="number">#f4f7f9</span>;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-id">#app</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">text-align</span>: center;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">background-color</span>: white;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">padding</span>: <span class="number">30px</span> <span class="number">40px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">border-radius</span>: <span class="number">12px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">10px</span> <span class="number">25px</span> <span class="built_in">rgba</span>(<span class="number">0</span>,<span class="number">0</span>,<span class="number">0</span>,<span class="number">0.1</span>);</span></span><br><span class="line"><span class="language-css">            <span class="attribute">width</span>: <span class="number">90%</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">max-width</span>: <span class="number">400px</span>;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-tag">h1</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">color</span>: <span class="number">#333</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin-bottom</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-tag">button</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">padding</span>: <span class="number">15px</span> <span class="number">35px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-size</span>: <span class="number">1.2em</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-weight</span>: bold;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">cursor</span>: pointer;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">border</span>: none;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">background</span>: <span class="built_in">linear-gradient</span>(<span class="number">45deg</span>, <span class="number">#ff8a00</span>, <span class="number">#e52e71</span>);</span></span><br><span class="line"><span class="language-css">            <span class="attribute">color</span>: white;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">border-radius</span>: <span class="number">8px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">transition</span>: transform <span class="number">0.2s</span>, box-shadow <span class="number">0.2s</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin</span>: <span class="number">25px</span> <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">4px</span> <span class="number">15px</span> <span class="built_in">rgba</span>(<span class="number">229</span>, <span class="number">46</span>, <span class="number">113</span>, <span class="number">0.4</span>);</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-tag">button</span><span class="selector-pseudo">:disabled</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">background</span>: <span class="number">#ccc</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">cursor</span>: not-allowed;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">box-shadow</span>: none;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-tag">button</span><span class="selector-pseudo">:hover</span><span class="selector-pseudo">:not</span>(<span class="selector-pseudo">:disabled</span>) &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">transform</span>: <span class="built_in">translateY</span>(-<span class="number">2px</span>);</span></span><br><span class="line"><span class="language-css">            <span class="attribute">box-shadow</span>: <span class="number">0</span> <span class="number">6px</span> <span class="number">20px</span> <span class="built_in">rgba</span>(<span class="number">229</span>, <span class="number">46</span>, <span class="number">113</span>, <span class="number">0.5</span>);</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-id">#result</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin-top</span>: <span class="number">20px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-size</span>: <span class="number">2em</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-weight</span>: bold;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">color</span>: <span class="number">#e52e71</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">height</span>: <span class="number">60px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">display</span>: flex;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">justify-content</span>: center;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">align-items</span>: center;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-class">.finished-message</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">color</span>: <span class="number">#555</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-weight</span>: bold;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-class">.prize-list</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">list-style</span>: none;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">padding</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin-top</span>: <span class="number">25px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">text-align</span>: left;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">        <span class="selector-class">.prize-list</span> <span class="selector-tag">li</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">margin-bottom</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-size</span>: <span class="number">1.1em</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">background-color</span>: <span class="number">#f9f9f9</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">padding</span>: <span class="number">10px</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">border-radius</span>: <span class="number">5px</span>;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">        <span class="comment">/* ドキドキ感を出すためのアニメーション */</span></span></span><br><span class="line"><span class="language-css">        <span class="keyword">@keyframes</span> drumroll &#123;</span></span><br><span class="line"><span class="language-css">            <span class="number">0%</span> &#123; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1</span>); <span class="attribute">opacity</span>: <span class="number">0.7</span>; &#125;</span></span><br><span class="line"><span class="language-css">            <span class="number">25%</span> &#123; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.1</span>); <span class="attribute">opacity</span>: <span class="number">1</span>; &#125;</span></span><br><span class="line"><span class="language-css">            <span class="number">50%</span> &#123; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1</span>); <span class="attribute">opacity</span>: <span class="number">0.7</span>; &#125;</span></span><br><span class="line"><span class="language-css">            <span class="number">75%</span> &#123; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1.1</span>); <span class="attribute">opacity</span>: <span class="number">1</span>; &#125;</span></span><br><span class="line"><span class="language-css">            <span class="number">100%</span> &#123; <span class="attribute">transform</span>: <span class="built_in">scale</span>(<span class="number">1</span>); <span class="attribute">opacity</span>: <span class="number">0.7</span>; &#125;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css">        <span class="selector-class">.drawing-text</span> &#123;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">font-size</span>: <span class="number">1.5em</span>; <span class="comment">/* 結果より少し小さく */</span></span></span><br><span class="line"><span class="language-css">            <span class="attribute">color</span>: <span class="number">#555</span>;</span></span><br><span class="line"><span class="language-css">            <span class="attribute">animation</span>: drumroll <span class="number">1.2s</span> infinite;</span></span><br><span class="line"><span class="language-css">        &#125;</span></span><br><span class="line"><span class="language-css">    </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;app&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>🥢Future Architectくじ🥢<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> @<span class="attr">click</span>=<span class="string">&quot;startDraw&quot;</span> <span class="attr">:disabled</span>=<span class="string">&quot;isFinished || isLoading&quot;</span>&gt;</span>くじを引く<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;result&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-if</span>=<span class="string">&quot;isLoading&quot;</span> <span class="attr">class</span>=<span class="string">&quot;drawing-text&quot;</span>&gt;</span>抽選中...<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-else-if</span>=<span class="string">&quot;result&quot;</span>&gt;</span>&#123;&#123; result &#125;&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-if</span>=<span class="string">&quot;isFinished&quot;</span> <span class="attr">class</span>=<span class="string">&quot;finished-message&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span>&gt;</span>くじはすべてなくなりました！<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">hr</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">h2</span>&gt;</span>残り本数<span class="tag">&lt;/<span class="name">h2</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ul</span> <span class="attr">class</span>=<span class="string">&quot;prize-list&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">li</span> <span class="attr">v-for</span>=<span class="string">&quot;prize in prizes&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;prize.name&quot;</span>&gt;</span></span><br><span class="line">            &#123;&#123; prize.name &#125;&#125;: 残り <span class="tag">&lt;<span class="name">strong</span>&gt;</span>&#123;&#123; prize.stock &#125;&#125;<span class="tag">&lt;/<span class="name">strong</span>&gt;</span> 本</span><br><span class="line">        <span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">const</span> &#123; createApp, ref, computed &#125; = <span class="title class_">Vue</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">createApp</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">setup</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> prizes = <span class="title function_">ref</span>([</span></span><br><span class="line"><span class="language-javascript">                &#123; <span class="attr">name</span>: <span class="string">&#x27;🥇 1等&#x27;</span>, <span class="attr">stock</span>: <span class="number">5</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">                &#123; <span class="attr">name</span>: <span class="string">&#x27;🥈 2等&#x27;</span>, <span class="attr">stock</span>: <span class="number">60</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">                &#123; <span class="attr">name</span>: <span class="string">&#x27;🥉 3等&#x27;</span>, <span class="attr">stock</span>: <span class="number">100</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">            ]);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> result = <span class="title function_">ref</span>(<span class="string">&#x27;&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript">            <span class="comment">// 抽選中かどうかを管理する状態</span></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> isLoading = <span class="title function_">ref</span>(<span class="literal">false</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> totalStock = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">                <span class="keyword">return</span> prizes.<span class="property">value</span>.<span class="title function_">reduce</span>(<span class="function">(<span class="params">total, prize</span>) =&gt;</span> total + prize.<span class="property">stock</span>, <span class="number">0</span>);</span></span><br><span class="line"><span class="language-javascript">            &#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> isFinished = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> totalStock.<span class="property">value</span> === <span class="number">0</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="comment">// くじを引く処理を2段階に分ける</span></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> <span class="title function_">startDraw</span> = (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">                <span class="keyword">if</span> (isFinished.<span class="property">value</span> || isLoading.<span class="property">value</span>) <span class="keyword">return</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">                isLoading.<span class="property">value</span> = <span class="literal">true</span>; <span class="comment">// 抽選開始</span></span></span><br><span class="line"><span class="language-javascript">                result.<span class="property">value</span> = <span class="string">&#x27;&#x27;</span>; <span class="comment">// 前回の結果をクリア</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">                <span class="comment">// 2500ミリ秒 (2.5秒) 後に結果を出す</span></span></span><br><span class="line"><span class="language-javascript">                <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">                    <span class="title function_">drawLottery</span>();</span></span><br><span class="line"><span class="language-javascript">                    isLoading.<span class="property">value</span> = <span class="literal">false</span>; <span class="comment">// 抽選終了</span></span></span><br><span class="line"><span class="language-javascript">                &#125;, <span class="number">2500</span>);</span></span><br><span class="line"><span class="language-javascript">            &#125;;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="comment">// 実際の抽選ロジック</span></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">const</span> <span class="title function_">drawLottery</span> = (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">                <span class="keyword">const</span> random = <span class="title class_">Math</span>.<span class="title function_">floor</span>(<span class="title class_">Math</span>.<span class="title function_">random</span>() * totalStock.<span class="property">value</span>) + <span class="number">1</span>;</span></span><br><span class="line"><span class="language-javascript">                <span class="keyword">let</span> accumulated = <span class="number">0</span>;</span></span><br><span class="line"><span class="language-javascript">                <span class="keyword">for</span> (<span class="keyword">const</span> prize <span class="keyword">of</span> prizes.<span class="property">value</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">                    <span class="keyword">if</span> (prize.<span class="property">stock</span> &gt; <span class="number">0</span>) &#123;</span></span><br><span class="line"><span class="language-javascript">                        accumulated += prize.<span class="property">stock</span>;</span></span><br><span class="line"><span class="language-javascript">                        <span class="keyword">if</span> (random &lt;= accumulated) &#123;</span></span><br><span class="line"><span class="language-javascript">                            result.<span class="property">value</span> = <span class="string">`🎉 <span class="subst">$&#123;prize.name&#125;</span> 当選！ 🎉`</span>;</span></span><br><span class="line"><span class="language-javascript">                            prize.<span class="property">stock</span>--;</span></span><br><span class="line"><span class="language-javascript">                            <span class="keyword">break</span>;</span></span><br><span class="line"><span class="language-javascript">                        &#125;</span></span><br><span class="line"><span class="language-javascript">                    &#125;</span></span><br><span class="line"><span class="language-javascript">                &#125;</span></span><br><span class="line"><span class="language-javascript">            &#125;;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">            <span class="keyword">return</span> &#123;</span></span><br><span class="line"><span class="language-javascript">                prizes,</span></span><br><span class="line"><span class="language-javascript">                result,</span></span><br><span class="line"><span class="language-javascript">                startDraw,</span></span><br><span class="line"><span class="language-javascript">                isFinished,</span></span><br><span class="line"><span class="language-javascript">                isLoading <span class="comment">// テンプレートで使えるようにする</span></span></span><br><span class="line"><span class="language-javascript">            &#125;;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript">    &#125;).<span class="title function_">mount</span>(<span class="string">&#x27;#app&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

</details>

<p>ありがたいことに、昨年を大幅に上回る人数の方がスポンサーブースに立ち寄っていただいたこともあり、ノベルティは13時頃にはすべてはけてしまったようです。<br>それ以降に立ち寄ってくださった皆様、申し訳ございません。</p>
<h2 id="さいごに">さいごに</h2><p>今回のセッションでお話しした「VueLynx」は、まだプロトタイプが公開された段階で、正直なところ、この先どうなるかは分かりませんし、Lynxそのものも、まだ日本語での情報が少ない状況です。登壇資料の最後に書いた通り、本登壇を通じて、実現した時のワクワク感が参加者の皆さんに届き、少しでも盛り上がれば幸いです。</p>
<p>最後になりましたが、このような素晴らしい技術コミュニティの「お祭り」を企画・運営し、LT登壇の機会をいただいたVue.js日本ユーザーグループの皆様、そして当日LTにご参加いただいた皆様、ありがとうございました。</p>
]]></content>
    <summary type="html">Vue Fes Japan 2025のライトニングトークに登壇いたしました。昨年のプラチナスポンサーセッションに続き、2年連続での登壇となります。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="VueFes" scheme="https://future-architect.github.io/tags/VueFes/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>【PoC】Vue + DuckDB-Wasm + ECharts によるセキュリティ投資判断アプリ</title>
    <link href="https://future-architect.github.io/articles/20251028b/"/>
    <id>https://future-architect.github.io/articles/20251028b/</id>
    <published>2025-10-27T15:00:01.000Z</published>
    <updated>2025-10-27T15:00:01.000Z</updated>
    <author><name>松本朝香</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、CSIG（Cyber Security Innovation Group）所属 の松本です♪</p>
<p>フロントエンド初心者ですが Vue連載 で頑張ってみました。</p>
<p>現在専門ドメインはセキュリティです。もともとデータのセマンティックに関心がありますが、加えて最近では脅威モデリングや脅威インテリジェンスに注目しています。</p>
<p>今回は、経営者がセキュリティ投資判断をしやすくする簡易ダッシュボードを構築しました！使用するデータ量が多くなってもブラウザで固まらず、今後の拡張性も加味した構成にしたかったため WASM (WebAssembly) を組み込んでいます。開発プロセスで遭遇するセキュリティ豆知識も掲載しているので、記事を通して一緒に学んでいければと思います。</p>
<h2 id="背景">背景</h2><p>本テーマを執筆した背景としては、セキュリティ投資意識を具体的な行動へつなげる支援ができたら、という思いからです。</p>
<p>セキュリティは売り上げに直接的に結びつかないため「守りの投資」と言われており、平時はコストとみなされがちです。しかし、有事になるとこれまで投資を着実に進めてきた企業とそうでない企業で結果は <strong>二極化</strong> します。</p>
<p>直近でも、国内大手企業においてインシデントが多発しており、特にランサムウェア被害が拡大しています。もはやサイバー攻撃を完全に防ぐことはできません。自社リソースを鑑みて <strong>“どこ”</strong> に <strong>“どの程度”</strong> セキュリティ投資すると最もROIが高くなるのかを真剣に見極め、現時点ベースでの対策を実行に移していくことが被害を最小限にする対策です。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>積立NISAのように、セキュリティも <strong>“投資先を見極める選球眼”</strong> と <strong>“コツコツ投資実行”</strong> が重要。</p>
</div></div>

<h2 id="本記事を読むメリット">本記事を読むメリット</h2><ul>
<li>Single Page Applicationでデータ分析～可視化の可能性を知ることができる</li>
<li>Vite環境でDuckDB-WASMを扱う際のポイントを知ることができる</li>
<li>VueとEChartsの連携方法と描画機能とUIの関係について知ることができる</li>
<li>現時点ベースでのセキュリティ投資判断の重要性を認識できる</li>
</ul>
<h2 id="本ツールがもたらす効果">本ツールがもたらす効果</h2><ul>
<li><strong>工数削減と迅速化</strong><br>これまで担当者が数時間～数日かけて定期的に行っていたセキュリティ対応の精査及び優先順位付け（情報の調査、分析、対策立案まで）のプロセスを短縮します。</li>
<li><strong>対策の精度向上</strong><br>「とりあえず全部のログを取る」といった闇雲な対策から、自社に合わせた費用対効果（ROI）の高い対策へシフトできます。</li>
<li><strong>組織内のコミュニケーション円滑化</strong><br>エンジニア、マネージャー、経営層が同じデータを見て会話できるため、セキュリティに関する意思決定がスムーズになります。</li>
</ul>
<p>※気になる方が多ければ何かしらの形でツール公開も視野に入るため、記事へのリアクションを いいね♥ や SNS などでいただければ幸いです。今回はあくまでフロントエンド開発の記事となります。</p>
<h2 id="アウトプット">アウトプット</h2><p>最終的に経営者ビューと技術者ビュー二画面切り替え仕様にし、ホバーやドラッグ&amp;ドロップができるInteractiveなアプリとなりました :)</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/jUJgXRPqGQQ?si=9A9N8BlqrtPkfNiS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<h3 id="【経営者ビュー】">【経営者ビュー】</h3><p>投資対効果を検討するため、その対策をしたときの想定損失低減率を縦軸、１つの対策で自社の資産をどの程度（範囲&#x2F;規模）守ることができるのかのカバレッジを横軸に4象限で区分けし、現状に即したセキュリティ投資対象を選別することを想定しています。バブルチャートは投資割合を表しており、左下の基準(1)となる円より大きければ過剰投資、小さければ投資不足を意味します。つまり、右上の “IDEAL AREA” に基準円と同じ大きさの円があれば、<strong>最適な投資対象</strong> に <strong>最適な投資額</strong> を投じていることになります。具体的な数値は、カーソルオン時のポップアップに記載しています。</p>
<img fetchpriority="high" src="/images/2025/20251028b/image.png" alt="image.png" width="1200" height="551">

<h3 id="【技術者ビュー】">【技術者ビュー】</h3><p>経営者に根拠をもって説明するため、技術的脅威（attack technique）と防御策と対象資産を網羅的に紐づけ、防御策の有効性を可視化することを想定しています。クリックすると、各ノードは該当するMITREのリンクへアクセスしてくれます。</p>
<img src="/images/2025/20251028b/21eb3673-f334-4431-9977-1871ffb96a81.png" alt="" width="1200" height="587" loading="lazy">

<h2 id="要件と仕様">要件と仕様</h2><p>個人の即席アプリ開発ということで、技術選定しました。</p>
<h3 id="要件">要件</h3><ul>
<li>素早い開発環境構築</li>
<li>高速処理</li>
<li>インタラクティブなUI</li>
<li>拡張性</li>
</ul>
<h3 id="仕様">仕様</h3><ul>
<li>Vite + vue：高速な開発環境とUI構築</li>
<li>DuckDB-WASM：ブラウザ内蔵の超高性能データベースエンジン</li>
<li>Echarts：インタラクティブ性の高い豊富なグラフ描画ライブラリ</li>
</ul>
<h3 id="関連用語">関連用語</h3><ul>
<li><strong>SPA（Single Page Application）</strong><br>処理の多くをサーバーと通信せずにブラウザ内（クライアントサイド）で完結させ、ページ遷移なしの高速なUIレスポンスを実現するアプリケーション全般を指します。</li>
<li><strong>DuckDB</strong><br>もともとは、データ分析（大量のデータを読み込んで集計や計算をすること）に特化した、高速なデータベースです。「データ分析版SQLite」のような存在です。操作デモ をweb上で行うことができるので是非使用感をお試しください。</li>
<li><strong>WASM (WebAssembly)</strong><br>C++やRustのような高速な言語で書かれたプログラムを、ブラウザ内でほぼネイティブの速度で実行可能にするための技術です。</li>
<li><strong>DuckDB-WASM</strong><br>DuckDB本体をWASMに変換することで、この強力なデータベースをブラウザの中で完結させる（&#x3D;サーバレス）ことを実現しました。DuckDB-WASMは外部サーバーと通信するのではなく、SPAが動いているブラウザの中で直接、強力なデータ処理能力を提供します。したがって、サーバーとの通信は最初のデータ取得1回だけに減り、その後の複雑なフィルタリングや集計はすべてブラウザ内で完結し、結果としてUIは常に高速なまま動作することになります。</li>
</ul>
<h2 id="開発環境準備">開発環境準備</h2><p>Windowsでの Duckdb-WASM開発はすでに 先駆者 がいらっしゃったため、今回は Rocky Linux を利用することとしました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -o- [https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh](https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh) | bash</span></span><br><span class="line">~~~ (中略) ~~~</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">node -v</span></span><br><span class="line">v22.19.0</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm -v</span></span><br><span class="line">11.6.0</span><br></pre></td></tr></table></figure>

<p>参照先：https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating</p>
<p>開発環境は、下記で進めます。</p>
<ul>
<li>プロジェクト名：security-invest</li>
<li>フレームワーク：Vue</li>
<li>言語：TypeScript</li>
</ul>
<p>今回使用したライブラリ群は以下の通りです。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm list</span></span><br><span class="line">security-invest@0.0.0 /home/user01/security-invest</span><br><span class="line">├── @duckdb/duckdb-wasm@1.29.0</span><br><span class="line">├── @types/node@24.9.1</span><br><span class="line">├── @vitejs/plugin-vue@6.0.1</span><br><span class="line">├── @vue/tsconfig@0.8.1</span><br><span class="line">├── echarts@5.6.0</span><br><span class="line">├── path@0.12.7</span><br><span class="line">├── typescript@5.6.3</span><br><span class="line">├── vite-plugin-static-copy@3.1.4</span><br><span class="line">├── vite@5.1.2</span><br><span class="line">├── vue-echarts@6.7.3</span><br><span class="line">├── vue-tsc@3.1.2</span><br><span class="line">└── vue@3.5.11</span><br></pre></td></tr></table></figure>

<p>※DuckDB APIは 1.29.0と1.30.0でAPIの仕様変更があり、事前によく確認してから利用することをお勧めします。</p>
<div class="note-container note-info note-has-title"><div class="note-title"><span class="note-icon"></span>ソフトウェアサプライチェーン問題</div><div class="note-body">

<p>npmは汚染されやすいという話を聞いたことがありますか？</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm audit</span><br></pre></td></tr></table></figure>

<p>上記コマンドは、プロジェクトが依存しているパッケージ（package.json にリストされているもの）に既知の脆弱性がないかを自動チェックし、脆弱性が見つかった場合はその詳細（深刻度、影響を受けるパッケージ、修正方法）をレポートしてくれます。</p>
<img src="/images/2025/20251028b/npm-report.png" alt="npm-report.png" width="971" height="224" loading="lazy">

<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm audit fix</span><br><span class="line">npm audit fix --force</span><br></pre></td></tr></table></figure>

<p>上は互換性を維持したまま（セマンティックバージョニングの範囲内で）修正可能な脆弱性の多くを自動でアップデートしてくれます。下はメジャーバージョンが上がるような、互換性が壊れる可能性のあるアップデートも強制的に実行します。基本は上のみで問題ないと思います。</p>
</div></div>

<p>開発ディレクトリへ移動して…</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm run dev</span><br></pre></td></tr></table></figure>

<img src="/images/2025/20251028b/image_2.png" alt="image.png" width="1182" height="217" loading="lazy">

<p>この時点で、表示されたURLにブラウザでアクセスすると、Vueの初期画面が表示されます。立ち上がりが早いですね！</p>
<h3 id="ディレクトリ構成">ディレクトリ構成</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">duckdb-graph-spa/</span><br><span class="line">├── index.html</span><br><span class="line">├── vite.config.ts</span><br><span class="line">├── package.json</span><br><span class="line">├── public/                &lt;-- WASMとCSVの配置場所 (Viteの標準)</span><br><span class="line">│   ├── inputdata_01.csv</span><br><span class="line">│   ├── inputdata_02.csv</span><br><span class="line">│   └── ...（材料はここへ）</span><br><span class="line">│   └── duckdb-wasm/       &lt;-- DuckDBのWASM/Worker関連ファイル</span><br><span class="line">│       ├── duckdb-mvp.wasm</span><br><span class="line">│       ├── duckdb-mvp.worker.js</span><br><span class="line">│       └── ... (その他必要なファイル)</span><br><span class="line">└── src/</span><br><span class="line">    ├── components/</span><br><span class="line">    │   ├── RiskCharts.vue</span><br><span class="line">    │   ├── MitreGraph.vue</span><br><span class="line">    │   └── ToggleSwitch.vue</span><br><span class="line">    ├── composables/</span><br><span class="line">    │   └── useDashboardData.ts  &lt;-- DBヘルパー呼び出し</span><br><span class="line">    ├── services/</span><br><span class="line">    │   └── duckdbService.ts     &lt;-- DB初期化、接続、クエリ実行のヘルパー</span><br><span class="line">    └── sql/                     &lt;-- SQLクエリファイルの置き場所</span><br><span class="line">        ├── getRiskData.sql</span><br><span class="line">        ├── getMitreData.sql</span><br><span class="line">        └── getPriorityscore.sql</span><br></pre></td></tr></table></figure>

<h3 id="実装">実装</h3><p>実装にあたってポイントをご紹介します。</p>
<h4 id="Viteビルドオプションの活用">Viteビルドオプションの活用</h4><p>Viteは、モダンなWeb開発で使われる非常に高速な開発ツール（ビルドツール）です。下記設定ファイルを使って、Viteの動作をプロジェクトに合わせてカスタマイズします。オプションがさまざまあるので こちら を参考に使いこなしてみましょう。</p>
<p><strong>クロスオリジン分離</strong></p>
<p>処理を高速化させたい場合に、DuckDB-WASM のマルチスレッド機能を利用するため COEP&#x2F;COOP ヘッダーを設定することでデフォルトではセキュリティ面（Spectre脆弱性）から無効化されている SharedArrayBuffer を有効化できます。が、「自ドメイン以外のリソースを読み込む際に、相手側が読み込み許可と宣言してくれていないと、ブラウザがブロックする」という制限がかかります。そのため、許可宣言を記載をすべてのリソースに行わないといけないのでかなり大変です。ここに高速処理とセキュリティのトレードオフを垣間見ました…。最初知らずに設定し続けていたため、画面に何も表示されず沼にはまりました(´;ω;｀) (3Dグラフィックスの計算などはWASM-Thread高速処理設定が必要だったりするのですかね…今回はそこまでの量を扱ってないので利用せず)<br>詳細解説は こちら</p>
<figure class="highlight ts"><figcaption><span>vite.config.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;vite&#x27;</span></span><br><span class="line"><span class="keyword">import</span> vue <span class="keyword">from</span> <span class="string">&#x27;@vitejs/plugin-vue&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// https://vitejs.dev/config/</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">plugins</span>: [<span class="title function_">vue</span>()],</span><br><span class="line">  <span class="attr">optimizeDeps</span>: &#123;</span><br><span class="line">    <span class="attr">include</span>: [</span><br><span class="line">      <span class="string">&#x27;echarts&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;vue&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;duckdb-wasm&#x27;</span>,</span><br><span class="line">    ],</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">server</span>: &#123;</span><br><span class="line">    <span class="attr">headers</span>: &#123;</span><br><span class="line">      <span class="comment">// Activate to use SharedArrayBuffer function</span></span><br><span class="line">      <span class="string">&#x27;Cross-Origin-Opener-Policy&#x27;</span>: <span class="string">&#x27;same-origin&#x27;</span>,     <span class="comment">// COOP</span></span><br><span class="line">      <span class="string">&#x27;Cross-Origin-Embedder-Policy&#x27;</span>: <span class="string">&#x27;require-corp&#x27;</span>,  <span class="comment">// COEP</span></span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p><strong>optimizeDeps</strong> では、Viteが自動検出しにくい可能性のある echarts, vue, duckdb-wasm の3つを、強制的に最適化（事前バンドル）の対象に含めて開発環境の安定化を図っています。</p>
<h4 id="DBインスタンスのシングルトンパターン管理">DBインスタンスのシングルトンパターン管理</h4><p>下記のような専用ファイルを作成し、そのファイル内でDBインスタンスを1回だけ生成し、exportすることでコンポーネントの再描画時にDuckDB初期化処理の実行をスキップさせます。Vue コンポーネント側は、常にこの export されたインスタンスを import して使います。</p>
<figure class="highlight ts"><figcaption><span>duckdbService.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> duckdb <span class="keyword">from</span> <span class="string">&#x27;@duckdb/duckdb-wasm&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> duckdb_wasm <span class="keyword">from</span> <span class="string">&#x27;@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> duckdb_worker <span class="keyword">from</span> <span class="string">&#x27;@duckdb/duckdb-wasm/dist/duckdb-mvp.worker.js?url&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1. DBインスタンスをここで生成</span></span><br><span class="line"><span class="keyword">const</span> dbInstance = <span class="keyword">new</span> duckdb.<span class="title class_">AsyncDuckDB</span>();</span><br><span class="line"><span class="keyword">let</span> isInitialized = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 初期化処理を関数として切り出す</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">initializeDB</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (isInitialized) <span class="keyword">return</span> dbInstance;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> dbInstance.<span class="title function_">instantiate</span>(duckdb_wasm, worker);</span><br><span class="line">  isInitialized = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">return</span> dbInstance;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. インスタンスそのものやクエリ関数をexportする</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> db = dbInstance;</span><br></pre></td></tr></table></figure>

<h4 id="Interactive-UI">Interactive UI</h4><p>こちらは、ホバー時のポップアップ表示の挙動を指示している箇所です。「データ要素にマウスを合わせたらツールチップを出す。その際、もしデータに詳細な description があればIDと説明文をリッチに表示し、なければ name だけをシンプルに表示する」を実現しています。</p>
<figure class="highlight html"><figcaption><span>ホバー時ポップアップ</span></figcaption><table><tr><td class="code"><pre><span class="line">tooltip: &#123;</span><br><span class="line">      trigger: &quot;item&quot;,</span><br><span class="line">      formatter: (p: any) =&gt;</span><br><span class="line">        p.data.description</span><br><span class="line">          ? `<span class="tag">&lt;<span class="name">b</span>&gt;</span>$&#123;p.data.id&#125;<span class="tag">&lt;/<span class="name">b</span>&gt;</span><span class="tag">&lt;<span class="name">br</span>/&gt;</span>$&#123;p.data.description&#125;`</span><br><span class="line">          : p.data.name,</span><br><span class="line">    &#125;,</span><br></pre></td></tr></table></figure>

<p>こちらは、<strong>ノード（点）</strong> と <strong>リンク（線）</strong> で構成されるデータの関係性を表現している箇所です。</p>
<figure class="highlight html"><figcaption><span>力学指向ネットワーク図</span></figcaption><table><tr><td class="code"><pre><span class="line">~~~（中略）~~~</span><br><span class="line">          color: &quot;#333&quot;,</span><br><span class="line">          fontSize: 11,</span><br><span class="line">        &#125;,</span><br><span class="line">        // --- 力学シミュレーション設定 ---</span><br><span class="line">        force: &#123;</span><br><span class="line">          repulsion: 180,</span><br><span class="line">          edgeLength: [80, 140],</span><br><span class="line">        &#125;,</span><br><span class="line">        // --- データ配列設定 ---</span><br><span class="line">        data: sortedNodes.map((n) =&gt; (&#123;</span><br><span class="line">          ...n,</span><br><span class="line">          itemStyle: &#123; color: n.color &#125;,</span><br><span class="line">          // --- リンク数（重要度）に応じてノードを巨大化 ---</span><br><span class="line">          symbolSize: 25 + nodeDegree(n.id) * 3,</span><br><span class="line">        &#125;)),</span><br><span class="line">        links: mitreGraph.value.links,</span><br><span class="line">        lineStyle: &#123;</span><br><span class="line">          color: &quot;rgba(150,150,150,0.4)&quot;,</span><br><span class="line">          width: 1.5,</span><br><span class="line">        &#125;,</span><br><span class="line">        // --- ホバー時のハイライト設定 ---</span><br><span class="line">        emphasis: &#123;</span><br><span class="line">          focus: &quot;adjacency&quot;,</span><br><span class="line">          lineStyle: &#123; color: &quot;#888&quot;, width: 3 &#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br></pre></td></tr></table></figure>

<h4 id="Parquet形式">Parquet形式</h4><p>DuckDB-WASMは <strong>Parquet形式</strong> の読み込みに最適化されています。したがって、大量のデータをS3等に保管し、そこから必要な分だけ読み出すという処理を得意とします。データ量が膨大になった際、こちらの機能を利用することが想定されます。CloudShell上にDuckDBをinstallしてデータDLなしで分析可能な体制を作る ことが可能（無料!!）なのでAthenaの代替として有効な手段かもしれません。</p>
<h3 id="インプットデータ">インプットデータ</h3><h4 id="管理台帳系">管理台帳系</h4><p>情報資産管理台帳で一般的に取得可能と想定されるデータを用意しました。それに加え、定量化するにあたって必要な情報を疑似的に保持させました。</p>
<h4 id="MITRE系">MITRE系</h4><p>今回利用する MITRE ATT&amp;ACK と MITRE D3FEND は STIX準拠のNoSQLでした。DuckDBを利用するためクエリ検索しやすい形に変換する必要があります。</p>
<img src="/images/2025/20251028b/image_3.png" alt="image.png" width="1200" height="455" loading="lazy">

<p>公式サイト内をよく探すと、STIXではない形式での取得が可能なAPIが提供されていました。興味のある方は見つけてみてくださいね :)</p>
<p>※今回利用しているデータは一般公開情報を元にしたサンプルデータとなります。</p>
<h3 id="参考">参考</h3><p>https://duckdb.org/docs/stable/clients/wasm/instantiation<br>https://duckdb.org/docs/stable/clients/wasm/data_ingestion<br>https://duckdb.org/docs/stable/clients/wasm/query<br>https://www.flatuicolorpicker.com/all-flat-ui-colors/<br>https://www.ipa.go.jp/security/guide/sme/ug65p90000019cbk-att/000055518.xlsx<br>https://www.ipa.go.jp/security/sme/f55m8k000000587z-att/outline_guidance_risk.pdf<br>https://attack.mitre.org/resources/attack-data-and-tools/<br>https://d3fend.mitre.org/api-docs/</p>
<h2 id="まとめ">まとめ</h2><p>今回は、実用性のあるアプリを題材に、Vue + DuckDB-wasm + ECharts を実装する上でのポイントやセキュリティ関連の知識を解説してみました。</p>
<p>この記事をきっかけに、</p>
<ul>
<li>サーバーレスでも分析アプリをつくってみたい！という方</li>
<li>セキュリティを意識した開発プロセスを実践できる技術者</li>
<li>セキュリティ投資の重要性を理解し投資判断できる経営者</li>
</ul>
<p>が増えるといいなと思います :)</p>
]]></content>
    <summary type="html">経営者がセキュリティ投資判断をしやすくする簡易ダッシュボードを構築しました！使用するデータ量が多くなってもブラウザで固まらず、今後の拡張性も加味した構成にしたかったため...</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="WebAssembly" scheme="https://future-architect.github.io/tags/WebAssembly/"/>
    <category term="可視化" scheme="https://future-architect.github.io/tags/%E5%8F%AF%E8%A6%96%E5%8C%96/"/>
  </entry>
  <entry>
    <title>Vue.jsで脆弱なアプリを作って学ぶ、セキュリティ面で気をつけたいポイント</title>
    <link href="https://future-architect.github.io/articles/20251027a/"/>
    <id>https://future-architect.github.io/articles/20251027a/</id>
    <published>2025-10-26T15:00:00.000Z</published>
    <updated>2025-10-26T15:00:00.000Z</updated>
    <author><name>永井優斗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251027a/image.png" alt="image.png" width="1200" height="800">

<p>※上記サムネイルはChatGPTにて本記事を読み込ませて生成しました。</p>
<p>Future Value Group（FVG）の永井優斗です。</p>
<p>Vue.js連載の6本目です。Vue.js は使いやすく、直感的なテンプレート構文でフロントエンド開発を加速してくれます。しかし、その便利さゆえに「セキュリティをうっかり見落とす」ことがあるのも事実です。</p>
<p>この記事では、Vue.jsで実際に<strong>わざと脆弱な</strong>アプリを作り、そこからXSS・Cookie・CSRF・レート制限といった脆弱性を一気に体験してみます。ちなみにソースの8割ぐらいは生成AIに生成してもらいました。この記事も生成AIと相談しながら書きました。すごいね。</p>
<p>教材アプリはこちらのGitHub上で公開しています。</p>
<p>※試すことを優先しているのでUIはシンプルに。そのうちきれいにしたいなあ…</p>
<h2 id="はじめに">はじめに</h2><p>今回の教材アプリは Vue 3 + Vite で構築し、バックエンドには Express を利用します。</p>
<p>「脆弱版」と「安全版」を切り替えて、落とし穴と対策を比較できる構成です。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">01-vulnerable/   <span class="comment"># ← わざと脆弱な実装</span></span><br><span class="line">02-safe/         <span class="comment"># ← 対策を施した実装</span></span><br></pre></td></tr></table></figure>

<p>ローカルで起動してお試しください。</p>
<p>教材アプリはセキュリティの知識を駆使して情報を抜き取るゲームである、「CTF（Capture The Flag）」のような形で作成しており、脆弱版では各課題にて「攻撃」が成功すると100点のスコアがGetできます。全部で3問あります。</p>
<p>この記事はCTFの「答え」にあたるものでもあるので、セキュリティ知識を試したい方は先に教材アプリを試してみてから本記事をお読みください。</p>
<div class="note-container note-warn note-has-title"><div class="note-title"><span class="note-icon"></span>警告</div><div class="note-body">

<p>意図的に脆弱性をふくめているアプリケーションですので、公開環境やインターネットに接続された環境で実行しないでください。学習用にローカルで閉じた環境でのみ利用してください。</p>
<p>また、本記事や教材アプリは代表的な脆弱性の学習のための資料であり、攻撃を目的としたものではございません。</p>
</div></div>

<p>起動方法：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> 01-vulnerable</span><br><span class="line">npm install</span><br><span class="line">npm run server   <span class="comment"># APIサーバ (http://localhost:3001)</span></span><br><span class="line">npm run dev      <span class="comment"># フロントエンド (http://localhost:5173)　上のnpm run serverとは別のターミナルで実行してください</span></span><br></pre></td></tr></table></figure>

<h2 id="1-DOM-XSS（v-htmlの乱用）">1. DOM XSS（v-htmlの乱用）</h2><p>Vue.js では、テンプレートの中で変数を埋め込む際、<code>&#123;&#123; userInput &#125;&#125;</code>のように記述すると、自動的にHTMLがエスケープされます。<br>そのため、通常の使い方ではユーザーの入力からXSSは発生しません。</p>
<p>しかし、「HTMLを直接描画したい」という欲求に負けて v-html を使うと、XSSの標的になるかもしれません。</p>
<h3 id="v-htmlとは">v-htmlとは</h3><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">v-html</span>=<span class="string">&quot;someHtml&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>このように書くと、someHtml の中身がそのままHTMLとして挿入されます。</p>
<p>つまり、ユーザー入力が <code>&lt;p&gt;こんにちは&lt;/p&gt;</code> なら、実際に <code>&lt;p&gt;</code> タグが生成されます。そしてユーザー入力が <code>&lt;img src=x onerror=&quot;alert(1)&quot;&gt;</code> だったら……？そのままスクリプトが実行されます。</p>
<h3 id="実際にXSSを起こしてみよう">実際にXSSを起こしてみよう</h3><p>教材アプリでは「脆弱UI（XSS）」という課題ページがあります。</p>
<p>手順：</p>
<ul>
<li>テキストエリアに以下を入力してください。</li>
</ul>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">x</span> <span class="attr">onerror</span>=<span class="string">&quot;alert(window.__FLAG_XSS)&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure>

<ul>
<li>下のプレビュー領域が v-html で描画されています。</li>
</ul>
<p>そこに画像タグが挿入され、onerror 属性が評価されるとアラートが表示されます。ここで表示される <code>window.__FLAG_XSS</code> は教材用のフラグでグローバルな秘密変数を想定しています。実運用ならこのような「グローバルな秘密変数」は致命的です。</p>
<h4 id="攻撃を試してみて何も起きないときは以下を確認してみてください。">攻撃を試してみて何も起きないときは以下を確認してみてください。</h4><ul>
<li><code>&lt;script&gt;</code> タグは innerHTML 経由では実行されません。イベント属性（onerror &#x2F; onload）を使ってください</li>
<li>CSP（Content-Security-Policy）が有効な場合、inline スクリプトがブロックされることがあります</li>
<li>要素が正しく挿入されているか、開発者ツールの Elements タブで確認しましょう</li>
</ul>
<h3 id="対策">対策</h3><ul>
<li>v-html を安易に使わない</li>
<li>どうしてもHTMLを扱う場合は、ホワイトリスト方式のサニタイズを適用する</li>
<li>ユーザー入力は常に <strong>「テキスト」として表示</strong> することを原則に</li>
</ul>
<h2 id="2-Cookieの危険な使い方">2. Cookieの危険な使い方</h2><p>続いて、Cookie周りの落とし穴です。</p>
<p>教材の2つ目の課題は「CookieにセッションIDを保存しているが、HttpOnly が設定されていない」例を取り扱います。</p>
<h3 id="document-cookieとは">document.cookieとは</h3><p>ブラウザ内でアクセス可能なCookieを読み取るAPIです。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">document</span>.<span class="property">cookie</span>)</span><br><span class="line"><span class="comment">// =&gt; &quot;sid=demo-session; theme=light&quot;</span></span><br></pre></td></tr></table></figure>

<p>しかし本来、セッションIDなどの機密情報はサーバー専用で使うべきです。<br>それをHttpOnly無しで発行すると、XSSを使って誰でも読める状態になります。</p>
<h3 id="Cookieからsidを盗みだす">Cookieから<code>sid</code>を盗みだす</h3><p>脆弱版では sid というCookieが HttpOnly: false で発行されています。<br>そのため、以下のコードで誰でも読み取れます。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">x</span> <span class="attr">onerror</span>=<span class="string">&quot;alert(document.cookie)&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>もしアラートに sid&#x3D;demo-session と出たら、XSSがCookieを盗み出せたということです。<br>攻撃者はこの値を使って、別の環境で「なりすましログイン」ができます。</p>
<h4 id="攻撃を試してみてアラートが空だった場合は以下を確認してみてください">攻撃を試してみてアラートが空だった場合は以下を確認してみてください</h4><ul>
<li>HttpOnly が有効になっている（安全版を開いていないか？）</li>
<li>オリジンが異なる（5173と3001問題）</li>
<li>Viteのプロキシ設定でcookieDomainRewriteが漏れている</li>
</ul>
<h3 id="対策-1">対策</h3><ul>
<li>   セッションIDなどのCookieには必ずHttpOnlyとSecureを付与</li>
<li>   SameSite&#x3D;Lax 以上を設定してクロスサイト送信を制限する</li>
<li>   機密情報をフロント側のJSから参照できないように設計する</li>
</ul>
<h2 id="3-CSRF">3. CSRF</h2><p>次に、CSRF（クロスサイトリクエストフォージェリ）の課題です。</p>
<p>バックエンドの話も入ってしまうんですが、フロントエンドの開発者も知っておくべきだと思うので入れてみました。</p>
<p>ここでは、ユーザーが意図していない送金が実行されてしまう脆弱なAPIを見ます。</p>
<h3 id="CSRFとは？">CSRFとは？</h3><p>ユーザーが認証済み状態（Cookieを持っている）で、攻撃者のサイトを開いたとき、そのブラウザが自動的にCookieを添えてリクエストを送ってしまう、これがCSRF攻撃の基本構造です。</p>
<img src="/images/2025/20251027a/image_2.png" alt="image.png" width="680" height="462" loading="lazy">

<p>情報処理推進機構（IPA）:安全なウェブサイトの作り方 - 1.6 CSRF（クロスサイト・リクエスト・フォージェリ）より引用</p>
<h3 id="意図せず残高を減らす「罠ページ」">意図せず残高を減らす「罠ページ」</h3><p>教材の「CSRF」課題では、次のようなAPIがあります。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">GET /api/csrf/transfer?to=attacker&amp;amount=100</span><br></pre></td></tr></table></figure>

<ol>
<li>&#x2F;api&#x2F;wallet を開くと、現在の残高が表示されます（例：1000）</li>
<li>攻撃者ページを開きます（以下を保存してブラウザで開く）：</li>
</ol>
  <figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>かわいいねこの画像だよ！<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">img</span> <span class="attr">src</span>=<span class="string">&quot;http://localhost:5173/api/csrf/transfer?to=attacker&amp;amount=100&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

<ol start="3">
<li>ページを開くと自動的にリクエストが発生します</li>
<li><code>/api/wallet</code>を再確認すると、残高が減っていることを確認します</li>
</ol>
<p>※ 教材アプリでは攻撃が成功した際にスコア付与のため、csrfCode が付与されています。それをフォームに入力して提出してください。</p>
<h3 id="対策-2">対策</h3><ol>
<li>   副作用のある操作はGETにしない（POST限定）</li>
<li>   CSRFトークンを導入する</li>
</ol>
<figure class="highlight javascript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 発行時</span></span><br><span class="line">res.<span class="title function_">cookie</span>(<span class="string">&#x27;csrfToken&#x27;</span>, token, &#123; <span class="attr">httpOnly</span>: <span class="literal">false</span>, <span class="attr">sameSite</span>: <span class="string">&#x27;lax&#x27;</span> &#125;)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 検証時</span></span><br><span class="line"><span class="keyword">if</span> (req.<span class="property">cookies</span>.<span class="property">csrfToken</span> !== req.<span class="property">headers</span>[<span class="string">&#x27;x-csrf-token&#x27;</span>]) &#123;</span><br><span class="line">  <span class="keyword">return</span> res.<span class="title function_">status</span>(<span class="number">403</span>).<span class="title function_">json</span>(&#123; <span class="attr">error</span>: <span class="string">&#x27;csrf check failed&#x27;</span> &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<ol start="3">
<li>SameSiteを適切に設定（Lax or Strict）</li>
<li>Referer &#x2F; Origin チェックを補助的にする</li>
<li>レート制限で濫用防止(次の章で説明します)</li>
</ol>
<h2 id="4-（補足）-レート制限なし">4.（補足） レート制限なし</h2><p>CSRFやXSSなどの被害を拡大させるのは「無限リクエスト」です。</p>
<p>同一IPやユーザーが短時間に何百回もアクセスできる状態は危険です。</p>
<h3 id="Expressで簡単にレート制限を導入する">Expressで簡単にレート制限を導入する</h3><p>以下のように<code>express-rate-limit</code>を設定することで簡単にレート制限を導入できます。</p>
<figure class="highlight js"><figcaption><span>index.js</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> rateLimit <span class="keyword">from</span> <span class="string">&#x27;express-rate-limit&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> limiter = <span class="title function_">rateLimit</span>(&#123;</span><br><span class="line">  <span class="attr">windowMs</span>: <span class="number">30_000</span>, <span class="comment">// 30秒</span></span><br><span class="line">  <span class="attr">max</span>: <span class="number">10</span>,</span><br><span class="line">  <span class="attr">message</span>: &#123; <span class="attr">ok</span>:<span class="literal">false</span>, <span class="attr">error</span>: <span class="string">&#x27;rate limited&#x27;</span> &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">app.<span class="title function_">post</span>(<span class="string">&#x27;/api/transfer&#x27;</span>, limiter, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 安全な送金処理</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>これだけで「30秒に10回まで」という制限を導入できます。</p>
<p>短いコードですが、セキュリティ上の効果は絶大です。</p>
<h2 id="まとめ">まとめ</h2><div class="scroll"><table>
<thead>
<tr>
<th align="center">脆弱性</th>
<th align="center">原因</th>
<th align="center">対策</th>
</tr>
</thead>
<tbody><tr>
<td align="center">DOM XSS</td>
<td align="center">v-htmlの乱用</td>
<td align="center">サニタイズ、<code>&#123;&#123; &#125;&#125;</code>で表示</td>
</tr>
<tr>
<td align="center">Cookie漏洩</td>
<td align="center">HttpOnlyなし</td>
<td align="center">HttpOnly, Secure, SameSite設定</td>
</tr>
<tr>
<td align="center">CSRF</td>
<td align="center">GETで副作用、トークン未検証</td>
<td align="center">POST限定、CSRFトークン</td>
</tr>
<tr>
<td align="center">レート制限なし</td>
<td align="center">無限アクセス</td>
<td align="center">express-rate-limit などで制限</td>
</tr>
</tbody></table></div>
<h2 id="さいごに">さいごに</h2><p>Vue.js は便利ですが、安全性を担保する仕組みは「フレームワークまかせ」では不十分です。</p>
<p>「なぜ危ないのか」を一度体験しておくことで、脆弱性への実感を持ってもらえたらうれしいです！</p>
<p>脆弱なアプリを作らないように気をつけながらフロントエンド開発をEnjoyしましょう！</p>
<p><strong>GitHubリポジトリ</strong><br>vue-ctfs（脆弱版と安全版を含む）</p>
]]></content>
    <summary type="html">Vue.jsで実際にわざと脆弱なアプリを作り、そこからXSS・Cookie・CSRF・レート制限といった脆弱性を一気に体験してみます。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="CSRF" scheme="https://future-architect.github.io/tags/CSRF/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="脆弱性" scheme="https://future-architect.github.io/tags/%E8%84%86%E5%BC%B1%E6%80%A7/"/>
  </entry>
  <entry>
    <title>Vueのフロントエンドをセキュリティのしっかりしたコンテナにする</title>
    <link href="https://future-architect.github.io/articles/20251023a/"/>
    <id>https://future-architect.github.io/articles/20251023a/</id>
    <published>2025-10-22T15:00:00.000Z</published>
    <updated>2025-10-22T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251023a/top.jpg" alt="" width="800" height="630">

<p>Vue.js連載です。ライトめなネタです。</p>
<p>Next.jsやNuxt.jsなどのサーバーサイドレンダリング必須なフレームワークであれば、Node.jsと一緒にコンテナ化するか、Vercelなどにデプロイする方法があります。こちらはJavaScriptのウェブアプリケーションなので実行環境を用意する必要があります。</p>
<p>一方、SPAとして作成したVue.jsなど、現代のフレームワークで作成したフロントエンドは、ビルドすると静的HTMLとJavaScriptコードになります。ただし、物理的なファイルが存在しないパスへのリクエストがあった場合にindex.htmlの内容をフォールバックとして返す必要があるため、動作させるにはそのあたりが設定可能なウェブサーバーを使う必要があります。index.htmlをロードするとブラウザ上でJavaScriptのコードが動作しますが、そのコードが内部で持っているURLのパス情報をみて適切なページを表示したり、それでも存在なければJavaScriptがエラー画面を出力します。</p>
<p>フューチャー作のガイドラインのWebフロントエンド設計ガイドラインのSPAのホスティングでは、いくつかホスティング方法を紹介しています。</p>
<ol>
<li>CloudFront+S3</li>
<li>LB+S3</li>
<li>LB+Webサーバー</li>
</ol>
<p>このうち、LB+S3サーバーはSPAで必要なフォールバックができないのでSPA不可となっていますが、最近、ALBでパスのリライトができるようになったので、拡張子がないパスはindex.htmlにリライトとかやれば実はいけるのでは？という気が少ししていますが、それはまたの機会に試そうと思います。</p>
<ul>
<li>classmethodブログ: Application Load Balancer のリスナールールでトランスフォームを構成し、ターゲットにルーティングする前にホストヘッダーや URL パスを書き換えれるようになりました</li>
</ul>
<p>これ以外には、ウェブアプリケーション側に配信機能を持たせてしまうというのも過去に技術ブログで紹介しました。比較的高速なGoとかRustならこれもありでしょう。</p>
<ul>
<li>Go 1.16のembedとchiとSingle Page Application</li>
</ul>
<p>今回はガイドラインではSPA用によいとしている1. 3のうち、Dockerイメージを作った3番目の方法を全力で試そうと思います。</p>
<h2 id="なぜDockerにするか">なぜDockerにするか</h2><p>S3とかオブジェクトストレージにおいて配信というのがお手軽ですが、コンテナにまとめておくことでデプロイ時にまとめてフロントエンド資材を入れ替えたり、戻したりがしやすいのがメリットと考えています。また、CloudFrontはインターネット公開するサービスには良いのですが、社内システムでは使えません。加えて、ビルド済みフロントエンドを軽量なサーバーで配信すればリソース消費は少なくて済みます。開発時もフロントエンドを触らない人がローカルで動作検証するにはありがたいでしょう。</p>
<p>せっかく作るのであればセキュリティを意識したコンテナを目指します。近年、ランサムウェアが流行っています。静的なHTML&#x2F;JSでアプリを作りフロントエンドを配信するだけのコンテナにしてバックエンドをプライベートネットワークの後ろ側に隠すことで攻撃面をかなり狭くできます。ですがHTMLなどが買い替えられると不正なプログラムを配る踏み台にされる可能性があるため、そうならないために次の項目にもチャレンジしてみます。</p>
<ul>
<li>シェルがないDistroless</li>
<li>フロントエンドのartifactは読み込み専用で実行ユーザーでは書き換えられない</li>
</ul>
<h2 id="テスト用アプリケーション作成">テスト用アプリケーション作成</h2><p>Viteの標準的なサンプルです。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">% </span><span class="language-bash">npm create vite@latest</span></span><br><span class="line">Need to install the following packages:</span><br><span class="line">create-vite@8.0.2</span><br><span class="line">Ok to proceed? (y) y</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">npx</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash"><span class="string">&quot;create-vite&quot;</span></span></span><br><span class="line"></span><br><span class="line">│</span><br><span class="line">◇  Project name:</span><br><span class="line">│  sample-app</span><br><span class="line">◇  Select a framework:</span><br><span class="line">│  Vue</span><br><span class="line">◇  Select a variant:</span><br><span class="line">│  TypeScript</span><br><span class="line">:</span><br></pre></td></tr></table></figure>

<p>シングルページアプリケーションとして正しく動作することをテストするために、vue-routerを入れてページをいくつか足します。</p>
<figure class="highlight ts"><figcaption><span>router/index.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createRouter, createWebHistory &#125; <span class="keyword">from</span> <span class="string">&#x27;vue-router&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Home</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/Home.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">About</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/About.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Products</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/Products.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">ProductDetail</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/ProductDetail.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Contact</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/Contact.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">NotFound</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/NotFound.vue&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> routes = [</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Home&#x27;</span>, <span class="attr">component</span>: <span class="title class_">Home</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/about&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;About&#x27;</span>, <span class="attr">component</span>: <span class="title class_">About</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/products&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Products&#x27;</span>, <span class="attr">component</span>: <span class="title class_">Products</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/products/:id&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;ProductDetail&#x27;</span>, <span class="attr">component</span>: <span class="title class_">ProductDetail</span>, <span class="attr">props</span>: <span class="literal">true</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/contact&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;Contact&#x27;</span>, <span class="attr">component</span>: <span class="title class_">Contact</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">path</span>: <span class="string">&#x27;/:pathMatch(.*)*&#x27;</span>, <span class="attr">name</span>: <span class="string">&#x27;NotFound&#x27;</span>, <span class="attr">component</span>: <span class="title class_">NotFound</span> &#125;,</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> router = <span class="title function_">createRouter</span>(&#123;</span><br><span class="line">  <span class="attr">history</span>: <span class="title function_">createWebHistory</span>(),</span><br><span class="line">  routes,</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> router</span><br></pre></td></tr></table></figure>

<p>ルーターを組み込みます。</p>
<figure class="highlight ts"><figcaption><span>main.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="string">&#x27;./style.css&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> router <span class="keyword">from</span> <span class="string">&#x27;./router&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="title function_">createApp</span>(<span class="title class_">App</span>).<span class="title function_">use</span>(router).<span class="title function_">mount</span>(<span class="string">&#x27;#app&#x27;</span>)</span><br></pre></td></tr></table></figure>

<p>アプリケーションのトップのレイアウト側にはページナビゲーションを起きます。</p>
<figure class="highlight html"><figcaption><span>App.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;nav&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">nav</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/&quot;</span>&gt;</span>Home<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span><br><span class="line">      |</span><br><span class="line">      <span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/products&quot;</span>&gt;</span>Products<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span><br><span class="line">      |</span><br><span class="line">      <span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/about&quot;</span>&gt;</span>About<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span><br><span class="line">      |</span><br><span class="line">      <span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/contact&quot;</span>&gt;</span>Contact<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">nav</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">router-view</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-id">#nav</span> &#123; <span class="attribute">margin-bottom</span>: <span class="number">1.5rem</span> &#125;</span></span><br><span class="line"><span class="language-css"><span class="selector-tag">nav</span> <span class="selector-tag">a</span> &#123; <span class="attribute">margin</span>: <span class="number">0</span> <span class="number">0.5rem</span>; <span class="attribute">color</span>: <span class="number">#646cff</span> &#125;</span></span><br><span class="line"><span class="language-css"><span class="selector-tag">nav</span> <span class="selector-tag">a</span><span class="selector-class">.router-link-active</span> &#123; <span class="attribute">font-weight</span>: <span class="number">600</span> &#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>ページを適当に作りました。全部紹介する必要性はあまりないと思うので2つだけ紹介します。</p>
<figure class="highlight html"><figcaption><span>pages/Home.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Home<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span>&gt;</span>Welcome to the SPA home page.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-tag">h1</span> &#123; <span class="attribute">margin-bottom</span>: <span class="number">1rem</span> &#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight html"><figcaption><span>pages/About.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>About<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span>&gt;</span>This is an example About page for the SPA.<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-tag">h1</span> &#123; <span class="attribute">margin-bottom</span>: <span class="number">1rem</span> &#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>npm run dev</code>で動作させて動いたら次はDockerファイルを作っていきます。</p>
<h2 id="Dockerfile作成">Dockerfile作成</h2><h3 id="nginxの設定">nginxの設定</h3><p>Vueアプリができたところで次はサーバーです。Rust製のstatic-web-serverとか安全そうだし良さそうだなとも思ったのですが、APIサーバーへのリクエストをプロキシするような設定がなく、今後も入らなそうということもあり見送りました。このプロキシ機能があればウェブフロントエンドとバックエンドが同じドメイン（ポート番号も含めて）動作するので、CORSを機にする必要がなくなります。もちろん、作ったイメージを本番デプロイするだけならALBがやってくれるはずなのでstatic-web-serverでも良いかと思います。ここはありきたりですがnginxにしておきます。なお、今回はイメージサイズは60MBほどになりました。static-web-serverはシングルバイナリで4MBほどらしいので小ささを極めたい場合はstatic-web-serverで試すと良いでしょう。</p>
<p>設定ファイルとしては、SPAで必要なフォールバックを入れたのと、ログは<code>/var/log</code>とかではなく、コンソールに出力するようにしています。</p>
<p>設定ファイル上に<code>user www-data;</code>と書けばユーザーが設定できます。ただし設定しなくてもワーカーは<code>nobody</code>ユーザーで動作します。最初はセキュリティ強化のためにサーバーは非ルートユーザーで動かすぞ！と設定していたのですが、ワーカーさえルートでなければ実用上は問題ないため、無視底の<code>nobody</code>で十分だと判断しました。なお、別ユーザーで動かすと、特権ポートの1024以下は使えないため、80番ポートでサーバーを動かすことはできなくなります。</p>
<figure class="highlight sh"><figcaption><span>nginx.conf</span></figcaption><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">worker_processes  auto;</span><br><span class="line"></span><br><span class="line">error_log  /dev/stderr info;</span><br><span class="line"></span><br><span class="line">events &#123;</span><br><span class="line">    worker_connections  1024;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line">    include       mime.types;</span><br><span class="line">    default_type  application/octet-stream;</span><br><span class="line"></span><br><span class="line">    sendfile        on;</span><br><span class="line">    keepalive_timeout  65;</span><br><span class="line"></span><br><span class="line">    access_log  /dev/stdout;</span><br><span class="line"></span><br><span class="line">    server &#123;</span><br><span class="line">        listen 80;</span><br><span class="line">        server_name  localhost;</span><br><span class="line"></span><br><span class="line">        root   /usr/share/nginx/html;</span><br><span class="line"></span><br><span class="line">        <span class="comment"># /api/ へのリクエストはバックエンドにプロキシするなど、必要に応じて設定を追加</span></span><br><span class="line">        <span class="comment">#location /api/ &#123;</span></span><br><span class="line">        <span class="comment">#    proxy_pass http://backend-host:3000/;</span></span><br><span class="line">        <span class="comment">#    proxy_set_header Host $host;</span></span><br><span class="line">        <span class="comment">#    proxy_set_header X-Real-IP $remote_addr;</span></span><br><span class="line">        <span class="comment">#    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span></span><br><span class="line">        <span class="comment">#    proxy_set_header X-Forwarded-Proto $scheme;</span></span><br><span class="line">        <span class="comment">#&#125;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment"># 存在しないファイルへのアクセスを index.html にフォールバック</span></span><br><span class="line">        location / &#123;</span><br><span class="line">            try_files <span class="variable">$uri</span> <span class="variable">$uri</span>/ /index.html;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        error_page   500 502 503 504  /50x.html;</span><br><span class="line">        location = /50x.html &#123;</span><br><span class="line">            root   /usr/share/nginx/html;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="Dockerfile">Dockerfile</h3><p>Dockerfileは以下の通りです。最初Geminiに雛形をざっと作ってもらいましたが、いろいろ細かいところを後から修正しました。その際に心がけたポイントは以下の通りです。</p>
<ol>
<li>ビルドステージ<ul>
<li>bindマウント、cacheマウントを駆使してキャッシュフレンドリーな高速ビルド(生成AIはいつもやってくれない)</li>
</ul>
</li>
<li>nginxの設定<ul>
<li>こちらもbindマウント、cacheマウントで効率化</li>
<li>最終イメージのDistrolessはシェルがなくてmkdirとかもできないので、こちらのステージですべての必要なフォルダを作ったり、ユーザーやグループの設定を引っこ抜いたり、ディレクトリの権限を設定したり、nginxの動作に必要ライブラリをコピーしたりも含めて全て行なっています</li>
</ul>
</li>
<li>実行イメージ<ul>
<li>Debianの新しいバージョンのtrixie(13)が使いたい→まだベータ扱いなのでいったん保留</li>
<li>ビルド済みのHTML&#x2F;JSを持ってきたり、nginxの設定を持ってきたり</li>
<li>実行ユーザー(nobody)から書き換えられないユーザーでHTML&#x2F;JSを配置</li>
</ul>
</li>
</ol>
<figure class="highlight dockerfile"><figcaption><span>Dockerfile</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment"># syntax=docker/dockerfile:1</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ARG</span> NODE_VERSION=<span class="number">24.8</span>.<span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="comment"># ステージ 1: ビルドステージ (Vite SPAのビルド)</span></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="keyword">FROM</span> node:$&#123;NODE_VERSION&#125;-slim AS builder</span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> --mount=<span class="built_in">type</span>=<span class="built_in">bind</span>,<span class="built_in">source</span>=package.json,target=package.json \</span></span><br><span class="line"><span class="language-bash">    --mount=<span class="built_in">type</span>=<span class="built_in">bind</span>,<span class="built_in">source</span>=package-lock.json,target=package-lock.json \</span></span><br><span class="line"><span class="language-bash">    --mount=<span class="built_in">type</span>=cache,target=/root/.npm \</span></span><br><span class="line"><span class="language-bash">    --mount=<span class="built_in">type</span>=cache,target=/app/node_modules \</span></span><br><span class="line"><span class="language-bash">    npm ci</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> --mount=<span class="built_in">type</span>=cache,target=/app/node_modules \</span></span><br><span class="line"><span class="language-bash">    npm run build</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="comment"># ステージ 2: nginxの実行ファイルとライブラリの取得</span></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="keyword">FROM</span> debian:bookworm-slim AS nginx-files</span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> --mount=<span class="built_in">type</span>=cache,target=/var/lib/apt/lists \</span></span><br><span class="line"><span class="language-bash">    --mount=<span class="built_in">type</span>=cache,target=/var/cache/apt/archives \</span></span><br><span class="line"><span class="language-bash">    apt-get update \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; apt-get install -y --no-install-recommends nginx-light</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">mkdir</span> -p /tmp/nginx-root/usr/sbin \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">mkdir</span> -p /tmp/nginx-root/etc \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">mkdir</span> -p /tmp/nginx-root/usr/share/nginx/html \</span></span><br><span class="line"><span class="language-bash">    \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">cp</span> -L /usr/sbin/nginx /tmp/nginx-root/usr/sbin/ \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">cp</span> -a /etc/nginx /tmp/nginx-root/etc/ \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">cp</span> -a /etc/passwd /tmp/nginx-root/etc/ \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">cp</span> -a /etc/group /tmp/nginx-root/etc/ \</span></span><br><span class="line"><span class="language-bash">    \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">mkdir</span> -p /tmp/nginx-root/var/lib/nginx/body \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">mkdir</span> -p /tmp/nginx-root/var/cache/nginx/client_temp \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">mkdir</span> -p /tmp/nginx-root/var/run \</span></span><br><span class="line"><span class="language-bash">    \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">chown</span> -R nobody:nogroup /tmp/nginx-root/var/lib/nginx \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">chown</span> -R nobody:nogroup /tmp/nginx-root/var/cache/nginx \</span></span><br><span class="line"><span class="language-bash">    \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">chmod</span> 775 /tmp/nginx-root/var/lib/nginx/body \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">chmod</span> 775 /tmp/nginx-root/var/cache/nginx/client_temp \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">chmod</span> 775 /tmp/nginx-root/var/run</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> LIBS=<span class="string">&quot;<span class="subst">$(ldd /usr/sbin/nginx | grep &#x27;=&gt;&#x27; | awk &#x27;&#123;print $3, $5&#125;&#x27; | sed &#x27;s/not found//g&#x27; | sort -u)</span>&quot;</span> \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="keyword">for</span> lib <span class="keyword">in</span> <span class="variable">$LIBS</span>; <span class="keyword">do</span> \</span></span><br><span class="line"><span class="language-bash">        <span class="keyword">if</span> [ -f <span class="string">&quot;<span class="variable">$lib</span>&quot;</span> ]; <span class="keyword">then</span> \</span></span><br><span class="line"><span class="language-bash">            <span class="built_in">mkdir</span> -p /tmp/nginx-root$(<span class="built_in">dirname</span> <span class="variable">$lib</span>) &amp;&amp; <span class="built_in">cp</span> -L <span class="variable">$lib</span> /tmp/nginx-root/<span class="variable">$lib</span>; \</span></span><br><span class="line"><span class="language-bash">        <span class="keyword">fi</span> \</span></span><br><span class="line"><span class="language-bash">    <span class="keyword">done</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="comment"># ステージ 3: ランタイムステージ (distroless)</span></span><br><span class="line"><span class="comment"># ----------------------------------------------------</span></span><br><span class="line"><span class="keyword">FROM</span> gcr.io/distroless/base-debian12</span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=nginx-files /tmp/nginx-root/ /</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> nginx.conf /etc/nginx/nginx.conf</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --<span class="built_in">chown</span>=root:root --from=builder /app/dist /usr/share/nginx/html</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">80</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">&quot;/usr/sbin/nginx&quot;</span>, <span class="string">&quot;-g&quot;</span>, <span class="string">&quot;daemon off;&quot;</span>]</span></span><br></pre></td></tr></table></figure>

<p>実行するには次のようにします。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">ビルド</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker build -t vue-spa .</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">実行</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run --<span class="built_in">rm</span> -it -p 8080:80 vue-spa</span></span><br></pre></td></tr></table></figure>

<h3 id="デバッグ実行">デバッグ実行</h3><p>設定を変えてみたい場合のデバッグ方法も紹介しておきます。Distrolessではセキュリティのためにシェルがイメージに含まれていませんが、デバッグ用のイメージが提供されています。</p>
<ul>
<li>ランタイムをイメージを<code>gcr.io/distroless/base-debian12</code>から<code>gcr.io/distroless/base-debian12:debug</code>に変更</li>
<li><code>docker run --rm -it -p 8080:80 --entrypoint=sh vue-spa</code> で、shをnginxの代わりに実行</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>Dockerfileは新旧の書き方がウェブには混在しているため、機会をみてはbind&#x2F;cacheを使ったモダンなDockerfileの記法をブログに書くように日頃からしていました。今回はVue連載ということで、Vue製のSPAの静的HTMLのコンテナを作ってみました。単に作るだけでは世の中の有象無象の記事と変わらないので、最新の記法を使ったビルドの効率、実行効率、セキュリティ、どれも妥協しないDockerfileを作りました。Vue以外の方にも参考にしてもらえる記事になったと思います。</p>
<p>明日は松本朝香さんです。</p>
]]></content>
    <summary type="html">Next.jsやNuxt.jsなどのサーバーサイドレンダリング必須なフレームワークであれば、Node.jsと一緒にコンテナ化するか、Vercelなどにデプロイする方法があります。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Docker" scheme="https://future-architect.github.io/tags/Docker/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="コンテナ" scheme="https://future-architect.github.io/tags/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A/"/>
  </entry>
  <entry>
    <title>【入門】Vueで始めるチャート分析</title>
    <link href="https://future-architect.github.io/articles/20251020a/"/>
    <id>https://future-architect.github.io/articles/20251020a/</id>
    <published>2025-10-19T15:00:00.000Z</published>
    <updated>2025-10-19T15:00:00.000Z</updated>
    <author><name>中村立基</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、フューチャーアーキテクト株式会社 テクノロジーイノベーショングループ所属の中村立基です。</p>
<p>Vue.js連載ということで、この記事ではVue.jsアプリケーションにチャート分析機能を実装するための、初心者向けの包括的なガイドを提供します。代表的な2つのライブラリ、vue-chartjsとvue-echartsを取り上げ、その概要から具体的な使い方、メリット・デメリット、そして実運用で考慮すべき点を解説していきます。</p>
<h2 id="チャート分析とは">チャート分析とは</h2><p>現代のビジネス環境は、かつてないほど膨大なデータに溢れています。しかし、生データの集合体は、それ自体が価値を持つわけではありません。その真価は、データを解釈し、実用的な洞察を引き出すことで初めて発揮されます。このデータ活用の「ラストワンマイル」を担うのが、データ可視化、すなわちチャート分析です。</p>
<p>伝統的に、チャート分析という言葉は金融市場で使われ、過去の価格変動パターンから将来の値動きを予測する手法を指してきました。現代のWebアプリケーションにおけるチャート分析は、<code>「どの製品カテゴリが最も売れているか？」</code> <code>「直近四半期のユーザーエンゲージメントの傾向は？」</code>といったビジネス上の問いに対して、視覚的に答えを提示します。これにより、ユーザーは数値の羅列からは見出すことのできない傾向、パターン、相関関係、異常値を直感的に発見できるようになります。</p>
<p>専門のアナリストに頼らずとも、現場の担当者が自らの手で日常的な疑問を解決し、データに基づいた行動を起こせるようにすること、それが現代のWebアプリケーションにおけるチャート分析の価値です。</p>
<h2 id="チャート分析のメリット">チャート分析のメリット</h2><p>アプリケーションにチャート分析機能を組み込むことは、単に見た目を良くする以上に下記の様なビジネスにおけるメリットがあります。</p>
<ul>
<li><em><strong>傾向とパターンの視覚的な把握</strong></em><ul>
<li>生データをグラフなどで視覚化することで、変化に気づきやすい（異常値や新たなトレンドに気が付きやすい）</li>
</ul>
</li>
<li><em><strong>共通認識の醸成と連携強化</strong></em><ul>
<li>同じチャートを利害関係者全員で見ることで、共通認識を持ち円滑にコミュニケーションを取れる</li>
</ul>
</li>
</ul>
<h2 id="Vueでの利用方法">Vueでの利用方法</h2><p>このセクションでは、<code>vue-chartjs</code>と<code>vue-echarts</code>の導入方法と使い方を、サンプルコードを交えて紹介します。</p>
<h3 id="1-vue-chartjs">1. vue-chartjs</h3><p>vue-chartjsは、JavaScriptのチャートライブラリChart.jsをVueで手軽に利用するためのラッパーライブラリです。標準的なチャートを迅速に実装できるシンプルさが特徴です。</p>
<h4 id="導入方法">導入方法</h4><p>vue-chartjsはchart.js本体に依存しているため、両方をインストールする必要があります。これはPeerDependency（仲間依存）と呼ばれる関係性です。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm install vue-chartjs chart.js</span><br></pre></td></tr></table></figure>

<h4 id="使い方">使い方</h4><p>vue-chartjsを使用する際の重要なポイントは、vue-chartjsからチャートコンポーネント（例：Bar）をインポートするだけでなく、chart.js本体からも必要なモジュール（スケール、要素、ツールチップなど）をインポートし、ChartJS.register()メソッドで登録することです。<br>（これはChart.jsのTree Shaking（不要なコードをバンドルから除外する仕組み）に対応するための必須のステップであり、つまずきやすい点でもあります。）</p>
<p>以下は、棒グラフを作成する基本的なコンポーネントの例です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&quot;height: 400px;&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Bar</span> <span class="attr">id</span>=<span class="string">&quot;my_chart_id&quot;</span> <span class="attr">:options</span>=<span class="string">&quot;chartOptions&quot;</span> <span class="attr">:data</span>=<span class="string">&quot;chartData&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">Bar</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vue-chartjs&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">Chart</span> <span class="keyword">as</span> <span class="title class_">ChartJS</span>, <span class="title class_">Title</span>, <span class="title class_">Tooltip</span>, <span class="title class_">Legend</span>, <span class="title class_">BarElement</span>, <span class="title class_">CategoryScale</span>, <span class="title class_">LinearScale</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;chart.js&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 必要なモジュールをChart.jsに登録</span></span></span><br><span class="line"><span class="language-javascript"><span class="title class_">ChartJS</span>.<span class="title function_">register</span>(<span class="title class_">Title</span>, <span class="title class_">Tooltip</span>, <span class="title class_">Legend</span>, <span class="title class_">BarElement</span>, <span class="title class_">CategoryScale</span>, <span class="title class_">LinearScale</span>)</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">name</span>: <span class="string">&#x27;BarChart&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">components</span>: &#123; <span class="title class_">Bar</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">data</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">return</span> &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="attr">chartData</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="attr">labels</span>: [<span class="string">&#x27;1月&#x27;</span>, <span class="string">&#x27;2月&#x27;</span>, <span class="string">&#x27;3月&#x27;</span>, <span class="string">&#x27;4月&#x27;</span>, <span class="string">&#x27;5月&#x27;</span>],</span></span><br><span class="line"><span class="language-javascript">        <span class="attr">datasets</span>: [</span></span><br><span class="line"><span class="language-javascript">          &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">label</span>: <span class="string">&#x27;データセット1&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">backgroundColor</span>: <span class="string">&#x27;#f87979&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">data</span>: [<span class="number">5</span>, <span class="number">10</span>, <span class="number">3</span>, <span class="number">7</span>, <span class="number">2</span>]</span></span><br><span class="line"><span class="language-javascript">          &#125;</span></span><br><span class="line"><span class="language-javascript">        ]</span></span><br><span class="line"><span class="language-javascript">      &#125;,</span></span><br><span class="line"><span class="language-javascript">      <span class="attr">chartOptions</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="attr">responsive</span>: <span class="literal">true</span>,</span></span><br><span class="line"><span class="language-javascript">        <span class="attr">maintainAspectRatio</span>: <span class="literal">false</span>,</span></span><br><span class="line"><span class="language-javascript">        <span class="attr">plugins</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">          <span class="attr">title</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">display</span>: <span class="literal">true</span>,</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">text</span>: <span class="string">&#x27;月別データ&#x27;</span></span></span><br><span class="line"><span class="language-javascript">          &#125;</span></span><br><span class="line"><span class="language-javascript">        &#125;</span></span><br><span class="line"><span class="language-javascript">      &#125;</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>表示例</p>
<img fetchpriority="high" src="/images/2025/20251020a/sample画像1.png" alt="sample画像1.png" width="1200" height="273">

<h4 id="データ構造">データ構造</h4><p>vue-chartjsのコンポーネントは、主にdataとoptionsという2つのpropsを受け取ります。</p>
<ul>
<li>chartDataオブジェクト: チャートに表示するデータを定義します<ul>
<li>labels: X軸の各項目を表す文字列の配列です (例: [‘1月’, ‘2月’, ‘3月’])</li>
<li>datasets: グラフにプロットするデータ系列の配列です。各オブジェクトが1つのデータ系列に対応します<ul>
<li>label: データ系列の名前（凡例に表示されます）</li>
<li>backgroundColor: 棒グラフの色などを指定します</li>
<li>data: labelsの各項目に対応する数値の配列です</li>
</ul>
</li>
</ul>
</li>
<li>chartOptionsオブジェクト: チャートの外観や挙動をカスタマイズするための設定です<ul>
<li>responsive: trueに設定すると、親要素のサイズ変更に応じてチャートがリサイズされます</li>
<li>maintainAspectRatio: falseに設定することが多く、これにより親要素の高さと幅に合わせて柔軟にリサイズできます</li>
<li>plugins: タイトル(title)や凡例(legend)、ツールチップ(tooltip)などのプラグインに関する設定をここで行います</li>
</ul>
</li>
</ul>
<h3 id="2-vue-echarts">2. vue-echarts</h3><p>vue-echartsは、Apache EChartsをVueで利用するためのラッパーです。EChartsは、非常に多機能でカスタマイズ性が高く、大規模データセットの描画パフォーマンスにも優れたライブラリです。</p>
<h4 id="導入方法-1">導入方法</h4><p>echarts本体とvue-echartsをインストールします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm install echarts vue-echarts</span><br></pre></td></tr></table></figure>

<h4 id="使い方：サンプルコード">使い方：サンプルコード</h4><p>vue-echartsの最大の特徴は、チャートのすべての設定を単一のoptionオブジェクトに集約して渡す点です。また、バンドルサイズを最適化するために、必要なモジュールのみをインポートしてuse()関数で登録する「オンデマンドインポート」が推奨されています。</p>
<p>以下は、vue-chartjsの例と同じ棒グラフをvue-echartsで作成するサンプルコードです。（Composition APIを使用）</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">v-chart</span> <span class="attr">class</span>=<span class="string">&quot;chart&quot;</span></span></span><br><span class="line"><span class="tag">           <span class="attr">:option</span>=<span class="string">&quot;chartOption&quot;</span></span></span><br><span class="line"><span class="tag">           <span class="attr">:autoresize</span>=<span class="string">&quot;true&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">v-chart</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; use &#125; <span class="keyword">from</span> <span class="string">&#x27;echarts/core&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">CanvasRenderer</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;echarts/renderers&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; <span class="title class_">BarChart</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;echarts/charts&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">TitleComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">TooltipComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">LegendComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">GridComponent</span></span></span><br><span class="line"><span class="language-javascript">&#125; <span class="keyword">from</span> <span class="string">&#x27;echarts/components&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> <span class="title class_">VChart</span> <span class="keyword">from</span> <span class="string">&#x27;vue-echarts&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; ref &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 必要なモジュールを登録</span></span></span><br><span class="line"><span class="language-javascript"><span class="title function_">use</span>([</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">CanvasRenderer</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">BarChart</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">TitleComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">TooltipComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">LegendComponent</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="title class_">GridComponent</span></span></span><br><span class="line"><span class="language-javascript">])</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> chartOption = <span class="title function_">ref</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">title</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">text</span>: <span class="string">&#x27;月別データ&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">left</span>: <span class="string">&#x27;center&#x27;</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">tooltip</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">trigger</span>: <span class="string">&#x27;axis&#x27;</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">legend</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">data</span>: [<span class="string">&#x27;データセット1&#x27;</span>],</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">bottom</span>: <span class="number">10</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">grid</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">left</span>: <span class="string">&#x27;3%&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">right</span>: <span class="string">&#x27;4%&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">bottom</span>: <span class="string">&#x27;10%&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">containLabel</span>: <span class="literal">true</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">xAxis</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">type</span>: <span class="string">&#x27;category&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">data</span>: [<span class="string">&#x27;1月&#x27;</span>, <span class="string">&#x27;2月&#x27;</span>, <span class="string">&#x27;3月&#x27;</span>, <span class="string">&#x27;4月&#x27;</span>, <span class="string">&#x27;5月&#x27;</span>]</span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">yAxis</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">type</span>: <span class="string">&#x27;value&#x27;</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">series</span>: [</span></span><br><span class="line"><span class="language-javascript">    &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="attr">name</span>: <span class="string">&#x27;データセット1&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">      <span class="attr">type</span>: <span class="string">&#x27;bar&#x27;</span>,</span></span><br><span class="line"><span class="language-javascript">      <span class="comment">// eslint-disable-next-line no-magic-numbers</span></span></span><br><span class="line"><span class="language-javascript">      <span class="attr">data</span>: [<span class="number">5</span>, <span class="number">10</span>, <span class="number">3</span>, <span class="number">7</span>, <span class="number">2</span>]</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">  ]</span></span><br><span class="line"><span class="language-javascript">&#125;)</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-class">.chart</span> &#123;</span></span><br><span class="line"><span class="language-css">  <span class="attribute">height</span>: <span class="number">400px</span>;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>表示例</p>
<img src="/images/2025/20251020a/sample画像2.png" alt="sample画像2.png" width="1200" height="274" loading="lazy">

<h4 id="データ構造-1">データ構造</h4><p>vue-echartsでは、すべての設定がoptionオブジェクトに集約されます 。</p>
<ul>
<li>chartOptionオブジェクト:<ul>
<li>title: チャートのタイトルに関する設定です (text、leftなど)</li>
<li>tooltip: マウスオーバー時に表示されるツールチップの設定です</li>
<li>legend: 凡例に関する設定です</li>
<li>grid: チャート描画領域のグリッドに関する設定です（位置や余白など）</li>
<li>xAxis: X軸に関する設定です (type: ‘category’でカテゴリ軸、dataにラベル配列を指定)</li>
<li>yAxis: Y軸に関する設定です (type: ‘value’で数値軸)</li>
<li>series: データ系列の配列です。各オブジェクトが1つの系列に対応します<ul>
<li>name: 系列名</li>
<li>type: チャートの種類 (‘bar’, ‘line’など)</li>
<li>data: 数値データの配列</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="チャート表示例">チャート表示例</h2><p>上記で紹介した以外のvue-chartjsで利用できる代表的なチャートは以下です。</p>
<p>※画像は vue-chartjs公式ガイドラインより引用</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">チャート種別</th>
<th align="left">ユースケース</th>
<th align="left">表示例</th>
</tr>
</thead>
<tbody><tr>
<td align="left">折れ線グラフ</td>
<td align="left">・月間売上高の追跡<br>・株価の推移<br>・Webサイトのトラフィック分析</td>
<td align="left"><img src="/images/2025/20251020a/line.png" alt="line.png" width="584" height="176" loading="lazy"></td>
</tr>
<tr>
<td align="left">レーダーチャート</td>
<td align="left">・個人のスキル評価<br>・製品の機能比較<br>・チームのパフォーマンス分析</td>
<td align="left"><img src="/images/2025/20251020a/rader.png" alt="rader.png" width="439" height="335" loading="lazy"></td>
</tr>
<tr>
<td align="left">円グラフ</td>
<td align="left">・市場シェアの表示<br>・予算の内訳<br>・アンケートの回答比率</td>
<td align="left"><img src="/images/2025/20251020a/pie.png" alt="pie.png" width="453" height="314" loading="lazy"></td>
</tr>
<tr>
<td align="left">ドーナツチャート</td>
<td align="left">円グラフに主要な数値を強調したい場合</td>
<td align="left"><img src="/images/2025/20251020a/doughnut.png" alt="doughnut.png" width="473" height="309" loading="lazy"></td>
</tr>
<tr>
<td align="left">散布図</td>
<td align="left">・広告費と売上の関係分析<br>・身長と体重の関係<br>・従業員の経験年数と生産性の相関分析</td>
<td align="left"><img src="/images/2025/20251020a/scatter.png" alt="scatter.png" width="590" height="171" loading="lazy"></td>
</tr>
<tr>
<td align="left">バブルチャート</td>
<td align="left">・プロジェクトのコスト／期間<br>・期待収益の比較<br>・製品の売上／利益率／市場規模の分析</td>
<td align="left"><img src="/images/2025/20251020a/Bubble.png" alt="Bubble.png" width="576" height="168" loading="lazy"></td>
</tr>
<tr>
<td align="left">ポーラーエリアチャート</td>
<td align="left">・月ごとの平均気温<br>・時間帯別のウェブサイトトラフィック<br>・曜日ごとの売上パターン</td>
<td align="left"><img src="/images/2025/20251020a/polararea.png" alt="polararea.png" width="542" height="308" loading="lazy"></td>
</tr>
<tr>
<td align="left">ミックスチャート</td>
<td align="left">・売上（棒グラフ）と利益率（折れ線グラフ）の同時表示<br>・降水量（棒グラフ）と気温（折れ線グラフ）の比較</td>
<td align="left"><img src="/images/2025/20251020a/custom.png" alt="custom.png" width="1200" height="273" loading="lazy"></td>
</tr>
</tbody></table></div>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>vue-echartsでは上記に加え下記の様な高度なチャートも実装可能です。</p>
<ul>
<li><p>ヒートマップ</p>
<img src="/images/2025/20251020a/70991434-0706-4bb0-8407-2b5d4cd51b12.png" alt="" width="962" height="512" loading="lazy">
</li>
<li><p>樹形図</p>
<img src="/images/2025/20251020a/fb860d7f-a17f-423e-a735-e1cb3cbfd032.png" alt="" width="995" height="842" loading="lazy"></li>
</ul>
</div></div>

<h3 id="チャートライブラリ毎のメリット-デメリット">チャートライブラリ毎のメリット&#x2F;デメリット</h3><p>両者の長所と短所を比較します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th></th>
<th align="left">vue-chartjs（Chart.jsベース）</th>
<th align="left">vue-echarts (Apache EChartsベース)</th>
</tr>
</thead>
<tbody><tr>
<td>学習コスト</td>
<td align="left">⭕️ 初学者でも利用しやすい</td>
<td align="left">❌️ 設定可能なオプションが膨大で学習コスト高</td>
</tr>
<tr>
<td>機能バリエーション</td>
<td align="left">❌️ 標準的なチャート種別のみ</td>
<td align="left">⭕️ 高度なチャート表現が可能</td>
</tr>
<tr>
<td>カスタマイズ性</td>
<td align="left">❌️ 少ない</td>
<td align="left">⭕️ 高い</td>
</tr>
<tr>
<td>軽量性</td>
<td align="left">⭕️ 高い</td>
<td align="left">❌️ 低い</td>
</tr>
<tr>
<td>参考ドキュメント</td>
<td align="left">⭕️ 比較的豊富</td>
<td align="left">❌️ 少ない</td>
</tr>
</tbody></table></div>
<h3 id="ライブラリ利用時の考慮事項">ライブラリ利用時の考慮事項</h3><p>チャート機能を実装するにあたって、堅牢かつユーザーフレンドリーに構築するためには、いくつかの重要な点を考慮する必要があります。</p>
<h4 id="パフォーマンス">パフォーマンス</h4><ul>
<li><strong>遅延読み込み (Lazy Loading)</strong><ul>
<li>多くのチャートを含むページでは、VueのdefineAsyncComponentを使用してチャートコンポーネントを遅延読み込みすることで、初期バンドルサイズが小さく保たれ、チャートが必要になったときに初めてそのJavaScriptがロードされるため、初期表示速度が向上します。</li>
</ul>
</li>
<li><strong>リアクティビティの管理</strong><ul>
<li>非常に大規模で、かつ変更されることのない静的なデータセットを扱う場合、Vueのデータとして渡す前にObject.freeze()でオブジェクトを凍結させることで、Vueはそのオブジェクトをリアクティブにするためのオーバーヘッドをスキップし、メモリ使用量とCPU負荷を大幅に削減できます。</li>
</ul>
</li>
<li><strong>アニメーションの無効化</strong><ul>
<li>リアルタイムでデータが更新されるチャートや、非常に大規模なデータセットを描画する場合、アニメーションを無効にすることで描画パフォーマンスが大幅に向上します。</li>
</ul>
</li>
<li><strong>データの間引き</strong><ul>
<li>500px幅のチャートに10,000点のデータをプロットするなど、ピクセル解像度に対してデータ点が多すぎる場合、描画するデータ点を間引く機能を利用することで、描画負荷を軽減し、パフォーマンスを向上させることができます。</li>
</ul>
</li>
</ul>
<h4 id="レスポンシブデザイン">レスポンシブデザイン</h4><p>canvas要素に描画されるチャートは、CSS要素のように本質的にレスポンシブではありません。その応答性は、親コンテナのサイズに依存します。常にチャートコンポーネントを <code>&lt;div&gt;</code> ような自然言語のプロンプトから、前項までで紹介しで囲み、そのdivに対してCSS（パーセンテージ、Flexbox、Gridなど）でレスポンシブなスタイルを適用することがベストプラクティスです。ライブラリは親コンテナのサイズ変更を検知し、チャートを適切に再描画します。</p>
<h4 id="アクセシビリティ">アクセシビリティ</h4><p>アクセシビリティは現代のWeb開発において不可欠な要素です。特にチャートの可視化においては、視覚的な情報にアクセスできないユーザーへの配慮が重要になります。</p>
<h5 id="canvas要素の課題">canvas要素の課題</h5><p><code>&lt;canvas&gt;</code>要素は、スクリーンリーダーなどの支援技術にとっては「ブラックボックス」であり、そのままでは内容を読み取ることができません。</p>
<ul>
<li>vue-chartjsでの対策<ul>
<li><code>&lt;canvas&gt;</code> 要素にrole&#x3D;”img”と、チャートの内容を要約した説明的なaria-labelを付与します。または、 <code>&lt;canvas&gt;</code> タグの内部に、データの表などの代替コンテンツを提供します</li>
<li>chartjs-plugin-a11y-legendのようなプラグインを使用して、凡例をキーボードで操作できるようにします</li>
</ul>
</li>
<li>vue-echartsでの対策<ul>
<li>EChartsは、より優れたアクセシビリティ機能を標準で備えています。オプションでaria: { show: true }を設定するだけで、EChartsがチャートとその要素に対してインテリジェントに説明的なラベルを生成します</li>
</ul>
</li>
</ul>
<h5 id="スクリーンリーダー以外の考慮事項">スクリーンリーダー以外の考慮事項</h5><ul>
<li><strong>色のコントラスト</strong><ul>
<li>すべてのテキストやデータ系列の色が、WCAG（Web Content Accessibility Guidelines）のコントラスト比の基準を満たしていることを確認します。</li>
</ul>
</li>
<li><strong>色のみに依存しない情報伝達</strong><ul>
<li>色覚多様性を持つユーザーのために、色だけで情報を伝えないようにします。EChartsがサポートするデカールパターンや、データラベル、凡例などを併用して、データ系列を区別できるように設計します。</li>
</ul>
</li>
<li><strong>キーボードナビゲーション</strong><ul>
<li>ツールチップの表示、凡例の切り替え、ズーム操作など、チャートのすべてのインタラクティブな要素がキーボードのみで操作可能であることを保証します。</li>
</ul>
</li>
</ul>
<h2 id="発展：AIを活用したチャート生成によるデータ分析">発展：AIを活用したチャート生成によるデータ分析</h2><p>本テーマから逸れる為、ここではサンプルコードの紹介等は省略して概要の提示に留めますが、今まで紹介してきたVue.jsにおけるデータ可視化の発展形として、AIを活用したチャート生成が注目されています。</p>
<p>例えば<code>「地域別の月間売上を棒グラフで表示して」</code>のような自然言語のプロンプトから、前項までで紹介したチャートで活用できるデータセットを自動で生成し、<code>vue-chartjs</code>のようなライブラリで描画する手法です。</p>
<p>こういったAI技術を活用することで、自前でデータセットを作成するより効率的に複雑かつ膨大なデータを視覚化し、分析できるようになりました。今後AI技術と組み合わせたライブラリの利用はデータ分析においてより重要性を増していくと考えています。</p>
<h2 id="まとめ">まとめ</h2><p>この記事では、Vue.jsでチャート分析を実装するための具体的な手法までを解説しました。vue-chartjsとvue-echartsという2つの代表的なライブラリを取り上げ、それぞれの特徴と使い方をご紹介しました。<br>ライブラリ利用にあたってはプロジェクトの特性に応じて使い分けることを推奨します。</p>
<ul>
<li><code>vue-chartjs</code>は、シンプルさ、開発の速さ、そして標準的なチャート要件が求められる場合に最適な選択です。プロトタイピングや、基本的なダッシュボード機能の実装に力を発揮します。</li>
<li><code>vue-echarts</code>は、複雑な要件、高度なカスタマイズ性、豊かなインタラクティビティ、そして大規模なデータセットを扱う必要がある場合にその真価を発揮します。本格的なデータ分析アプリケーションや、独自のビジュアル表現が求められる場面で適しています。</li>
</ul>
<p>個人的にですが、昨今爆発的に性能発展が進む生成AI技術によって、新しい洞察やデータ分析のプロセスにおいて、よりデータ可視化の有用性が増してきていると感じており、こういったWebアプリケーションのライブラリと上手く組み合わせることで、プロダクトの価値向上に繋がると思っています。</p>
<p>ぜひ興味を持ってくださった方はチャートを用いたデータ分析にTryしてみてください。</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>Vue.js 公式ドキュメント</li>
<li>vue-chartjs 公式ドキュメント</li>
<li>Chart.js 公式ドキュメント</li>
<li>vue-echarts GitHubリポジトリ</li>
<li>Apache ECharts 公式ドキュメント</li>
</ul>
]]></content>
    <summary type="html">Vue.jsアプリケーションにチャート分析機能を実装するための、初心者向けの包括的なガイドを提供します。代表的な2つのライブラリ、vue-chartjsとvue-echartsを取り上げ、その概要から具体的な使い方、メリット・デメリット、そして実運用で考慮すべき点を解説していきます。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="可視化" scheme="https://future-architect.github.io/tags/%E5%8F%AF%E8%A6%96%E5%8C%96/"/>
  </entry>
  <entry>
    <title>Vue製のUIをStorybook+Vitest Browser Modeでテストする</title>
    <link href="https://future-architect.github.io/articles/20251017a/"/>
    <id>https://future-architect.github.io/articles/20251017a/</id>
    <published>2025-10-16T15:00:00.000Z</published>
    <updated>2025-10-16T15:00:00.000Z</updated>
    <author><name>小杉山護</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251017a/testing-you-can-trust.png" alt="" width="800" height="640">

<p>Vue.js連載の2本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは。TIGでフロントエンドをガリガリやっている小杉山です。</p>
<p>昨今では生成AIを用いたコーディング、<strong>Vibe Coding</strong>が盛んに行われていますね。人間の代わりに生成AIがコードを書いてくれるというのは非常に魅力的ですが、その過程で生み出されるコードの品質はまだまだ発展途上の段階にあります。</p>
<p>そのため、必要最低限の品質を担保するガードレールとしての役割を果たす<strong>テストコード</strong>、そしてその<strong>自動化</strong>の重要性が非常に高まってきています。</p>
<p>今回はVue.js連載ということもあり、Vue.jsで記述されたUIコンポーネントや画面のテスト基盤を構築する選択肢の1つとして、<strong>StorybookとVitest Browser Modeを組み合わせたテスト手法</strong>を簡単に紹介します。</p>
<h2 id="最近のStorybook">最近のStorybook</h2><p>Storybookはフロントエンド開発の生産性を高める素晴らしいツールです。以前はUIコンポーネントを一覧表示する「コンポーネントカタログ」としての利用が主流でした。</p>
<p>しかし、近年のStorybookはテスト関連の機能を大幅に強化しています。その中心的な機能の1つが、今回紹介する<strong>Vitest</strong>との連携です。この連携により、フロントエンド開発において信頼性の高いテストを効率よく進めていくことが可能になりました。</p>
<p>この「信頼性の高いテスト」を実現するための重要な要素の1つが、後述するVitest Browser Modeです。</p>
<h2 id="Vitest-Browser-Mode">Vitest Browser Mode</h2><p>従来JestやVitestを使用してフロントエンドのテストをする際、DOMに関してはjsdomやhappy-domを使用することで擬似的なブラウザ環境を再現していました。</p>
<p>しかしあくまで「擬似的な」ブラウザ環境なので、<code>window</code>オブジェクトや<code>ResizeObserver</code>などのブラウザAPIはモックした上でテストしなければなりませんでした。</p>
<p>そんな時、VitestにBrowser Mode (<strong>Vitest Browser Mode</strong>)という機能が実装されました。</p>
<p>この機能によりテストコードの実行環境が擬似ブラウザ環境から（ユーザが使用する）ChromeやFirefoxといった実際のブラウザそのものに置き換わるので、より信頼性の高いテストが実施できます。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>VitestからPlaywrightやWebdriverIOといったE2Eテストツールを呼び出すことでブラウザを起動しています。</p>
</div></div>

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>Vitest Browser Modeは活用事例が増えつつあるものの、公式的にはまだ実験的な機能です。採用の検討は慎重に行なってください。またVitest Browser Modeはブラウザを起動するので、jsdomやhappy-domを使用している場合に比べてテスト実行時間が伸びてしまうことに注意が必要です。</p>
</div></div>

<h2 id="検証コード">検証コード</h2><p>テスト対象のUI、Storyファイル、そしてテストコードをざっくりと見ていきます。</p>
<h3 id="リポジトリ">リポジトリ</h3><p>検証コードはこちらのGitHubリポジトリにあります。</p>
<p>https://github.com/koralle/mock-webapp-with-vue-for-writing-article</p>
<h3 id="Vueコンポーネント">Vueコンポーネント</h3><p>今回はテスト対象の画面として、以下の依存関係で構成される非常に簡易的なログイン画面を用意しました。<br>メールアドレスとパスワードを入力して「ログイン」ボタンを押下すると、ログインフォームの下部に「ログインに成功しました。」というメッセージを表示します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left"></th>
<th align="left">名称</th>
<th align="left">バージョン</th>
</tr>
</thead>
<tbody><tr>
<td align="left">スタイリング</td>
<td align="left">Tailwind CSS</td>
<td align="left">v4.x</td>
</tr>
<tr>
<td align="left">フォームバリデーション</td>
<td align="left">VeeValidate</td>
<td align="left">v4.x</td>
</tr>
<tr>
<td align="left">スキーマ</td>
<td align="left">Zod</td>
<td align="left">v4.x</td>
</tr>
</tbody></table></div>
<figure class="highlight html"><figcaption><span>LoginPage.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> * <span class="keyword">as</span> z <span class="keyword">from</span> <span class="string">&#x27;zod&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> <span class="title class_">Button</span> <span class="keyword">from</span> <span class="string">&#x27;../components/Button.vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> <span class="title class_">Input</span> <span class="keyword">from</span> <span class="string">&#x27;../components/Input.vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; useForm &#125; <span class="keyword">from</span> <span class="string">&#x27;vee-validate&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; toTypedSchema &#125; <span class="keyword">from</span> <span class="string">&#x27;@vee-validate/zod&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; ref, useId &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> hasCompletedSubmit = <span class="title function_">ref</span>(<span class="literal">false</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> loginSchema = z.<span class="title function_">object</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">email</span>: z.<span class="title function_">email</span>(<span class="string">&#x27;有効なメールアドレスを入力してください。&#x27;</span>),</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">password</span>: z</span></span><br><span class="line"><span class="language-javascript">    .<span class="title function_">string</span>(<span class="string">&#x27;パスワードは必須です。&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript">    .<span class="title function_">max</span>(<span class="number">128</span>, <span class="string">&#x27;パスワードは128文字以内で入力してください。&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript">    .<span class="title function_">regex</span>(<span class="regexp">/^[a-zA-Z0-9]+$/</span>, <span class="string">&#x27;パスワードは半角英数字で入力してください。&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript">&#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; defineField, errors, handleSubmit &#125; = <span class="title function_">useForm</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">validationSchema</span>: <span class="title function_">toTypedSchema</span>(loginSchema)</span></span><br><span class="line"><span class="language-javascript">&#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> [email, emailAttrs] = <span class="title function_">defineField</span>(<span class="string">&#x27;email&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> [password, passwordAttrs] = <span class="title function_">defineField</span>(<span class="string">&#x27;password&#x27;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> onSubmit = <span class="title function_">handleSubmit</span>(<span class="function">() =&gt;</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  hasCompletedSubmit.<span class="property">value</span> = <span class="literal">true</span>;</span></span><br><span class="line"><span class="language-javascript">&#125;);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> emailId = <span class="title function_">useId</span>();</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> passwordId = <span class="title function_">useId</span>();</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">form</span></span></span><br><span class="line"><span class="tag">    <span class="attr">class</span>=<span class="string">&quot;grid gap-10&quot;</span></span></span><br><span class="line"><span class="tag">    @<span class="attr">submit</span>=<span class="string">&quot;onSubmit&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">fieldset</span> <span class="attr">class</span>=<span class="string">&quot;grid gap-6&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:for</span>=<span class="string">&quot;emailId&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">class</span>=<span class="string">&quot;grid gap-1&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">span</span>&gt;</span>メールアドレス<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Input</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:id</span>=<span class="string">&quot;emailId&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">name</span>=<span class="string">&quot;email&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">required</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:invalid</span>=<span class="string">&quot;Boolean(errors.email)&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">autocomplete</span>=<span class="string">&quot;email&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-model</span>=<span class="string">&quot;email&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-bind</span>=<span class="string">&quot;emailAttrs&quot;</span></span></span><br><span class="line"><span class="tag">        /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;text-red-500&quot;</span>&gt;</span>&#123;&#123; errors.email &#125;&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line"></span><br><span class="line">      <span class="tag">&lt;<span class="name">label</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:for</span>=<span class="string">&quot;passwordId&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">class</span>=<span class="string">&quot;grid gap-1&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">span</span>&gt;</span>パスワード<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Input</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:id</span>=<span class="string">&quot;passwordId&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">name</span>=<span class="string">&quot;password&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">required</span></span></span><br><span class="line"><span class="tag">          <span class="attr">type</span>=<span class="string">&quot;password&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:invalid</span>=<span class="string">&quot;Boolean(errors.password)&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">autocomplete</span>=<span class="string">&quot;current-password&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-model</span>=<span class="string">&quot;password&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-bind</span>=<span class="string">&quot;passwordAttrs&quot;</span></span></span><br><span class="line"><span class="tag">        /&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">&quot;text-red-500&quot;</span>&gt;</span>&#123;&#123; errors.password &#125;&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">fieldset</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span> <span class="attr">label</span>=<span class="string">&quot;ログイン&quot;</span> /&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">form</span>&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span></span></span><br><span class="line"><span class="tag">    <span class="attr">v-if</span>=<span class="string">&quot;hasCompletedSubmit&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">class</span>=<span class="string">&quot;font-bold&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span></span><br><span class="line">    ログインに成功しました。</span><br><span class="line">  <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<img src="/images/2025/20251017a/login-screen.png" class="img-frame-line" alt="login-screen.png" width="800" height="800" loading="lazy">

<h3 id="Storyファイル">Storyファイル</h3><p>先述のログイン画面をStorybook上で管理するためのStoryファイルはこちらです。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>簡単のため、一部のStoryは省略しています。</p>
</div></div>

<figure class="highlight ts"><figcaption><span>LoginPage.stories.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">Meta</span>, <span class="title class_">StoryObj</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;@storybook/vue3-vite&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; expect, within, userEvent &#125; <span class="keyword">from</span> <span class="string">&#x27;storybook/test&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">LoginPage</span> <span class="keyword">from</span> <span class="string">&#x27;../pages/LoginPage.vue&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> meta = &#123;</span><br><span class="line">  <span class="attr">title</span>: <span class="string">&#x27;画面 / ログイン画面&#x27;</span>,</span><br><span class="line">  <span class="attr">component</span>: <span class="title class_">LoginPage</span></span><br><span class="line">&#125; <span class="keyword">satisfies</span> <span class="title class_">Meta</span>&lt;<span class="keyword">typeof</span> <span class="title class_">LoginPage</span>&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> meta;</span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Story</span> = <span class="title class_">StoryObj</span>&lt;<span class="keyword">typeof</span> meta&gt;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">InitialState</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;初期表示&#x27;</span></span><br><span class="line">&#125; <span class="keyword">satisfies</span> <span class="title class_">Story</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">LoginCompleted</span> = &#123;</span><br><span class="line">  <span class="attr">name</span>: <span class="string">&#x27;ログイン成功&#x27;</span>,</span><br><span class="line">  <span class="attr">play</span>: <span class="title function_">async</span> (&#123; canvasElement, step &#125;) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> canvas = <span class="title function_">within</span>(canvasElement);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">step</span>(<span class="string">&#x27;メールアドレスを入力する&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> emailInput = <span class="keyword">await</span> canvas.<span class="title function_">findByRole</span>(<span class="string">&#x27;textbox&#x27;</span>, &#123; <span class="attr">name</span>: <span class="string">&#x27;メールアドレス&#x27;</span> &#125;);</span><br><span class="line">      <span class="keyword">await</span> userEvent.<span class="title function_">type</span>(emailInput, <span class="string">&#x27;frontend@example.com&#x27;</span>, &#123; <span class="attr">delay</span>: <span class="number">100</span> &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">step</span>(<span class="string">&#x27;パスワードを入力する&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> passwordInput = <span class="keyword">await</span> canvas.<span class="title function_">findByLabelText</span>(<span class="string">&#x27;パスワード&#x27;</span>);</span><br><span class="line">      <span class="keyword">await</span> userEvent.<span class="title function_">type</span>(passwordInput, <span class="string">&#x27;password&#x27;</span>, &#123; <span class="attr">delay</span>: <span class="number">100</span> &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title function_">step</span>(<span class="string">&#x27;「ログイン」ボタンをクリックする&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">const</span> submitButton = <span class="keyword">await</span> canvas.<span class="title function_">findByRole</span>(<span class="string">&#x27;button&#x27;</span>, &#123; <span class="attr">name</span>: <span class="string">&#x27;ログイン&#x27;</span> &#125;);</span><br><span class="line">      <span class="keyword">await</span> userEvent.<span class="title function_">click</span>(submitButton);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125; <span class="keyword">satisfies</span> <span class="title class_">Story</span>;</span><br></pre></td></tr></table></figure>

<p>「ログイン成功（<code>LoginCompleted</code>）」というStoryは文字通り「正常にログインが完了できる」というユーザシナリオを想定しており、そのユーザシナリオに沿った画面操作をPlay Functionで記述しています。</p>
<p>Storybookを起動すると以下の画像のようになります。</p>
<p>「ログイン成功（<code>LoginCompleted</code>）」のStoryを開くと以下の画面操作が自動で実行され、ログイン後の画面状態を確認できます。</p>
<ol>
<li>メールアドレスの入力</li>
<li>パスワードの入力</li>
<li>「ログイン」ボタンの押下</li>
</ol>
<img src="/images/2025/20251017a/output-login-story.png" alt="output-login-story.png" width="1200" height="626" loading="lazy">

<h2 id="Storybook-Vitest-Browser-ModeでVueのテストを書いてみよう">Storybook + Vitest Browser ModeでVueのテストを書いてみよう</h2><p>先述の「ユーザが正常にログインが完了できるシナリオ」を、StorybookとVitest Browser Modeを使ってテストしてみます。</p>
<p>テストコードは以下の手順となるように書きます。</p>
<ol>
<li>Storyファイルに定義したPlay FunctionをVitestのテストコード上で再利用し、ブラウザ上でユーザ操作する</li>
<li>手順1の結果として得られるDOMの状態を検証する</li>
</ol>
<p>Storyオブジェクトの<code>run()</code>メソッドを実行することで、Play Functionに記述した画面操作がブラウザ上で自動的に実行されます。これが手順1です。</p>
<p>手順2では、Vitest Browser ModeのAPIを用いて画面操作後のDOM状態を検証します。</p>
<p>これを実現するVitestのテストコードがこちらです。</p>
<figure class="highlight ts"><figcaption><span>LoginPage.browser.test.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; composeStories &#125; <span class="keyword">from</span> <span class="string">&#x27;@storybook/vue3-vite&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; page &#125; <span class="keyword">from</span> <span class="string">&#x27;@vitest/browser/context&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> <span class="title class_">LoginPageStories</span> <span class="keyword">from</span> <span class="string">&#x27;../stories/LoginPage.stories&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> composedStories = <span class="title function_">composeStories</span>(<span class="title class_">LoginPageStories</span>);</span><br><span class="line"></span><br><span class="line"><span class="title function_">describe</span>(<span class="string">&#x27;LoginPage&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="title function_">describe</span>(<span class="string">&#x27;`ログイン成功`Storyに対するテスト&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123; <span class="title class_">LoginCompleted</span> &#125; = composedStories;</span><br><span class="line"></span><br><span class="line">    <span class="title function_">test</span>(<span class="string">&#x27;ログインに成功すること&#x27;</span>, <span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="comment">// ログイン操作の実行</span></span><br><span class="line">      <span class="keyword">await</span> <span class="title class_">LoginCompleted</span>.<span class="title function_">run</span>();</span><br><span class="line"></span><br><span class="line">      <span class="comment">// ログイン操作の実行結果を検証する</span></span><br><span class="line">      <span class="keyword">const</span> completedMessage = page.<span class="title function_">getByText</span>(<span class="string">&#x27;ログインに成功しました。&#x27;</span>);</span><br><span class="line">      <span class="keyword">await</span> expect.<span class="title function_">element</span>(completedMessage).<span class="title function_">toBeVisible</span>();</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>あとはターミナル上で<code>npx vitest</code>を実行することでテストを実行できます。</p>
<h2 id="まとめ">まとめ</h2><p>今回紹介したテスト手法の採用には以下のようなメリットがあります。</p>
<ul>
<li>jsdomやhappy-domを利用した擬似ブラウザ環境の代わりに実際のブラウザでテストを実行するため、より本番に近い環境でコンポーネントの動作を検証できる</li>
<li>StorybookのPlay Functionで定義したユーザシナリオをそのままテストコードとして再利用できるため、テスト実装の効率が大幅に向上する</li>
</ul>
<p>ただし、前述の通りVitest Browser Modeはまだ実験的な機能です。</p>
<p>導入の際はその点を考慮し、動作確認をしながら慎重に進めてください。</p>
]]></content>
    <summary type="html">昨今では生成AIを用いたコーディング、Vibe Codingが盛んに行われていますね。人間の代わりに生成AIがコードを書いてくれるというのは非常に魅力的ですが、その過程で生み出されるコードの品質はまだまだ発展途上の段階にあります。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vite" scheme="https://future-architect.github.io/tags/Vite/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
  </entry>
  <entry>
    <title>Vue.js + vee-validate + Zod（+ shadcn/vue + @tanstack/vue-query）での実践的なフォーム開発</title>
    <link href="https://future-architect.github.io/articles/20251016b/"/>
    <id>https://future-architect.github.io/articles/20251016b/</id>
    <published>2025-10-15T15:00:01.000Z</published>
    <updated>2025-10-15T15:00:01.000Z</updated>
    <author><name>長谷川寛人</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251016b/top.jpg" alt="" width="600" height="600">

<p>Vue.js連載の1本目です。</p>
<h2 id="はじめに">はじめに</h2><p>私は普段Reactを触ることが多く、フォーム開発では<code>react-hook-form</code> + <code>zod</code>(+ <code>shadcn/ui</code>)の組み合わせでの開発をよく行なっていました。</p>
<p>今回Vueでプロジェクトを進めるにあたり、vee-validateを使ってフォーム開発を行ないました。コンポーネントやZodとの組み合わせに試行錯誤したことに加え、アクセシビリティ対応にも力を入れたので、そこで得た知見を共有いたします。</p>
<p><strong>ここで紹介する内容を理解すれば、ほとんどのフォーム実装に対応できるはずです。</strong> より複雑なケースでも、この記事の内容を組み合わせたり派生させることで実現できると思います。ぜひ参考にしてみてください！</p>
<p>また、以下にサンプルとなるリポジトリを用意しましたので、こちらもご参照ください。</p>
<p>https://github.com/hasehiro0828/sample-vue-form</p>
<h2 id="技術スタック概要">技術スタック概要</h2><h3 id="vee-validate">vee-validate</h3><p>https://vee-validate.logaretm.com/v4/</p>
<p>Vue.js向けのフォームライブラリです。Zodなどとの統合が容易で、Composition APIとの親和性も高いです。</p>
<p>Reactの場合はフォームライブラリにいくつか選択肢がある印象ですが、<strong>Vueの場合は基本的にvee-validateを使うことになりそう</strong>です（2025年10月時点）。</p>
<h3 id="Zod">Zod</h3><p>https://zod.dev/</p>
<p>TypeScript-firstなスキーマ定義ライブラリです。型安全性を担保しながらランタイムバリデーションを実現でき、フレームワークによらず同じように利用できます。</p>
<h3 id="vue-query">vue-query</h3><p>https://tanstack.com/query/latest/docs/framework/vue/overview</p>
<p>サーバーステートの管理を行なうライブラリで、Tanstack QueryのVue版です。実践的なアプリケーションを見据えて導入しましたが、今回のメイン要素ではないので詳細は割愛します。</p>
<h3 id="shadcn-vue">shadcn-vue</h3><p>https://www.shadcn-vue.com/</p>
<p>Tailwind CSSベースのUIコンポーネント集で、Reactのshadcn&#x2F;uiのVue版です。こちらも今回のメイン要素ではないので詳細は割愛します。</p>
<p>取得したデータを <code>watch</code> で <code>resetForm</code> することで、フォームの初期値を設定します。</p>
<h2 id="アクセシビリティ対応">アクセシビリティ対応</h2><p>今回のプロジェクトではアクセシビリティ対応にも力を入れました。デジタル庁のデザインシステムを参考に、日付入力の実装やボタン要素の扱い（<code>disabled</code>属性を利用しない）など、アクセシビリティの観点から工夫を行なっています。</p>
<p>本記事では<strong>とくに試行錯誤した日付入力の実装について後述します。</strong></p>
<h2 id="vee-validateの基本方針">vee-validateの基本方針</h2><p>vee-validateはさまざまなコンポーネントやAPIを提供してくれています。それらを利用するにあたり、利用するもの・利用しないものの基本方針を明確にしておくことで、チーム開発での一貫性を保ち、実装の迷いを減らすことができます。<br>今回は以下のような基本方針を設けて実装を行ないました。</p>
<h3 id="利用するもの">利用するもの</h3><ul>
<li><strong>Fieldコンポーネント</strong><ul>
<li>単一のフォームフィールドを扱う際の基本となるコンポーネント</li>
<li><code>v-slot</code>でfield、meta、errorMessageを受け取り、入力欄とバリデーション結果を紐づけ</li>
<li><code>&lt;script&gt;</code>内でフィールドの値を直接操作する必要がない場合に使用</li>
</ul>
</li>
<li><strong>ErrorMessageコンポーネント</strong><ul>
<li>フォーム全体やグループ全体に対するエラーメッセージの表示に利用</li>
<li>相関チェックの結果を親要素に設定するようなバリデーションエラーの表示などに適している（例: 日付の存在チェックなど）</li>
</ul>
</li>
<li><strong>useFieldArray</strong><ul>
<li>動的に追加・削除可能な配列形式のフィールドを管理するComposable関数</li>
<li><code>fields</code>、<code>push</code>、<code>remove</code>などの関数を提供し、配列形式のフィールドの操作を行なえる</li>
<li>FieldArrayコンポーネントではなくこちらを使用する理由は、<code>push</code>や<code>remove</code>などの操作を<code>&lt;script&gt;</code>内で柔軟に扱えるため</li>
</ul>
</li>
<li><strong>useForm</strong><ul>
<li>フォーム全体の状態管理を行なうComposable関数</li>
<li>バリデーションスキーマの設定、送信処理、フォームの値やエラー状態の取得などを行なう</li>
</ul>
</li>
<li><strong>useField</strong><ul>
<li>複数の入力欄を持つフォーム機能を統合したカスタムコンポーネントを作成する際に使用</li>
<li>基本的にはFieldコンポーネントを使用する方がシンプルだが、複数のinputを持つカスタムコンポーネントを作成するような場合は<code>useField</code>でそれぞれのフィールドを個別に定義する方が見通しが良くなる</li>
</ul>
</li>
</ul>
<h3 id="利用しないもの">利用しないもの</h3><ul>
<li><strong>FieldArrayコンポーネント</strong><ul>
<li>配列の操作（追加・削除）を<code>&lt;script&gt;</code>内で行なうケースが多く、<code>useFieldArray</code>の方が柔軟性が高い</li>
</ul>
</li>
<li><strong>defineField from useForm</strong><ul>
<li><code>meta</code>（touched、dirtyなど）が使えない？みたいなので利用用途がわからず・・</li>
<li><code>useField</code>を使って正しく動作したので利用しなくて良さそう・・？</li>
</ul>
</li>
</ul>
<h3 id="具体的な実装例">具体的な実装例</h3><p>以降では、上記の基本方針に基づいた具体的な実装例を紹介していきます。</p>
<h4 id="Fieldを使った基本的な入力">Fieldを使った基本的な入力</h4><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">Field</span> <span class="attr">v-slot</span>=<span class="string">&quot;&#123; field, meta, errorMessage &#125;&quot;</span> <span class="attr">name</span>=<span class="string">&quot;name&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;flex flex-col gap-2&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">label</span> <span class="attr">:for</span>=<span class="string">&quot;nameId&quot;</span>&gt;</span></span><br><span class="line">      名前</span><br><span class="line">      <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;text-red-600 bg-red-100 px-1 py-0.5 rounded-md text-xs ml-1&quot;</span>&gt;</span>必須<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Input</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:id</span>=<span class="string">&quot;nameId&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:model-value</span>=<span class="string">&quot;field.value&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">type</span>=<span class="string">&quot;text&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:aria-invalid</span>=<span class="string">&quot;shouldShowError(submitCount, meta)&quot;</span></span></span><br><span class="line"><span class="tag">      @<span class="attr">update:model-value</span>=<span class="string">&quot;field.onChange&quot;</span></span></span><br><span class="line"><span class="tag">      @<span class="attr">blur</span>=<span class="string">&quot;field.onBlur&quot;</span></span></span><br><span class="line"><span class="tag">    /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-if</span>=<span class="string">&quot;shouldShowError(submitCount, meta)&quot;</span> <span class="attr">class</span>=<span class="string">&quot;text-sm text-red-600&quot;</span>&gt;</span></span><br><span class="line">      &#123;&#123; errorMessage &#125;&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">Field</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>シンプルな単一フィールドです。<br>テキスト入力やテキストエリア、セレクトボックスなど、1つの入力欄で完結するフィールドに使用します。</p>
<p><strong>ポイント</strong>:</p>
<ul>
<li><code>field.value</code>で現在の値を取得</li>
<li><code>field.onChange</code>で値の変更を検知</li>
<li><code>field.onBlur</code>でフォーカスが外れたことを検知</li>
<li><code>shouldShowError</code>でバリデーションエラーを表示するかを判定</li>
<li><code>errorMessage</code>でバリデーションエラーメッセージを取得</li>
</ul>
<p>これらは以降のフォーム実装でも同様です。</p>
<h4 id="useFieldArrayを使った配列管理">useFieldArrayを使った配列管理</h4><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; fields, remove, push &#125; = <span class="title function_">useFieldArray</span>(<span class="string">&quot;conditions&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="title function_">addCondition</span> = (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">push</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="comment">// 初期値</span></span></span><br><span class="line"><span class="language-javascript">  &#125;);</span></span><br><span class="line"><span class="language-javascript">&#125;;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-for</span>=<span class="string">&quot;(condition, idx) in fields&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;condition.key&quot;</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- ネストした配列は別コンポーネントで管理 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ConditionalParams</span> <span class="attr">:index</span>=<span class="string">&quot;idx&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span> @<span class="attr">click</span>=<span class="string">&quot;() =&gt; remove(idx)&quot;</span>&gt;</span>削除<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>動的に追加・削除可能なフィールドです。<br>ユーザー操作で追加・削除できるような入力に使用します。</p>
<p><strong>ポイント</strong>:</p>
<ul>
<li><code>push</code>で配列に新しい要素を追加</li>
<li><code>remove</code>で指定したインデックスの要素を削除</li>
<li>v-forのkeyには<code>&#123;array&#125;.key</code>（インデックスではなくkey）を使用</li>
</ul>
<h4 id="ネストした配列要素はコンポーネント分離する">ネストした配列要素はコンポーネント分離する</h4><p>配列の中に配列があるような場合には、ネストされる配列を別コンポーネントに分離すると扱いやすくなります。<br>サンプルでは以下のような構造にしています。</p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">conditions（配列）← NestForm.vue で管理</span><br><span class="line">  └── params（配列）← ConditionalParams.vue で管理</span><br></pre></td></tr></table></figure>

<h5 id="親コンポーネント（NestForm-vue）">親コンポーネント（NestForm.vue）</h5><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 外側の配列を管理</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; <span class="attr">fields</span>: conditionFields, remove, push &#125; = <span class="title function_">useFieldArray</span>(<span class="string">&quot;conditions&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-for</span>=<span class="string">&quot;(condition, conditionIdx) in conditionFields&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;condition.key&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;mb-2&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;font-bold&quot;</span>&gt;</span>&#123;&#123; condition.value.readonly.title &#125;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;text-gray-500 text-sm&quot;</span>&gt;</span>&#123;&#123; condition.value.readonly.description &#125;&#125;<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- ネストした配列の管理は子コンポーネントで行う --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ConditionalParams</span> <span class="attr">:index</span>=<span class="string">&quot;conditionIdx&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span> @<span class="attr">click</span>=<span class="string">&quot;() =&gt; remove(conditionIdx)&quot;</span>&gt;</span>条件を削除<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h5 id="子コンポーネント（ConditionalParams-vue）">子コンポーネント（ConditionalParams.vue）</h5><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> props = defineProps&lt;&#123; <span class="attr">index</span>: number &#125;&gt;();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 親から受け取ったindexを使ってパスを構築</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> paramKey = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> <span class="string">`conditions[<span class="subst">$&#123;<span class="built_in">String</span>(props.index)&#125;</span>].params`</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; <span class="attr">fields</span>: params, remove, push &#125; = useFieldArray&lt;<span class="title class_">Param</span>&gt;(paramKey.<span class="property">value</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> <span class="title function_">handleAddParam</span> = (<span class="params"></span>) =&gt; &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="title function_">push</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">type</span>: <span class="string">&quot;text&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">value</span>: &#123; <span class="attr">text</span>: <span class="string">&quot;&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">required</span>: <span class="literal">true</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">readonly</span>: &#123; <span class="attr">title</span>: <span class="string">&quot;&quot;</span>, <span class="attr">description</span>: <span class="string">&quot;&quot;</span> &#125;</span></span><br><span class="line"><span class="language-javascript">  &#125;);</span></span><br><span class="line"><span class="language-javascript">&#125;;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-for</span>=<span class="string">&quot;(param, paramIdx) in params&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;param.key&quot;</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- paramの内容を表示/編集 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-if</span>=<span class="string">&quot;param.value.type === &#x27;text&#x27;&quot;</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">Field</span> <span class="attr">:name</span>=<span class="string">&quot;`$&#123;paramKey&#125;[$&#123;paramIdx&#125;].value.text`&quot;</span> <span class="attr">v-slot</span>=<span class="string">&quot;&#123; field &#125;&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Input</span> <span class="attr">v-model</span>=<span class="string">&quot;field.value&quot;</span> /&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">Field</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Button</span> @<span class="attr">click</span>=<span class="string">&quot;() =&gt; remove(paramIdx)&quot;</span>&gt;</span>パラメータ削除<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">Button</span> @<span class="attr">click</span>=<span class="string">&quot;handleAddParam&quot;</span>&gt;</span>パラメータを追加<span class="tag">&lt;/<span class="name">Button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>この設計のメリット</strong>:</p>
<ul>
<li><code>useFieldArray</code>を使用する際にパス名が必要だが、コンポーネント分離することでindexを渡すことが可能に</li>
<li>ネストが深くなっても各コンポーネントは自分が管理する要素だけを見ればいいので、コードが読みやすくなる</li>
</ul>
<p>この構成は3階層以上の深いネストにも応用できます。各階層をコンポーネントに分離することで個々のコンポーネントの複雑性を増すことなく、複雑なフォームでも保守性の高いコードを維持できます。</p>
<p>ちなみに・・・<br>nameの値は <code>hoge[index]</code> の形式と <code>hoge.index</code> の形式のどちらでも可能なようです。ただ、<code>errors</code>から値を取得する際には <code>hoge[index]</code>の形式で取得する必要があるため、基本的には <code>hoge[index]</code> の形式で指定するのが良さそうです。</p>
<h4 id="useFieldを使ったカスタムコンポーネント">useFieldを使ったカスタムコンポーネント</h4><figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> props = defineProps&lt;&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">namePrefix</span>: string;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">type</span>: <span class="string">&quot;date&quot;</span> | <span class="string">&quot;month&quot;</span> | <span class="string">&quot;year&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">&#125;&gt;();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// 個別のフィールドを定義</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> yearField = useField&lt;string&gt;(<span class="string">`<span class="subst">$&#123;props.namePrefix&#125;</span>.year`</span>);</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> monthField = useField&lt;string&gt;(<span class="string">`<span class="subst">$&#123;props.namePrefix&#125;</span>.month`</span>);</span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> dayField = useField&lt;string&gt;(<span class="string">`<span class="subst">$&#123;props.namePrefix&#125;</span>.day`</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="comment">// フォーム全体のエラーを取得</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; errors &#125; = <span class="title function_">useFormContext</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> rootErrorMessage = <span class="title function_">computed</span>(<span class="function">() =&gt;</span> errors.<span class="property">value</span>[props.<span class="property">namePrefix</span>]);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">AppDateInput</span></span></span><br><span class="line"><span class="tag">    <span class="attr">:year</span>=<span class="string">&quot;yearField.value.value&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">:month</span>=<span class="string">&quot;monthField.value.value&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">:day</span>=<span class="string">&quot;dayField.value.value&quot;</span></span></span><br><span class="line"><span class="tag">    @<span class="attr">blur:year</span>=<span class="string">&quot;yearField.handleBlur&quot;</span></span></span><br><span class="line"><span class="tag">    @<span class="attr">blur:month</span>=<span class="string">&quot;monthField.handleBlur&quot;</span></span></span><br><span class="line"><span class="tag">    @<span class="attr">blur:day</span>=<span class="string">&quot;dayField.handleBlur&quot;</span></span></span><br><span class="line"><span class="tag">  /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>複数の入力欄で1つのコンポーネントとするような場合に使用します。<br>とくに、日付入力（年・月・日）のように、個別のフィールドでもバリデーションが必要かつ全体としてもバリデーションが必要な場合に適しています。</p>
<p><strong>ポイント</strong>:</p>
<ul>
<li>各フィールドを個別に<code>useField</code>で定義することで、年・月・日それぞれのバリデーションが可能</li>
<li><code>useFormContext</code>でフォーム全体のエラー情報を取得し、親要素のエラー（例:「年月日をすべて入力してください」）を取得<ul>
<li>（<code>useField</code>でルートの要素を定義すると上手く動作しなかったのでこの形に）</li>
</ul>
</li>
<li>カスタムコンポーネント内で複数フィールドを統合し、外部からは1つのコンポーネントとして扱えるように</li>
</ul>
<h4 id="エラー表示のタイミング">エラー表示のタイミング</h4><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">submittedOrTouchedAndDirty</span> = (<span class="params"><span class="attr">submitCount</span>: <span class="built_in">number</span>, <span class="attr">meta</span>: &#123; touched: <span class="built_in">boolean</span>; dirty: <span class="built_in">boolean</span> &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> submitCount &gt; <span class="number">0</span> || (meta.<span class="property">touched</span> &amp;&amp; meta.<span class="property">dirty</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title function_">shouldShowError</span> = (<span class="params"><span class="attr">submitCount</span>: <span class="built_in">number</span>, <span class="attr">meta</span>: &#123; touched: <span class="built_in">boolean</span>; dirty: <span class="built_in">boolean</span>; valid: <span class="built_in">boolean</span> &#125;</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">submittedOrTouchedAndDirty</span>(submitCount, meta) &amp;&amp; !meta.<span class="property">valid</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>エラー表示の条件は <code>meta.valid</code> 以外にも以下のような条件を設けています。</p>
<ul>
<li><strong><code>touched &amp;&amp; dirty</code>:</strong> フィールドにフォーカスを当て（touched）、かつ値を変更した（dirty）後にエラーを表示</li>
<li><strong><code>submitCount &gt; 0</code>:</strong> 送信ボタンを押した後は、すべてのフィールドのエラーを表示<ul>
<li>エラーがなくなるまでボタンを<code>disabled</code>にする実装も考えられるが、アクセシビリティを考慮し<code>disabled</code>を使用しない形にしている</li>
<li>その場合<code>touched &amp;&amp; dirty</code> のみだとsubmitしてもエラーが表示されないため、この条件を追加</li>
</ul>
</li>
</ul>
<p>以上のようにすることで、ユーザーが何も操作していない状態でエラーが表示されることや入力中にエラーが表示されてしまうことを避けられます。また、送信ボタンを押した際にはすべてのフィールドのエラーが表示されます。</p>
<h2 id="Zodスキーマ設計">Zodスキーマ設計</h2><h3 id="typeフィールドによる分岐">typeフィールドによる分岐</h3><p>今回、配列形式かつ種類の違うフィールドを動的に管理する必要がありました。これを解決するために、typeフィールドによるスキーマの分岐を行なっています。こうすることで、画面表示の分岐を自動的に行ないつつ型安全にデータを扱えるようになります。</p>
<p>以下のように<code>base</code>のスキーマを拡張し<code>type</code>で分岐させるように実装しています。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> baseParamSchema = z.<span class="title function_">object</span>(&#123;</span><br><span class="line">  <span class="attr">required</span>: z.<span class="title function_">boolean</span>(),</span><br><span class="line">  <span class="attr">type</span>: z.<span class="title function_">unknown</span>(), <span class="comment">// 各サブスキーマで具体的な型に</span></span><br><span class="line">  <span class="attr">value</span>: z.<span class="title function_">unknown</span>(), <span class="comment">// 各サブスキーマで具体的な型に</span></span><br><span class="line">  <span class="attr">readonly</span>: z.<span class="title function_">object</span>(&#123;</span><br><span class="line">    <span class="comment">// フォームで入力しない値</span></span><br><span class="line">    <span class="attr">title</span>: z.<span class="title function_">string</span>(),</span><br><span class="line">    <span class="attr">description</span>: z.<span class="title function_">string</span>(),</span><br><span class="line">  &#125;),</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> textParamSchema = baseParamSchema.<span class="title function_">extend</span>(&#123;</span><br><span class="line">  <span class="attr">type</span>: z.<span class="title function_">literal</span>(<span class="string">&quot;text&quot;</span>), <span class="comment">// 👈 typeで分岐</span></span><br><span class="line">  <span class="attr">value</span>: z.<span class="title function_">object</span>(&#123;</span><br><span class="line">    <span class="attr">text</span>: z.<span class="title function_">string</span>(),</span><br><span class="line">  &#125;),</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> dateParamSchema = baseParamSchema.<span class="title function_">extend</span>(&#123;</span><br><span class="line">  <span class="attr">type</span>: z.<span class="title function_">literal</span>(<span class="string">&quot;date&quot;</span>), <span class="comment">// 👈 typeで分岐</span></span><br><span class="line">  <span class="attr">value</span>: zodDateValue,</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Union型で統合</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> paramSchema = z.<span class="title function_">union</span>([</span><br><span class="line">  textParamSchema,</span><br><span class="line">  dateParamSchema,</span><br><span class="line">  monthParamSchema,</span><br><span class="line">  yearParamSchema,</span><br><span class="line">  dateRangeParamSchema,</span><br><span class="line">  monthRangeParamSchema,</span><br><span class="line">  yearRangeParamSchema,</span><br><span class="line">]);</span><br></pre></td></tr></table></figure>

<p>コンポーネント側では以下のように利用します。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span> <span class="attr">v-if</span>=<span class="string">&quot;param.value.type === &#x27;text&#x27;&quot;</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- param.value.value.text が型安全にアクセス可能 --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">Input</span> <span class="attr">v-model</span>=<span class="string">&quot;param.value.value.text&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span> <span class="attr">v-else-if</span>=<span class="string">&quot;param.value.type === &#x27;date&#x27;&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">DateInputField</span> <span class="attr">:name-prefix</span>=<span class="string">&quot;`$&#123;paramKey&#125;[$&#123;paramIdx&#125;].value`&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h3 id="readonlyフィールド（フォームで入力しない値）の活用">readonlyフィールド（フォームで入力しない値）の活用</h3><p>フォームでは入力しない値もスキーマに含めて定義することにしています。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="attr">readonly</span>: z.<span class="title function_">object</span>(&#123;</span><br><span class="line">  <span class="attr">title</span>: z.<span class="title function_">string</span>(),        <span class="comment">// フィールドのラベル</span></span><br><span class="line">  <span class="attr">description</span>: z.<span class="title function_">string</span>(),  <span class="comment">// フィールドの説明</span></span><br><span class="line">&#125;),</span><br></pre></td></tr></table></figure>

<p>この構成にしておくと、フォームデータと表示用データを1つのオブジェクトで管理できます。とくに配列操作において恩恵が大きく、追加・削除する際に表示情報も一緒に管理でき、画面表示やAPI送信時のデータの扱いが楽になります。<br>一緒に管理しない場合、フォームの入力とAPIから取得したデータを突き合わせる必要が出てきてしまうのでこのようにしました。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 配列に要素を追加する際、表示情報も一緒に管理</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleAddParam</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="title function_">push</span>(&#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&quot;text&quot;</span>,</span><br><span class="line">    <span class="attr">value</span>: &#123; <span class="attr">text</span>: <span class="string">&quot;&quot;</span> &#125;,</span><br><span class="line">    <span class="attr">required</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">readonly</span>: &#123;</span><br><span class="line">      <span class="attr">title</span>: <span class="string">&quot;新しいフィールド&quot;</span>,</span><br><span class="line">      <span class="attr">description</span>: <span class="string">&quot;説明文&quot;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<p>フォームの入力値との区別がつきやすいように <code>readonly</code> の下に定義することとしましたが、これは議論の余地があるかもしれません。</p>
<h3 id="グループ入力のバリデーション">グループ入力のバリデーション</h3><p><code>refine</code>を使って複数のinputを組み合わせたチェックを行なっています。個々のinputではなくルートにエラーメッセージを登録し、画面側の表示ロジックを工夫することでエラーの優先順位を調整しています。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> dateParamSchema = baseParamSchema</span><br><span class="line">  .<span class="title function_">extend</span>(&#123;</span><br><span class="line">    <span class="attr">type</span>: z.<span class="title function_">literal</span>(<span class="string">&quot;date&quot;</span>),</span><br><span class="line">    <span class="attr">value</span>: zodDateValue,</span><br><span class="line">  &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(</span><br><span class="line">    <span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (!data.<span class="property">required</span>) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">      <span class="keyword">return</span> data.<span class="property">value</span>.<span class="property">year</span> &amp;&amp; data.<span class="property">value</span>.<span class="property">month</span> &amp;&amp; data.<span class="property">value</span>.<span class="property">day</span>;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">message</span>: <span class="string">&quot;日付を入力してください&quot;</span>,</span><br><span class="line">      <span class="attr">path</span>: [<span class="string">&quot;value&quot;</span>],</span><br><span class="line">    &#125;</span><br><span class="line">  );</span><br></pre></td></tr></table></figure>

<p>コンポーネントのエラー表示は以下のようにしています。<br>ルートのエラーを優先し、ルートのエラーがない場合は個別のフィールドのエラーを表示するようにしました。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;text-sm text-red-600&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-if</span>=<span class="string">&quot;shouldShowRootError&quot;</span>&gt;</span></span><br><span class="line">    &#123;&#123; rootErrorMessage &#125;&#125;</span><br><span class="line">  <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">template</span> <span class="attr">v-else</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-if</span>=<span class="string">&quot;shouldShowError(submitCount, yearField.meta)&quot;</span>&gt;</span></span><br><span class="line">      &#123;&#123; yearField.errorMessage.value &#125;&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-else-if</span>=<span class="string">&quot;shouldShowError(submitCount, monthField.meta)&quot;</span>&gt;</span></span><br><span class="line">      &#123;&#123; monthField.errorMessage.value &#125;&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span> <span class="attr">v-else-if</span>=<span class="string">&quot;shouldShowError(submitCount, dayField.meta)&quot;</span>&gt;</span></span><br><span class="line">      &#123;&#123; dayField.errorMessage.value &#125;&#125;</span><br><span class="line">    <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h2 id="日付入力の実装について">日付入力の実装について</h2><p>デジタル庁のデザインシステムでは、年月日を個別の入力フィールドに分けることが推奨されており、今回はそれに準拠した形で実装を行ないました。<br>しかし、この方式を採用すると、以下の点で実装が複雑になります。</p>
<ul>
<li>3つのフィールドを1つのバリデーション対象として扱う必要もあり、個別のエラーとグループ全体のエラーを使い分ける必要がある</li>
<li>3つのフィールド全体からフォーカスが外れたことを検知するフォーカス管理も必要</li>
</ul>
<p>この複雑さに対処するため、上で実装した<code>useField</code>を使ったカスタムコンポーネントを実装しています。</p>
<p>この章では、上では触れなかった部分についてもう少し補足します。</p>
<h3 id="type-number-ではなく-type-text-を利用する"><code>type=&quot;number&quot;</code> ではなく <code>type=&quot;text&quot;</code> を利用する</h3><p>年月日の入力には<code>type=&quot;text&quot;</code>を採用しています。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">input</span></span></span><br><span class="line"><span class="tag">  <span class="attr">v-model</span>=<span class="string">&quot;yearModel&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> &lt;!<span class="attr">--</span> <span class="attr">type</span>=<span class="string">&quot;number&quot;</span> <span class="attr">ではない</span> <span class="attr">--</span>&gt;</span></span><br><span class="line">/&gt;</span><br></pre></td></tr></table></figure>

<p>理由は以下の通りです。</p>
<ul>
<li><strong>ゼロ埋め対応</strong><ul>
<li><code>type=&quot;number&quot;</code>では先頭のゼロが自動的に削除されてしまう</li>
<li>しかし日付では「03月」「09日」のようにゼロ埋めした入力を許容したい</li>
</ul>
</li>
<li><strong>そもそもの用途の違い</strong><ul>
<li><code>type=&quot;number&quot;</code>は本来、数量や個数など計算対象となる数値のためのもの</li>
<li>HTML Standardでも「数字のみで構成されているが、厳密には数値ではない入力には<code>type=&quot;number&quot;</code>の使用は適さない」といったことが記載されている</li>
<li><blockquote>
<p>The type&#x3D;number state is not appropriate for input that happens to only consist of numbers but isn’t strictly speaking a number</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="日付入力のZodスキーマ定義">日付入力のZodスキーマ定義</h3><p>日付入力の具体的なZodスキーマ実装の一部を紹介します。<br>年月日の各要素は以下のように定義することで、textとして入力しつつ数値としての妥当性をチェックしています。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> zodYear = z</span><br><span class="line">  .<span class="title function_">string</span>()</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || !<span class="built_in">isNaN</span>(<span class="title class_">Number</span>(val)), &#123; <span class="attr">message</span>: <span class="string">&quot;年は数値で入力してください&quot;</span> &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &gt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">year</span>.<span class="property">min</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`年は<span class="subst">$&#123;DATE_CONFIG.year.min&#125;</span>以上で入力してください`</span>,</span><br><span class="line">  &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &lt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">year</span>.<span class="property">max</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`年は<span class="subst">$&#123;DATE_CONFIG.year.max&#125;</span>以下で入力してください`</span>,</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> zodMonth = z</span><br><span class="line">  .<span class="title function_">string</span>()</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || !<span class="built_in">isNaN</span>(<span class="title class_">Number</span>(val)), &#123; <span class="attr">message</span>: <span class="string">&quot;月は数値で入力してください&quot;</span> &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &gt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">month</span>.<span class="property">min</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`月は<span class="subst">$&#123;DATE_CONFIG.month.min&#125;</span>以上で入力してください`</span>,</span><br><span class="line">  &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &lt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">month</span>.<span class="property">max</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`月は<span class="subst">$&#123;DATE_CONFIG.month.max&#125;</span>以下で入力してください`</span>,</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> zodDay = z</span><br><span class="line">  .<span class="title function_">string</span>()</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || !<span class="built_in">isNaN</span>(<span class="title class_">Number</span>(val)), &#123; <span class="attr">message</span>: <span class="string">&quot;日は数値で入力してください&quot;</span> &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &gt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">day</span>.<span class="property">min</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`日は<span class="subst">$&#123;DATE_CONFIG.day.min&#125;</span>以上で入力してください`</span>,</span><br><span class="line">  &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(<span class="function">(<span class="params">val</span>) =&gt;</span> val === <span class="string">&quot;&quot;</span> || <span class="title class_">Number</span>(val) &lt;= <span class="variable constant_">DATE_CONFIG</span>.<span class="property">day</span>.<span class="property">max</span>, &#123;</span><br><span class="line">    <span class="attr">message</span>: <span class="string">`日は<span class="subst">$&#123;DATE_CONFIG.day.max&#125;</span>以下で入力してください`</span>,</span><br><span class="line">  &#125;);</span><br></pre></td></tr></table></figure>

<p>上記は全画面で共通の基本的なスキーマを定義しましたが、各画面のスキーマでは、範囲チェックなど画面固有のスキーマを定義しています。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> dateRangeParamSchema = baseParamSchema</span><br><span class="line">  .<span class="title function_">extend</span>(&#123;</span><br><span class="line">    <span class="attr">type</span>: z.<span class="title function_">literal</span>(<span class="string">&quot;date_range&quot;</span>),</span><br><span class="line">    <span class="attr">value</span>: zodDateRangeValue,</span><br><span class="line">  &#125;)</span><br><span class="line">  .<span class="title function_">refine</span>(</span><br><span class="line">    <span class="function">(<span class="params">data</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// 3段階目: 範囲の制約チェック</span></span><br><span class="line">      <span class="keyword">if</span> (!data.<span class="property">value</span>.<span class="property">from</span>.<span class="property">year</span> || <span class="comment">/* ... */</span>) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">      <span class="keyword">return</span> <span class="title function_">isWithinYears</span>(data.<span class="property">value</span>.<span class="property">from</span>, data.<span class="property">value</span>.<span class="property">to</span>, <span class="variable constant_">CONFIG</span>.<span class="property">range</span>.<span class="property">date</span>.<span class="property">maxYears</span>);</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123; <span class="attr">message</span>: <span class="string">`範囲は<span class="subst">$&#123;CONFIG.range.date.maxYears&#125;</span>年以内で入力してください`</span>, <span class="attr">path</span>: [<span class="string">&quot;value&quot;</span>] &#125;</span><br><span class="line">  );</span><br></pre></td></tr></table></figure>

<p>他にも、「必須チェック」や「年月日すべて入力したかのチェック」なども実装しているので、より詳しくはサンプルリポジトリのコードを参照してください。</p>
<ul>
<li>https://github.com/hasehiro0828/sample-vue-form/blob/main/sample-vue-form-frontend/src/lib/schemas/date-schemas.ts</li>
<li>https://github.com/hasehiro0828/sample-vue-form/blob/main/sample-vue-form-frontend/src/pages/NestForm/form-schema.ts</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>vee-validateとZodを使ったVue.jsでのフォーム開発について紹介しました。</p>
<p>@tanstack&#x2F;vue-queryとの組み合わせやユーザビリティを考慮したエラー表示、アクセシビリティを踏まえた実装など、かなり実践的な内容も盛り込みました。</p>
<p>この記事で紹介した内容をおさえていれば、基本的にはどのようなフォーム開発にも対応できるはずです。</p>
<p>ぜひ、サンプルリポジトリも参照して、実際の開発にご活用ください！</p>
<p>https://github.com/hasehiro0828/sample-vue-form</p>
<p>最後までお読みいただきありがとうございました。</p>
]]></content>
    <summary type="html">私は普段Reactを触ることが多く、フォーム開発ではreact-hook-form + zodの組み合わせでの開発をよく行なっていました。今回Vueでプロジェクトを進めるにあたり、vee-validateを使ってフォーム開発を行いました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="バリデーション" scheme="https://future-architect.github.io/tags/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/"/>
  </entry>
  <entry>
    <title>Vue.js連載 2025 を始めます</title>
    <link href="https://future-architect.github.io/articles/20251016a/"/>
    <id>https://future-architect.github.io/articles/20251016a/</id>
    <published>2025-10-15T15:00:00.000Z</published>
    <updated>2025-10-15T15:00:00.000Z</updated>
    <author><name>admin</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251016a/top.png" alt="" width="800" height="378">

<p>近年、フロントエンド開発の世界は目まぐるしく進化を続けており、その中でもVue.jsは、学習のしやすさや機能性から、フューチャーでも多くの採用実績があります。この度、そんなVue.jsの魅力と可能性をさらに深く探るべく、フューチャーの有志エンジニアたちによるブログリレーを開催します！</p>
<p>公開スケジュールです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">日付</th>
<th align="left">担当者</th>
<th align="left">テーマ</th>
</tr>
</thead>
<tbody><tr>
<td align="left">10&#x2F;16（木）</td>
<td align="left">長谷川 寛人</td>
<td align="left">vee-validate + zod または vue-keycloak の話</td>
</tr>
<tr>
<td align="left">10&#x2F;17（金）</td>
<td align="left">小杉山 護</td>
<td align="left">Vitest Browser Mode x Vue (x Storybook x MSW) の話</td>
</tr>
<tr>
<td align="left">10&#x2F;20（月）</td>
<td align="left">中村 立基</td>
<td align="left">【入門】Vueで始めるチャート分析</td>
</tr>
<tr>
<td align="left">10&#x2F;21（火）</td>
<td align="left">村田 靖拓</td>
<td align="left">TBD</td>
</tr>
<tr>
<td align="left">10&#x2F;22（水）</td>
<td align="left">山本 竜玄</td>
<td align="left">Vueでモバイルアプリ開発</td>
</tr>
<tr>
<td align="left">10&#x2F;23（木）</td>
<td align="left">澁川 喜規</td>
<td align="left">Vueのフロントエンドをセキュリティのしっかりしたコンテナにする</td>
</tr>
<tr>
<td align="left">10&#x2F;28（火）</td>
<td align="left">松本 朝香</td>
<td align="left">【PoC】Vue + DuckDB-Wasm + ECharts によるセキュリティ投資判断アプリ</td>
</tr>
<tr>
<td align="left">10&#x2F;27（月）</td>
<td align="left">永井 優斗</td>
<td align="left">脆弱なアプリを作って&amp;使って学ぶ、Vue.jsのセキュリティ観点で気をつけたいポイント</td>
</tr>
</tbody></table></div>
<p>Vue Fes Japan 2025も10&#x2F;25と開催間近、フューチャーもゴールドスポンサーとして協賛しております。一緒に盛り上げていきましょう。</p>
<p>このブログリレーが、Vue.jsを既に使いこなしている方はもちろん、これから学ぼうとしている方にとっても、新たな発見や学びのきっかけとなれば幸いです。</p>
]]></content>
    <summary type="html">Vue.jsの魅力と可能性をさらに深く探るべく、フューチャーの有志エンジニアたちによるブログリレーを開催します！</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>Webフロントエンド設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20250911a/"/>
    <id>https://future-architect.github.io/articles/20250911a/</id>
    <published>2025-09-10T15:00:00.000Z</published>
    <updated>2025-09-10T15:00:00.000Z</updated>
    <author><name>長谷川寛</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250911a/top.jpg" alt="" width="615" height="615">

<h2 id="はじめに">はじめに</h2><p>こんにちは。TIGの長谷川です。</p>
<p>フューチャー社内の有志メンバーでWebフロントエンド設計ガイドラインを作成し公開しました！</p>
<p>本ガイドラインではガイドライン策定の背景やガイドラインの特徴に加えて、内容の一部をピックアップしてご紹介します。</p>
<h2 id="本ガイドラインの背景">本ガイドラインの背景</h2><p>昨今のWebフロントエンド領域は、単なるHTML、CSS、JavaScriptでのページ制作から大きく変化しました。React、Vue.jsなどのモダンなフレームワークを活用した大規模かつ動的なWebアプリケーションの構築が主流となっています。</p>
<p>これにより開発の効率化とユーザー体験（UX）が向上しています。しかし一方で、設計の考慮点も多様化し、セキュリティやアクセシビリティなど、より多面的な品質も求められるようになりました。</p>
<p>本ガイドラインでは、Webフロントエンド設計における考慮点・設計パターン・推奨手法を提示し、<strong>開発チームが設計方針を決めるための土台を提供</strong>します。</p>
<h2 id="前提">前提</h2><p>本ガイドラインの対象読者は、Webフロントエンドの開発者・アーキテクトです。基本的なWeb技術（HTML、CSS、JavaScript、HTTP通信など）やReact、Vue.jsなどのフレームワークの基礎知識は前提としています。</p>
<p>適用範囲はブラウザ上で動作するWebフロントエンドのみです。サーバーサイドやCDN、BFF（Backend For Frontend）などは対象外です。</p>
<p>Web APIの設計については、別途公開しているWeb API設計ガイドラインとその紹介記事をぜひご参照ください！</p>
<h2 id="ガイドラインの特徴">ガイドラインの特徴</h2><p>本ガイドラインは、基本的に業務アプリケーションの開発を想定して作成しています。これらのアプリケーションは、大規模なtoC向けサービスとは異なる、以下のような特性を持ちます。</p>
<ul>
<li>権限管理が細かく所属部署や役職で表示内容が細かく変わる</li>
<li>複雑なデータ入力フォームや多機能なテーブル表示など状態管理やUI制御が複雑</li>
<li>利用するデバイスは比較的絞り込みやすい</li>
</ul>
<p>本ガイドラインは全体を通して読むことも、興味のあるセクションをピックアップして読むことも可能です。<br>ガイドラインは以下の8つの流れで構成されており、Webフロントエンド設計における幅広い領域を体系的にカバーしています。</p>
<ul>
<li><strong>構成方針</strong>（ホスティング、レンダリング方式、コンポーネント設計など）</li>
<li><strong>要件</strong>（対応ブラウザ、サポートバージョンなど）</li>
<li><strong>動作方針</strong>（バリデーション、ルーティング、状態管理など）</li>
<li><strong>表示制御</strong>（レスポンシブ、画像など）</li>
<li><strong>オフライン対応</strong>（PWAやオフライン判定方式など）</li>
<li><strong>認証認可</strong>（認証方式、認可方針など）</li>
<li><strong>開発</strong>（静的解析など）</li>
<li><strong>テスト</strong>（テスト方針など）</li>
</ul>
<h2 id="一部コンテンツの抜粋">一部コンテンツの抜粋</h2><p>ガイドラインはかなりのボリューム（A4のPDFで100ページ越え…!）となっています。<br>ここでは、皆さんにとくに見ていただきたいコンテンツをいくつかピックアップしてご紹介します！</p>
<h3 id="ホスティング方式">ホスティング方式</h3><p>レンダリング方式として、CSR（Client Side Rendering）とSSR（Server Side Rendering）の特徴を比較しています。</p>
<p>その上で、以下のように使い分けることを推奨しています。</p>
<ul>
<li><strong>CSR:</strong> 高度なインタラクティブ性など表現力が強く要求される場合や、開発のシンプルさを重視する場合に推奨</li>
<li><strong>SSR:</strong> SEO効果や初期表示速度（Core Web Vitals等）が強く要求されるtoC向けサービスで推奨</li>
</ul>
<p>複数のレンダリング手法を組み合わせるハイブリッドレンダリングについては、UXや性能の最適化を目指せる一方で、開発・保守・デバッグの複雑性とコストが増加するデメリットを考慮に入れる必要があります。</p>
<p>また、CSRにおける具体的なホスティング方式を3パターンで比較検討しています。具体的には<code>CloudFront+S3</code>、<code>ALB+S3</code>、<code>LB+Webサーバー</code>について比較しました。<br>閉域対応、性能、サーバー管理、運用費用、B&#x2F;Gデプロイなど多角的な評価軸で比較し、要件に応じた推奨案を提示しています。</p>
<p>上記の例ではAWSを前提としていますが、Google CloudやAzureなど他の主要なクラウドベンダーでも同様のアーキテクチャ設計が可能です。</p>
<h3 id="コンポーネント設計">コンポーネント設計</h3><p>コンポーネントを「共通コンポーネント」と「業務コンポーネント」に分類し、それぞれの特性と役割を整理しています。</p>
<ul>
<li><strong>共通コンポーネント:</strong> アプリケーション横断的に使用される、ビジネスロジックに依存しない汎用的なUI基盤</li>
<li><strong>業務コンポーネント:</strong> 特定の機能やビジネスドメインに特化した、固有のロジックを含むコンポーネント</li>
</ul>
<p>共通コンポーネントの実装については3つのアプローチを比較しています。</p>
<ul>
<li><strong>既存ライブラリ活用:</strong> Material UI、Ant Designなどのサードパーティライブラリを利用</li>
<li><strong>ハイブリッドアプローチ:</strong> 既存ライブラリを拡張したり、Radix UIなどヘッドレスUIライブラリに独自デザインを組み合わせて開発</li>
<li><strong>フルスクラッチ:</strong> 完全に社内で共通コンポーネントを開発</li>
</ul>
<p>それぞれのPros&#x2F;Consや適用ケースを示し　「まず既存ライブラリの活用を第一に検討することを推奨」 としています。</p>
<p>また、コンポーネント設計でよく発生するアンチパターンの具体例も挙げ、それらに対する解決策も併せて提示しています。</p>
<ul>
<li><strong>神オブジェクト:</strong> 単一責任原則に違反した過剰な処理を行なうコンポーネント</li>
<li><strong>密結合:</strong> コンポーネントが互いに過度に依存している状態</li>
<li><strong>ビジネスロジックとUIロジックの混在:</strong> UIコンポーネントに過度なビジネスロジックを埋め込む問題</li>
</ul>
<h3 id="バリデーション設計">バリデーション設計</h3><p>バリデーションは、ユーザーによる誤入力を防ぎ適切なメッセージで自己修正を促すことで、ユーザー体験（UX）を向上させるために不可欠です。</p>
<p>フロントエンドのバリデーションは、ユーザーへの即時フィードバックを通じたUX向上を主目的とします。一方で、システムのデータ整合性維持やセキュリティ対策（XSSなど）は、サーバーサイドでのバリデーションが必須です。</p>
<p>バリデーションの実施タイミングには複数の選択肢があり、それぞれUXへの影響と実装コストが異なります。<br>UX要件・リリース速度・開発工数を総合的に考慮し、最適なバランスの実装を選択する必要があります。</p>
<p>また、アクセシビリティも重要な考慮点です。<code>disabled</code>なボタンはスクリーンリーダーやキーボード操作でのアクセスに問題を生じさせるため、非活性化することは推奨されません。ボタンを活性化したままエラーメッセージで問題を指摘するなど、アクセシブルな状態を保つよう設計しましょう。</p>
<h3 id="状態管理の設計">状態管理の設計</h3><p>React、Vue.jsなどのモダンフレームワークでは、アプリケーションの複雑さに応じて適切な状態管理手法を選択することが重要です。</p>
<p>本ガイドラインでは以下の観点で状態管理手法を整理しています。</p>
<ul>
<li><strong>ローカル状態 vs グローバル状態:</strong> コンポーネント固有の状態と、アプリケーション全体で共有すべき状態の適切な切り分け</li>
<li><strong>状態管理ライブラリの導入判断:</strong> Redux、Zustand、Recoilなど、プロジェクト規模や要件に応じたライブラリ選択の基準</li>
<li><strong>サーバー状態とクライアント状態の分離:</strong> API通信で取得するデータと、UI固有の状態を分離する設計方針</li>
</ul>
<p>グローバル状態の過剰な利用を避けたり、必要以上にファットなライブラリを利用しないなど、状態管理のベストプラクティスを示しています。</p>
<h3 id="認証・認可の設計">認証・認可の設計</h3><p>セキュリティの中核となる認証・認可について、フロントエンド固有の考慮事項を詳しく解説しています。業務アプリケーションで一般的な認証方式は以下の4つに大別されます。</p>
<ul>
<li>外部のOpenID ConnectのIDプロバイダーを利用し、フロントエンドだけで行なう</li>
<li>外部のOpenID ConnectなどのIDプロバイダーを利用し、コールバックをサーバーで受け、サーバーがセッションCookieを発行する</li>
<li>外部のOpenID ConnectなどのIDプロバイダーを利用し、コールバックをサーバーで受け、サーバーがJWTトークンを発行する</li>
<li>自前でID&#x2F;パスワード入力を実装する</li>
</ul>
<p>これらの認証方式について、それぞれのメリット・デメリットを比較しています。</p>
<p>フロントエンドにおける認可制御は、ユーザーが権限のない操作を誤って行なわないよう、行動を制限するなど、UX向上を目的とします。</p>
<p>複雑性を抑えるために、画面内におけるコンポーネントの認可制御が可能な限り最小限になるような画面単位で設計することを推奨します。</p>
<h3 id="ブラウザ対応戦略">ブラウザ対応戦略</h3><p>ブラウザ機能の採用について、Web Platform Baselineという客観的な指標を活用した判断基準を示しています。</p>
<ul>
<li><strong>Widely Available:</strong> 安心して利用できる（利用推奨）</li>
<li><strong>Newly Available:</strong> 総合的判断で採用（Experimentalでなければ基本OK）</li>
<li><strong>Limited availability:</strong> 本番環境での採用は見送り推奨</li>
</ul>
<p>また、サポートするブラウザに関しては、構築するシステムの要件を考慮したうえで、ミニマムに絞ることを推奨します。</p>
<h3 id="テスト方針">テスト方針</h3><p>品質の高いWebアプリケーションを継続的に提供するため、適切なテスト戦略の策定が重要です。本ガイドラインでは、フロントエンド開発と相性が良いとされる 「テストトロフィー」の考えに基づき、テスト設計の方針を示しています。</p>
<p>テストピラミッド（高速なユニットテストを土台とする従来の考え方）に対して、テストトロフィーはコンポーネント&#x2F;インテグレーションテスト（ユーザー視点での動作検証）をより重視します。ユニットテストは実装の詳細に依存し脆く、E2Eテストはコストが高いため、過度に依存しないバランスの取れたアプローチを推奨しています。</p>
<h2 id="まとめ">まとめ</h2><p>Webフロントエンド設計ガイドラインは、現代の複雑化したフロントエンド開発における設計判断を支援し、知見共有を促進することを目的として作成しました。単なる技術Tips集ではなく、ビジネス要件と技術選択のバランスを重視し、開発チームの状況に応じた実践的な指針を提供しています。</p>
<p>ぜひ一度ガイドラインをご覧いただき、皆さまのプロジェクトにお役立てください。</p>
<p>また、本ガイドラインは公開後も継続的にアップデートしていく予定です。<strong>社内外問わずPRは大歓迎</strong>ですので、ぜひ設計ガイドライン(GitHub)からご協力ください。</p>
<p>他にもたくさんのガイドラインがございますので、ぜひご覧ください！</p>
<ul>
<li>ガイドラインの記事一覧</li>
<li>ガイドラインのトップページ</li>
</ul>
]]></content>
    <summary type="html">フューチャー社内の有志メンバーでWebフロントエンド設計ガイドラインを作成し公開しました！本ガイドラインではガイドライン策定の背景やガイドラインの特徴に加えて、内容の一部をピックアップしてご紹介します。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="React" scheme="https://future-architect.github.io/tags/React/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="設計" scheme="https://future-architect.github.io/tags/%E8%A8%AD%E8%A8%88/"/>
  </entry>
  <entry>
    <title>Mermaid図の調整ツールで図をさらに見やすく！〜現場の課題を解決する自作ツール〜</title>
    <link href="https://future-architect.github.io/articles/20250905a/"/>
    <id>https://future-architect.github.io/articles/20250905a/</id>
    <published>2025-09-04T15:00:00.000Z</published>
    <updated>2025-09-04T15:00:00.000Z</updated>
    <author><name>荒木昂平</name></author>
    <content type="html"><![CDATA[<p>こんにちは、starswirl_kです。</p>
<p>今回は、チーム開発でMermaid図を頻繁に利用している中で、「もっとこうなったら良いのに！」という現場の声を形にした自作ツールをご紹介します。</p>
<p>特に、フローチャートやクラス図の<strong>矢印の長さ（ノード間の間隔）や垂直方向の間隔</strong>といった細かい調整に悩んでいる方には、きっと役立つはずです。</p>

<img fetchpriority="high" src="/images/2025/20250905a/作成したツール.png" alt="作成したツール" width="1010" height="835">


<p>ツールはこちら：GitHub Pages&#x2F;MermaidViewer.html</p>
<h2 id="なぜこのツールを作ったのか？">なぜこのツールを作ったのか？</h2><p>Mermaidは手軽に図を作成できる非常に便利なツールですが、デフォルトの設定では図の要素間の間隔が固定されているため、複雑な図になると<strong>矢印が短く見づらかったり</strong>、全体のレイアウトが窮屈に感じられることがありました。</p>
<p>特に、以下のような課題がありました。</p>
<ul>
<li><strong>矢印が短い</strong>: 図の要素が増えると、関係を示す矢印が短くなり、どこからどこへ繋がっているのか視認性が悪くなる。</li>
<li><strong>レイアウト調整の難しさ</strong>: ノードが増えると、図全体が小さくまとまりすぎてしまい、情報量が多くなると見づらい。</li>
</ul>
<img src="/images/2025/20250905a/窮屈な図の例.png" alt="窮屈な図の例" width="1200" height="934" loading="lazy">

<p>これらの課題を解決し、より見やすいMermaid図を効率的に作成するために、この調整ツールを開発しました。</p>
<h2 id="ツールの概要とできること">ツールの概要とできること</h2><p>このツールは、ブラウザ上で動作するHTMLファイルとして提供されており、ダウンロードしてクリックするだけで簡単に利用できます。ツールは GitHub で公開しています。</p>
<p>主な機能は以下の通りです。</p>
<ol>
<li><strong>Mermaidコードのプレビュー</strong>: 入力したMermaidコードがリアルタイムでレンダリングされ、すぐに図の見た目を確認できます。</li>
<li><strong>ノード間隔（水平方向）の調整</strong>: スライダーを動かすだけで、ノード間の水平方向の間隔を自由に調整できます（<code>flowchart.nodeSpacing</code>, <code>class.nodeSpacing</code>）。これにより、矢印の長さを適切に保ち、図全体の視認性を向上させることができます。</li>
</ol>
<img src="/images/2025/20250905a/nodeSpacing.gif" alt="nodeSpacing.gif" width="1000" height="474" loading="lazy">

<ol start="3">
<li><strong>ランク間隔（垂直方向）の調整</strong>: 同様に、スライダーで垂直方向の間隔も調整可能です（<code>flowchart.rankSpacing</code>, <code>class.rankSpacing</code>）。これにより、図の上下方向の広がりを調整し、よりゆとりのあるレイアウトを実現できます。</li>
<li><strong>テーマの切り替え</strong>: Mermaid標準で用意されているテーマ（<code>default</code>, <code>forest</code>, <code>dark</code>, <code>neutral</code>, <code>base</code>, <code>base-dark</code>）を簡単に切り替えることができます。これにより、図の雰囲気を変更し、ドキュメントの雰囲気に合わせたり、視認性を高めたりできます。</li>
</ol>
<img src="/images/2025/20250905a/default.gif" alt="default.gif" width="1000" height="474" loading="lazy">

<ol start="5">
<li><strong>PDFダウンロード</strong>: 作成したMermaid図をPDFとして出力できます。ドキュメントに添付したり、共有する際に便利です。</li>
</ol>
<h2 id="ツールの使い方">ツールの使い方</h2><ol>
<li><strong>ツールにアクセス</strong>:  GitHub Pagesにアクセスします。</li>
<li><strong>Mermaidコードを入力</strong>: 画面左側のテキストエリアにMermaidコードを入力します。入力と同時に右側のプレビューエリアに図が表示されます。</li>
<li><strong>間隔を調整</strong>: 「水平方向の間隔 (nodeSpacing)」と「垂直方向の間隔 (rankSpacing)」のスライダーを動かして、お好みのレイアウトに調整します。</li>
<li><strong>テーマを変更</strong>: 「テーマを選択」ドロップダウンから、好きなテーマを選んで図の見た目を変更します。</li>
<li><strong>PDFをダウンロード</strong>: 「PDFとしてダウンロード」ボタンをクリックすると、表示されているMermaid図がPDFファイルとして保存されます。</li>
</ol>
<h2 id="開発のこだわりポイント">開発のこだわりポイント</h2><h3 id="リアルタイムプレビューとスムーズな操作感">リアルタイムプレビューとスムーズな操作感</h3><p>スライダーによる間隔調整は、レンダリングの遅延を最小限に抑え、<strong>リアルタイムに近い感覚でプレビュー</strong>が更新されるように工夫しました。この実現のために、Mermaidコードの入力やスライダー操作時には<code>setTimeout</code>を活用しています。これにより、短い間隔での連続的な入力や操作があった場合でも、不要な再レンダリングを抑制し、パフォーマンスを維持しています。</p>
<p>このように、ユーザーの操作に応じて適切なタイミングで再レンダリングを行うことで、スムーズな体験を提供しています。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Mermaidコード入力時のイベントリスナー</span></span><br><span class="line">inputElement.<span class="title function_">addEventListener</span>(<span class="string">&#x27;input&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">saveStateToSessionStorage</span>();</span><br><span class="line">    <span class="built_in">clearTimeout</span>(renderTimer); <span class="comment">// 前のタイマーをクリア</span></span><br><span class="line">    renderTimer = <span class="built_in">setTimeout</span>(renderMermaid, <span class="number">500</span>); <span class="comment">// 500msの遅延後にレンダリング</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// スライダー操作時のイベントリスナー（例: nodeSpacing）</span></span><br><span class="line">nodeSpacingSlider.<span class="title function_">addEventListener</span>(<span class="string">&#x27;input&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">saveStateToSessionStorage</span>();</span><br><span class="line">    nodeSpacingValue.<span class="property">innerText</span> = nodeSpacingSlider.<span class="property">value</span>;</span><br><span class="line">    <span class="variable language_">window</span>.<span class="property">currentMermaidConfig</span>.<span class="property">flowchart</span>.<span class="property">nodeSpacing</span> = <span class="built_in">parseInt</span>(nodeSpacingSlider.<span class="property">value</span>);</span><br><span class="line">    <span class="variable language_">window</span>.<span class="property">currentMermaidConfig</span>.<span class="property">class</span>.<span class="property">nodeSpacing</span> = <span class="built_in">parseInt</span>(nodeSpacingSlider.<span class="property">value</span>);</span><br><span class="line">    <span class="variable language_">window</span>.<span class="property">mermaid</span>.<span class="title function_">initialize</span>(<span class="variable language_">window</span>.<span class="property">currentMermaidConfig</span>);</span><br><span class="line">    <span class="built_in">clearTimeout</span>(renderTimer);</span><br><span class="line">    renderTimer = <span class="built_in">setTimeout</span>(renderMermaid, <span class="number">200</span>); <span class="comment">// 200msの遅延後にレンダリング</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<h3 id="現場の声を反映した機能">現場の声を反映した機能</h3><p>チームメンバーからの「矢印が短くて見づらい」という具体的なフィードバックから、<code>nodeSpacing</code>と<code>rankSpacing</code>の調整機能を優先的に実装しました。これにより、<strong>現場で本当に求められていた課題</strong>を解決できたと感じています。</p>
<h2 id="今後の展望">今後の展望</h2><p>このツールは、まだまだ発展途上です。今後、以下のような機能拡張も検討しています。</p>
<ul>
<li><strong>カスタムテーマの保存機能</strong>: 自分好みのテーマ設定を保存・ロードできるようにする</li>
<li><strong>SVG&#x2F;PNGエクスポート</strong>: PDFだけでなく、他の画像形式でのエクスポートオプションを追加</li>
<li><strong>より詳細なスタイル調整</strong>: 特定の要素の色や線の太さなどをGUIで調整できるようにする</li>
</ul>
<p>もし、このツールを使ってみたい、あるいはこんな機能が欲しいという要望があれば、ぜひコメントやフィードバックをいただけると嬉しいです！</p>
<h2 id="まとめ">まとめ</h2><p>Mermaid図をより見やすくするために開発した、自作の調整ツールについてご紹介しました。シンプルなツールですが、日々のドキュメント作成の効率化と、より伝わりやすい図の作成に貢献できると信じています。</p>
<p>ぜひ一度お試しいただき、ご自身のMermaid図作成に役立ててみてください！</p>
]]></content>
    <summary type="html">チーム開発でMermaid図を頻繁に利用している中で、「もっとこうなったら良いのに！」という現場の声を形にした自作ツールをご紹介します。特に、フローチャートやクラス図の矢印の長さ（ノード間の間隔）や垂直方向の間隔といった細かい調整に悩んでいる方には、きっと役立つはずです。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Mermaid.js" scheme="https://future-architect.github.io/tags/Mermaid-js/"/>
    <category term="OSS" scheme="https://future-architect.github.io/tags/OSS/"/>
    <category term="チーム開発" scheme="https://future-architect.github.io/tags/%E3%83%81%E3%83%BC%E3%83%A0%E9%96%8B%E7%99%BA/"/>
    <category term="便利ツール" scheme="https://future-architect.github.io/tags/%E4%BE%BF%E5%88%A9%E3%83%84%E3%83%BC%E3%83%AB/"/>
  </entry>
  <entry>
    <title>Tailwind CSSのドキュメントから見えてくる使い方とCSS設計のヒント</title>
    <link href="https://future-architect.github.io/articles/20250314a/"/>
    <id>https://future-architect.github.io/articles/20250314a/</id>
    <published>2025-03-13T15:00:00.000Z</published>
    <updated>2025-03-13T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>CSSをわかりやすくメンテナンス性高く書くというのは長い間試行錯誤され続けてきました。命名規則でがんばる、SCSSのようなプリプロセッサを使う、CSS in JSなどいろいろな仕組みがかつて作られたりしてきましたが、現在一番シェアを集めているのがTailwind CSSです。State of CSS 2024の調査でも他のカテゴリ含めても一位です。</p>
<img fetchpriority="high" src="/images/2025/20250314a/Screenshot_2025-03-07_at_19.49.08.png" alt="" width="1200" height="589">

<p>1&#x2F;23に最新バージョンの4系がリリースされました。そんでもってぼちぼちドキュメントの読書会をしているのですが、いろいろ利用方法のヒントがあったのでまとめてみます。一部「これから使う場合はこうしたい」という僕の意見もありますが、そこに関しては異論とか「実際やってみたけどこうだった」とかあると思いますので、そういうご意見などはXとかでいただければと思います。</p>
<h2 id="そもそもなぜTailwind-CSSを使うのか？">そもそもなぜTailwind CSSを使うのか？</h2><p>Tailwind CSSは万人に支持されているわけではなく「かえってメンテナンス性が下がるのではないか？」という意見もたまに見られます。その答えとなる考え方もドキュメントにあります。5項目書かれています。V3のドキュメントは3つでしたが増えました。原文の方の説明もご覧ください。</p>
<ul>
<li>すばやく実装できるようになる</li>
<li>安心して変更できるようになる</li>
<li>古いプロジェクトのメンテナンスが楽になる</li>
<li>コードがよりポータブルになる</li>
<li>CSSが膨大に育たなくなる</li>
</ul>
<p>このあたり、実際にTailwind CSSがなかった時代の開発を知っていると「これこれ！」といいたくなるようなことばかりです。</p>
<p>クラス名をいちいち考えなければなりません。最近はReact&#x2F;Vueのコンポーネントに閉じたCSSが書けるようにビルドツールががんばってくれるので衝突を気にする必要性は減っていますが、それでも時間がかかります。また、大きなディスプレイで、CSSとHTMLの両方のファイルをにらめっこしてあっちに飛んだり、こっちに飛んだりしながら直す必要もなくなります。</p>
<p>また、どのクラスがどこで使われているのかの影響を考えながら修正する必要もなくなります。一部修正しようとして想定外のところまで崩れてしまうという事故もなくなります。</p>
<p>タグ構造とCSSが密接なので、結局再利用するのは難易度が高いというところがポータビリティが以前はなかったという理由になるかと思います。</p>
<p>あと、影響範囲がわからないことで、どのクラスを削除していいのかわからない、というところがCSSファイルにゴミが溜まり続ける理由になっているのかな、という気がしています。だいたい、CSS経験者の誰に聞いても、「他の人が書いたCSSの修正はまず無理」「スタイルシートなら良かったがカスケーディングがあったせいでカオスの扉が開いた」と言うかと思います。</p>
<p>「インラインがいいなら、classじゃなくてsytle直接書けば？」という意見への反論も書かれています。</p>
<ul>
<li>デザインにある程度制約をあたえる</li>
<li>ホバー、フォーカス、メディアクエリーなどインラインスタイルでは表現できないものもある</li>
</ul>
<h2 id="ちょっと高度なセレクタ">ちょっと高度なセレクタ</h2><p>Tailwind CSSのドキュメントにちょっと高度な書き方が紹介されているので特に便利そうなものをいくつか紹介します。それぞれ、以下のようなご利益があります。</p>
<ul>
<li>スタイリングのためのタグを減らす</li>
<li>スタイリング変化のためのJSコードを減らす</li>
<li>スタイリングのための<code>class</code>の定義を減らす</li>
</ul>
<h3 id="before-after"><code>::before</code>, <code>::after</code></h3><p>ちょっとしたテキストの色付けのために<code>&lt;span&gt;</code>タグを生やすというのをやったことがある人は多いでしょう。そんな無駄な作業は今後の人生でやる必要はもうありません。<code>after:</code>プレフィックスを駆使することでCSSを書かなくても<code>::after</code>が実現できます。</p>
<img src="/images/2025/20250314a/Screenshot_2025-03-07_at_20.40.02.png" alt="ラベルに赤色のアスタリスクがついたEmailの入力フォーム" width="369" height="90" loading="lazy">

<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">label</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">&quot;after:ml-0.5 after:text-red-500 after:content-[&#x27;*&#x27;] text-gray-700 ...&quot;</span>&gt;</span>Email<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;email&quot;</span> <span class="attr">name</span>=<span class="string">&quot;email&quot;</span> <span class="attr">class</span>=<span class="string">&quot;...&quot;</span> <span class="attr">placeholder</span>=<span class="string">&quot;you@example.com&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>ダイアログの背景も <code>backdrop:bg-gray-50</code>で背景指定ができて、全体を覆うdivタグとか作らなくていいです。</p>
<h3 id="条件つきのスタイリング">条件つきのスタイリング</h3><p>データの状態、例えば有効か無効かで色を変えたいということはよくあります。そのときにReactだと<code>className</code>をあの手この手で動的に変更するようなコードを書いたことはあるでしょう。テンプレートリテラル使ったりとか。</p>
<p>属性をもとにスタイルのON&#x2F;OFFを切り替えるようなプレフィックスを使うと、classは文字列操作を除外した固定値にできて、条件分岐はその属性のON&#x2F;OFFだけにつければ良くなりシンプルになります。 hasで子供の状態を使ったスタイリング、groupを使って親、peerを使って兄弟の状態を元にしたスタイル変更もできます。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="comment">&lt;!-- disabledのON/OFFで背景のスタイルが変わる --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">input</span> <span class="attr">:disabled</span>=<span class="string">&quot;disabled&quot;</span> <span class="attr">class</span>=<span class="string">&quot;border bg-blue-400 disabled:bg-gray-400 m-4 p-4&quot;</span> <span class="attr">placeholder</span>=<span class="string">&quot;placeholder&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 子のチェックボックスの選択でテキストのスタイルを変える --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">label</span> <span class="attr">class</span>=<span class="string">&quot;text-gray-400 has-checked:text-black&quot;</span>&gt;</span><span class="tag">&lt;<span class="name">input</span> <span class="attr">type</span>=<span class="string">&quot;checkbox&quot;</span> <span class="attr">v-model</span>=<span class="string">&quot;checked&quot;</span>&gt;</span>checked<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>disabled</code>, <code>enabled</code>, <code>checked</code>, <code>indeterminate</code>, <code>required</code>, <code>valid</code>, <code>invalid</code>, <code>read-only</code>, <code>open</code>, <code>close</code> などのセマンティックにあった属性が使えるならそれを使います。もしそうでない場合も、 <code>data-not-saved:border-gray-400</code> のようにdata属性も使えるのでアプリケーションで自由に必要な属性が追加できます。Vue3はfalseを与えると「falseがある」となってしまうので空文字列かnull&#x2F;undefinedを与えるようにしないといけないのでちょっと冗長になりますが・・・</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="comment">&lt;!-- data-toggle属性でスタイルを変更 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">:data-toggle</span>=<span class="string">&quot;toggle ? &#x27;&#x27; : null&quot;</span> <span class="attr">class</span>=<span class="string">&quot;border border-red-400 data-toggle:border-blue-400 m-4 p-4&quot;</span>&gt;</span></span><br><span class="line">  Toggle</span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h3 id="子供の要素のスタイリング">子供の要素のスタイリング</h3><p><code>*:</code>というプレフィックスで、子要素、<code>**:</code>で子孫全部に適用するスタイルを宣言できます。リストの親要素にだけ書いておけば全部の子要素のclassを書かなくてよくなるので、場合によっては記述量が減ります。ReactとかVueでループを回して作成とかだと変わらないですが。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">h2</span>&gt;</span>Categories<span class="tag">&lt;<span class="name">h2</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">ul</span> <span class="attr">class</span>=<span class="string">&quot;*:rounded-full *:border *:border-sky-100 *:bg-sky-50 *:px-2 *:py-0.5 dark:text-sky-300 dark:*:border-sky-500/15 dark:*:bg-sky-500/10 ...&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span>Sales<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span>Marketing<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">li</span>&gt;</span>SEO<span class="tag">&lt;/<span class="name">li</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- ... --&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">ul</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h2 id="レスポンシブデザイン対応">レスポンシブデザイン対応</h2><h3 id="コンテナクエリーが使えないか検討する">コンテナクエリーが使えないか検討する</h3><p>Tailwind CSSには<code>md:flex</code>などの「このサイズ以上ならこの定義を利用する」というプレフィックスがあるのをご存知の方は多いと思います。この<code>sm</code>, <code>md</code>, <code>lg</code>, <code>xl</code>, <code>2xl</code>という5つのプレフィックスを駆使してレスポンシブデザインや！という説明はよく見かけます。こちらはビューポートサイズを基準に適用するスタイルを変更するメディアクエリーで実現されています。</p>
<p>コンテナクエリーは2020年代になって実装されたものでいくつかの技術要素で構成されています。基本機能はbaseline 2023となっています。コンテナクエリーは親要素のサイズ（もしくは名前をつければ直接の親でなくても良い）のサイズでスタイルを切り替える機能です。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;@container&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">&quot;flex flex-col @md:flex-row&quot;</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- ... --&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>メディアクエリーが5つしかないのにコンテナクエリーのプレフィックスは13個もあるあたり、こちらの方が手厚い気がします。</p>
<p>コンテナクエリーは親要素なので、デスクトップではあるがサイドバーなので幅があまりないよ、というケースで小さいコンポーネントを表示させるという使い方ができます。デスクトップ用、モバイル用、という使い分けよりもよりレスポンシブなコンポーネントが作れるので良さそうです。</p>
<p>コンテナクエリーの測定基準と実際にクエリーを書くタグは同じコンポーネントに閉じておくのが良さそうです。</p>
<h3 id="モバイルファースト">モバイルファースト</h3><p>プレフィックスは「これより大きい場合」という意味です。「これより小さい」という <code>max-xl</code>のようなプレフィックスもありますが、この両方を混ぜない方が良いでしょう。</p>
<p>上記サンプルでは<code>flex-col @md:flex-row</code>という書き方をしていました。これはデフォルトではflex-colだが、448px(28rem)よりも上ならflex-rowになる、という定義です。このように「プレフィックスなし」「特定の条件で有効になるプレフィックスあり」の順番で書くことを公式ドキュメントでは「モバイルファースト」と呼んでいます。</p>
<p>デスクトップで開発していてあとでモバイルを追加するとデスクトップファーストの方が書きやすいとかはあるかもしれませんが、並び方とか<code>max-</code>の利用可否は同じルールで統一されている方が読みやすいので公式ドキュメントに従ってモバイルファーストを心がけると良いでしょう。</p>
<h3 id="ブレークポイントに名前をつける">ブレークポイントに名前をつける</h3><p>コンポーネントはコンテナクエリーを使った方が良さそう、というのは最初に説明しましたが、といってもページトップとかは通常のメディアクエリーを使うかもしれません。その場合に、<code>sm:</code>とかを<strong>たくさん</strong>書く代わりに<code>desktop</code>という名前を定義して使う方が可読性はあがるでしょう。以下のコードは5つのメディアクエリーのプレフィックスを消去し（<code>initial</code>の行）、<code>desktop:</code>というプレフィックスを追加します。</p>
<figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="keyword">@import</span> <span class="string">&quot;tailwindcss&quot;</span>;</span><br><span class="line"><span class="keyword">@theme</span> &#123;</span><br><span class="line">  <span class="attr">--breakpoint-</span>*: initial;     // デフォルトのプリフィックス削除</span><br><span class="line">  <span class="attr">--breakpoint-desktop</span>: <span class="number">80rem</span>; // desktopを追加</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>あくまでも「たくさん同じプレフィックスを書く場合にはセマンティックに合わせた別名を定義すると良い」「それがたまたまブレークポイントだった」というのがここの趣旨です。ポータビリティは下がるけれど、アプリケーションで使うはずだからコンポーネント切り出して共有の対象ではない、最初から別名を定義することを推奨しているわけではないので最初のTailwind CSSのメリットからは外れません。</p>
<h2 id="テーマ">テーマ</h2><h3 id="形式">形式</h3><p>テーマ定義はV3はJavaScript形式でしたが、CSS形式が今は標準のようです。JavaScript形式のテーマは明示的に指定しないと読み込まなくなりました。後方互換性のために残されている状態なので、今後はCSS形式に寄せていく方がよいでしょう。</p>
<figure class="highlight css"><table><tr><td class="code"><pre><span class="line">// 新しい書き方</span><br><span class="line"><span class="keyword">@theme</span> &#123;</span><br><span class="line">  <span class="attr">--font-sans</span>: ui-sans-serif, system-ui, sans-serif, <span class="string">&quot;Apple Color Emoji&quot;</span>, <span class="string">&quot;Segoe UI Emoji&quot;</span>, <span class="string">&quot;Segoe UI Symbol&quot;</span>, <span class="string">&quot;Noto Color Emoji&quot;</span>;</span><br><span class="line">  <span class="attr">--font-serif</span>: ui-serif, Georgia, Cambria, <span class="string">&quot;Times New Roman&quot;</span>, Times, serif;</span><br><span class="line">  <span class="attr">--font-mono</span>: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, <span class="string">&quot;Liberation Mono&quot;</span>, <span class="string">&quot;Courier New&quot;</span>, monospace;</span><br><span class="line">  <span class="attr">--color-red-50</span>: <span class="built_in">oklch</span>(<span class="number">0.971</span> <span class="number">0.013</span> <span class="number">17.38</span>);</span><br><span class="line">  <span class="attr">--color-red-100</span>: <span class="built_in">oklch</span>(<span class="number">0.936</span> <span class="number">0.032</span> <span class="number">17.717</span>);</span><br><span class="line">  <span class="attr">--color-red-200</span>: <span class="built_in">oklch</span>(<span class="number">0.885</span> <span class="number">0.062</span> <span class="number">18.334</span>);</span><br><span class="line">  <span class="comment">/* ... */</span></span><br><span class="line">  <span class="attr">--shadow-2xs</span>: <span class="number">0</span> <span class="number">1px</span> <span class="built_in">rgb</span>(<span class="number">0</span> <span class="number">0</span> <span class="number">0</span> / <span class="number">0.05</span>);</span><br><span class="line">  <span class="attr">--shadow-xs</span>: <span class="number">0</span> <span class="number">1px</span> <span class="number">2px</span> <span class="number">0</span> <span class="built_in">rgb</span>(<span class="number">0</span> <span class="number">0</span> <span class="number">0</span> / <span class="number">0.05</span>);</span><br><span class="line">  <span class="attr">--shadow-sm</span>: <span class="number">0</span> <span class="number">1px</span> <span class="number">3px</span> <span class="number">0</span> <span class="built_in">rgb</span>(<span class="number">0</span> <span class="number">0</span> <span class="number">0</span> / <span class="number">0.1</span>), <span class="number">0</span> <span class="number">1px</span> <span class="number">2px</span> -<span class="number">1px</span> <span class="built_in">rgb</span>(<span class="number">0</span> <span class="number">0</span> <span class="number">0</span> / <span class="number">0.1</span>);</span><br><span class="line">  <span class="comment">/* ... */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>こちらが古い形式です。</p>
<figure class="highlight js"><figcaption><span>tailwind.config.js</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 古い書き方</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">  <span class="attr">theme</span>: &#123;</span><br><span class="line">    <span class="attr">fontFamily</span>: &#123;</span><br><span class="line">      <span class="attr">sans</span>: [<span class="string">&#x27;ui-sans-serif&#x27;</span>, <span class="string">&#x27;system-ui&#x27;</span>, <span class="string">&#x27;sans-serif&#x27;</span>, <span class="string">&#x27;&quot;Apple Color Emoji&quot;&#x27;</span>, <span class="string">&#x27;&quot;Segoe UI Emoji&quot;&#x27;</span>, <span class="string">&#x27;&quot;Segoe UI Symbol&quot;&#x27;</span>, <span class="string">&#x27;&quot;Noto Color Emoji&quot;&#x27;</span>],</span><br><span class="line">      <span class="attr">serif</span>: [<span class="string">&#x27;ui-serif&#x27;</span>, <span class="string">&#x27;Georgia&#x27;</span>, <span class="string">&#x27;Cambria&#x27;</span>, <span class="string">&#x27;&quot;Times New Roman&quot;&#x27;</span>, <span class="string">&#x27;Times&#x27;</span>, <span class="string">&#x27;serif&#x27;</span>],</span><br><span class="line">      <span class="attr">mono</span>: [<span class="string">&#x27;ui-monospace&#x27;</span>, <span class="string">&#x27;SFMono-Regular&#x27;</span>, <span class="string">&#x27;Menlo&#x27;</span>, <span class="string">&#x27;Monaco&#x27;</span>, <span class="string">&#x27;Consolas&#x27;</span>, <span class="string">&#x27;&quot;Liberation Mono&quot;&#x27;</span>, <span class="string">&#x27;&quot;Courier New&quot;&#x27;</span>, <span class="string">&#x27;monospace&#x27;</span>],</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">colors</span>: &#123;</span><br><span class="line">      <span class="attr">red</span>: &#123;</span><br><span class="line">        <span class="number">50</span>: <span class="string">&#x27;oklch(0.971 0.013 17.38)&#x27;</span>,</span><br><span class="line">        <span class="number">100</span>: <span class="string">&#x27;oklch(0.936 0.032 17.717)&#x27;</span>,</span><br><span class="line">        <span class="number">200</span>: <span class="string">&#x27;oklch(0.885 0.062 18.334)&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">boxShadow</span>: &#123;</span><br><span class="line">      <span class="string">&#x27;2xs&#x27;</span>: <span class="string">&#x27;0 1px rgb(0 0 0 / 0.05)&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;xs&#x27;</span>: <span class="string">&#x27;0 1px 2px 0 rgb(0 0 0 / 0.05)&#x27;</span>,</span><br><span class="line">      <span class="string">&#x27;sm&#x27;</span>: <span class="string">&#x27;0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">plugins</span>: [],</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="デザインの共有">デザインの共有</h3><p>複数システム間で共通のデザインを共有したいということがあるかと思います。</p>
<p>そういうのは基本的にテーマとしてまとめて、共有できるようにします。どの要素にも変わらず同じスタイルを当てるという場合も、Tailwind CSSデフォルトのテーマの変数値を上書きするのではなく、次のように用途ごとに新しい変数を定義していく方が安全です。</p>
<p>アプリ内部でもよくつかうグループは名前をつけておくと一括で変更できて便利かもしれません。まあこういう話があると「必ず名前をつけなくてはいけない&#x2F;Tailwindのユーティリティを直接使ってはいけない」みたいなマナー講師が出てくるかもしれませんが、繰り返し何度も出てきたら考える、ぐらいでいいかと思います。まあ元のTailwind CSSの思想を理解してたらそんなことを言わないとは思いますが。</p>
<figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="keyword">@import</span> <span class="string">&quot;tailwindcss&quot;</span>;</span><br><span class="line"><span class="keyword">@theme</span> &#123;</span><br><span class="line">  <span class="attr">--font-title</span>: sans-serif;</span><br><span class="line">  <span class="attr">--font-body</span>: serif;</span><br><span class="line"></span><br><span class="line">  <span class="attr">--radius-button</span>: <span class="number">0.375rem</span>;</span><br><span class="line">  <span class="attr">--radius-card</span>: <span class="number">0.5rem</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="大きさの単位系をremに統一">大きさの単位系をremに統一</h3><p>テーマ等で使う大きさの単位は主にremとpxがあります。pxは言わずもがなですが、remはrootのem。emはアルファベットの大文字のMのサイズ、rootはルートのということになります。要するにデフォルトフォントサイズです。ほとんどのブラウザは1rem&#x3D;16pxです。</p>
<p>Tailwind CSSのレスポンシブデザインはV3まではピクセルでブレークポイントが定義されていました。V4ではremとなっています。smはV3までは640px、V4からは40remです。</p>
<p>検索するといろいろ賛否両論ありますが、開発側の意見としてはフォントサイズが変わるなら、それに合わせてパディングとかも変えられる方が便利である、というものです。</p>
<p>ピクセルとフォントサイズの両方が混ざってレイアウトされると、フォントサイズにより、テキストが予期せぬ位置で改行されたりしていろいろな条件でテストしないと正しいと判断できないことになりますが、スペースとかもすべてフォントサイズ依存になればフォントサイズ変更はズームと同じであり、レイアウト崩れの検証は減るものと思います。</p>
<p>なるべくremを統一的に使っていくのがV4以降は良いように思えます。</p>
<h2 id="まとめ">まとめ</h2><p>Tailwind CSSはユーティリティファーストですが、記述量を減らす工夫や、意味が伝わる別名を定義して可読性を上げる方法はいろいろ用意されています。それにより、スタイリング都合でHTMLの構造が複雑になったりというのをCSSレベルで抑制したりできます。このあたりを駆使すると、一歩レベルが上がるのではないかと思います。生成AIと人類の共通言語として定着したというメリットもありますし、賢くTailwind CSSを使っていけるようになる価値もさらに増えていると言えます。</p>
]]></content>
    <summary type="html">CSSをわかりやすくメンテナンス性高く書くというのは長い間試行錯誤され続けてきました。命名規則でがんばる、SCSSのようなプリプロセッサを使う、CSS in JSなどいろいろな仕組みがかつて作られたりしてきましたが...</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="CSS" scheme="https://future-architect.github.io/tags/CSS/"/>
    <category term="TailwindCSS" scheme="https://future-architect.github.io/tags/TailwindCSS/"/>
    <category term="ドキュメント" scheme="https://future-architect.github.io/tags/%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>構造化テキスト(URL)を文字列結合で作らないようにするライブラリを作ってみた</title>
    <link href="https://future-architect.github.io/articles/20250109a/"/>
    <id>https://future-architect.github.io/articles/20250109a/</id>
    <published>2025-01-08T15:00:00.000Z</published>
    <updated>2025-01-08T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250109a/urltidy.png" alt="" width="803" height="304">

<p>SQL、ファイルパスなどの構造化テキストを文字列結合で作ると、不正な文字列が入ってきた時に困るよ、というのはプログラミングの基本原則ですが、URLはついついやってしまいがちな部分です。</p>
<p>だいたいの言語にはURLクラスとかURIクラスとかその手のものがあり、それを使うことで安全にパースしたり組み立てたりできるのですが、いかんせんコードが長くなりがち、ということがあります。</p>
<p>TypeScriptをビルドしてnpmパッケージを作るのに便利なtsupというツールを使ってみたかったので、その題材としてURLを簡単かつ安全に組み立てるユーティリティを作ってみました。Node.js、Deno、Bunで動作確認しています。</p>
<ul>
<li>NPM: https://www.npmjs.com/package/url-tidy</li>
<li>GitHub: https://github.com/shibukawa/url-tidy</li>
</ul>
<p>テンプレートリテラルの前に関数をつける記法、タグ付きテンプレートリテラルというのがあります。文字列にする代わりに、テンプレートの文字列と間の値がこの関数の入力値になり、関数の返り値が実際のリテラルの評価値になる、というものです。litのHTMLテンプレートとして使われているやつですね。</p>
<h2 id="作ったユーティリティの紹介">作ったユーティリティの紹介</h2><p>それを使ってURLを組み立てます。 <code>url</code>というのがこの変換関数です。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; url &#125; <span class="keyword">from</span> <span class="string">&quot;url-tidy&quot;</span>;</span><br></pre></td></tr></table></figure>

<p>こんな感じで、文字列テンプレートとあまり変わらない感じですが、固定の文字列部分もきちんとURLの要素（プロトコルとかホストとかパスとかクエリーとか）にパースして要素分解します。さらに、固定部分もプレースホルダーで渡されるパス部分は<code>encodeURI()</code>を通すし、最後のURLの組み立ては<code>URL</code>クラスとか<code>URLSearchParams</code>を裏で使うので、不正な文字が入って不正なURLになるということは防げているかと思います。まああまり遅いことはなさそうですが、固定文字列部分は一度パースしたらその状態をキャッシュするようにしています。</p>
<p>プレースホルダーはパス、クエリーの値、フラグメントなど、1つの要素に対してのみしか使えないようにしています。パスの末尾とクエリーをまるごと文字列で渡す、みたいなことはできません。</p>
<p>一番基本的な使い方はパスの一部の置き換えでしょう。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> id = <span class="number">1000</span>;</span><br><span class="line">url<span class="string">`https://example.com/api/users/<span class="subst">$&#123;id&#125;</span>/profile`</span></span><br><span class="line"><span class="comment">// =&gt; &#x27;https://example.com/api/users/1000/profile&#x27;</span></span><br></pre></td></tr></table></figure>

<p>配列を渡すと<code>/</code>区切りで繋いだURLにするので階層が可変なURLでも安心ですね。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> areaList = [<span class="string">&quot;japan&quot;</span>, <span class="string">&quot;tokyo&quot;</span>, <span class="string">&quot;shinjuku&quot;</span>];</span><br><span class="line">url<span class="string">`https://example.com/menu/<span class="subst">$&#123;areaList&#125;</span>`</span></span><br><span class="line"><span class="comment">// =&gt; &#x27;https://example.com/menu/japan/tokyo/shinjuku&#x27;</span></span><br></pre></td></tr></table></figure>

<p>プロトコル、ポート、クエリーの値を設定する場合に<code>null</code>を渡すと、前後の記号やクエリーならキー部分も出力からは消去します。検索条件のクエリーの入った文字列を作るけど、無駄に長くはしたくない時はこういうの欲しくなりますよね？こういうのをきちんとやろうとすると、<code>URLSearchParams</code>を使うことになりますが、直接扱うとコードがかなりやりたいことのわりに増えちゃうな、という痒いところに届くようにしてみました。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> word = <span class="string">&quot;spicy food&quot;</span>;</span><br><span class="line"><span class="keyword">const</span> page = <span class="number">10</span>;</span><br><span class="line"><span class="keyword">const</span> perPage = <span class="literal">null</span>; <span class="comment">// デフォルト値を使うので設定しない</span></span><br><span class="line"><span class="keyword">const</span> limit = <span class="literal">null</span>;   <span class="comment">// デフォルト値を使うので設定しない</span></span><br><span class="line"></span><br><span class="line">url<span class="string">`https://example.com/api/search?word=<span class="subst">$&#123;word&#125;</span>&amp;page=<span class="subst">$&#123;page&#125;</span>&amp;perPage=<span class="subst">$&#123;perPage&#125;</span>&amp;limit=<span class="subst">$&#123;limit&#125;</span>`</span></span><br><span class="line"><span class="comment">// =&gt; &#x27;https://example.com/api/search?word=spicy+food&amp;page=10&#x27;</span></span><br></pre></td></tr></table></figure>

<p>逆にクエリー部分はZodやReact Hook Formでバリデーションした結果をオブジェクト形式で渡すよ、という場合も多いと思うので、オブジェクトや<code>URLSearchParams</code>でまとめて渡せるようにしています。固定のクエリーや他のクエリーのプレースホルダーとマージした結果を作ります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> searchParams = &#123;</span><br><span class="line">    <span class="attr">word</span>: <span class="string">&quot;spicy food&quot;</span>,</span><br><span class="line">    <span class="attr">safeSearch</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">spicyLevel</span>: <span class="title class_">Infinity</span>,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">url<span class="string">`https://example.com/api/search?<span class="subst">$&#123;searchParams&#125;</span>`</span></span><br><span class="line"><span class="comment">// =&gt; &#x27;https://example.com/api/search?word=spicy+food&amp;safeSearch=false&amp;spicyLevel=Infinity&#x27;</span></span><br></pre></td></tr></table></figure>

<p>URL周りでよくあるユースケースだと、開発環境や本番などで、接続先のホスト部分が変わるよ、というのもあります。あとは、ユーザー名とパスワードはソースコード中にハードコーディングしたくないはずなのでテンプレートリテラルの中には存在することはなさそうということで、この方法でしか設定できないようになっています。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; customFormatter &#125; <span class="keyword">from</span> <span class="string">&#x27;url-tidy&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> apiUrl = <span class="title function_">customFormatter</span>(&#123;</span><br><span class="line">    <span class="attr">hostname</span>: process.<span class="property">env</span>.<span class="property">API_SERVER_HOST</span>, <span class="comment">// &#x27;https://localhost:8080&#x27;</span></span><br><span class="line">    <span class="attr">username</span>: process.<span class="property">env</span>.<span class="property">API_USER</span>,        <span class="comment">// &#x27;user&#x27;</span></span><br><span class="line">    <span class="attr">password</span>: process.<span class="property">env</span>.<span class="property">API_PASSWORD</span>,    <span class="comment">// &#x27;pAssw0rd&#x27;</span></span><br><span class="line"></span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> id = <span class="number">1000</span>;</span><br><span class="line"></span><br><span class="line">apiUrl<span class="string">`https://api-server/api/users/<span class="subst">$&#123;id&#125;</span>/profile`</span></span><br><span class="line"><span class="comment">// =&gt; &#x27;https://user:pAssw0rd@localhost:8080/api/users/1000/profile&#x27;</span></span><br></pre></td></tr></table></figure>

<h2 id="開発環境">開発環境</h2><p>TypeScriptでライブラリを作るのは、tscを駆使すれば可能ではありますが、配布するならバンドルしたいし、モジュール形式も複数対応しないと、など考えることはたくさんあります。いろんなゼロコンフィグとか設定が少ない便利ツールは雨後の筍のごとくたくさん登場しますが、それらを活用して「設定のメンテには手間をかけない」「新しいことをやりくなったら、すぐに捨てて、別のツールに乗り換え」がフロントエンド周りではベストかな、と思っています。式年遷宮し続ける方式。</p>
<p>今回は、tsdx・Viteのライブラリモードも試してみました。前者は依存のツール類がちょっと古くて、最近の高速ツールの恩恵がなさそうでした。後者は開発サーバー付きでReactとかVueのコンポーネントライブラリ開発なら便利そうですが、今回のような純粋なロジックの開発だと余計なものが多いな、と思い、tsupを選びました。</p>
<p>設定はpackage.jsonに直接書く方式で書きましたがこのぐらいで済みました。</p>
<figure class="highlight json"><figcaption><span>package.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;tsup&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;target&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es2020&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;format&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">            <span class="string">&quot;cjs&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="string">&quot;esm&quot;</span></span><br><span class="line">        <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;entry&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">            <span class="string">&quot;src/index.ts&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="string">&quot;!src/*.spec.ts&quot;</span></span><br><span class="line">        <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;splitting&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;sourcemap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;clean&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;dts&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>tsup固有要素以外のパッケージ化に必要だった設定はこれぐらいですかね。あとはリポジトリのURLを書いたり、ライセンスを書いたり、バンドルするファイル一覧を書いたり、private: falseにしたり。</p>
<figure class="highlight json"><figcaption><span>package.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;module&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;exports&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;.&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;types&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.d.ts&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;require&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.cjs&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;import&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.js&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.cjs&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;module&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.js&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>今回はテストランナーはVitestを使いました。Node.jsもDenoもBunも内蔵のテストランナーを押す流れで、そちらを使うと高速という話も見ますが、Deno、BunのNode.js互換性も高くなり、Vitestで書いたテストを3つの環境で実行できました。GitHub Actionsで3つのテストを実行するようにしています。</p>
<p>コードチェックとフォーマッターは最近はBiomeを押す声が多いです。高速ではあるものの、ESLint+Prettierの方が個人的には好きかも。ESLintとPrettierの共存設定も以前よりもだいぶシンプルですし、Prettierが何もしなくても対応するEditorConfig対応はBiomeでは明示的に有効にしないといけないとかまああまり手間は変わらないかな、と。</p>
<h2 id="おまけ">おまけ</h2><p>Go版も作りました。Goにはタグ付きテンプレートリテラル構文がないので、PrintfスタイルのAPIで実装しました。</p>
<ul>
<li>github.com&#x2F;shibukawa&#x2F;urlf</li>
</ul>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;github.com/shibukawa/urlf&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">urlf.Urlf(<span class="string">`https://example.com/api/users/&#123;&#125;?key1=&#123;&#125;&amp;key2=&#123;&#125;`</span>, userCode, value1, value2)</span><br></pre></td></tr></table></figure>

<h2 id="まとめ">まとめ</h2><p>新しいツールの使い方を知るついでに前々から気になっていた、構造化文字列なのについ文字列結合で作ってしまいがちなURLの組み立てのユーティリティを作ってみました、というお話でした。</p>
]]></content>
    <summary type="html">SQL、ファイルパスなどの構造化テキストを文字列結合で作ると、不正な文字列が入ってきた時に困るよ、というのはプログラミングの基本原則ですが、URLはついついやってしまいがちな部分です。だいたいの言語には</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="OSS" scheme="https://future-architect.github.io/tags/OSS/"/>
    <category term="TypeScript" scheme="https://future-architect.github.io/tags/TypeScript/"/>
    <category term="npm" scheme="https://future-architect.github.io/tags/npm/"/>
  </entry>
  <entry>
    <title>TypeScript/JavaScript Array完全攻略2024</title>
    <link href="https://future-architect.github.io/articles/20241205a/"/>
    <id>https://future-architect.github.io/articles/20241205a/</id>
    <published>2024-12-04T15:00:00.000Z</published>
    <updated>2024-12-04T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241205a/ts2024.png" alt="" width="900" height="513">

<p>TypeScriptアドベントカレンダーの12&#x2F;5のエントリーです。昨日は@nanasi-1さんの【TypeScript】ジェネレーターによる遅延評価でフィボナッチ数列を生成するでした。</p>
<p>イマドキのJavaScriptの書き方2018というのを以前書いたのだけど、配列周りはかなり変わっているな、というのを思ったので、そこの部分だけアップデートするつもりで書いてみました。</p>
<h2 id="実環境で使えるECMAScriptバージョン">実環境で使えるECMAScriptバージョン</h2><p>今時のブラウザは常に最新に更新されるはずなのでECMAScript 2024の機能もフルに使えるはずですが、おそらくNode.jsのLTSが一番古いJavaScriptエンジンということになるのかな、と思います。本記事執筆時点でサポート中のバージョンは以下の4つです。軽くメソッドを調べたりした感じ、こんな感じかと。202x年の11月ぐらいになると、ES202xがLTSバージョンで使えるようになって・・・というサイクルのようですね。DenoとBunはよくわかりません。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">Nodeバージョン</th>
<th align="center">ESバージョン</th>
<th align="left">アクティブ</th>
<th align="left">メンテナンス</th>
</tr>
</thead>
<tbody><tr>
<td align="left">18.20.5(Maintenance)</td>
<td align="center">2022相当</td>
<td align="left">終了</td>
<td align="left">2025&#x2F;4&#x2F;30まで</td>
</tr>
<tr>
<td align="left">20.18.1(Maintenance)</td>
<td align="center">2023相当</td>
<td align="left">終了</td>
<td align="left">2026&#x2F;4&#x2F;30まで</td>
</tr>
<tr>
<td align="left">22.11.0(LTS)</td>
<td align="center">2024相当</td>
<td align="left">2025&#x2F;10&#x2F;21まで</td>
<td align="left">2027&#x2F;4&#x2F;30まで</td>
</tr>
<tr>
<td align="left">23.3.0(Current)</td>
<td align="center">2024相当</td>
<td align="left">2025&#x2F;3&#x2F;31まで</td>
<td align="left">2025&#x2F;6&#x2F;1まで</td>
</tr>
</tbody></table></div>
<p>VSCodeでコードを修正する場合、ES2023とか2024のメソッドを使おうとすると <code>lib</code> を上げろと言ってきますが、実際に使えるECMAScriptバージョンにあわせて、tsconfig.jsonのtargetを上げてしまう方が良いかと思います。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npx -p typescript tsc --init</span><br></pre></td></tr></table></figure>

<figure class="highlight json"><figcaption><span>tsconfig.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;target&quot;</span><span class="punctuation">:</span> <span class="string">&quot;es2023&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>MDNを見ると、Widely Availableは2.5年前のバージョン基準ということで、今だとECMAScript 2022以前、という感じですかね。</p>
<ul>
<li>https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility</li>
</ul>
<h2 id="配列の作成">配列の作成</h2><h3 id="新規で作る">新規で作る</h3><p><code>Array.of()</code> というクラスメソッドはありますが、指定した要素を持つ配列はリテラルで作れば良いかなと思います。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// リテラルで配列を作成</span></span><br><span class="line"><span class="keyword">const</span> array1 = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 空の場合は型指定必須</span></span><br><span class="line"><span class="keyword">const</span> <span class="attr">array2</span>: <span class="built_in">string</span>[] = [];   <span class="comment">// なるべくこちら推奨</span></span><br><span class="line"><span class="keyword">const</span> array3 = [] <span class="keyword">as</span> <span class="built_in">string</span>[]; <span class="comment">// 式として書かないといけない場合は右辺に</span></span><br></pre></td></tr></table></figure>

<h3 id="既存のデータ構造からコピーして作る">既存のデータ構造からコピーして作る</h3><p>旧来の書き方だと<code>slice()</code>を使って行っていたと思いますが、旧来のメソッドはインプレースの変更なのか新規作成なのかが分かりにくいため、見た目で新しいオブジェクトを作成していることが明確なスプレッド構文が良いかと思います。</p>
<figure class="highlight ts"><figcaption><span>古い書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧: slice利用</span></span><br><span class="line"><span class="keyword">var</span> copied1 = original.<span class="title function_">slice</span>(<span class="number">0</span>);</span><br></pre></td></tr></table></figure>

<figure class="highlight ts"><figcaption><span>新しい書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 新: 浅いコピーはスプレッド構文</span></span><br><span class="line"><span class="keyword">const</span> copeid2 = [...original];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 新: Map, Setなどのイテレータ対応オブジェクトはfrom利用</span></span><br><span class="line"><span class="keyword">const</span> copied3 = <span class="title class_">Array</span>.<span class="title function_">from</span>(map);</span><br></pre></td></tr></table></figure>

<p>ECMAScriptではないのですが、Node.jsでも使える <code>structuredClone()</code> 関数を使うと、深いコピーができます。サポートが遅れていたSafariでも2022&#x2F;10には使えるようになったので、現時点で全ての環境で使えると判断しても良いでしょう。</p>
<figure class="highlight ts"><figcaption><span>新しい書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 新: 深いコピーはstructuredClone</span></span><br><span class="line"><span class="keyword">const</span> copied3 = <span class="title function_">structuredClone</span>(original);</span><br></pre></td></tr></table></figure>

<h3 id="プログラマブルに配列を作る">プログラマブルに配列を作る</h3><p>プログラマブルに「全部0で初期化された配列を作る」ということを行いたい、ということがあるかと思います。</p>
<p>長さ指定で配列を作る場合は <code>Array(n)</code> で作れますが、これだと配列の中身が <code>undefined</code> になります。その後初期化が必要なため、疎な配列を作りたい場合以外は使わない方が良いでしょう。</p>
<figure class="highlight ts"><figcaption><span>古い書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧: Array(n)を利用。作成後にループで初期化、mapで初期化</span></span><br><span class="line"><span class="keyword">var</span> array1 = <span class="title class_">Array</span>(<span class="number">20</span>);</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; array.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    array1[i] = <span class="number">10</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>ECMAScript 2015の<code>Array.from()</code>はイテレータ対応のオブジェクト以外に<code>length</code>を持つオブジェクトが使えて、2つ目に<code>map()</code>と同じ変換関数を持てるので、これを使って任意の数を設定できます。<code>map()</code>と同じなので2つめにインデックスが渡されるのでインデックスごとに値を変えたい場合も対応可能です。</p>
<figure class="highlight ts"><figcaption><span>新しい書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 新: Array(n)を利用。作成後にループで初期化、mapで初期化</span></span><br><span class="line"><span class="keyword">const</span> array2 = <span class="title class_">Array</span>.<span class="title function_">from</span>(&#123; <span class="attr">length</span>: <span class="number">20</span> &#125;, <span class="function">() =&gt;</span> <span class="number">10</span>);</span><br></pre></td></tr></table></figure>

<h2 id="ループ">ループ</h2><p>ループの書き方は3種類あります。一番使うことになるのが <code>for ... of</code> ループです。3つ紹介するなかでは最後発で、2015年のECMAScript6で導入されました。この構文は<code>Array</code>、<code>Set</code>、<code>Map</code>、<code>String</code>などの繰り返し可能（iterable）オブジェクトに対してループします。配列の場合で、インデックス値が欲しい場合は、<code>entries()</code>メソッドを使います。</p>
<figure class="highlight ts"><figcaption><span>まず最初に使うべきループ</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> iterable = [<span class="string">&quot;小金井&quot;</span>, <span class="string">&quot;小淵沢&quot;</span>, <span class="string">&quot;小矢部&quot;</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// for ofループ</span></span><br><span class="line"><span class="comment">// 要素のみ欲しいときは for (const value of iterable)</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> value <span class="keyword">of</span> iterable) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// for ofループで配列のインデックスが欲しい</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> [i, value] <span class="keyword">of</span> iterable.<span class="title function_">entries</span>()) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(i, value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>こちらは関数呼び出しを伴わないフラットなコードなので、<code>async/await</code>とも一緒に使えます。配列の要素を引数にして、1つずつ<code>await</code>したい場合などです。</p>
<figure class="highlight ts"><figcaption><span>asyncと新しいループ</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> iterable = [<span class="number">10</span>, <span class="number">20</span>, <span class="number">30</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">const</span> value <span class="keyword">of</span> iterable) &#123;</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">doSomething</span>(value);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>2つ目に紹介するのはループ変数が必要となる、最初期からあるC言語由来のループです。順番に配列の最後までループする用途であれば、（2重ループとかで）変数名を間違えて意図せずループが終わったり、無限ループになるリスクがあるため、前述の <code>for ... of</code> がおすすめです。</p>
<p>このループが活躍するのは、アルゴリズムの実装で、ループ変数の開始位置や終了位置が先頭や末尾以外にする必要があるケースなどです。次のコードは、重複を効率よく探すために、 <code>i</code> と <code>j</code> の組が重複しないようにループ範囲を調整しています。</p>
<figure class="highlight ts"><figcaption><span>ループ変数を使うループ</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> iterable = [<span class="string">&quot;中野駅&quot;</span>, <span class="string">&quot;中村屋&quot;</span>, <span class="string">&quot;中尊寺&quot;</span>, <span class="string">&quot;中央線&quot;</span>, <span class="string">&quot;中野駅&quot;</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// C言語由来のループ</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; iterable.<span class="property">length</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">var</span> value = iterable[i];</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// スタート、終了位置が特殊なケース</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; iterable.<span class="property">length</span> - <span class="number">1</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">let</span> j = i + <span class="number">1</span>; j &lt; iterable.<span class="property">length</span>; j++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (iterable[i] === iterable[j]) &#123;</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`重複: <span class="subst">$&#123;iterable[i]&#125;</span> (<span class="subst">$&#123;i&#125;</span>, <span class="subst">$&#123;j&#125;</span>)`</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>2009年のECMAScript5で追加された<code>forEach()</code>もあります。関数型主義的なスタイルで統一するために、<code>for</code>を禁止して<code>forEach()</code>のみを使うというコーディング標準を規定している会社（Airbnb）も10年ぐらい前に話題になりましたが、積極的に使う理由はないでしょう。</p>
<figure class="highlight ts"><figcaption><span>ES5の書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> iterable = [<span class="string">&quot;大判焼&quot;</span>, <span class="string">&quot;大納言&quot;</span>, <span class="string">&quot;大所帯&quot;</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// forEach()ループ</span></span><br><span class="line">iterable.<span class="title function_">forEach</span>(<span class="function"><span class="params">value</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(value);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>速度の面で言えば、旧来の <code>for</code> ループが最速です。 <code>for ... of</code> や <code>forEach()</code> は、ループ1周ごとに関数呼び出しが挟まるため、実行コストが多少上乗せされますが、ゲームの座標計算で1フレームごとに数万要素のループを回さなければならない、といったケース以外ではほぼ気にする必要はないでしょうし、特に <code>for ... of</code> の場合は処理系が最適化を行ってくれているため、パフォーマンス低下は少ないです。</p>
<p>軽く検証したところ、最速の<code>for</code>と比べてChromeで<code>for...of</code>の処理時間が20%増し、<code>forEach()</code>が135%増しでした。Safariだと、Chromeで<code>for...of</code>が110%増し、<code>forEach()</code>が460%増しぐらい。<code>forEach()</code>はもう忘れましょう。</p>
<h2 id="配列のデータ取得">配列のデータ取得</h2><h3 id="1要素の取得">1要素の取得</h3><p>データの取得方法も選択肢がいくつか増えています。前からn番目の要素取得に関しては変わりませんが、末尾からの取得については、ECMAScriptで2022で追加された <code>at()</code> メソッドが便利です。正の値を使う場合は動作はインデックスアクセスと変わりませんが、負の数を与えると末尾からのインデックスで値が取得できます。</p>
<figure class="highlight ts"><figcaption><span>今でも有効な書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 共: 1要素の取得(前からのインデックス)</span></span><br><span class="line"><span class="keyword">const</span> v = array[<span class="number">10</span>];</span><br></pre></td></tr></table></figure>

<figure class="highlight ts"><figcaption><span>古い書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧: 末尾の要素の取得</span></span><br><span class="line"><span class="keyword">var</span> last = array[array.<span class="property">length</span> - <span class="number">1</span>];</span><br></pre></td></tr></table></figure>

<figure class="highlight ts"><figcaption><span>新しい書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 新: 末尾の要素の取得</span></span><br><span class="line"><span class="keyword">const</span> last = array.<span class="title function_">at</span>(-<span class="number">1</span>);</span><br></pre></td></tr></table></figure>

<h3 id="複数要素の取得">複数要素の取得</h3><p>複数の要素を取り出す場合は今までは1つずつ取り出していたと思います。また、残りの部分をまとめて取り出すときは<code>slice()</code>で取り出します。<br>もし配列が短い場合にデフォルト値を与えたい場合はORの演算子 <code>||</code> を使うことが多いでしょう。ただし、0とかfalsyな値に真面目に対応しようとするともうちょっとがんばらないといけないのですが。</p>
<figure class="highlight ts"><figcaption><span>古い書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 旧: 末尾の要素の取得</span></span><br><span class="line"><span class="keyword">var</span> a = array[<span class="number">0</span>];</span><br><span class="line"><span class="keyword">var</span> b = array[<span class="number">1</span>];</span><br><span class="line"><span class="keyword">var</span> c = array[<span class="number">2</span>] || <span class="number">10</span>; <span class="comment">// デフォルト値</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 旧: 残りの部分をまとめて配列にするにはslice()</span></span><br><span class="line"><span class="keyword">var</span> rest = array.<span class="title function_">slice</span>(<span class="number">3</span>);</span><br></pre></td></tr></table></figure>

<p>ECMAScript 2015から導入された分割代入では複数要素をまとめて取り出せます。ECMAScript 2018から導入された残りプロパティ(<code>...</code>)を使うと、残りを全てまとめて取得、というのもできます。</p>
<figure class="highlight ts"><figcaption><span>新しい書き方</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 新: 要素をまとめて取得。デフォルト値も指定可能。</span></span><br><span class="line"><span class="keyword">const</span> [a, b, c = <span class="number">10</span>] = array;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 新: 末尾の要素と残りをまとめて取得</span></span><br><span class="line"><span class="keyword">const</span> [a, b, c, ...rest] = array;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 新: いくつか読み飛ばしつつ抜き出し</span></span><br><span class="line"><span class="keyword">const</span> [, , , ...rest] = array;</span><br></pre></td></tr></table></figure>

<p>ただし、まとめて取得する場合のスタート位置が状況によって変わる場合は以前の通り、<code>slice()</code>を使うことになるでしょう。</p>
<h2 id="配列の変更">配列の変更</h2><p>ウェブフロントエンドの状態管理だと、配列を変更する場合には、インプレースで値を変更するのではなく、新しいオブジェクトを作って返す、ということが行われます。Reactが差分検知でそれを期待しており、Reduxとかもそれに従っていました。Redux Toolkitの裏で使われているimmer.jsみたいに、インプレースの変更をもとにイミュータブルに新しいオブジェクトを作ってくれるライブラリもあるにはありますが、本体にもイミュータブルスタイルのメソッド追加が頻繁に行われています。</p>
<p>どちらが新か旧かというのはなく、用途次第だと思うので、破壊的な方法と、イミュータブルの方法を並べて紹介します。</p>
<h3 id="先頭・末尾の要素の追加">先頭・末尾の要素の追加</h3><p>末尾に追加するのは<code>push()</code>メソッドが昔からある方法です。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line">a.<span class="title function_">push</span>(<span class="number">6</span>);    <span class="comment">// 末尾に追加</span></span><br><span class="line">a.<span class="title function_">push</span>(<span class="number">7</span>, <span class="number">8</span>); <span class="comment">// あまり知られてないけど2つ同時に追加もできる。</span></span><br><span class="line">a.<span class="title function_">unshift</span>(<span class="number">0</span>); <span class="comment">// 先頭に追加</span></span><br></pre></td></tr></table></figure>

<p>スプレッド構文を使うと、メソッド名と役割を覚えずに、リテラル上の位置のまま挿入できて、見た目もわかりやすいでしょう。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> newA  = [...a, <span class="number">6</span>, <span class="number">7</span>];  <span class="comment">// こちらも任意の数を追加できる</span></span><br><span class="line"><span class="keyword">const</span> newA2 = [<span class="number">0</span>, ...a];    <span class="comment">// 先頭にも追加できる</span></span><br></pre></td></tr></table></figure>

<h3 id="先頭・末尾の要素の削除">先頭・末尾の要素の削除</h3><p>先頭・末尾の要素の削除は<code>pop()</code>と<code>shift()</code>を使います。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line">a.<span class="title function_">pop</span>();    <span class="comment">// 末尾の要素の削除</span></span><br><span class="line">a.<span class="property">length</span>--; <span class="comment">// これでも可能</span></span><br><span class="line">a.<span class="title function_">shift</span>();  <span class="comment">// 先頭の要素の削除</span></span><br></pre></td></tr></table></figure>

<p>先頭の要素の削除はスプレッド構文を使えばできますが、末尾の要素の削除はスプレッド構文ではできません。<code>slice()</code>を使って、現在よりも1小さい長さを指定してコピーする方法しかないでしょう。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 末尾の要素の削除</span></span><br><span class="line"><span class="keyword">const</span> newA = a.<span class="title function_">slice</span>(<span class="number">0</span>, a.<span class="property">length</span> - <span class="number">1</span>);</span><br><span class="line"><span class="comment">// 先頭の要素の削除</span></span><br><span class="line"><span class="keyword">const</span> [, ...newA2] = a; <span class="comment">// 読み飛ばしたい数だけカンマを前におく</span></span><br></pre></td></tr></table></figure>

<h3 id="要素の変更">要素の変更</h3><p>これは一番シンプルな最初に覚える書き方ですね。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>];</span><br><span class="line">a[<span class="number">1</span>] = <span class="number">4</span>;</span><br></pre></td></tr></table></figure>

<p>こちらはECMAScript 2023の新顔の<code>with()</code>メソッドです。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> newA = a.<span class="title function_">with</span>(<span class="number">1</span>, <span class="number">4</span>);</span><br></pre></td></tr></table></figure>

<h3 id="配列の結合">配列の結合</h3><p><code>push()</code>は複数要素対応できるため、スプレッド構文を使えば破壊的な変更は難しくないでしょう。一昔前は<code>b.push.apply(b, c)</code>みたいなコードを書いていた気がしますが、それは忘れましょう。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> b = [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>];</span><br><span class="line"></span><br><span class="line">a.<span class="title function_">push</span>(...b);</span><br></pre></td></tr></table></figure>

<p>昔からある<code>concat()</code>メソッドは新しい配列も作りますが、スプレッド構文1つで配列結合もできるのでこちらだけ覚えておけば良いでしょう。昔のメソッドは破壊的なのかそうじゃないのかが名前だけでは分からない欠点があります。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> b = [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> newA1 = [...a, ...b]; <span class="comment">// スプレッド構文で結合も可能</span></span><br><span class="line"><span class="keyword">const</span> newA2 = a.<span class="title function_">concat</span>(b);  <span class="comment">// 昔からあるこちらもイミュータブル</span></span><br></pre></td></tr></table></figure>

<h3 id="途中への要素-配列の挿入・削除">途中への要素&#x2F;配列の挿入・削除</h3><p>昔のJavaScriptには「これが使いこなせれば初級脱出」という<code>splice()</code>というメソッドがありました。1つめの引数に処理を開始したい要素のインデックス、2つ目に削除したい要素の数、3つ目以降に挿入したい要素（複数可）というものです。これを使うと、任意の箇所に要素を挿入できます。</p>
<p>2つめの引数を1以上にすると削除します。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> b = [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>];</span><br><span class="line"></span><br><span class="line">a.<span class="title function_">splice</span>(<span class="number">2</span>, <span class="number">0</span>, <span class="number">3.5</span>);  <span class="comment">// 2と3の間に3.5を挿入</span></span><br><span class="line">a.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">0</span>, ...b); <span class="comment">// 1と2の間にbの要素を挿入</span></span><br><span class="line">a.<span class="title function_">splice</span>(<span class="number">1</span>, <span class="number">1</span>);       <span class="comment">// 2番目の要素を1つ削除</span></span><br></pre></td></tr></table></figure>

<p>これはスプレッド構文だけではやや難しかったのですが、ECMAScript 2023では<code>slice()</code>のイミュータブルバージョンが追加されました。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line"><span class="keyword">const</span> b = [<span class="number">4</span>, <span class="number">5</span>, <span class="number">6</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> newA = a.<span class="title function_">toSpliced</span>(<span class="number">2</span>, <span class="number">0</span>, <span class="number">3.5</span>);      <span class="comment">// 2と3の間に3.5を挿入</span></span><br><span class="line"><span class="keyword">const</span> newA2 = newA.<span class="title function_">toSpliced</span>(<span class="number">1</span>, <span class="number">0</span>, ...b); <span class="comment">// 1と2の間にbの要素を挿入</span></span><br><span class="line"><span class="keyword">const</span> newA3 = newA2.<span class="title function_">toSpliced</span>(<span class="number">1</span>, <span class="number">1</span>);      <span class="comment">// 2番目の要素を1つ削除</span></span><br></pre></td></tr></table></figure>

<h3 id="ソート・反転">ソート・反転</h3><p>ミュータブルなソートや反転はおなじみの<code>sort()</code>や<code>reverse()</code>を使います。</p>
<figure class="highlight ts"><figcaption><span>破壊的な方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">5</span>, <span class="number">8</span>, <span class="number">3</span>, <span class="number">6</span>, <span class="number">2</span>];</span><br><span class="line">arr.<span class="title function_">sort</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> b - a); <span class="comment">// ソート</span></span><br><span class="line">arr.<span class="title function_">reverse</span>();             <span class="comment">// 反転</span></span><br></pre></td></tr></table></figure>

<p><code>splice()</code>に対する<code>toSpliced()</code>みたいに、ECMAScript 2023で<code>sort()</code>に対する<code>toSorted()</code>と<code>reverse()</code>に対する<code>toReversed()</code>が追加されました。</p>
<figure class="highlight ts"><figcaption><span>イミュータブルな方法</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">1</span>, <span class="number">5</span>, <span class="number">8</span>, <span class="number">3</span>, <span class="number">6</span>, <span class="number">2</span>];</span><br><span class="line"><span class="keyword">const</span> arr2 = arr.<span class="title function_">toSroted</span>(<span class="function">(<span class="params">a, b</span>) =&gt;</span> b - a); <span class="comment">// ソート</span></span><br><span class="line"><span class="keyword">const</span> arr3 = arr2.<span class="title function_">toReversed</span>();             <span class="comment">// 反転</span></span><br></pre></td></tr></table></figure>

<p>Pythonは昔から<code>sorted()</code>とか<code>reversed()</code>という関数を提供していましたが、「ソート済みかどうか」の判定関数にも見えてしまう名前なのでこちらの方が良いですね。</p>
<h3 id="ネストを解消">ネストを解消</h3><p><code>flat()</code>は配列の中に配列がある場合に、それを平坦にならします。デフォルトでは配列の中の配列までが対象ですが、パラメータの数値で、何段まで探索するかが変わります。これはイミュータブルなメソッドです。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> arr = [<span class="number">0</span>, <span class="number">1</span>, [<span class="number">2</span>, [<span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>]]]; <span class="comment">// 3層の配列</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr.<span class="title function_">flat</span>());  <span class="comment">// デフォルトでは1階層目の配列を展開</span></span><br><span class="line"><span class="comment">// [0, 1, 2, [3, 4, 5]]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(arr.<span class="title function_">flat</span>(<span class="number">2</span>)); <span class="comment">// 数値を大きくすれば1次元配列に</span></span><br><span class="line"><span class="comment">// [0, 1, 2, 3, 4, 5]</span></span><br></pre></td></tr></table></figure>

<h2 id="リスト処理">リスト処理</h2><p>フロントエンドで関数型スタイルのコーディング技法として広まっているが関数型由来のリスト処理です。昔からあって一番有名なのが<code>map</code>&#x2F;<code>filter</code>&#x2F;<code>reduce</code>ですが、他にもいくつかあります。</p>
<p>1つだけだと<code>for...of</code>を使うのとあまり変わりませんが、メソッドチェーンで複数接続すると、小さい関数で期待する結果を得る、関数型スタイルの処理になります。</p>
<p>残念ながら、これらはリストのメソッドであり、DOMのAPIにいくつかある「リストっぽいけどリストじゃない」オブジェクトでは使えなかったりします。今ステージ2のパイプライン演算子が入ることになれば、これらのメソッドも純粋な関数に再定義されて、すべてのイテレーション対応オブジェクトで使えるようになるんじゃないかと期待しています。このリスト処理だと同期処理しか対応できないのですが、それもこのパイプライン演算子が入れば解決の見込みです。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> result = input</span><br><span class="line">    |&gt; <span class="title function_">validation</span>(%) <span class="comment">// 他の言語とは違い、%が前の関数の返り値みたい</span></span><br><span class="line">    |&gt; <span class="title function_">normalize</span>(%)</span><br><span class="line">    |&gt; <span class="title function_">sanitize</span>(%);</span><br></pre></td></tr></table></figure>

<h3 id="リスト→リスト変換">リスト→リスト変換</h3><p>リストからリストを返すメソッド群が以下の3つです。</p>
<ul>
<li><code>map(変換処理)</code>: ループの要素ごとに変換処理の関数を呼び出し、その結果で新しい配列を作って返す</li>
<li><code>filter(判定処理)</code>: ループの要素ごとに判定処理の関数を呼び出し、真の値を返すもののみの新しい配列を作って返す</li>
<li><code>flatMap(変換関数)</code>: ループの要素ごとに変換処理の関数を呼び出し、その結果を <code>flat()</code> した新しい配列を作って返す</li>
</ul>
<p>これらを結合すると柔軟な処理が可能ですが、可能であれば<code>filter()</code>で要素を減らす処理を先に行うと、トータルのループの回数が減るため、処理が高速化されます。</p>
<p>Reactの仮想DOM構築では、通常はすべて1つの式の中でDOMを作ります。そうなるとif&#x2F;forが使えません。ifの代わりに三項演算子、<code>for</code>の代わりに<code>map</code>がよく使われます。</p>
<figure class="highlight ts"><figcaption><span>リスト→リスト処理</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> items = [</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;りんご&quot;</span>, <span class="attr">price</span>: <span class="number">100</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;バナナ&quot;</span>, <span class="attr">price</span>: <span class="number">50</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;オレンジ&quot;</span>, <span class="attr">price</span>: <span class="number">80</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;牛乳&quot;</span>, <span class="attr">price</span>: <span class="number">150</span>, <span class="attr">category</span>: <span class="string">&quot;乳製品&quot;</span> &#125;,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 税金を加算したリストを作る</span></span><br><span class="line"><span class="keyword">const</span> itemsWithTax = items.<span class="title function_">map</span>(<span class="function">(<span class="params">&#123;name, price, category&#125;</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &#123;name, <span class="attr">price</span>: price * <span class="number">1.1</span>, category&#125;;</span><br><span class="line">&#125;);</span><br><span class="line"><span class="comment">// [</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;りんご&quot;, price: 110.00000000000001, category: &quot;果物&quot; &#125;,</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;バナナ&quot;, price: 55.00000000000001, category: &quot;果物&quot; &#125;,</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;オレンジ&quot;, price: 88, category: &quot;果物&quot; &#125;,</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;牛乳&quot;, price: 165, category: &quot;乳製品&quot; &#125;</span></span><br><span class="line"><span class="comment">// ]</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 値段が100以上の商品を抽出する</span></span><br><span class="line"><span class="keyword">const</span> cheapItems = items.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">price</span> &lt; <span class="number">100</span>);</span><br><span class="line"><span class="comment">// [</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;バナナ&quot;, price: 50, category: &quot;果物&quot; &#125;,</span></span><br><span class="line"><span class="comment">//   &#123; name: &quot;オレンジ&quot;, price: 80, category: &quot;果物&quot; &#125;</span></span><br><span class="line"><span class="comment">// ]</span></span><br></pre></td></tr></table></figure>

<p><code>flatMap()</code>は<code>map()</code>と<code>flat()</code>をくっつけたものです。ただし、配列を返すとそれを展開する点が異なります。くっつけたことにより、<code>map()</code>よりも柔軟です。</p>
<p>次のサンプルのように、定数を返すだけなら<code>map()</code>互換ですが、空配列を返すとその要素は消滅します。また、配列を返すことで、1要素から複数の要素を生み出せます。ただし、配列そのものを要素にしたい場合は2重配列にする必要があります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line"><span class="keyword">const</span> result = a.<span class="title function_">flatMap</span>(<span class="function"><span class="params">i</span> =&gt;</span> &#123;</span><br><span class="line">   <span class="keyword">if</span> (i === <span class="number">1</span>) <span class="keyword">return</span> [];             <span class="comment">// 空配列を返すと削除</span></span><br><span class="line">   <span class="keyword">if</span> (i === <span class="number">2</span>) <span class="keyword">return</span> <span class="number">2</span>;              <span class="comment">// 配列以外を返すとmapと同じくそのまま結果に入る</span></span><br><span class="line">   <span class="keyword">if</span> (i === <span class="number">3</span>) <span class="keyword">return</span> [<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>];      <span class="comment">// 配列を返すと展開される</span></span><br><span class="line">   <span class="keyword">if</span> (i === <span class="number">4</span>) <span class="keyword">return</span> [[<span class="number">4</span>, <span class="number">4</span>, <span class="number">4</span>, <span class="number">4</span>]]; <span class="comment">// 配列そのものを要素にしたい場合は2重配列にする</span></span><br><span class="line">&#125;);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(result);</span><br><span class="line"><span class="comment">// =&gt; [2, 3, 3, 3, [4, 4, 4, 4]]</span></span><br></pre></td></tr></table></figure>

<h3 id="1つの要素を返す">1つの要素を返す</h3><ul>
<li><code>reduce(結合処理[, 初期値])</code>: 要素の配列ごとに結合処理を行い、最終的に1つの値にして返す</li>
<li><code>some(判定処理)</code>: 要素の1つでも判定処理関数が真の値を返せば <code>true</code> を返す</li>
<li><code>every(判定処理)</code>: すべての要素に対して判定処理関数が真の値を返せば <code>true</code> を返す</li>
</ul>
<figure class="highlight ts"><figcaption><span>リスト→1つの要素</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> items = [</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;りんご&quot;</span>, <span class="attr">price</span>: <span class="number">100</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;バナナ&quot;</span>, <span class="attr">price</span>: <span class="number">50</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;オレンジ&quot;</span>, <span class="attr">price</span>: <span class="number">80</span>, <span class="attr">category</span>: <span class="string">&quot;果物&quot;</span> &#125;,</span><br><span class="line">  &#123; <span class="attr">name</span>: <span class="string">&quot;牛乳&quot;</span>, <span class="attr">price</span>: <span class="number">150</span>, <span class="attr">category</span>: <span class="string">&quot;乳製品&quot;</span> &#125;,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 合計値</span></span><br><span class="line"><span class="keyword">const</span> totalPrice = items.<span class="title function_">reduce</span>(<span class="function">(<span class="params">acc, &#123;price&#125;</span>) =&gt;</span> acc + price, <span class="number">0</span>);</span><br><span class="line"><span class="comment">// =&gt; 380</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 果物が含まれる？</span></span><br><span class="line">items.<span class="title function_">some</span>(<span class="function">(<span class="params">&#123; category &#125;</span>) =&gt;</span> category === <span class="string">&quot;果物&quot;</span>);</span><br><span class="line"><span class="comment">// =&gt; true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// すべての商品が果物？</span></span><br><span class="line">items.<span class="title function_">every</span>(<span class="function">(<span class="params">&#123; category &#125;</span>) =&gt;</span> category === <span class="string">&quot;果物&quot;</span>);</span><br><span class="line"><span class="comment">// =&gt; false</span></span><br></pre></td></tr></table></figure>

<p><code>reduce()</code>の結合処理は前の処理結果が1つめの引数に渡され2つ目に配列の要素が入ります。これを要素数分繰り返します。初期値を省略すると2つ目の要素から処理され、最初の要素は前の処理の結果として渡されます。上記の処理は4回呼ばれます。最後の結果が関数の結果となります。</p>
<ul>
<li><code>(0,   &#123; price: 100 &#125;)</code> &#x3D;&gt; 100</li>
<li><code>(100, &#123; price: 50  &#125;)</code> &#x3D;&gt; 150</li>
<li><code>(150, &#123; price: 80  &#125;)</code> &#x3D;&gt; 230</li>
<li><code>(230, &#123; price: 150 &#125;)</code> &#x3D;&gt; 380</li>
</ul>
<p><code>some()</code>は条件似合うものが1つでもあればすぐにループを終えるので、<code>every()</code>はすべての要素を探索するので、not条件を検証したい場合は、<code>some()</code>で代替できないかは検討してみると良いでしょう。</p>
<h2 id="まとめ">まとめ</h2><p>以前の配列は破壊的操作と、イミュータブルな操作が混ざっていてわかりにくかったのですが、関数型スタイルブームに合わせてか、イミュータブルな処理が増えました。リファレンスを見ても、その辺りの区別がつきにくいと思われるため整理のためにまとめてみました。</p>
<p>リスト処理、<code>toXXXed()</code>メソッドの追加と<code>with()</code>メソッド、<code>flat()</code>メソッド、スプレッド構文を中心に使えばイミュータブルなデータ加工はだいぶ思いのままにできそうだな、ということがわかりました。</p>
]]></content>
    <summary type="html">イマドキのJavaScriptの書き方2018]というのを以前書いたのだけど、配列周りはかなり変わっているな、というのを思ったので、そこの部分だけアップデートするつもりで書いてみました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="ECMAScript" scheme="https://future-architect.github.io/tags/ECMAScript/"/>
    <category term="JavaScript" scheme="https://future-architect.github.io/tags/JavaScript/"/>
    <category term="TypeScript" scheme="https://future-architect.github.io/tags/TypeScript/"/>
  </entry>
  <entry>
    <title>Deno × Vueを触ってみた(2024年冬)</title>
    <link href="https://future-architect.github.io/articles/20241129a/"/>
    <id>https://future-architect.github.io/articles/20241129a/</id>
    <published>2024-11-28T15:00:01.000Z</published>
    <updated>2024-11-28T15:00:01.000Z</updated>
    <author><name>山本竜玄</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241129a/image.png" alt="" width="273" height="165">

<p>本記事はVue.js連載5本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>HealthCare Innovation Group（HIG）所属の山本です。</p>
<p>Vue.jsは素晴らしいフロントエンドのフレームワークとして、世界中で広く使用されています。公式ドキュメントでも非常に整っておりインストール手順やチュートリアルについて記載されていますが、Node.jsを利用することが前提となっているものが多いです。</p>
<p>実際に私が関わったVue.jsを使用したプロジェクトでも、Node.jsおよびnpmを使用しており、かなり一般的な選択肢ではないかと感じています。</p>
<p>そんな中、Node.jsに替わるJavaScriptランタイムとして新たに注目を集めているのがDenoです。2024年にはDeno v2がリリースされ、フォーマットやテストなどなどのDenoの機能の改善以外にも、Node.jsおよびnpmとの下位互換性を取り入れたことがとても大きな変更としてあります。</p>
<p>この記事では、Deno v2とVue.jsを組み合わせた公式チュートリアルの実施を通して、2024年冬時点での現状や特徴について解説していきます。</p>
<h2 id="Denoについて">Denoについて</h2><p>JavaScript実行環境のNode.jsの開発者であるRyan Dahlが2018年に発表した、JavaScript&#x2F;TypeScript実行環境がDenoです。</p>
<p>Node.jsの設計上の問題点を解決するために0から作り直されたことが特徴です。</p>
<p>実際に、JSConf 2018にて「Node.jsに関する10の反省点」としてRyan Dahl自身が登壇し、以下のようなことを述べています。</p>
<p>https://www.youtube.com/watch?v=M3BM9TB-8yA</p>
<ol>
<li>Promiseを使い続けなかったため、Nodeの非同期APIに課題があること</li>
<li>セキュリティ設計について。例えばlinterのnetworkフルアクセスなど</li>
<li>ビルドシステム(GYP)を継続使用したこと</li>
<li>package.jsonの存在、肥大化について</li>
<li>node_modulesの存在、モジュール解決アルゴリズムの複雑化について</li>
<li>require(“module”)で.jsの拡張子なしで読み込み可能としたこと</li>
<li>index.jsの存在、モジュール読み込みが複雑化したこと</li>
</ol>
<p>これらの設計に関する課題について述べたあと、Ryan Dahlはまだまだプロダクトレベルであることを前置きしたうえで、新しく開発したDenoを紹介しています。</p>
<p>そこからときが経ち2024年、Deno v2のリリースが発表されました。</p>
<p>https://deno.com/blog/v2.0</p>
<p>Denoの設計思想に基づき、ネイティブのTypeScriptのサポート、Promiseなどのサポート、組み込みのフォーマッタや型チェック、セキュリティの考慮などが特徴である他、v2からはNode.jsおよびnpmの下位互換性が追加されています。</p>
<h2 id="Deno-×-Vue-jsについて">Deno × Vue.jsについて</h2><p>ここからは、実際にDeno × Vue.jsでアプリ構築を実際に試していきます。<br>記事執筆時点では、公式ドキュメントのチュートリアルとして以下があるので、その手順に従っていきます。</p>
<p>https://docs.deno.com/runtime/tutorials/how_to_with_npm/vue/</p>
<h3 id="1-インストール">1. インストール</h3><p>Deno自体のインストールについては、下記のドキュメントに従います。</p>
<p>https://docs.deno.com/runtime/getting_started/installation/</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">curl -fsSL https://deno.land/install.sh | sh</span><br></pre></td></tr></table></figure>

<p>シェルスクリプトの一発でインストールできてお手軽ですね。記事執筆時点では、v2.1.2がインストールされました。</p>
<p>Denoのインストール後、以下のコマンドでプロジェクトのセットアップを進めます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno run -A npm:create-vite</span><br></pre></td></tr></table></figure>

<p>フレームワークの選択が求められるので、Vueを選択します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">❯ deno run -A npm:create-vite</span><br><span class="line">✔ Project name: … vite-project</span><br><span class="line">? Select a framework: › - Use arrow-keys. Return to submit.</span><br><span class="line">    Vanilla</span><br><span class="line">❯   Vue</span><br><span class="line">    React</span><br><span class="line">    Preact</span><br><span class="line">    Lit</span><br><span class="line">    Svelte</span><br><span class="line">    Solid</span><br><span class="line">    Qwik</span><br><span class="line">    Angular</span><br><span class="line">    Others</span><br></pre></td></tr></table></figure>

<p>こちらも選択が求められますが、せっかくなので、DenoのネイティブのTypeScriptのサポートを確認するために、TypeScriptを選択します。</p>
<p>TypeScriptがネイティブでサポートされているため、トランスパイル設定などの手間が不要なのが嬉しいポイントです。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">❯ deno run -A npm:create-vite</span><br><span class="line">✔ Project name: … vite-project</span><br><span class="line">✔ Select a framework: › Vue</span><br><span class="line">? Select a variant: › - Use arrow-keys. Return to submit.</span><br><span class="line">❯   TypeScript</span><br><span class="line">    JavaScript</span><br><span class="line">    Customize with create-vue ↗</span><br><span class="line">    Nuxt ↗</span><br></pre></td></tr></table></figure>

<p>次に、プロジェクトフォルダに移動して依存関係のインストールを行います。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno install</span><br></pre></td></tr></table></figure>

<p>これでインストールは完了です。<code>deno install</code>の実行時点で、<code>node_modules</code>フォルダが作成されます。<br>フォルダの内容としては以下のようになっています。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">node_modules/</span><br><span class="line">├── typescript -&gt; .deno/typescript@5.6.3/node_modules/typescript</span><br><span class="line">├── vite -&gt; .deno/vite@6.0.1/node_modules/vite</span><br><span class="line">├── @vitejs</span><br><span class="line">│   └── plugin-vue -&gt; ../.deno/@vitejs+plugin-vue@5.2.1/node_modules/@vitejs/plugin-vue</span><br><span class="line">├── vue -&gt; .deno/vue@3.5.13/node_modules/vue</span><br><span class="line">└── vue-tsc -&gt; .deno/vue-tsc@2.1.10/node_modules/vue-tsc</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><code>npm install</code>などのコマンドを実行することなく、npmレジストリよりパッケージをインストールできていますね。</p>
<p>ここまででテンプレートは作成できており、以下のコマンドでアプリ起動ができます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno task dev</span><br></pre></td></tr></table></figure>

<img src="/images/2024/20241129a/image_2.png" alt="" width="1134" height="741" loading="lazy">

<h3 id="2-バックエンドの追加">2. バックエンドの追加</h3><p>ここからもチュートリアあるに従っていきます。以下コマンドで、バックエンドに必要なものを追加していきます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno add jsr:@oak/oak jsr:@tajpouria/cors</span><br></pre></td></tr></table></figure>

<p>構成としては、プロジェクト配下にapiフォルダを作成して、以下2つのファイルを作成します。</p>
<ul>
<li>main.ts</li>
<li>data.json</li>
</ul>
<figure class="highlight ts"><figcaption><span>main.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Application</span>, <span class="title class_">Router</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@oak/oak&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; oakCors &#125; <span class="keyword">from</span> <span class="string">&quot;@tajpouria/cors&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> data <span class="keyword">from</span> <span class="string">&quot;./data.json&quot;</span> <span class="keyword">with</span> &#123; <span class="attr">type</span>: <span class="string">&quot;json&quot;</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> router = <span class="keyword">new</span> <span class="title class_">Router</span>();</span><br><span class="line"></span><br><span class="line">router</span><br><span class="line">    .<span class="title function_">get</span>(<span class="string">&quot;/&quot;</span>, <span class="function">(<span class="params">context</span>) =&gt;</span> &#123;</span><br><span class="line">        context.<span class="property">response</span>.<span class="property">body</span> = <span class="string">&quot;Welcome to dinosaur API!&quot;</span>;</span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">get</span>(<span class="string">&quot;/dinosaurs&quot;</span>, <span class="function">(<span class="params">context</span>) =&gt;</span> &#123;</span><br><span class="line">        context.<span class="property">response</span>.<span class="property">body</span> = data;</span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">get</span>(<span class="string">&quot;/dinosaurs/:dinosaur&quot;</span>, <span class="function">(<span class="params">context</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (!context?.<span class="property">params</span>?.<span class="property">dinosaur</span>) &#123;</span><br><span class="line">            context.<span class="property">response</span>.<span class="property">body</span> = <span class="string">&quot;No dinosaur name provided.&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">const</span> dinosaur = data.<span class="title function_">find</span>(<span class="function">(<span class="params">item</span>) =&gt;</span></span><br><span class="line">            item.<span class="property">name</span>.<span class="title function_">toLowerCase</span>() === context.<span class="property">params</span>.<span class="property">dinosaur</span>.<span class="title function_">toLowerCase</span>()</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        context.<span class="property">response</span>.<span class="property">body</span> = dinosaur ? dinosaur : <span class="string">&quot;No dinosaur found.&quot;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="keyword">new</span> <span class="title class_">Application</span>();</span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">oakCors</span>());</span><br><span class="line">app.<span class="title function_">use</span>(router.<span class="title function_">routes</span>());</span><br><span class="line">app.<span class="title function_">use</span>(router.<span class="title function_">allowedMethods</span>());</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> app.<span class="title function_">listen</span>(&#123; <span class="attr">port</span>: <span class="number">8000</span> &#125;);</span><br></pre></td></tr></table></figure>

<p>サンプルの量が多かったので一部抜粋しますが、以下のようなjsonファイルを作成します。</p>
<figure class="highlight json"><figcaption><span>data.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Aardonyx&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;An early stage in the evolution of sauropods.&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Abelisaurus&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;\&quot;Abel&#x27;s lizard\&quot; has been reconstructed from a single skull.&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure>

<p>これで恐竜のデータを取得するバックエンドapiの構築は完了です。以下のコマンドでサーバーを起動できます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno run --allow-env --allow-net api/main.ts</span><br></pre></td></tr></table></figure>

<p>起動できると、例えば<code>localhost:8000/dinosaurs/Aardonyx</code>へのアクセスで以下のようなレスポンスが帰ってくることが確認できます。</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">&#123;<span class="attr">name</span>: <span class="string">&quot;Aardonyx&quot;</span>, <span class="attr">description</span>: <span class="string">&quot;An early stage in the evolution of sauropods.&quot;</span>&#125;</span><br></pre></td></tr></table></figure>

<p>ここまでで無事にバックエンドサーバーの構築・起動までチュートリアルで確認できました。</p>
<hr>
<p>先程実行した、起動コマンドについてですが起動時に与えている引数がDenoの大きな特徴の1つです。</p>
<p>デフォルトではホストマシンまたはネットワークへのアクセス権限は最小限にする思想ですね。</p>
<p>これらの明示的に与えられるPermissionについては以下のようなものがあります。</p>
<ul>
<li>–allow-read : ファイルの読み取りアクセス</li>
<li>–allow-write : ファイルシステムの書き込みアクセス</li>
<li>–allow-net : ネットワークアクセス</li>
<li>–allow-env : 環境変数へのアクセス</li>
<li>–allow-run : サブプロセスの実行</li>
</ul>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno run --allow-env --allow-net api/main.ts</span><br></pre></td></tr></table></figure>

<p>例えば上記のコマンドでは、<code>--allow-env</code>では環境変数へのアクセス権、<code>--allow-net</code>ではネットワークアクセスの許可を<code>api/main.ts</code>に与えるように指定しています。</p>
<p>試しにネットワークアクセスを許可しないまま起動しようとすると、以下のようにエラーが発生します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">❯ deno run api/main.ts</span><br><span class="line">Permission flags have likely been incorrectly <span class="built_in">set</span> after the script argument.</span><br><span class="line">To grant permissions, <span class="built_in">set</span> them before the script argument. For example:</span><br><span class="line">    deno run --allow-read=. main.js</span><br><span class="line">❌ Denied net access to <span class="string">&quot;0.0.0.0:8000&quot;</span>.</span><br><span class="line">error: Uncaught (<span class="keyword">in</span> promise) NotCapable: Requires net access to <span class="string">&quot;0.0.0.0:8000&quot;</span>, run again with the --allow-net flag</span><br><span class="line">        this.#httpServer = serve?.(&#123;</span><br><span class="line">                           ^</span><br><span class="line">    at listen (ext:deno_net/01_net.js:504:35)</span><br><span class="line">    at Object.serve (ext:deno_http/00_serve.ts:555:16)</span><br><span class="line">    at Object.start (https://jsr.io/@oak/oak/17.1.3/http_server_native.ts:84:28)</span><br><span class="line">    at Module.invokeCallbackFunction (ext:deno_webidl/00_webidl.js:1105:16)</span><br><span class="line">    at startAlgorithm (ext:deno_web/06_streams.js:3661:14)</span><br><span class="line">    at setUpReadableStreamDefaultController (ext:deno_web/06_streams.js:3625:23)</span><br><span class="line">    at setUpReadableStreamDefaultControllerFromUnderlyingSource (ext:deno_web/06_streams.js:3691:3)</span><br><span class="line">    at new ReadableStream (ext:deno_web/06_streams.js:5160:7)</span><br><span class="line">    at Server.listen (https://jsr.io/@oak/oak/17.1.3/http_server_native.ts:82:20)</span><br><span class="line">    at Application.listen (https://jsr.io/@oak/oak/17.1.3/application.ts:840:35)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>別の例としては、先程の実装ではimportでdata.jsonを読み込んでいたため、<code>--allow-read</code>の権限許可は必要としていませんでした。</p>
<p>以下のように読み込み部分を変更してみましょう。</p>
<figure class="highlight diff"><table><tr><td class="code"><pre><span class="line"><span class="deletion">- import data from &quot;./data.json&quot; with &#123; type: &quot;json&quot; &#125;;</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+ let data: Array&lt;&#123; name: string; description: string &#125;&gt; = [];</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+ // ファイルシステムからJSONデータを読み込む関数</span></span><br><span class="line"><span class="addition">+ async function loadData() &#123;</span></span><br><span class="line"><span class="addition">+     const rawData = await Deno.readTextFile(&quot;./api/data.json&quot;);</span></span><br><span class="line"><span class="addition">+     data = JSON.parse(rawData);</span></span><br><span class="line"><span class="addition">+ &#125;</span></span><br><span class="line"></span><br><span class="line"><span class="addition">+ // サーバー起動時にデータをロード</span></span><br><span class="line"><span class="addition">+ await loadData();</span></span><br></pre></td></tr></table></figure>

<p>再度同様にサーバー起動をしてみようとしましたが、以下のエラーにより弾かれてしまいました。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">❯  deno run --allow-env --allow-net api/main.ts</span><br><span class="line">❌ Denied <span class="built_in">read</span> access to <span class="string">&quot;/home/penryu/dev/projects/deno-vue/vite-project/api/data.json&quot;</span>.</span><br><span class="line">error: Uncaught (<span class="keyword">in</span> promise) NotCapable: Requires <span class="built_in">read</span> access to <span class="string">&quot;./api/data.json&quot;</span>, run again with the --allow-read flag</span><br><span class="line">    const rawData = await Deno.readTextFile(<span class="string">&quot;./api/data.json&quot;</span>);</span><br><span class="line">                               ^</span><br><span class="line">    at Object.readTextFile (ext:deno_fs/30_fs.js:779:24)</span><br><span class="line">    at loadData (file:///home/penryu/dev/projects/deno-vue/vite-project/api/main.ts:8:32)</span><br><span class="line">    at file:///home/penryu/dev/projects/deno-vue/vite-project/api/main.ts:13:7</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>このように、デフォルトで安全性が確保されているのは嬉しいポイントですね。</p>
<h3 id="3-フロントエンドの構築">3. フロントエンドの構築</h3><p>Vue Routerモジュールを以下のように追加します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno add npm:vue-router</span><br></pre></td></tr></table></figure>

<p>チュートリアルでは、このあといくつかのファイルを実装することになります。対象としては、以下の7ファイルです。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">├── src</span><br><span class="line">│   ├── App.vue</span><br><span class="line">│   ├── components</span><br><span class="line">│   │   ├── Dinosaurs.vue</span><br><span class="line">│   │   ├── Dinosaur.vue</span><br><span class="line">│   │   └── HomePage.vue</span><br><span class="line">│   ├── main.ts</span><br><span class="line">│   ├── router</span><br><span class="line">│   │   └── index.ts</span><br><span class="line">│   ├── type.d.ts</span><br></pre></td></tr></table></figure>

<p>実際の内容の紹介です。　main.tsとindex.tsでは以下のようにルーターを実装しています。</p>
<figure class="highlight ts"><figcaption><span>main.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&quot;vue&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> router <span class="keyword">from</span> <span class="string">&quot;./router/index.ts&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">&quot;./style.css&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&quot;./App.vue&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="title function_">createApp</span>(<span class="title class_">App</span>)</span><br><span class="line">  .<span class="title function_">use</span>(router)</span><br><span class="line">  .<span class="title function_">mount</span>(<span class="string">&quot;#app&quot;</span>);</span><br></pre></td></tr></table></figure>

<figure class="highlight ts"><figcaption><span>router/index.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createRouter, createWebHistory &#125; <span class="keyword">from</span> <span class="string">&quot;vue-router&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">HomePage</span> <span class="keyword">from</span> <span class="string">&quot;../components/HomePage.vue&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Dinosaur</span> <span class="keyword">from</span> <span class="string">&quot;../components/Dinosaur.vue&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">createRouter</span>(&#123;</span><br><span class="line">  <span class="attr">history</span>: <span class="title function_">createWebHistory</span>(<span class="string">&quot;/&quot;</span>),</span><br><span class="line">  <span class="attr">routes</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">path</span>: <span class="string">&quot;/&quot;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&quot;Home&quot;</span>,</span><br><span class="line">      <span class="attr">component</span>: <span class="title class_">HomePage</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">path</span>: <span class="string">&quot;/:dinosaur&quot;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&quot;Dinosaur&quot;</span>,</span><br><span class="line">      <span class="attr">component</span>: <span class="title class_">Dinosaur</span>,</span><br><span class="line">      <span class="attr">props</span>: <span class="literal">true</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  ],</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>App.vueの内容について、以下のようにします。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">RouterView</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>あとはコンポーネントたちを以下のように実装します。</p>
<figure class="highlight html"><figcaption><span>HomePage.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> <span class="title class_">Dinosaurs</span> <span class="keyword">from</span> <span class="string">&#x27;./Dinosaurs.vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Welcome to the Dinosaur App! 🦕<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>Click on a dinosaur to learn more about them<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">Suspense</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">template</span> #<span class="attr">default</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">Dinosaurs</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">template</span> #<span class="attr">fallback</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">div</span>&gt;</span>Loading...<span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">Suspense</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight html"><figcaption><span>Dinosaur.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; defineComponent &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineComponent</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="attr">props</span>: &#123; <span class="attr">dinosaur</span>: <span class="title class_">String</span> &#125;,</span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">data</span>(): <span class="title class_">ComponentData</span> &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">return</span> &#123;</span></span><br><span class="line"><span class="language-javascript">            <span class="attr">dinosaurDetails</span>: <span class="literal">null</span></span></span><br><span class="line"><span class="language-javascript">        &#125;;</span></span><br><span class="line"><span class="language-javascript">    &#125;,</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">async</span> <span class="title function_">mounted</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">`http://localhost:8000/dinosaurs/<span class="subst">$&#123;<span class="variable language_">this</span>.dinosaur&#125;</span>`</span>);</span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">this</span>.<span class="property">dinosaurDetails</span> = <span class="keyword">await</span> res.<span class="title function_">json</span>();</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;&#123; dinosaurDetails?.name &#125;&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">p</span>&gt;</span>&#123;&#123; dinosaurDetails?.description &#125;&#125;<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">RouterLink</span> <span class="attr">to</span>=<span class="string">&quot;/&quot;</span>&gt;</span>🠠 Back to all dinosaurs<span class="tag">&lt;/<span class="name">RouterLink</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight html"><figcaption><span>Dinosaurs.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; defineComponent &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineComponent</span>(&#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">async</span> <span class="title function_">setup</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&quot;http://localhost:8000/dinosaurs&quot;</span>)</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">const</span> dinosaurs = <span class="keyword">await</span> res.<span class="title function_">json</span>() <span class="keyword">as</span> <span class="title class_">Dinosaur</span>[];</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">return</span> &#123; dinosaurs &#125;;</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;);</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">v-for</span>=<span class="string">&quot;dinosaur in dinosaurs&quot;</span> <span class="attr">:key</span>=<span class="string">&quot;dinosaur.name&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">RouterLink</span> <span class="attr">:to</span>=<span class="string">&quot;&#123; name: &#x27;Dinosaur&#x27;, params: &#123; dinosaur: `$&#123;dinosaur.name.toLowerCase()&#125;` &#125; &#125;&quot;</span> &gt;</span></span><br><span class="line">            &#123;&#123; dinosaur.name &#125;&#125;</span><br><span class="line">        <span class="tag">&lt;/<span class="name">RouterLink</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>ここまででフロントエンド側の実装も完了です。以下のコマンドにて、フロントエンド側のサーバーが起動できます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">deno run -A npm:vite</span><br></pre></td></tr></table></figure>

<img src="/images/2024/20241129a/image_3.png" alt="image.png" width="1104" height="789" loading="lazy">

<p>これでチュートリアルは完了です！</p>
<hr>
<p>今回のチュートリアルでは、Vueの構築についてはNode.jsおよびnpmの下位互換性を活用する形で行われていました。</p>
<p>npmを用いない形で利用できる候補として、以下のリポジトリでは「vno」としてライブラリ開発がされていましたが、3年前ほどでコミットが途絶えているようです。。</p>
<p>https://github.com/open-source-labs/vno</p>
<h2 id="まとめ">まとめ</h2><p>この記事では、Deno v2の簡単な紹介と、Vue.jsアプリの構築のチュートリアルを触ってみました。</p>
<p>TypeScriptのネイティブサポートや、セキュリティの概念など優れた特徴があり、Node.jsおよびnpmとの下位互換性についてもサポートされるものが増えてきています。</p>
<p>そのため、TypeScriptを使用したアプリケーション開発には優れた体験ができそうだと感じました。また、フロントエンドについても本体機能・ライブラリの様々な開発が進んでいるようです！</p>
<p>チュートリアル以上のアプリ構築についても、試してみたいと感じた2024年の冬でした。</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>https://www.youtube.com/watch?v=M3BM9TB-8yA</li>
<li>https://docs.deno.com/</li>
<li>https://github.com/open-source-labs/vno</li>
</ul>
]]></content>
    <summary type="html">eno v2とVue.jsを組み合わせた公式チュートリアルの実施を通して、2024年冬時点での現状や特徴について解説していきます。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Deno" scheme="https://future-architect.github.io/tags/Deno/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
  </entry>
  <entry>
    <title>Vue Fes Japan 2024のプラチナスポンサーセッションに登壇しました</title>
    <link href="https://future-architect.github.io/articles/20241128a/"/>
    <id>https://future-architect.github.io/articles/20241128a/</id>
    <published>2024-11-27T15:00:00.000Z</published>
    <updated>2024-11-27T15:00:00.000Z</updated>
    <author><name>永井優斗</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><img fetchpriority="high" src="/images/2024/20241128a/1000002251.jpg" alt="1000002251.jpg" width="600" height="856">

<p>写真はVue Fes Japan 2024のAfterPartyで配られたPiniaをイメージしたオリジナルカクテルです。</p>
<p>HealthCare Innovation Group<sup id="fnref:1">1</sup><sup id="fnref:2">2</sup>の永井優斗です。</p>
<p>Vue Fes JapanはVue.jsとエコシステムに関する日本最大の国際カンファレンスです。</p>
<p>毎年Vue.js製作者Evan Youさんをはじめ、海外の豪華スピーカーが参加されています。</p>
<p>フューチャーアーキテクト株式会社は10月19日（土）に開催されたVue Fes Japan 2024のプラチナスポンサーとして参加し、私はプラチナスポンサーセッションに登壇させていただきました。</p>
<p>2023年は会社関係なく個人としてボランティアスタッフ（当日スタッフ）としてVue Fesに参加しました。Vue Fesの熱量・活気がとても楽しかったです。参加者のVue愛が伝わる、まさに”Fes”な感じにとても居心地が良く、来年も参加しようと心に決めました。</p>
<p>2024年は一参加者としての参加の予定でしたが、ひょんなことからスポンサーセッションを登壇することになりました。このような機会は初めてでしたのでとても緊張しました。</p>
<h2 id="フューチャーの新人研修とVue-js">フューチャーの新人研修とVue.js</h2>

<p>登壇資料はこちらです。</p>
<p>[2025&#x2F;05&#x2F;19追記] Vue fes Japan の運営団体であるVue.js日本ユーザーグループのYoutubeチャンネルに登壇動画がアップロードされました。<br>動画はこちら</p>
<p>新人研修という1対N（それもNがめちゃくちゃ多い）の教育において、どのようにVue.jsを身につけてもらうか？という観点でカリキュラムと運営の話を中心にお話しました。</p>
<p>資料の通り、Futureでは「教えない研修」を実施しています。手と頭を動かして、自分で終わらせないと終わらないというものになっています。1対Nの新人研修だとどうしても一方通行になりがちなものを、なるべく1on1でのいわゆるオンボーディングに近しいことができるように設計しています。</p>
<p>そのような特徴ある新人研修の話を説明するだけでもかなりのボリュームになってしまい、技術的な話はやや薄めになってしまったので、そういったものを期待されていた方にはちょっと申し訳ないのですが、いわゆるエンタープライズ向けのIT企業でどのように人を育てて、開発体制を整えるのか、という点でご参考になる点があれば幸いです。</p>
<p>正直登壇を参加者の皆さんにどう受け止められるのか不安だったのですが、ブースやAfter Partyや後日行われたAfter Eventの場で多くの方お声がけいただき、うれしかったです。</p>
<p>お声がけいただいたみなさん「人を育てる」ということにはそれぞれ悩みがあるようで、Vue.jsの教え方にとどまらず、コミュニケーションの話だったり、育成体制の整え方など情報交換できました。</p>
<h2 id="おわりに">おわりに</h2><p>2023年のアドベントカレンダー記事でも触れたのですが、私の友人は（オンラインイベントにはない）オフラインイベントの良さは「ロビー」 である、と表現しています。</p>
<p>これは物理的な「受付の場所」という意味以上に、なんとなく人が集まっていることでうまれる思いがけぬ出会いやそこでの雑談、ワクワク感や熱量のことを比喩的に表現しているものです。</p>
<p>特にVue Fesは運営の皆様と参加者が作り上げる熱気が高く、今年も参加者のVue愛が伝わる、まさに”Fes”なイベントでした。</p>
<p>VueFes運営スタッフの皆様、ありがとうございました。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">Vue Fesのセッション紹介では人材の育成などフューチャーの価値向上がミッションであり、新人研修の運営・推進も担っている「Future Value Group」の所属としていました。これは、Vue Fes運営サイドに提出した8月時点ではその所属であったためです。話す内容的にこちらのセッションで話す内容的にこちら肩書きの方が良いかなと思い、あえて訂正はいれませんでした。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">フューチャーアーキテクト株式会社は10月19日（土）に開催されたVue Fes Japan 2024のプラチナスポンサーとして参加し、私はプラチナスポンサーセッションに登壇させていただきました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="VueFes" scheme="https://future-architect.github.io/tags/VueFes/"/>
    <category term="新人研修" scheme="https://future-architect.github.io/tags/%E6%96%B0%E4%BA%BA%E7%A0%94%E4%BF%AE/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>Vue3で作ったWebサイトを Vite PWA でPWA化する方法 2024年版</title>
    <link href="https://future-architect.github.io/articles/20241127a/"/>
    <id>https://future-architect.github.io/articles/20241127a/</id>
    <published>2024-11-26T15:00:00.000Z</published>
    <updated>2024-11-26T15:00:00.000Z</updated>
    <author><name>大岩潤矢</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><img fetchpriority="high" src="/images/2024/20241127a/ogp-2.png" alt="ogp-2.png" width="1200" height="730">

<p>本記事は Vue連載2024 3日目の記事です。</p>
<p>こんにちは。Technology Innovation Group (TIG) 所属の、大岩(@920OJ)です。</p>
<p>本記事では、Vue3で作ったWebサイトをPWA(Progressive Web Apps)化する方法を紹介します。</p>
<p>これまでも多くの方がVueで作られたWebサイトをPWA化する方法について発信されていますが、Webpackプラグインを導入するものであったり、メンテナスモードに入ってしまった <code>@vue/cli-plugin-pwa</code> を利用されているものが多いため、ここでは改めて2024年現在どのように実装すれば良いか紹介します。</p>
<h2 id="PWA技術のいま">PWA技術のいま</h2><p>Progressive Web Apps、直訳すると「革新的なWebアプリ」です。かっこいいですね。</p>
<p>誤解を恐れずに言えば、Webページを各プラットフォームのネイティブアプリのように表示・利用できるようにする技術です。ただネイティブアプリのように振る舞うだけでなく、ServiceWorkerを併用することでPush通知を受信可能にしたり、オフライン時にもキャッシュから表示できるようにしたり、バックグラウンド処理をはじめとする様々な機能を利用できます。</p>
<p>Google Trendsによると、PWAは2017年頃から盛り上がりを見せ、2020年頃に一度ピークが落ち着いたものの、最近じわじわと話題が盛り返しているようです。2017年〜2020年頃にPWAに触れたものの、それっきり追っていないという方も多いのではないでしょうか？</p>
<img src="/images/2024/20241127a/Pasted_image_20241125132625.png" alt="Pasted_image_20241125132625.png" width="1200" height="713" loading="lazy">

<p>個人的な主観とはなりますが、一時期PWAが次世代のアプリの姿として持て囃されていたものの、日本の約半数のシェアを占めるiOSがPWAサポートに消極的であったことが採用の障壁となり話題も落ち着いたものと推測します。</p>
<p>AndroidのGoogle ChromeにおいてPWAサポートが始まったのが2015年。一方で、iOS SafariがWeb App ManifestとService Workerのサポートを始めたのがiOS 11.3リリースの2018年3月です。しかしその当時も限定的なサポートにとどまり、別ドメインの遷移がPWA内で完結しない、カメラが起動できない等の問題が発生していました。</p>
<p>参考: https://qiita.com/zprodev/items/e5db743727c5722874cb</p>
<p>しかし、iOSのバージョンが上がるにつれてそれらの問題も徐々に改善していきます。特に昨今一番のアップデートとして、バックグラウンドでのPush通知に対応したことが挙げられます。2023年3月27日リリースのiOS16.4から、Service WorkerでバックグラウンドのPush通知を受信する <code>ServiceWorkerRegistration.showNotification()</code> に対応しました。これにより、iOSでのPWA実現の一番の障壁だったPush通知がそれなりに動くようになり、一層PWAが完成に近づいたと言えるでしょう。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125133903.png" alt="Pasted_image_20241125133903.png" width="1200" height="615" loading="lazy">

<p>https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/showNotification</p>
<p>ただし今年頭にネガティブなニュースもあり、AppleはiOS17.4のβ版にてEU圏内においてPWAサポートを削除すると発表したことがあります。理由はEUのデジタル市場法（DMA）への準拠により、iOSでのPWAをWebkitエンジン以外のレンダリングエンジンに対応させることによりセキュリティ・プライバシーの懸念を引き起こすため、とのことです。これは後にユーザからの反発によって撤回されたようですが、App Storeを主体とするiOSの収益モデルと競合し得る技術を忌避しようとする動きはこれからも警戒する必要があると考えます。</p>
<p>……と長々と書いてしまいましたが、結局のところ伝えたいのは「iOSでもやっとPWAがちゃんと使えるようになってきたぜ！」です。早速その威力を体感するためにも、Vue3でサクッとアプリを作り、PWA化してみましょう。</p>
<h2 id="Vite-PWA-を使って-PWA化してみよう">Vite PWA を使って PWA化してみよう</h2><p>その前に、今回利用する Vite PWA について紹介します。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125170224.png" alt="Pasted_image_20241125170224.png" width="1200" height="401" loading="lazy">

<p>https://vite-pwa-org.netlify.app/</p>
<p>最近のVue3は、開発サーバやビルドツールとしてViteを利用しています。このViteのプラグインとして、PWAに必要なmanifestファイルやServiceWorkerのスクリプトファイルを出力してくれるのが Vite PWA です。</p>
<blockquote>
<p>Vite PWAは、既存のアプリケーションをほとんど設定不要でPWA（プログレッシブウェブアプリ）に変換するのをサポートします。一般的な使用例に適した実用的なデフォルト設定があらかじめ用意されています。<br>vite-plugin-pwaプラグインでは以下のことが可能です：</p>
<ul>
<li>Webアプリケーションのマニフェストを生成し、それをエントリーポイントに追加する（マニフェスト生成についてはセットアップガイドを参照してください）。</li>
<li>strategies オプションを使用してサービスワーカーを生成する（詳細は「サービスワーカーの戦略」セクションを参照してください）。</li>
<li>ブラウザにサービスワーカーを登録するスクリプトを生成する（「サービスワーカーを登録する」セクションを参照してください）。<br>https://vite-pwa-org.netlify.app/guide/#vite-pwa</li>
</ul>
</blockquote>
<p>本来であれば自分で用意しないといけない複雑な設定ファイルを、Viteのコンフィグファイルにオプションを記載するだけで自動で出力してくれるというすぐれものです。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125200146.png" alt="Pasted_image_20241125200146.png" width="1200" height="699" loading="lazy">

<p>PWAはウェブマニフェストファイルとServiceWorkerのファイルから成り立ちます。前者はPWAをインストールする際に使われるメタ情報的な役割で、後者はキャッシュ対応やバックグラウンド処理等のJSコードです。Vite PWAはこの2つのファイルを自動生成してくれます。</p>
<h3 id="アプリを準備する">アプリを準備する</h3><p>まずはVue3で作成したアプリをPWA化してみましょう。ここでは、簡易的に天気予報APIを叩いて各地の天気予報を取得する簡単なアプリを用意しました。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125170033.png" alt="Pasted_image_20241125170033.png" width="1200" height="672" loading="lazy">

<p>ソースコードとデモサイトは以下に配置しています。</p>
<ul>
<li>GitHub: https://github.com/920oj/pwa-example-20241126/tree/main/vue-example</li>
<li>デモサイト: https://pwa-example-20241126.vercel.app/</li>
</ul>
<p>今回は以下をPWAで実現できるようにしてみます。</p>
<ul>
<li>PWAをインストールできるようにする</li>
<li>プリキャッシュでオフライン対応する</li>
<li>外部リソースをキャッシュする</li>
</ul>
<h3 id="Vite-PWA-のインストール">Vite PWA のインストール</h3><p>まずはプラグインをインストールします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm install -D vite-plugin-pwa</span><br></pre></td></tr></table></figure>

<p>次に、 <code>vite.config.ts</code> に以下を記載します。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// (前略)</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">VitePWA</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vite-plugin-pwa&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">plugins</span>: [</span><br><span class="line">    <span class="title function_">vue</span>(),</span><br><span class="line">    <span class="title function_">vueDevTools</span>(),</span><br><span class="line">    <span class="title class_">VitePWA</span>(&#123; <span class="comment">// プラグインを追加</span></span><br><span class="line">      <span class="attr">registerType</span>: <span class="string">&#x27;autoUpdate&#x27;</span>,</span><br><span class="line">      <span class="attr">devOptions</span>: &#123;</span><br><span class="line">        <span class="attr">enabled</span>: <span class="literal">true</span>, <span class="comment">// 開発サーバでも動作させる設定</span></span><br><span class="line">      &#125;,</span><br><span class="line">    &#125;),</span><br><span class="line">  ],</span><br><span class="line">  <span class="comment">// (後略)</span></span><br></pre></td></tr></table></figure>

<p>最後に <code>npm run dev</code> で開発サーバを立ち上げてみましょう。立ち上がったら、F12で開発者ツールを開き、「Application」タブから「Service workers」を開いてください。</p>
<p>このように <code>dev-sw.js</code> がService Workerとして読み込まれている事がわかります。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125173130.png" alt="Pasted_image_20241125173130.png" width="1200" height="621" loading="lazy">

<p>ターミナルのほうを見てみると、 <code>sw.js</code> と <code>workbox-xxxxxxxx.js</code> が自動生成されていることがわかります。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125173237.png" alt="Pasted_image_20241125173237.png" width="889" height="180" loading="lazy">

<h3 id="PWAをインストールできるようにする（Webマニフェスト設定）">PWAをインストールできるようにする（Webマニフェスト設定）</h3><p>上記設定を実施しても、Webマニフェストの設定が不十分であるためPWAとしてインストールできません。Webマニフェスト用の設定を書く必要がありますが、その前にPWAのアイコンを用意しましょう。ただし、Webマニフェストで指定する必要があるアイコンは種類が非常に多く、すべてを手作業で用意するには骨が折れる作業です。</p>
<p>そこで、Vite PWA にはアセットを自動生成してくれるツールが用意されています。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm i @vite-pwa/assets-generator -D</span><br></pre></td></tr></table></figure>

<p>インストール後、 <code>pwa-assets.config.ts</code> として以下を記載します。今回は <code>public/icon.png</code> にアプリアイコンとして利用したいファイルを配置しました。</p>
<figure class="highlight typescript"><figcaption><span>pwa-assets.config.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; defineConfig &#125; <span class="keyword">from</span> <span class="string">&#x27;@vite-pwa/assets-generator/config&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title function_">defineConfig</span>(&#123;</span><br><span class="line">  <span class="attr">images</span>: [</span><br><span class="line">    <span class="string">&#x27;public/icon.png&#x27;</span>, <span class="comment">// ここに用意した画像を指定する</span></span><br><span class="line">  ],</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>記載できたら、 <code>npx pwa-assets-generator --preset minimal-2023</code> で実行しましょう。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125203153.png" alt="Pasted_image_20241125203153.png" width="1072" height="746" loading="lazy">

<p>正しく書き出せていそうです。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125203351.png" alt="Pasted_image_20241125203351.png" width="1200" height="710" loading="lazy">

<p>後は <code>vite.config.ts</code> に設定を記載するだけです。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="title class_">VitePWA</span>(&#123;</span><br><span class="line">  <span class="attr">registerType</span>: <span class="string">&#x27;autoUpdate&#x27;</span>,</span><br><span class="line">  <span class="attr">devOptions</span>: &#123;</span><br><span class="line">    <span class="attr">enabled</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">manifest</span>: &#123;</span><br><span class="line">    <span class="attr">name</span>: <span class="string">&#x27;PWAテストアプリ&#x27;</span>, <span class="comment">// アプリケーションリスト等で表示される名前</span></span><br><span class="line">    <span class="attr">short_name</span>: <span class="string">&#x27;PWAテスト&#x27;</span>, <span class="comment">// ホーム画面で表示される名前</span></span><br><span class="line">    <span class="attr">description</span>: <span class="string">&#x27;PWAテストアプリです。天気予報を確認できます。&#x27;</span>,</span><br><span class="line">    <span class="attr">theme_color</span>: <span class="string">&#x27;#e6eeff&#x27;</span>, <span class="comment">// テーマカラー　最低限指定が必要</span></span><br><span class="line">    <span class="attr">display</span>: <span class="string">&#x27;standalone&#x27;</span>, <span class="comment">// 表示モード standaloneにするとネイティブアプリっぽくなる</span></span><br><span class="line">    <span class="attr">icons</span>: [ <span class="comment">// 先ほどコンソールに出力されたものをコピペでOK</span></span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">src</span>: <span class="string">&#x27;pwa-64x64.png&#x27;</span>,</span><br><span class="line">        <span class="attr">sizes</span>: <span class="string">&#x27;64x64&#x27;</span>,</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;image/png&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">src</span>: <span class="string">&#x27;pwa-192x192.png&#x27;</span>,</span><br><span class="line">        <span class="attr">sizes</span>: <span class="string">&#x27;192x192&#x27;</span>,</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;image/png&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">src</span>: <span class="string">&#x27;pwa-512x512.png&#x27;</span>,</span><br><span class="line">        <span class="attr">sizes</span>: <span class="string">&#x27;512x512&#x27;</span>,</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;image/png&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="attr">src</span>: <span class="string">&#x27;maskable-icon-512x512.png&#x27;</span>,</span><br><span class="line">        <span class="attr">sizes</span>: <span class="string">&#x27;512x512&#x27;</span>,</span><br><span class="line">        <span class="attr">type</span>: <span class="string">&#x27;image/png&#x27;</span>,</span><br><span class="line">        <span class="attr">purpose</span>: <span class="string">&#x27;maskable&#x27;</span>,</span><br><span class="line">      &#125;,</span><br><span class="line">    ],</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;),</span><br></pre></td></tr></table></figure>

<p><code>manifest</code> にオプションを記載していきます。ブラウザごとに対応している値は異なるので、MDNのサイトで対応状況を確認しておくと良いでしょう。</p>
<p>https://developer.mozilla.org/en-US/docs/Web/Manifest</p>
<p>保存したら、再度開発サーバを立ち上げてみましょう。アドレスバー横にアイコンが表示され、クリックするとPWAのインストールダイアログが表示されます。</p>
<img src="/images/2024/20241127a/Pasted_image_20241125204440.png" alt="Pasted_image_20241125204440.png" width="544" height="242" loading="lazy">

<p>iPhoneでも試してみましょう。cloudflaredを使って外からアクセスできるようにして、ページを開いて「ホーム画面に追加」をタップします。</p>
<img src="/images/2024/20241127a/スライド3.png" alt="スライド3.png" width="1200" height="675" loading="lazy">

<p>問題なく表示できていますね。</p>
<img src="/images/2024/20241127a/スライド4.png" alt="スライド4.png" width="1200" height="675" loading="lazy">

<h3 id="プリキャッシュを導入する">プリキャッシュを導入する</h3><p>続いて、PWAをオフライン状態でも機能するように、プリキャッシュ設定を導入しましょう。ここまではWebマニフェストと呼ばれるJSONファイルによって機能が動作していましたが、ここからはServiceWorkerのコードによって動作します。本来であればServiceWorkerのソースコードを一から書いていく必要がありますが、Vite PWAでは <code>generateSW</code> (generate ServiceWorkerの略)オプションを有効化することで、ServiceWorkerのコードを自動生成してくれます。</p>
<p>実際にオプションを記載していく前に、Workboxというライブラリについて説明する必要があります。</p>
<p>Workboxとは、ServiceWorkerをより簡単に制御できるようにGoogleが開発しているモジュール群です。</p>
<p>https://developer.chrome.com/docs/workbox/what-is-workbox?hl=ja</p>
<p>Workboxには以下のモジュールが用意されています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>#</th>
<th>モジュール名</th>
<th>機能</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>workbox-routing</td>
<td>リクエスト</td>
</tr>
<tr>
<td>2</td>
<td>workbox-strategies</td>
<td>キャッシュ戦略</td>
</tr>
<tr>
<td>3</td>
<td>workbox-precaching</td>
<td>プリキャッシュ設定の導入</td>
</tr>
<tr>
<td>4</td>
<td>workbox-expiration</td>
<td>キャッシュの管理</td>
</tr>
<tr>
<td>5</td>
<td>workbox-window</td>
<td>ServiceWorkerの登録・更新作業</td>
</tr>
<tr>
<td>6</td>
<td>workbox-build</td>
<td>ServiceWorkerの自動生成</td>
</tr>
</tbody></table></div>
<p>Vite PWAでは内部的にこの <code>workbox-build</code> を呼んでおり、<code>vite.config.ts</code> に記載した設定値をWorkboxモジュールに渡すことでPWAに必要なServiceWorkerのソースコードを生成しています。そのため、プリキャッシュの導入においてもVite PWAがWorkboxを使ってよしなに生成してくれるので、我々は設定値をコンフィグファイルに記載するだけで良いのです。</p>
<p> Vite PWAでは、 <code>globePatterns</code> オプションを変更することでVueアプリのキャッシュに必要なファイルをプリキャッシュできます。一度この設定をしないままビルドをしてみて、書き出されたファイルの拡張子を指定するとやりやすいと思います。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">find . -<span class="built_in">type</span> f -name <span class="string">&quot;*.*&quot;</span> | sed <span class="string">&#x27;s/.*\.//&#x27;</span> | <span class="built_in">sort</span> -u</span></span><br><span class="line">css</span><br><span class="line">html</span><br><span class="line">ico</span><br><span class="line">js</span><br><span class="line">png</span><br><span class="line">webmanifest</span><br></pre></td></tr></table></figure>

<p> 今回の場合、css、html、ico、js、png、webmanifestを指定すると良さそうです。globパターンを記載します。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="title class_">VitePWA</span>(&#123;</span><br><span class="line">  <span class="attr">registerType</span>: <span class="string">&#x27;autoUpdate&#x27;</span>,</span><br><span class="line">  <span class="attr">devOptions</span>: &#123;</span><br><span class="line">	<span class="attr">enabled</span>: <span class="literal">true</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">manifest</span>: &#123;</span><br><span class="line">    <span class="comment">// (中略)</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">workbox</span>: &#123; <span class="comment">// 追加</span></span><br><span class="line">    <span class="attr">globPatterns</span>: [<span class="string">&#x27;**/*.&#123;css,html,ico,js,png,webmanifest&#125;&#x27;</span>], <span class="comment">// ここにプリキャッシュ対象としたいファイルを記載する</span></span><br><span class="line">  &#125;,</span><br></pre></td></tr></table></figure>

<p>この状態でビルドすると、ターミナルにVite PWAが書き出されたファイルをVite PWAが探して、プリキャッシュに含めてくれます。</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">PWA v0.21.0</span><br><span class="line">mode      generateSW</span><br><span class="line">precache  22 entries (195.30 KiB)</span><br><span class="line">files generated</span><br><span class="line">  dist/sw.js</span><br><span class="line">  dist/workbox-5ffe50d4.js</span><br></pre></td></tr></table></figure>

<p>書き出された <code>dist/sw.js</code> を見てみると、 <code>e.precacheAndRoute()</code> の引数に、書き出されたファイルが一覧されているようです。</p>
<img src="/images/2024/20241127a/Pasted_image_20241128142353.png" alt="Pasted_image_20241128142353.png" width="1200" height="1165" loading="lazy">

<p>では実際に試してみましょう。ビルドした後、先ほどと同様 cloudflared を使ってhttps通信でスマホからアクセスし、ホーム画面に追加。PWAアプリを一度開き、その後オフライン状態にして再度開くと……</p>
<img src="/images/2024/20241127a/スライド5.png" alt="スライド5.png" width="1200" height="675" loading="lazy">

<p>オフライン状態でも表示できました！しかし、外部リソース（ここでは天気予報APIやそこから取得した画像ファイル）からの取得は失敗してしまっていますね。</p>
<h3 id="外部リソースのキャッシュを有効化する">外部リソースのキャッシュを有効化する</h3><p>先程も軽く触れましたが、ServiceWorkerはブラウザとネットワーク間に入り仮想キャッシュとして機能します。すなわち、ネットワークへのアクセス(fetch)が発生するたびにServiceWorkerがそれをハンドリングし、そのリソースをキャッシュから返すか、ネットワークへアクセスしに行くかを判断し挙動を変えることができます。</p>
<p>このキャッシュ戦略にはいくつかあり、workbox-strategiesの機能で切り替えることができます。以下にその種類を簡単に記載しますが、詳細はweb.davの記事を参考にしてください。</p>
<ul>
<li>Network First(ネットワーク優先)<ul>
<li>まずはネットワークを見に行き、最新のデータを返す。リクエストが失敗したり、時間がかかりすぎたりすると、最新のキャッシュを返す。</li>
</ul>
</li>
<li>Cache First(キャッシュ優先)<ul>
<li>まずは最新のキャッシュを返す。キャッシュが存在しない場合やキャッシュに不備がある場合はネットワークを見に行く。</li>
</ul>
</li>
<li>Stale While Revalidate(SWR)<ul>
<li>まずは最新のキャッシュを返すが、その裏でネットワークを見に行き、キャッシュを最新化する。</li>
</ul>
</li>
</ul>
<p>今回はNetwork Firstで実装してみようと思います。Vite PWAで外部リソースのキャッシュを有効化するには、 <code>runtimeCaching</code> オプションを利用します。また、今回は tsukumijima 氏によって公開されている「天気予報API(livedoor 天気互換)」のドメイン <code>weather.tsukumijima.net</code> と、気象庁のドメイン <code>www.jma.go.jp</code> から外部リソースが読み込まれているので、この2ドメインをruntimeCacheの対象としましょう。</p>
<figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="comment">// (前略)</span></span><br><span class="line"><span class="attr">workbox</span>: &#123;</span><br><span class="line">  <span class="attr">globPatterns</span>: [<span class="string">&#x27;**/*.&#123;css,html,ico,js,png,webmanifest&#125;&#x27;</span>],</span><br><span class="line">  <span class="attr">runtimeCaching</span>: [ <span class="comment">// 追加</span></span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">urlPattern</span>: <span class="regexp">/^https:\/\/weather\.tsukumijima\.net\/.*/i</span>, <span class="comment">//正規表現で記載</span></span><br><span class="line">      <span class="attr">handler</span>: <span class="string">&#x27;NetworkFirst&#x27;</span>, <span class="comment">// &#x27;CacheFirst&#x27; | &#x27;CacheOnly&#x27; | &#x27;NetworkFirst&#x27; | &#x27;NetworkOnly&#x27; | &#x27;StaleWhileRevalidate&#x27;;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">urlPattern</span>: <span class="regexp">/^https:\/\/www\.jma\.go\.jp\/.*/i</span>,</span><br><span class="line">      <span class="attr">handler</span>: <span class="string">&#x27;NetworkFirst&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">  ],</span><br><span class="line">&#125;,</span><br><span class="line"><span class="comment">// (後略)</span></span><br></pre></td></tr></table></figure>

<p>では実際に試してみましょう。build後、PWAをインストールし直してみます。</p>
<img src="/images/2024/20241127a/スライド6.png" alt="スライド6.png" width="1200" height="675" loading="lazy">

<p>今回はオフライン状態でもAPIリクエストをキャッシュできました！</p>
<h2 id="もう一つのモード-injectManifest-について">もう一つのモード: injectManifest について</h2><p>さて、ここまで <code>generateSW</code> を指定しServiceWorkerファイルを自動生成する方法で説明してきましたが、もう1つ <code>injectManifest</code> というモードがあります。これは、自前で用意したServiceWorkerのコードにプリキャッシュや外部リソースのキャッシュに対応させるコードをWorkboxが挿入(inject)してくれるモードです。</p>
<p>https://vite-pwa-org.netlify.app/workbox/inject-manifest.html</p>
<p>本記事では尺（？）の都合上触れませんでしたが、例えばPush通知を受信可能とするには別途自前でServiceWorkerコードを作成する必要があります。 <code>generateSW</code> ではServiceWorkerコードが自動生成されてしまうため、組み入れることができません。これを解決するのが <code>injectManifest</code> モードです。</p>
<p>Chrome for Developersのウェブサイトでは、各モードの選択を以下のように実施するべきとしています。</p>
<ul>
<li>generateSWを使用するケース<ul>
<li>ファイルをプリキャッシュしたい場合</li>
<li>単純なランタイムキャッシュ（外部リソースのキャッシュ）が必要な場合</li>
</ul>
</li>
<li>injectManifestを使用するケース<ul>
<li>ServiceWorkerをより詳細に制御したい場合</li>
<li>ファイルをプリキャッシュしたい場合</li>
<li>ルーティングおよびキャッシュ戦略をカスタマイズする必要がある場合</li>
<li>ServiceWorkerを他のプラットフォーム機能（Web Push等）と併用する場合</li>
</ul>
</li>
</ul>
<p><code>injectManifest</code> モードを利用したVite PWAの設定についてもある程度知見が溜まってきたので、別途記事にできればと思います。</p>
<h2 id="おわりに">おわりに</h2><p>色々ドキュメントを読み解いていくにつれて、やはりPWA技術は難しいと感じました。その難解さを簡易なインターフェースに抽象化してくれるVite PWAは非常に助かるプラグインですが、どうしてもその裏側で使われているライブラリやPWAの仕様をちゃんと把握しておかなければ嵌まってしまう落とし穴が多いように思います。</p>
<p>この記事に書ききれなかった様々な試行錯誤も沢山ストックしているので、また別の機会に記事化できればと思います。それでは！</p>
<h2 id="おまけ">おまけ</h2><p>色々調べるうえで役に立ったWebサイトを以下に記載します。Google系のドキュメントは機械翻訳されており、誤った訳も多いので、なるべく英語のドキュメントを読むことをおすすめします。</p>
<ul>
<li>ServiceWorker についての理解<ul>
<li>Service worker overview - Chrome for Developers</li>
</ul>
</li>
<li>初心者向けのServiceWorkerの図解<ul>
<li>【イラスト付き】Servie Worker API【利用方法】</li>
</ul>
</li>
<li>プリキャッシュ対応<ul>
<li>Workboxを使ってPWAを完全オフライン対応した</li>
<li>workbox-precaching - Chrome for Developers</li>
</ul>
</li>
</ul>
]]></content>
    <summary type="html">Vue3で作ったWebサイトをPWA(Progressive Web Apps)化する方法を紹介します。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="PWA" scheme="https://future-architect.github.io/tags/PWA/"/>
    <category term="Service Worker" scheme="https://future-architect.github.io/tags/Service-Worker/"/>
    <category term="Vite" scheme="https://future-architect.github.io/tags/Vite/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="Vue3" scheme="https://future-architect.github.io/tags/Vue3/"/>
  </entry>
  <entry>
    <title>2015年頃のフロントエンジニアだってvoid(0)のワクワクを理解したい</title>
    <link href="https://future-architect.github.io/articles/20241126a/"/>
    <id>https://future-architect.github.io/articles/20241126a/</id>
    <published>2024-11-25T15:00:00.000Z</published>
    <updated>2024-11-25T15:00:00.000Z</updated>
    <author><name>村田靖拓</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241126a/voidzero.png" alt="" width="990" height="607">

<p>本記事はVue連載2本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、FVGの村田です。FVGとは「Future Value Group」の略称で、フューチャーの人材や知財の価値向上を目的として様々な活動をしていくグループです。</p>
<p>私はフューチャーに入社して10年ちょっと経つのですが、新人研修修了直後はとあるプロジェクトに参画しUrushiというWeb UIコンポーネントの開発に携わっていました。当時ハンバーガーメニューなどのコンポーネント開発を担当していたのですが、CSS覚えたての私はアニメーションにて180°回転させれば良いところをノリと勢いで1080°回転させてしまい、先輩に「好き勝手回せばいいってもんじゃない。ユーザに何が起こってるか迷子にさせない意識を持て」と呆れ顔で指摘された記憶があります。懐かしいです。そんな私ですが、ひょんなことから7年ぶりにフロントエンドの開発(Vue.jsを利用)に携わる機会があり、せっかくなので今回のVue連載に参加しようと思った次第です。</p>
<h2 id="void-0-に行き着いたワケ">void(0)に行き着いたワケ</h2><p>記事執筆にあたり、なにか面白いネタはないかとネットを徘徊していたのですが、如何せん7年ぶりなもので色々と知らない単語がたくさんありました。私がゴリゴリにJavaScriptを書いていたのは2014〜2016年頃で、UI的にはスキューモフィズムからフラットデザインへの転換期、ライブラリ的にはBackbone.jsなどを主軸に使っていた時代です。Vueは2014年2月に初版がリリースされていましたが、当時の私は把握していませんでした。</p>
<p>幸い2024年10月(記事執筆の1ヶ月前)にはVue Fes Japan 2024が開催されており、そのセッション情報などを調べることで最近話題になってる物事がなにかを知ることができました。Vite&#x2F;Nuxt.js&#x2F;pinia&#x2F;Vapor&#x2F;svelte5などピックしたキーワードはいくつかあったのですが、将来性など含め一番気になるなと思ったのが「void(0)」でした。どんなものなのか、どんなうれしみや期待があるのかなど、これを期に自分なりに調査・整理しようというのが本記事のモチベーションです。</p>
<p>なので本記事は、「void(0)がなんなのか知りたい」「しばらくフロントエンド畑から離れていていたけど最近どんな感じなのか気になる」といった方を想定読者として執筆します。</p>
<p>※ロゴ表記は「void(0)」ですが、各種記事では「VoidZero」と書かれているので、以降それに従います。</p>
<h2 id="VoidZeroとは">VoidZeroとは</h2><p>VoidZeroは次世代のツールチェインを開発すべく設立された会社です（次世代ツールチェインそのもの自体もVoidZeroと呼称するんでしょうか?…）背景を詳しく知るには2024年10月1日に公開されたEvan You(Vueを作った人)さんによるAnnouncing VoidZero - Next Generation Toolchain for JavaScriptを読むのが一番なのですが、VoidZeroは「次のVite」を考えた時に直面した課題を乗り越えるために着想されたアイデアです。</p>
<h2 id="ビルドツールの変遷">ビルドツールの変遷</h2><p>「次のVite」を語る前に、そもそもViteがなんなのかを理解しておきましょう。JavaScript界隈はエコシステムの変化が激しく、ちょっと目を離した隙に全く知らない世界に様変わりしてしまう印象がありますが、今の私がまさにそれ、浦島太郎状態です。</p>
<p>この記事を書きながらふと思い出したのですが、2016年に開催した社内LT大会にて私はGulpの話をしていました。Webpack(2014年2月初版リリース)が”新しい”と言われていた時代に、Gulpを使って様々なジョブを組んでいた記憶が蘇ります。</p>
<p>さて、話を戻しますがViteはフロントエンドビルドツールです。公式サイト曰く「次世代のWebアプリケーションを支える超高速フロントエンドビルドツール」とのこと。”超高速”を謳うからには、比較対象となる既存のビルドツール群もいるはず。ということで更に調べてみます。</p>
<p>フロントエンド開発におけるビルドツールには様々なものがあります。漏れはあると思いますが、調べる中でヒットした代表的なものとその特徴を時系列順（あくまで発表年ベース）で以下に列挙します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">ツール</th>
<th align="left">発表年</th>
<th align="left">特徴</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Grunt</td>
<td align="left">2012</td>
<td align="left">JavaScriptタスクランナー。コマンドを介して各種タスク（例えば、ファイルの縮小、結合、テストの実行など）を自動化する。</td>
</tr>
<tr>
<td align="left">Gulp</td>
<td align="left">2013</td>
<td align="left">ストリーミングビルドシステム。コードはNode.jsライブラリとして書かれ、シンプルなAPIでタスクを定義する。</td>
</tr>
<tr>
<td align="left">Webpack</td>
<td align="left">2014</td>
<td align="left">モジュールバンドラ。JavaScriptやその他のアセット（CSS、画像など）を1つのバンドルにまとめる。</td>
</tr>
<tr>
<td align="left">Rollup</td>
<td align="left">2015</td>
<td align="left">JavaScriptモジュールバンドラ。主にライブラリ作成に焦点を当てており、ツリーシェイキング（未使用コードの除去）に優れている。</td>
</tr>
<tr>
<td align="left">Parcel</td>
<td align="left">2017</td>
<td align="left">ゼロコンフィギュレーションのバンドラ。設定なしでも動作し、開発者が迅速にプロジェクトを開始できる。</td>
</tr>
<tr>
<td align="left">esbuild</td>
<td align="left">2020</td>
<td align="left">非常に高速なJavaScriptバンドラとMinifier。Go言語で書かれており、他のバンドラに比べて圧倒的に高速。</td>
</tr>
<tr>
<td align="left">Vite</td>
<td align="left">2020</td>
<td align="left">モダンなフロントエンドビルドツール。HMR（Hot Module Replacement）をサポートし、開発者体験を向上させるために設計されている。初期のビルドはesbuildを使用し、最終的なバンドルにはRollupを採用。</td>
</tr>
</tbody></table></div>
<p>色々ありますね。私がバリバリフロントエンジニアをやっていた2014～2016年頃から見てもここ10年で5種類(and more)の新たなツールが登場し順次デファクトが移り変わっている事実を鑑みると、たしかに少し界隈から離れているだけで浦島太郎状態になってしまいそうだなと感じます。</p>
<p>今回はすべてのツールにじっくり触れることはしませんが、「次のVite」の解像度をあげるためにもう少しViteを深堀ってみます。</p>
<h2 id="Viteの存在感と直面している課題">Viteの存在感と直面している課題</h2><p>State of Frontend 2024にて、フロントエンドエンジニアが利用しているビルドツールについての調査が行われています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">ツール</th>
<th align="left">A.使った(All)</th>
<th align="left">B.使った(Like)</th>
<th align="left">C.使った(Dislike)</th>
<th align="left">D. その他</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Grunt</td>
<td align="left">21.7%</td>
<td align="left">4.6%</td>
<td align="left">17.1%</td>
<td align="left">78.3%</td>
</tr>
<tr>
<td align="left">Gulp</td>
<td align="left">32.4%</td>
<td align="left">12.9%</td>
<td align="left">19.5%</td>
<td align="left">67.6%</td>
</tr>
<tr>
<td align="left">Webpack</td>
<td align="left">82.5%</td>
<td align="left">44%</td>
<td align="left">38.5%</td>
<td align="left">17.5%</td>
</tr>
<tr>
<td align="left">Rollup</td>
<td align="left">40.3%</td>
<td align="left">33.9%</td>
<td align="left">6.4%</td>
<td align="left">59.7%</td>
</tr>
<tr>
<td align="left">Parcel</td>
<td align="left">19.7%</td>
<td align="left">13.2%</td>
<td align="left">6.5%</td>
<td align="left">80.3%</td>
</tr>
<tr>
<td align="left">esbuild</td>
<td align="left">54.4%</td>
<td align="left">50.2%</td>
<td align="left">4.2%</td>
<td align="left">45.6%</td>
</tr>
<tr>
<td align="left">Vite</td>
<td align="left">84.1%</td>
<td align="left">82.4%</td>
<td align="left">1.7%</td>
<td align="left">15.9%</td>
</tr>
</tbody></table></div>
<p>State of Frontendの調査結果を元に先ほどピックしたツール群のみについて一部抜粋および加筆したのが上の表です。各ツールごとに「使ったかどうか・興味があるか」など調査しており、Aの数値が高ければ界隈でより高頻度に使われていることを意味し、更にBの数値が高ければ使った人が好印象だったことを意味します。（各行にて、A&#x3D;B+C、A+D&#x3D;100%、となっています）</p>
<p>A列の数値だけ見るとTopがViteで次点がWebpackですが、BC列の数値も踏まえるとWebpackの使い勝手に否定的な印象を持っている人も少なくないことが分かります。また、B列をベースに考えると、ツールの使用頻度および使い勝手に対する印象共に良い数値を叩き出してるのはTopがViteで次点がesbuildであることが分かります。</p>
<p>細かい機能面での優位性などについては正直まだ私の理解が追いついていないのですが、いずれにせよViteはフロントエンドのビルドツール界隈にて圧倒的な存在感を発揮しているツールであるということが調査からも見て取れます。</p>
<p>さて、デファクトに近い存在であると言ってよいであろうViteですが、ViteConf 2024にてEvan Youさんより語られた内容（Vite and the Future of JavaScript Tooling）によると、Viteはまだいくつかの課題をはらんでいるようです。例えば、内部的に様々なツール群に依存しており依存関係が複雑であること、また、複数ツールを組み合わせるがゆえにほぼ同一内容のパース処理だったりシリアライズ処理がツール間で重複して実行され、処理パフォーマンスの観点で不利に働いています。</p>
<p>これらの課題を解消するためのアイデアとしてたどり着いたのがVoidZeroです。</p>
<h2 id="VoidZeroが目指す世界">VoidZeroが目指す世界</h2><p>Evanさんの言葉を借りれば、「Viteが直面している課題は、JavaScriptのエコシステムが直面している課題そのもの」です。私のようにフロントエンドから少し離れていた人であっても、エンジニアであれば「JavaScript周りはフレームワークだったりツールがたくさんあってキャッチアップ大変」という印象を持っているのではないでしょうか。それはもちろんこの領域が非常に活発な熱量を持っており様々なエコシステム活動が実を結んでいるからこそなのですが、一方で各種ツール群が独立して発展し、それぞれが互換性を持っていなかったりあるいは互換性があったとしても一連の流れで見ると非効率な処理を行っていたりするのもまた事実のようです。</p>
<p>この状況を打破するためにも「統合されたツールチェイン」が必要であるとEvanさんは提唱しています。Viteを次の段階に進めるだけでなく、JavaScriptエコシステム全体に大きな進展をもたらすことが期待されるこのアイデアですが、実現には専門チームが必要だと考えVoidZeroとしてチームを設立、資金調達も経て開発プロジェクトが立ち上がっているというのがいまの状況です。</p>
<p>ここまでの歴史経緯等踏まえると、VoidZero自体あるいはVoidZeroをきっかけとしてなにか大きな流れが生まれそうな予感がしておりワクワクしてきますね。</p>
<h2 id="さいごに">さいごに</h2><p>“ビルドツール”という切り口で最近の動向をキャッチアップする良い機会となりました。個人的には昔私が携わったツール（Gulp!!!）と再会できたのも胸熱でした。</p>
<p>Vue連載記事として書き始めた本記事ですが、結果的にVue本体の内容から逸れはしたものの、FAQを見ると「VueはVoidZeroで開発されたツールの1stクラスサポートを受ける」とのことで、全く関係ないわけでもないはず…!!!</p>
<p>進化の早い領域ではありますが、浦島太郎になりすぎないように定期的な情報収集とキャッチアップを大事にしたいなと改めて感じました。VoidZero、ならびにJavaScriptエコシステムの今後の動向にも益々期待が高まります。</p>
]]></content>
    <summary type="html">「void(0)」がどんなものなのか、どんなうれしみや期待があるのかなど、これを期に自分なりに調査・整理しようというのが本記事のモチベーションです</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="JavaScript" scheme="https://future-architect.github.io/tags/JavaScript/"/>
    <category term="Vite" scheme="https://future-architect.github.io/tags/Vite/"/>
  </entry>
  <entry>
    <title>Vue.js連載始めます &amp; Nuxtの通信パターンも見てみる</title>
    <link href="https://future-architect.github.io/articles/20241125a/"/>
    <id>https://future-architect.github.io/articles/20241125a/</id>
    <published>2024-11-24T15:00:00.000Z</published>
    <updated>2024-11-24T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241125a/top.png" alt="" width="800" height="378">

<p>Vue.js連載企画を始めます。今年は勤労感謝の日が土曜日で、勤労に感謝できなくて残念でしたね。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">Date</th>
<th align="left">Name</th>
<th align="left">Title</th>
</tr>
</thead>
<tbody><tr>
<td align="left">11&#x2F;25(月)</td>
<td align="left">渋川よしき</td>
<td align="left">Nuxtの通信パターンも見てみる（この記事）</td>
</tr>
<tr>
<td align="left">11&#x2F;26(火)</td>
<td align="left">村田靖拓さん</td>
<td align="left">2015年頃のフロントエンジニアだってvoid(0)のワクワクを理解したい</td>
</tr>
<tr>
<td align="left">11&#x2F;27(水)</td>
<td align="left">大岩潤矢さん</td>
<td align="left">Vue3・Nuxt3アプリをPWA化する方法 2024年版</td>
</tr>
<tr>
<td align="left">11&#x2F;28(木)</td>
<td align="left">永井優斗さん</td>
<td align="left">Vue Fes Japan 2024報告</td>
</tr>
<tr>
<td align="left">11&#x2F;29(金)</td>
<td align="left">山本竜玄さん</td>
<td align="left">Deno × Vueを触ってみた(2024年冬)</td>
</tr>
</tbody></table></div>
<h2 id="Nuxtの通信パターンも見てみる">Nuxtの通信パターンも見てみる</h2><p>フロントエンドフレームワークからサーバーにアクセスするパターンという記事を書いたところ、ちょっとバズったので、そういえば調べきれてなかったNuxtの話でも書こうかな、と思って調べてみた記事です。</p>
<h2 id="Nuxt3の通信機能">Nuxt3の通信機能</h2><p>Nuxt3のドキュメントには以下の3つの基本の通信のための機能があります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">関数</th>
<th align="left">機能</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>$fetch()</code></td>
<td align="left">ofetchというブラウザAPIの<code>fetch()</code>互換のライブラリの関数</td>
</tr>
<tr>
<td align="left"><code>useAsyncData()</code></td>
<td align="left">データ取得のライフサイクル管理やキャッシュ管理をするラッパー</td>
</tr>
<tr>
<td align="left"><code>useFetch()</code></td>
<td align="left"><code>$fetch()</code> + <code>useAsyncData()</code></td>
</tr>
</tbody></table></div>
<p>現在サポートされているNode.js18以降はみなオプションをつかわずに<code>fetch()</code>が使えるのに、なぜ<code>$fetch()</code>なんてものを別に用意しているかというと、単なる<code>fetch()</code>ではなく、ヘッダーを付与したりプリセットが設定できるような<code>create()</code>メソッドが使えたり、拡張されていたりします。何もしなくても、サーバー側で実行すると、ホスト名を省略できるなども設定されています。</p>
<p>https://nuxt.com/docs/guide/recipes/custom-usefetch</p>
<p><code>useAsyncData()</code>そのものには通信機能はなく、中のコールバックで<code>$fetch()</code>を呼びます。ただし、通信をキャッシュして2度目以降は呼ばないようにしたり、通信中かどうかのフラグだったり、通信結果のデータだったりを返してくれます。単なる<code>async</code>な情報取得関数をこの中で呼ぶだけで表示管理やキャッシュが行えてしまうという優れものです。OpenAPIで作ったクライアントをラップしたりといった使い方もできるでしょう。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; data, status, error, refresh, clear &#125; = <span class="keyword">await</span> <span class="title function_">useAsyncData</span>(</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&#x27;mountains&#x27;</span>, <span class="comment">// キャッシュのキー</span></span></span><br><span class="line"><span class="language-javascript">  <span class="function">() =&gt;</span> $fetch(<span class="string">&#x27;https://api.nuxtjs.dev/mountains&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript">)</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><code>useFetch()</code>は、上の2つをまとめて呼ぶヘルパー関数です。コードが短くなります。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">const</span> &#123; data, status, error, refresh, clear &#125; = <span class="keyword">await</span> <span class="title function_">useFetch</span>(</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&#x27;https://api.nuxtjs.dev/mountains&#x27;</span></span></span><br><span class="line"><span class="language-javascript">)</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>これ以外に、<code>useAsyncData()</code>と、<code>useLazyFetch()</code>という派生の関数もあります。</p>
<h2 id="NuxtのuseFetch-useAsyncData-のライフサイクル">Nuxtの<code>useFetch()</code>&#x2F;<code>useAsyncData()</code>のライフサイクル</h2><p><code>$fetch</code>単体では、通常の<code>fetch()</code>とだいたい同じなので特別なことはないのですが、他の2つは末尾にオブジェクト型でオプションを追加すると、動作が大きく変わります。例えば、<code>server: false</code>をつけると、サーバーサイドレンダリング時においても、サーバーからリクエストを行わず、ブラウザの表示後にリクエストされるようになります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; data, status, error, refresh, clear &#125; = <span class="keyword">await</span> <span class="title function_">useFetch</span>(</span><br><span class="line">  <span class="string">&#x27;https://api.nuxtjs.dev/mountains&#x27;</span>,</span><br><span class="line">  &#123; <span class="attr">server</span>: <span class="literal">false</span> &#125; <span class="comment">// これを追加</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure>

<p>オプションはいろいろあります。</p>
<ul>
<li><code>server</code>：サーバー上のデータを取得するかどうか（デフォルトは<code>true</code>）</li>
<li><code>lazy</code>：クライアント側のナビゲーションをブロックする代わりに、ルートをロードした後に非同期関数を解決するかどうか（デフォルトは<code>false</code>）、<code>true</code>にすると、動作が<code>useLazyFetch()</code>、<code>useLazyAsyncData()</code>と同じになる</li>
<li><code>immediate</code>：<code>false</code>に設定すると、リクエストがすぐに起動できなくなります。（デフォルトは<code>true</code>）</li>
<li><code>dedupe</code>: すでに保留中のサーバー呼び出しがあった場合の動作<ul>
<li><code>&#39;cancel&#39;</code>: 呼び出し中の古いリクエストをキャンセルする(こちらがデフォルト)</li>
<li><code>&#39;defer&#39;</code>: 新しい呼び出しの方をキャンセルする</li>
</ul>
</li>
<li><code>watch</code>: 何かしらのリアクティブを設定すると、それが変更されたときに再リクエストを行う</li>
<li><code>default</code>: デフォルト値を返す</li>
</ul>
<p>デフォルトではサーバー側のレンダリング時にリクエストを行います。<code>server: false</code>にすると、ブラウザからリクエストが飛ぶようになります。あるいは、そのコードが書かれたコンポーネントがClientOnlyコンポーネントでラップされた中に置かれていた場合は、それはすべてクライアント側でおこなれわれています。</p>
<p><code>default</code>のデフォルト値設定と、<code>lazy: true</code>もしくは、Lazyがつく方のメソッドを使う、<code>immediate: false</code>を組み合わせると、まず初期値を返し、後から結果を返すことができます。これが<code>server: false</code>であれば前のエントリーのStale-While-Revalidateのようになりますし、<code>server: true</code>であればサーバーコンポーネントのような動きになります。</p>
<p>初期値は<code>default</code>を設定すれば最初から返せますし、<code>useFetch()</code>、<code>useAsyncData()</code>が返す<code>status</code>を見ることで、未ロードかどうかをハンドリングできます。</p>
<p>これらのオプションをうまく使い分けると多くのパターンが実現できることがわかります。</p>
<h3 id="少し問題">少し問題</h3><p>ただ、色々試していて、サーバーアクセスする2つのコンポーネントがあった場合に、それぞれの通信でブロックしているような動作をしていました。わかりやすくするためにサーバーAPI側で1秒間のウェイトをかけていたのですが、コンポーネントが2つあると初期表示が2秒になりました。<code>&lt;Suspense&gt;</code>とか使ってもカバーできず、直列に待っているようでした。検索してみたら・・・このissueですかね。</p>
<p>Next.jsで同じようなプログラムを作ってみたところ、2つのコンポーネントが並列でリクエストを送っても（URLなどは変えてキャッシュされないようにして）、1秒ですみました。このあたりの並列処理とかはまだNext.jsに一日の長がありますね。</p>
<p>通信するコンポーネントが複数あると遅くなるので、一箇所で行なって<code>Promise.all()</code>で並列で待つとかをすれば良いとは思いますが・・・・</p>
<p>あと、experimentalなサーバーコンポーネントを試してみたけども、どうもクライアント側で動いているような感じでした。</p>
<h2 id="まとめ">まとめ</h2><p>Nuxtの通信周りのAPIをさらっとみて実験等をしてみました。これ1つで、さまざまなパターンに対応できる機能で、これはReactとかでも欲しいな、とちょっと思いました。</p>
<p>なお、最初、間違ってページ遷移時のリンクを<code>&lt;NuxtLink to=&quot;遷移先&quot;&gt;</code>ではなく、<code>&lt;a href=&quot;遷移先&quot;&gt;</code>と書いてしまい、ページ遷移後のレンダリングも全てがサーバーサイドレンダリングになってしまいました。「サーバーコンポーネントと言っているReactよりもかなり前衛的ですごいじゃん！！！！」と勘違いしてしまい、方針変更してそちらについて書こうと思ってNuxt2との動作比較とかも調べたりしました。しかし、<code>&lt;NuxtLink&gt;</code>の存在に気づいて書き換えたら、Next.js 12以前と同じような動きになって、原稿を全消ししたりしました。</p>
]]></content>
    <summary type="html">フロントエンドフレームワークからサーバーにアクセスするパターンという記事を書いたところ、ちょっとバズったので、そういえば調べきれてなかったNuxtの話でも書こうかな、と思って調べてみた記事です。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Nuxt" scheme="https://future-architect.github.io/tags/Nuxt/"/>
    <category term="Vue.js" scheme="https://future-architect.github.io/tags/Vue-js/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>フロントエンドフレームワークからサーバーにアクセスするパターン</title>
    <link href="https://future-architect.github.io/articles/20241111a/"/>
    <id>https://future-architect.github.io/articles/20241111a/</id>
    <published>2024-11-10T15:00:00.000Z</published>
    <updated>2024-11-10T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>僕が触り始めた頃のウェブフロントエンド開発はデバッガーもなく、ダイナミックHTMLと呼ばれて文字をチカチカさせたりするようなものでした。IE6という超安定ブラウザが出てきたり（Netscape 4.xも7.xも不安定だった）その後jQueryが登場したときは、天使が降臨したように思えたものです。</p>
<p>そこから長い年月が経ち、ウェブフロントエンドの比重が大きくなるにつれ、フロントエンドのコードはどんどん複雑化しました。OpenAPIなどのコードジェネレータなども普及した結果、通信というものが隠され、イベントの中で<code>await</code>や<code>.then()</code>で呼ばれる何か、みたいな理解をしているメンバーも今後増えていくのではないかという懸念があります。</p>
<p>現在ではウェブフロントエンド開発はReactやVueといったフレームワーク上で行われ、イベントというのはそのフレームワークの提供するライフサイクルイベントに対応付けられて処理されます。この手の原理原則の理解というと「フレームワークを全部ひっぺがせ！ そしたらシンプルだ！」みたいな言説もよく見かけますし、ブラウザを実装しよう的な記事も増えました。しかし、藤井九段を理解するのに、コマの動かし方の本を読んでも遠すぎるし、恋愛小説について解説するのに「ドーパミンとアドレナリンが」という話をすると1000年の恋も動物的物語になって幻滅してしまうのと同様（そちらの方が趣向に刺さる人もいるかもしれませんが）、いくら勉強しても普段の実装力がつくかというと微妙な気がしています。</p>
<h2 id="現代のウェブフレームワークの構成">現代のウェブフレームワークの構成</h2><p>ブラウザが持っている基本機能は「HTMLを表示する」しかないため、雑な表現をすれば、動的なウェブアプリケーションは最終的に「HTMLを組み立てる」のが仕事です。HTMLはWebサイトのテキスト表現なので、正確にはHTMLを読み込んで作られるDOMという内部の木構造のドキュメントのオブジェクトツリーを動的に書き換えます。</p>
<p>現代のウェブフレームワークであるReact, Vue, Angular, SolidJS, Svelteなど、ほとんどのフレームワークはどれも同じ構成をしています。コンポーネントと呼ばれる部品を作り、それを階層構造に並べていきます。そのコンポーネントにはHTMLタグへの変換ルールが記述されており、処理の中でHTMLが作られて表示されます。コンポーネントの親子関係とHTMLの親子関係は基本的に一致しています。</p>
<h3 id="ライフサイクルメソッド">ライフサイクルメソッド</h3><p>フレームワークは初期化時に指定された特定のDOM要素以下を自分の管轄下の自由にしていい階層としてDOMを操作していきます。そのDOMのところにルートのコンポーネントを配置します。配置された後にいくつかのライフサイクルのタイミングでコールバックが呼ばれます。</p>
<p>通常は初期化時は以下のようなイベントが順番に自動で呼ばれます。だいたいどのフレームワークでも共通です。</p>
<ol>
<li>createイベント: 初期化時に呼ばれる</li>
<li>renderイベント: ここでDOMの設計図を作る</li>
<li>mountイベント: DOMが反映されてブラウザ上に表示される</li>
</ol>
<p>DOMを作るときに、ボタンなどのフォーム要素にもイベントが設定されます。そのボタンをクリックすると、何かしら情報更新が行われたりページ遷移などが発生したりします。</p>
<ol start="4">
<li>unmountイベント: コンポーネントがこれから削除される(DOMはまだある)</li>
<li>deleteイベント: コンポーネントがDOM上から削除される</li>
</ol>
<p>Vue.jsは公式にこのライフサイクルをドキュメントに乗せています。</p>
<img fetchpriority="high" src="/images/2024/20241111a/image.png" alt="image.png" width="1200" height="1898">

<p>https://vuejs.org/assets/lifecycle.MuZLBFAS.png</p>
<p>Reactの以前のクラスコンポーネントはVueそっくりな感じでした。現在の関数型コンポーネントのReactの公式では図はないのですが、だいたいこんな感じです。<code>useInsertionEffect()</code>はCSS in JSライブラリがスタイルを挿入する目的、<code>useLayoutEffect()</code>はサイズの変更など、描画後に実行すると画面のちらつきに影響するような特殊ケースで使うので、基本的には<code>useEffect()</code>だけをみておけば問題ありません。<code>useInsertionEffect</code>はドキュメントではDOM操作の前後どちらかで実行とありますが、ここでは前の方に書いています。</p>
<img src="/images/2024/20241111a/react-lifecycle.drawio.png" alt="react-lifecycle.drawio.png" width="529" height="843" loading="lazy">

<p>Reactにおいては、<code>useEffect()</code>はコンポーネント初期化時だけではなく、特定の状態に関連して引き起こされる汎用な「（副）作用」を表します。コンポーネントの状態変化も、属性の状態変化も両方等しく扱う、ジェネリックな作用となっています。1つのコンポーネント内部で複数の作用を定義できますし、ドキュメントを見ると「コンポーネントのライフサイクル」と表現するのは不適切で、効果自身がそれぞれライフサイクルを持っていて、コンポーネントはそれらが属している物、ぐらいの扱いになっています。</p>
<h3 id="fetch">fetch()</h3><p><code>fetch()</code>がデータ取得の基本要素です。時代を作ったのはXMLHttpRequest（XHR）ですが、今後は<code>fetch()</code>だけをみておけば良いでしょう。</p>
<p>ウェブアプリケーション開発でHTML生成がサーバー側の役割で、作成するコードのほとんどのがJavaだったりした時代だと、POSTメソッドで情報取得をするといったものも過去ありましたが、現在では少数派でしょう。</p>
<p>何か通信するというときは基本的にはこの<code>fetch()</code>が最後に呼ばれます。このエントリーでは触れませんが、サーバーからストリーミングで結果を随時受け取るような場合には<code>fetch()</code>の最近の更新で追加されたStream対応でもできますが、WebSocketやServerSentEventも使われます。</p>
<h2 id="初期化ライフサイクル">初期化ライフサイクル</h2><p>ウェブ画面の表示時に最新情報を取得してそれを詰めこんだ画面を表示します。いくつかの作戦があります。</p>
<h3 id="初期化時のライフサイクルメソッドの中から情報取得">初期化時のライフサイクルメソッドの中から情報取得</h3><p>一番シンプルなのが初期化のライフサイクルメソッドからのサーバーアクセスです。Reactでは<code>useEffect(処理, [])</code>でコンポーネントが画面に表示された直後に呼ばれるロジックが記述できるので、ここでサーバーのデータアクセスを行います。Vue.jsのComposition APIだと<code>onMounted(処理)</code>ですね。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-10-31_9.09.56.png" alt="スクリーンショット_2024-10-31_9.09.56.png" width="906" height="725" loading="lazy">

<p>「コンポーネントの初期化のイベントハンドラで必要な情報を取得する」というのはコンポーネント単位で見れば独立性が高く、一番ナイーブでソフトウェアの設計としては正しい姿ではありますが、ユーザーがレンダリング結果を見るまでの行程が長く、時間がかかります。特に通信待ちが2往復あります。レスポンスの結果に画像データのリンクが含まれていて、それが画面に<code>&lt;img&gt;</code>タグとして置かれてからブラウザがその情報を取得しにいくとしたらさらに1往復追加されます。</p>
<ol>
<li>フロントエンドのJSコードの取得(多いと数MB)</li>
<li>JSをロードしてコンポーネントの描画(First Paint)</li>
<li>サーバーへのデータリクエストとレスポンス待ち</li>
<li>再描画</li>
</ol>
<p>Reactを開発していると「<code>useEffect()</code>の使用は最小限に」と言われます。最上位の親コンポーネントを除けば、公式ドキュメントのこの場合は使うなユースケース集にコンポーネント単位でのこの方法は載っていません。Reactは初期化の後処理などが怪しいアプリケーションを炙り出すためにStrictModeかつ開発モードではマウント時の<code>useEffect()</code>が2回呼ばれるようになっていたりする点は要注意です。</p>
<h3 id="Stale-While-Revalidate">Stale-While-Revalidate</h3><p>ブラウザ本体のキャッシュ戦略のStale-While-Revalidateのアイデアをウェブフレームワークに取り入れたデータ取得のライブラリに、Vercelが開発したSWRがあります。Vueにもそれをインスパイアして作られたswrvがあります。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-10-31_9.46.11.png" alt="スクリーンショット_2024-10-31_9.46.11.png" width="914" height="637" loading="lazy">

<p>Stale-While-Revalidateはキャッシュがあればまずはそれを返して表示してしまって、裏でこっそりとサーバーにあとから情報を取得しにいき、変更があれば更新をするといった動きをします。キャッシュはLocalStorageなどにも入れられますし、メモリにキャッシュする場合でも、他の箇所で同じURLにアクセスしていた場合はそれを即座に返します。</p>
<p>最初に紹介したライフサイクルメソッドと比べると、ロード中かどうかの管理やとってきた情報をstate化するところもSWRが面倒をみてくれるため、コードはシンプルになります。なお、キャッシュがない場合の最悪ケースのパフォーマンスは、ライフサイクルメソッドからの<code>fetch()</code>と同じです。</p>
<h3 id="サーバー側で取得して一緒に送信">サーバー側で取得して一緒に送信</h3><p>Next.jsやNuxt.jsで一般的になったサーバーサイドレンダリング(SSR)はさらに攻めた最適化を行います。前述の2つはウェブサイトが初回レンダリングされてから初めてサーバー通信を開始します。スタートが遅くなれば最終的な結果が得られるのも遅くなります。サーバー上で必要なコンテンツを全て集めてそれを初回のレスポンスに一緒に返してしまえばよい、レンダリングも終わらせて完成系のHTMLを返せばSEO上も良いと考えられたのがSSRです。</p>
<p>サーバー上でAPIを呼び出しその結果を使ってページに必要な情報をまずはまとめて取得します。Next.jsであれば<code>getServerSideProps</code>を、Nuxt.jsは<code>useFetch()</code>や<code>useAsyncData()</code>をつかってサーバー上でデータアクセスを行います。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-10-31_17.38.27.png" alt="スクリーンショット_2024-10-31_17.38.27.png" width="896" height="697" loading="lazy">

<p>図では便宜上サーバーを1つしか書いてないですが、他のAPIサーバーがある場合なども同様です。ブラウザ↔︎サーバー間よりも、サーバーから他のAPIサーバーや自分自身の方が距離の方が一般的に近いため、往復の時間ロスが少ないため、ブラウザから情報をわざわざ取りに行くよりも高速です。現代のフロントエンドアプリはサイズも大きいため、JSがロードされて結果が表示されるのも時間がかかります。SSRでは初回はレンダリング済みのHTMLを返すため、初回表示が最速です。あと、一度結果を表示させたあとにReactコンポーネントをロードしてレンダリングして返していますが、これはハイドレーションという処理になっており、フロントでReactで作り直すことで、イベントハンドラ類が全部きちんと設定された完全なアプリケーションになります。</p>
<p>初回表示はサーバー側でレンダリングしたHTMLを返しますが、そこからページ遷移して新しいページを表示するときは、レンダリングに必要な、<code>getServerSideProps</code>や<code>useFetch()</code>や<code>useAsyncData()</code>をサーバーで実行し、結果のJSONだけをブラウザに送って描画します。</p>
<p>問題点としては、サーバー側の技術スタックがNode.jsなどのJavaScript系の処理系にする必要があったりします。Next.jsではページトップのコンポーネントにしか<code>getServerSideProps</code>が書けないため、親子関係の依存が強くなりがちといった問題もあります。</p>
<p>ブログやニュースなど閲覧者ごとに違いがないページでしか使えませんが、Next.jsではさらに情報の取得を事前に静的に行ってHTMLを生成しておく、静的サイトジェネレーション(SSG)もあります。</p>
<h3 id="サーバーコンポーネント">サーバーコンポーネント</h3><p>Reactがパフォーマンス改善のキーとして現在取り組んでいるのがサーバーコンポーネントです。Vue.js本体が取り入れるかはわかりませんが、Nuxt.jsも実験的に取り組んでいます。</p>
<p>Reactはコンポーネントは再描画のたびに実行される前提であったため、通信コードは<code>useEffect()</code>などのライフサイクルメソッド側に書く必要がありました。しかし、Reactサーバーコンポーネントが登場したことで、「必ず一度だけサーバー側でレンダリングされるコンポーネント」が登場しました。通常はコンポーネント定義では仮想DOMの構築以外はせず、サーバーアクセスは<code>useEffect()</code>でやるのが通例でしたが、サーバーコンポーネントでは直接サーバー通信コードが書けます。</p>
<figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">AsyncComponent</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> data = <span class="title function_">use</span>(<span class="title function_">fetchMessage</span>()) <span class="comment">// サーバー通信呼び出し</span></span><br><span class="line">    <span class="keyword">return</span> (</span><br><span class="line">        <span class="language-xml"><span class="tag">&lt;<span class="name">div</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">            <span class="tag">&lt;<span class="name">h1</span>&gt;</span>&#123;data.message&#125;<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span></span><br><span class="line"><span class="language-xml">        <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">    )</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">fetchMessage</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">&quot;http://localhost:3000/api/data&quot;</span>)</span><br><span class="line">    <span class="keyword">const</span> data = <span class="keyword">await</span> res.<span class="title function_">json</span>()</span><br><span class="line">    <span class="keyword">return</span> data</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>このコンポーネントを含む親ページに遷移すると、まずはこの通信中で結果が決まらない箇所以外をサーバーでレンダリングし、JSON形式でフロントエンドに返します。半完成状態のJSONを元とにすばやくDOMを更新してユーザーに結果を返します。その裏でサーバーはAPIアクセスを行い、結果が帰ってきたらこの通信した箇所のみの仮想DOMの差分を作ってフロントに返します。</p>
<p>ずいぶんと複雑な機構に見えますが、最初のサーバーへのアクセスのタイミングでサーバーから外部サーバーへの通信を開始するため、外部APIが遅い場合にはリードタイムが節約できるのがこの方式のメリットです。また、サーバーで処理するコードはフロントエンド側にロジックなどを送信しないため、JavaScriptコードの転送時間が節約できます。レンダリングが大変で巨大なライブラリを必要とするようなものがあれば効果はさらに顕著に現れるでしょう。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-11-01_22.29.12.png" alt="スクリーンショット_2024-11-01_22.29.12.png" width="957" height="715" loading="lazy">

<h2 id="データの追加受信・更新">データの追加受信・更新</h2><p>次にデータの追加受信や更新のサーバーアクセスのパターンを見ていきます。データの追加受信というのは、補完機能付きのプルダウンメニューでユーザーの入力を受けて絞り込まれたリスト項目をサーバーからとってくる、表のページングで次のページの情報を取得してくる、といったことに該当するものとここでは定義します。画面が表示された時に自動発動するGETリクエスト以外です。</p>
<p>更新というのはフォームの送信などの編集作業のことです。どちらも似ているのでここでは両方扱います。後者だけのものもあります。</p>
<h3 id="DOMのコールバック">DOMのコールバック</h3><p>通常、データの追加取得や更新処理はユーザーの明確な操作時のみ行われます。明確な操作というのは次のような操作です。そしてこれらは、DOMのイベントに設定したイベントのコールバックが起点となります。</p>
<ul>
<li>ボタンを押す</li>
<li>キーボード操作</li>
<li>ファイルをドロップ</li>
<li>マウスのスクロール</li>
</ul>
<p>コールバックの中からサーバーリクエストを行います。その結果を待って情報を更新したり、ページ遷移したりして結果の画面をユーザーに見せます。基本的にやっていることはVisual BasicやDelphiとかQtとかのデスクトップアプリと変わりませんし、生HTMLやjQueryとかの古来のウェブフロントエンドとも違いはありません。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-11-01_22.54.04.png" alt="スクリーンショット_2024-11-01_22.54.04.png" width="960" height="698" loading="lazy">

<h3 id="状態管理ライブラリ">状態管理ライブラリ</h3><p>近年のウェブフロントエンド開発では、大規模化するにつれて、コードの関心で分離するために画面表示と、状態管理をレイヤー分けすることがよく行われます。コンポーネントを跨いだ情報を共有しやすくなりますし、通信部分を隠蔽し、フロントとサーバーの状態の同期を取る形式で通信を任せることも行われます。ReactであればRedux ToolkitやJotaiなどが使われます。VueだとPinia(Vuexの後継)が使われます。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-11-01_23.09.01.png" alt="スクリーンショット_2024-11-01_23.09.01.png" width="988" height="626" loading="lazy">

<p>ウェブのコンポーネントからは状態管理の状態を変更する、もしくは変更アクションを実行するなどするとそこでサーバーアクセスが発生し、状態管理の持っている情報と同期を取ったり、追加の情報を取ってきます。</p>
<h3 id="サーバーアクション">サーバーアクション</h3><p>Reactの19から入る機能で、Next.jsがすでにサポートしているのがサーバーアクションです。状態管理ライブラリの方は、ビューにまつわるコンポーネントコードから通信部分を別レイヤーとして切り出すことでエントロピーを下げる方向性でしたが、これは逆方向に振り切った機能です。</p>
<p>サーバーアクションは<code>&quot;use server&quot;</code>という文字列がボディの先頭にディレクティブとして書かれた関数です。コンポーネントの<code>&lt;form&gt;</code>の<code>action</code>属性、もしくは<code>&lt;button&gt;</code>の<code>formAction</code>属性に設定されるイベントハンドラで、見たい目的には単なるコールバック関数です。ですが、このコードの実体はサーバーにあり、サーバーの中だけでしかできない外部APIアクセスやDBアクセスもここの中に直接書けます。</p>
<p>フォームやボタンの操作が行われるとこのコールバックが直接呼ばれているように見えます。しかし、実態はサーバーにあるので自動で作られたサーバー側のAPIを呼び、その中でサーバーアクションが処理されます。あたかもフロントエンドから直接DBアクセスを行っているかのように見える点がポイントで、サーバーアクセスが完全に隠蔽化されています。</p>
<img src="/images/2024/20241111a/スクリーンショット_2024-11-01_23.51.16.png" alt="スクリーンショット_2024-11-01_23.51.16.png" width="972" height="609" loading="lazy">

<h2 id="まとめ">まとめ</h2><p>ウェブフロントエンドの仕組みは複雑化の一途を辿っています。すべて還元して中身を理解しようとすると大事です。ただ、イジワルをしたくて複雑化しようとしているわけではありません。目的があります。このエントリーを見れば、待ち時間をいかに減らすかのために苦心しているのがわかるでしょう。また、構造が複雑であっても、実際にコーディングしてみると開発体験は悪くないことがわかります。</p>
<p>ただ、最適な設計を考えたり、デバッグ時の挙動を追いかけるには多少は裏で何をやっているかを知っておくことは価値があります。本エントリーでは今時のフレームワーク中で利用されるパターンを紹介しました。</p>
]]></content>
    <summary type="html">僕が触り始めた頃のウェブフロントエンド開発はデバッガーもなく、ダイナミックHTMLと呼ばれて文字をチカチカさせたりするようなものでした。IE6という超安定ブラウザが出てきたり</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="Next.js" scheme="https://future-architect.github.io/tags/Next-js/"/>
    <category term="Nuxt" scheme="https://future-architect.github.io/tags/Nuxt/"/>
    <category term="React" scheme="https://future-architect.github.io/tags/React/"/>
    <category term="サーバーコンポーネント" scheme="https://future-architect.github.io/tags/%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>Playwrightでなぜかボタンにヒットしない</title>
    <link href="https://future-architect.github.io/articles/20241024a/"/>
    <id>https://future-architect.github.io/articles/20241024a/</id>
    <published>2024-10-23T15:00:00.000Z</published>
    <updated>2024-10-23T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20241024a/playwright.jpg" alt="" width="200" height="200">

<p><code>getByRole()</code>でボタンがヒットしないです、という相談を受けて調べた内容のメモです。次のようなHTMLになっていました。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">label</span> <span class="attr">for</span>=<span class="string">&quot;my-button&quot;</span>&gt;</span>ラベル<span class="tag">&lt;/<span class="name">label</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">button</span> <span class="attr">id</span>=<span class="string">&quot;my-button&quot;</span>&gt;</span></span><br><span class="line">    ボタンキャプション</span><br><span class="line"><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>ボタンに対して、ラベルがついていない場合は、<code>&lt;button&gt;</code>タグの中のキャプション（ここでは「ボタンキャプション」という文字列）を指定して、<code>getByRole(&#39;button&#39;, &#123;name: &#39;ボタンキャプション&#39;&#125;)</code>でヒットします。ただ、上記のようなラベルがついていると、そちらのキャプションにはヒットしなくなって、ラベルの方にのみヒットするようになります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="title function_">test</span>(<span class="string">&#x27;has title&#x27;</span>, <span class="title function_">async</span> (&#123; page &#125;) =&gt; &#123;</span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">goto</span>(<span class="string">&#x27;http://localhost:5173/&#x27;</span>);</span><br><span class="line">  <span class="comment">// ヒットする</span></span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">getByRole</span>(<span class="string">&#x27;button&#x27;</span>, &#123;<span class="attr">name</span>: <span class="string">&#x27;ボタンキャプション&#x27;</span>&#125;).<span class="title function_">click</span>();</span><br><span class="line">  <span class="comment">// ヒットしない</span></span><br><span class="line">  <span class="keyword">await</span> page.<span class="title function_">getByRole</span>(<span class="string">&#x27;button&#x27;</span>, &#123;<span class="attr">name</span>: <span class="string">&#x27;ラベル&#x27;</span>&#125;).<span class="title function_">click</span>();</span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">expect</span>(page.<span class="title function_">getByRole</span>(<span class="string">&quot;status&quot;</span>)).<span class="title function_">toHaveText</span>(<span class="string">&#x27;1&#x27;</span>)</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>W3Cのアクセシビリティの実践的ガイドのボタンのページを見ると次のように書かれています。</p>
<blockquote>
<p>The button has an accessible label. By default, the accessible name is computed from any text content inside the button element. However, it can also be provided with aria-labelledby or aria-label.</p>
<p>訳: ボタンはアクセス可能なラベルを持っています。デフォルトではボタン要素の中のテキストコンテンツから算出されます。しかし、aria-labelledbyやaria-labelでも提供できます。</p>
</blockquote>
<p>ということで、ボタンキャプションはラベルがない場合にのみ有効なデフォルト値ということで、こちらはバグなどではなく、アクセシビリティのガイドラインに従った動きをしているということがわかります。</p>
]]></content>
    <summary type="html">getByRoleでボタンがヒットしないです、という相談を受けて調べた内容のメモです。次のようなHTMLになっていました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="E2Eテスト" scheme="https://future-architect.github.io/tags/E2E%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="Playwright" scheme="https://future-architect.github.io/tags/Playwright/"/>
    <category term="accessibility" scheme="https://future-architect.github.io/tags/accessibility/"/>
    <category term="トラブルシュート" scheme="https://future-architect.github.io/tags/%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>［入門］Webフロントエンド E2E テスト を出版しました</title>
    <link href="https://future-architect.github.io/articles/20240701a/"/>
    <id>https://future-architect.github.io/articles/20240701a/</id>
    <published>2024-06-30T15:00:00.000Z</published>
    <updated>2024-06-30T15:00:00.000Z</updated>
    <author><name>武田大輝</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240701a/d0988e2d-efbc-e4c5-5c8a-1bf25d2db216.png" alt="" width="1187" height="1500">

<p>先日（2024年6月19日）、技術評論社さまから「［入門］Webフロントエンド E2E テスト ──PlaywrightによるWebアプリの自動テストから良いテストの書き方まで」をフューチャーの有志と共に出版いたしました。<br>本書はエンジニア選書と言われるシリーズの1つとなります。</p>
<p>既にご購入いただいた方、誠にありがとうございます。<br>購入を検討されている方、詳しくはこちらをご覧ください。</p>
<p>https://gihyo.jp/book/2024/978-4-297-14220-9</p>
<h2 id="書籍の概要">書籍の概要</h2><p>E2Eテストをこれから導入しようとしている方を主な対象とし、必要なノウハウを詰め込んだ書籍となっています。本書では、E2Eテストを学ぶ際の実装手段（ツール）として、近年人気が急上昇しているPlaywrightを中心に据えました。</p>
<p>章の構成は次のようになっています。</p>
<ul>
<li>第1章 Playwrightハンズオン</li>
<li>第2章 E2Eテストツールの紹介</li>
<li>第3章 Playwrightのテスト用ツールセット（1）ロケーター</li>
<li>第4章 Playwrightのテスト用ツールセット（2）ナビゲーション・アクション・マッチャー</li>
<li>第5章 テストコードの組み立て方</li>
<li>第6章 実践的なテクニック</li>
<li>第7章 ソフトウェアテストに向き合う心構え</li>
<li>第8章 E2Eの枠を超えたPlaywrightの応用例</li>
<li>第9章 Web APIのテスト</li>
<li>第10章 E2Eテストの実戦投入</li>
<li>第11章 Playwrightの内部構造</li>
</ul>
<p>「習うより慣れろ」の精神のもと、前半部は主にPlaywrightを触りながらE2Eテストを体感する章、後半部は実際の現場でE2Eテストを導入するにあたっての心構えや勘所を学べる章となっています。<br>CypressやNightwatchなど、Playwright以外のツールを採用している方でも、第5章・第7章・第10章・第11章をはじめとして、E2Eテストに関わる上で汎用的に役立つ内容が含まれています。</p>
<p>E2Eテストを初めて学ぶ方は、ぜひ、実際にご自身のPCでテストを書いて動かしながらこの書籍を読み進めていただくと、効果的に学習できると思います。</p>
<h2 id="執筆の経緯">執筆の経緯</h2><p>振り返ると、執筆の出発点となったのは、著者の一人である枇榔さんが弊社の技術ブログにてCypressの記事を綴ったことに遡ります。これは今から4年前、2020年の出来事です。<br>そこからCypressを中心にE2Eテストについての外部発信を継続的に行なっていたところ、技術評論社の方からご連絡をいただき、月刊誌「Software Design」の2022年1月号から4月号にかけてCypressの短期連載を行う運びとなりました。<br>そして連載終了後、同じく技術評論社の方から書籍の執筆についてお声がけをいただき、メンバーを追加して6名で執筆がスタートしました。</p>
<p>このような流れで始まった企画なので、当初はCypressを中心とした企画として予定をしていました。しかしPlaywrightの成長の勢いはすごく、途中でCypressとPlaywrightの両方を紹介する企画に一度ピボットし、最終的にはPlaywrightのみを扱う企画になりました。</p>
<p>今回、通常より多い6名で執筆にあたったのは、社内にE2Eテストの自動化を実践しているメンバーが数多くいたため、より多くの実践的な知見を盛り込んだ書籍にできると考えたからです。<br>私自身はというと、当時はFlutterにおけるGolden TestやIntegration Testの自動化に取り組んでいましたが、スコープの広いテストの自動化に強い興味を持っていたため、手を上げて執筆に関わらせていただきました。</p>
<p>技術ブログでの発信がこのような大きな活動につながることは非常にありがたいことです。これからも、地道な努力と継続的な発信を続けていきたいという想いが一層強くなりました。</p>
<h2 id="執筆を終えて感じたこと">執筆を終えて感じたこと</h2><p>私自身、技術ブログの執筆やSoftware Designへの寄稿など、技術文書を書くことにはある程度慣れていましたが、書籍を出版するという経験は初めてでした。<br>執筆を一通り終え、実際の本を手にして抱いた感想としては、「また何か本を書きたい」という非常にポジティブなものでした。その理由をいくつか紹介したいと思います。</p>
<h3 id="完成までに十分な労力と時間を費やすことができた">完成までに十分な労力と時間を費やすことができた</h3><p>1つ目は「完成までに十分な労力と時間を費やすことができた」ことです。<br>執筆作業自体は2023年の3月頃から始まったので、（本業務の合間を縫って執筆活動を行うという前提でも）時間的猶予が十分にありました。<br>そのため、書籍に盛り込む内容の選定から、文章の細部に至るまで、納得いくまでこだわれたことが大きな満足度につながっています。</p>
<h3 id="非常に多くの有識者によるレビューを頂くことができた">非常に多くの有識者によるレビューを頂くことができた</h3><p>2つ目は「非常に多くの有識者によるレビューを頂くことができた」ことです。<br>書籍の内容については、著者同士でのレビューだけでなく、著者以外の社内の有志によるレビュー、社外の有識者の方々によるレビュー、そして編集者の方によるレビューと、何段階にもわたってレビューを実施させていただきました。<br>自身がこだわりを持って書いた文章を多くの方にレビューしていただく経験は初めてで、非常に学びのあるフィードバックを数多く頂戴しました。</p>
<p>社外の有識者としては、テスト駆動開発の第一人者である和田卓人さん（@t_wada）、組み込み系システムのテスターとしてテスト系イベントで発表をよくされている深谷美和さん（@miwa719）、太田健一郎さん、そしてソフトウェア教育にも造詣の深い、株式会社Renewerの堀内亮平さんにレビューをいただきました。<br>単なる言い回しの指摘や誤字脱字の修正にとどまらず、このような内容も伝えるべきだという指摘まで幅広くコメントをいただき、書籍の品質が大きく向上しました。改めて深く感謝申し上げます。</p>
<p>そして、本書のきっかけとなったSoftware Designでの短期連載から、本書の編集まで一貫してお付き合いいただいた、技術評論社の中田瑛人さん。<br>先述の通り企画がCypressからPlaywrightにピボットしたこともあり、執筆に想定以上の時間を要する形になりましたが、最後まで伴奏していただき良い本になる手助けを数多くいただきました。深く感謝申し上げます。</p>
<p>最後のレビュアは、読者の皆様となります。どのようなレビューでも真摯に受け止め、糧にさせていただきたいと思っておりますので、忌憚なきご意見をお待ちしております。</p>
<h3 id="経験豊富なベテラン執筆者と共に進めることができた">経験豊富なベテラン執筆者と共に進めることができた</h3><p>3つ目は「執筆経験の豊富な澁川さんの元で、執筆活動を進められた」ことです。<br>技術的な内容の正確さや豊富さはもちろん、「読み物」として文章を構築していく点において、コラムの入れ方であったり、注釈の入れ方であったり、文章が淡白にならないような工夫をはじめ、普段自分があまり意識しない部分で多くの学びがありました。<br>出版に関わるメンバーを増やすという裏ミッションのもと、社外の有識者の方々へのレビュー依頼や出版社の方とのやりとりをはじめ全体をリードしてくださり、非常に心強く執筆を進めることができました。</p>
<h2 id="改訂版に向けて">改訂版に向けて</h2><p>初版の原稿には間に合わず、入れ込むことができなかったネタもいくつかあります。<br>また、社内では本書籍を用いて勉強会を開催する動きもあり、実際にやってみると、追加のネタ（例えば、メールで送られてくるワンタイムパスコードを入力するケースのテスト方法など）が出てきたりしています。</p>
<p>初版で書ききれなかったネタは技術ブログとして適宜公開しつつ、改訂版を出すタイミングがあればそこで取り入れる方針です。</p>
<p>WebフロントエンドのE2Eテストの世界は進化が早く、新しいツールやベストプラクティスが次々と登場しています。初版の売上が好調であれば、さらなる内容の充実を図り、改訂版としてお届けできればと願っております。</p>
<p>皆様のご支援とご愛顧が、私たちの次なるステップに繋がる大きな力となりますので、引き続きよろしくお願いいたします。</p>
]]></content>
    <summary type="html">[入門］Webフロントエンド E2E テスト ――PlaywrightによるWebアプリの自動テストから良いテストの書き方まで をフューチャーの有志と共に出版いたしました。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="E2Eテスト" scheme="https://future-architect.github.io/tags/E2E%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="Playwright" scheme="https://future-architect.github.io/tags/Playwright/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>package.json dependencies メンテの仕方 最短ルート</title>
    <link href="https://future-architect.github.io/articles/20240624a/"/>
    <id>https://future-architect.github.io/articles/20240624a/</id>
    <published>2024-06-23T15:00:00.000Z</published>
    <updated>2024-06-23T15:00:00.000Z</updated>
    <author><name>枇榔晃裕</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240624a/top.png" alt="" width="800" height="623">

<p>本記事は「珠玉のアドベントカレンダー記事をリバイバル公開します」企画のために、以前Qiitaに投稿した記事をブラッシュアップし、フューチャー技術ブログに転載したものになります。Qiita側の元記事もアップデートしています。</p>
<h2 id="はじめに">はじめに</h2><p>package.json の dependencies をメンテナンスするにはどこから手を付ければいいか、を解説します。</p>
<p>Node.js を使っている人にはおなじみ package.json。</p>
<p>package.json の中で一番よく更新されるのが dependencies（個人の感想、次点で scripts）。<br>そして、依存パッケージが着々とバージョンアップしていくにも関わらず放置されてしまって後々問題になりがちなのも dependencies 。</p>
<p>「npm install で追加したっきり。パッケージのアップデートなんて考えたことなかった」という人や「GitHub から security alert が届いてるけども見て見ぬふりをしている」という人向けに、package.json の dependencies をアップデートして、依存パッケージの最新版に追従していく方法について、個人の考えを書いてみます。</p>
<h2 id="npm-install-したときの-dependencies">npm install したときの dependencies</h2><p>まずはおさらい。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install lodash</span><br></pre></td></tr></table></figure>

<p>とすると、package.json の dependencies に以下の一行が追加されます。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;lodash&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.17.19&quot;</span></span><br></pre></td></tr></table></figure>

<p>すこし古い記事とかだと「オプションに <code>--save</code> をつけること」と書いていたりしますが、npm 5.0.0 以降デフォルトで save オプションが付くようになったので書かなくて大丈夫。</p>
<h3 id="依存パッケージをメンテせず放置すると">依存パッケージをメンテせず放置すると</h3><p>npm install したパッケージをアップデートせずにいると、新しく追加された機能が使えないのはもちろんのこと、新しいバージョンで修正された脆弱性がそのまま残ってしまう危険があります。</p>
<p>特に npm からインストールしたパッケージの場合、パッケージが依存しているパッケージの、さらにその中で依存しているパッケージに脆弱性が見つかった、みたいなことも多々。2020年7月に見つかった lodash の脆弱性（CVE-2020-8203<sup id="fnref:1">1</sup>）が起因で 500 万件以上の GitHub Dependabot のアラートが発生したとか。<sup id="fnref:2">2</sup></p>
<p>Node.js で書かれたパッケージは大体何かしらのパッケージに依存しているので、汎用的なパッケージに脆弱性が見つかると影響する可能性が非常に高いです。</p>
<p>そして、GitHub Dependabot からアラートが来たときに慌ててバージョンを一気にあげてしまうと、いままで使っていた機能がいつの間にか廃止されてアプリが動かなくなった、みたいなこともよく起こるので、出来れば日頃からこまめに上げていきたいところ。</p>
<h2 id="npm-outdated-npm-update-の利点・欠点">npm outdated &amp;&amp; npm update の利点・欠点</h2><p>では、パッケージをアップデートしよう、となったときによく使われるコマンドが以下の 2 つ。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm outdated</span><br><span class="line">npm update</span><br></pre></td></tr></table></figure>

<p><code>npm outdated</code> で最新バージョンがあるかどうかを確認して、<code>npm update</code> で outdated なパッケージを一括更新します。</p>
<p>ただし注意点があります。</p>
<p>npm update では package.json の更新はしません。package-lock.json と node_modules の更新だけ行います。<br>加えて、<code>^</code> 付きのバージョン指定でもメジャーバージョンのアップデートまでは行いません。</p>
<p>これは <code>npm update</code> だけでなく <code>npm audix fix</code> も同様です。</p>
<h3 id="npm-update-では-package-json-の更新をしない">npm update では package.json の更新をしない</h3><p>例えば package.json の dependencies に下のように書きかえてから、npm install してみましょう。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;lodash&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.10.0&quot;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm install</span></span><br><span class="line">added 1 package, and audited 1 package in 1s</span><br><span class="line">found 0 vulnerabilities</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm list</span></span><br><span class="line">test@1.0.0</span><br><span class="line">└── lodash@4.17.19</span><br></pre></td></tr></table></figure>

<p>lodash の 4.10.0 ではなく、4.17.19 がインストールされています。</p>
<p>そして、package-lock.json には以下のように記載されます。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;packages&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;node_modules/lodash&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;4.17.19&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;resolved&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;integrity&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sha512-xxxxxxxxxxxxx&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>そうこうしている間に lodash が 4.17.19 から 4.17.20 へとアップデートされました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm outdated</span></span><br><span class="line">Package  Current   Wanted   Latest  Location             Depended by</span><br><span class="line">lodash   4.17.19  4.17.20  4.17.20  node_modules/lodash  test</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm update</span></span><br><span class="line">changed 1 package, and audited 1 package in 974ms</span><br><span class="line">found 0 vulnerabilities</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npm list</span></span><br><span class="line">test@1.0.0</span><br><span class="line">└── lodash@4.17.20</span><br></pre></td></tr></table></figure>

<p>無事アップデートできました。package-lock.json 内も <code>&quot;version&quot;: &quot;4.17.20&quot;</code> となっています。<br>しかしながら、package.json の中は <code>&quot;lodash&quot;: &quot;^4.10.0&quot;</code> のままです。</p>
<h3 id="付きのバージョン指定">^ 付きのバージョン指定</h3><p>そもそも、<code>^</code> 付きのバージョン指定とは何か。<br>npm install から dependencies に追加したパッケージには <code>&quot;^4.17.19&quot;,</code> のように <code>^</code>（キャレット）がくっついています。</p>
<p><code>^</code> が付いていると最新版を取ってきてくれそうな感じを受けますが、微妙にニュアンスが違います。</p>
<p>この <code>^</code> は互換性のあるバージョンという意味。互換性のあるバージョン内、つまりメジャーバージョンアップを避けてアップデートを行います。jQuery 2.x.x を使っている場合、3.x.x に上げず 2.x.x の中で最新版をとってくるわけですね。</p>
<p>互換性のあるバージョンというのがまた微妙に複雑。</p>
<ul>
<li><code>^0.2.3</code> の場合、0.2.x 内で最新版を探す（0.3.x までは上げない）</li>
<li><code>^0.0.3</code> の場合、0.0.3 内(-bata 付きとか)で最新版を探す（0.0.4 までは上げない）</li>
<li><code>^0.2.3 -beta2</code> の場合、0.2.3 内(-bata3 とか)で最新版を探す（0.2.4 までは上げない）</li>
</ul>
<p>などのルールがあるのですが、長くなるので詳しく知りたい方は公式<sup id="fnref:3">3</sup>を。</p>
<p>メジャーバージョンアップができないというのが親切ながらも微妙に厄介なところ。<br><code>&quot;lodash&quot;: &quot;^3.10.1&quot;</code> のときに　<code>npm update</code> をしても 4.17.20 には上がらず、先の脆弱性 CVE-2020-8203 は解消しません。</p>
<h3 id="メジャーバージョンのアップデートを行う-latest">メジャーバージョンのアップデートを行う @ latest</h3><p>npm update ではメジャーバージョンアップができない。最新版にするにはどうすればいいか。</p>
<p>答えの１つとしては以下。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install &lt;package-name&gt;@latest</span><br></pre></td></tr></table></figure>

<p><code>@latest</code> を末尾に付けるとメジャーバージョン含めた最新版に更新してくれます。package.json も更新してくれます。</p>
<p>複数インストールするときは並べて書きます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm install &lt;package-name1&gt;@latest &lt;package-name2&gt;@latest</span><br></pre></td></tr></table></figure>

<p>しかしながら、1 つ 1 つパッケージ名を打ち込んで @ latest をつけていくのは面倒です。</p>
<h3 id="npm-audit-fix-–force">npm audit fix –force</h3><p>npm install したときに脆弱性があると出てくる <code>npm audit fix --force</code>も答えの１つです。こちらも package.json 内の <code>&quot;lodash&quot;: &quot;3.10.1&quot;</code> を <code>&quot;lodash&quot;: &quot;^4.17.20&quot;</code> に書き換えます。</p>
<p>メジャーバージョンアップや <code>^</code> も無視して強制的にアップデートしてしまう、<code>--force</code> の付いた npm オプションです。</p>
<p>脆弱性が出てきたときの最終手段といった趣がありますね。</p>
<p>脆弱性があったときにしか使えないので日常の運用でこのコマンドを頼るのは悪手。もちろんメジャーバージョンアップをまたぐので、作ったアプリが動かなくなる可能性も十分あります。</p>
<p>日頃からこまめにパッケージのバージョンアップを確認しつつ、メジャーバージョンアップに対応しておきたいところです。</p>
<h2 id="アップデート可能なパッケージ一覧表示・更新する-ncu">アップデート可能なパッケージ一覧表示・更新する ncu</h2><p>日頃からこまめにパッケージのバージョンアップをするのに便利なのが、npm-check-updates。略して ncu。</p>
<p>依存パッケージを見て、アップデート可能なものを一覧にしつつ、package.json の dependencies をメジャーバージョン含めて最新版に書き換えてくれる便利な子です。</p>
<p>使い方は簡単です。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g npm-check-updates</span><br><span class="line">ncu</span><br></pre></td></tr></table></figure>

<p>とするだけ。そうすると、 アップデート可能なパッケージを一覧にしてくれます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ncu</span></span><br><span class="line">Checking xxx\package.json</span><br><span class="line">[====================] 1/1 100%</span><br><span class="line"></span><br><span class="line"> lodash  ^4.10.1  →  ^4.17.20</span><br><span class="line"></span><br><span class="line">Run ncu -u to upgrade package.json</span><br></pre></td></tr></table></figure>

<p>npxコマンドとしても呼び出せるのでお試しで使ってみたい場合はこちらでも。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">npx npm-check-updates</span></span><br><span class="line"></span><br><span class="line">Checking xxx\package.json</span><br><span class="line">[====================] 1/1 100%</span><br><span class="line"></span><br><span class="line"> lodash  ^4.10.1  →  ^4.17.20</span><br><span class="line"></span><br><span class="line">Run npx npm-check-updates -u to upgrade package.json</span><br></pre></td></tr></table></figure>

<p>package.json の dependencies を最新版に書き換えたいときは以下。<br>ncu は package.json の更新だけするので、パッケージと package-lock.json を更新するために npm install はも忘れずに。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ncu -u</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure>

<p>特定のパッケージだけ最新版に書き換えたいときは、-u の後にパッケージ名を指定すれば OK。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ncu -u lodash</span><br></pre></td></tr></table></figure>

<p>ncu を覚えておけば、依存パッケージをさくっと最新バージョンにできます。</p>
<h2 id="どのパッケージからアップデートしていくか">どのパッケージからアップデートしていくか</h2><p>依存パッケージが全部最新版になったよ、良かったね。で、終わればラッキー。長らく package.json を更新していない場合、動かしてみると何かしら不具合が出るのではないかと。</p>
<p>理由はおそらくいろいろと。いつのまにか webpack loader のオプションの渡し方が変わってただとか、使っていた function が unstable_function にリネームされているだとか、機能ごと廃止されているだとか。</p>
<p>長らく蓄積された度重なるアップデートに追いつくのは大変ですが、一度元に戻して、少しづつ切り崩していきましょう。</p>
<h3 id="パッケージのナンバリング">パッケージのナンバリング</h3><p>幸いにも npm のパッケージバージョンは規則が設けられています。</p>
<p>例として先ほど追加した lodash を見てみましょう。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;lodash&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^4.17.19&quot;</span></span><br></pre></td></tr></table></figure>

<ul>
<li>メジャーバージョンアップ<br>一番左の <code>4</code> は大きな機能追加や仕様変更、根本から変更する場合などに数字が 1 増えるというのがメジャーバージョンアップです。メジャーバージョンアップのときに機能が廃止になって動かなくなったとか、いままでと動きが変わったとかいうことが起きやすいので、該当ライブラリをアップデートした後にちゃんとテストをしておきましょう。メジャーバージョンアップしたパッケージは１つアップデートしたあとに動作確認しておくのが吉。横着してまとめてやろうとすると大変つらい思いをします（しました）。</li>
<li>マイナーバージョンアップ<br>真ん中の <code>17</code> がマイナーバージョンアップです。便利オプションが増えたよとかちょっとした機能改善系のアップデート。これまでと動きが変わる、ということは起きにくいので、他のライブラリと一緒にまとめてアップデートして動作確認で十分かなという感じです。</li>
<li>パッチバージョンアップ<br>一番右の <code>19</code> はパッチバージョンアップです。基本的にバグ修正。こちらもまとめてアップデートで大丈夫という印象。</li>
</ul>
<p>上で紹介した <code>ncu</code> を使うと、</p>
<ul>
<li>メジャーバージョンアップがある場合は赤</li>
<li>マイナーバージョンアップは水色</li>
<li>パッチバージョンアップは緑</li>
</ul>
<p>で、表示してくれます。<br><img src="/images/2024/20240624a/image.png" alt="image.png" width="554" height="59" loading="lazy"></p>
<p>まずは水色・緑で表示されているパッケージをまとめてアップデートしてから動作確認して、いつでも戻れるように Git commit してからメジャーバージョンアップの赤色に取り掛かるのがよさそうだなという経験則があります。</p>
<p><code>ncu</code> で <code>-t patch</code> をつけるとパッチバージョンアップのみを、 <code>-t minor</code> をつけるとマイナーバージョンアップとパッチバージョンアップをターゲットにしてくれるので、こちらも覚えておくと便利。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ncu -u -t minor</span><br><span class="line">npm install <span class="comment"># =&gt; 水色・緑で表示されているパッケージが最新バージョンになる</span></span><br></pre></td></tr></table></figure>

<p>また、メジャーバージョンアップは破壊的変更にあわせてコードの修正が入ることも多いので、バージョンを上げてうまく動作したら１パッケージごとに Git commit をしていきましょう。commit 打ち忘れると切り戻したいときに大変です（n 敗）。</p>
<h3 id="破壊的変更の確認">破壊的変更の確認</h3><p>パッケージのバージョンアップ時にどのような変更が入ったかは、基本的に各パッケージのReleasesページを見るしかないです。</p>
<p>しかしながら、動作の変わるような大きい変更が入る場合は Breaking Change の見出しとともにしっかりとアナウンスをしている場合が多いですし、親切なパッケージであれば代替手段の紹介やコードの自動修正ツールを用意してくれていることもあります。</p>
<p>お知らせやアナウンスといったようなものが見当たらないときは、残念ですが README やPR、issue などを見ながら変更箇所を調査しましょう。</p>
<p>いっそのこと別のパッケージを探すのも手。当時なかった便利パッケージや使いやすい loader が新たに生まれていることもあります。</p>
<h2 id="まとめ">まとめ</h2><p>dependenciesのメンテナンスは <code>ncu</code> でアップデートを確認して、マイナー・パッチバージョンアップを片付けてからメジャーバージョンアップを慎重に行う。</p>
<p>月一を目安に最新バージョンへの追従を心掛けておくと、GitHub から security alert が届いたときに冷静に対処できるのではないかと。</p>
<p>チームで脆弱性の管理などをしている場合、脆弱性の管理が追いつかなくなりがち。</p>
<p>npm パッケージだけでなくサーバの脆弱性の可視化やチケット管理を行える SaaS を Future から提供している<sup id="fnref:4">4</sup>ので、ぜひ検討してみてください。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">https://jvndb.jvn.jp/ja/contents/2020/JVNDB-2020-008656.html</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">https://www.zdnet.com/article/open-source-software-security-vulnerabilities-exist-for-over-four-years-before-detection-study/</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">https://docs.npmjs.com/cli/v6/using-npm/semver#caret-ranges-123-025-004</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">チームで脆弱性を管理するFutureVuls（ https://vuls.biz/）</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">package.json の dependencies をメンテナンスするにはどこから手を付ければいいか、を解説します。</summary>
    <category term="Frontend" scheme="https://future-architect.github.io/categories/Frontend/"/>
    <category term="JavaScript" scheme="https://future-architect.github.io/tags/JavaScript/"/>
    <category term="Node.js" scheme="https://future-architect.github.io/tags/Node-js/"/>
    <category term="npm" scheme="https://future-architect.github.io/tags/npm/"/>
    <category term="バージョンアップ" scheme="https://future-architect.github.io/tags/%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%A2%E3%83%83%E3%83%97/"/>
    <category term="パッケージ管理" scheme="https://future-architect.github.io/tags/%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E7%AE%A1%E7%90%86/"/>
  </entry>
</feed>
