<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	
	xmlns:georss="http://www.georss.org/georss"
	xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
	>

<channel>
	<title>Nuxt.js | みんたく</title>
	<atom:link href="https://mintaku-blog.net/category/develop/nuxt/feed/" rel="self" type="application/rss+xml" />
	<link>https://mintaku-blog.net</link>
	<description>みんたくの技術ブログ</description>
	<lastBuildDate>Mon, 06 Jan 2025 12:47:27 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.0.11</generator>

<image>
	<url>https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2018/06/cropped-ipad-820272_640.jpg?fit=32%2C32&#038;ssl=1</url>
	<title>Nuxt.js | みんたく</title>
	<link>https://mintaku-blog.net</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">144480658</site>	<item>
		<title>Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</title>
		<link>https://mintaku-blog.net/io-api-ga4/</link>
					<comments>https://mintaku-blog.net/io-api-ga4/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 05 Jan 2025 01:50:27 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[サイト運営]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2565</guid>

					<description><![CDATA[<p>Webサイトでユーザーの行動を分析する際、要素が画面に表示されたタイミングを検知したいと思い、Vue.jsのコンポーネントで Intersection Ob …</p>
The post <a href="https://mintaku-blog.net/io-api-ga4/">Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Webサイトでユーザーの行動を分析する際、要素が画面に表示されたタイミングを検知したいと思い、Vue.jsのコンポーネントで Intersection Observer API を使用して要素の表示を検知し、Google Analytics 4（GA4）でトラッキングする実装方法を紹介します。</p>
<h2>目的</h2>
<ul>
<li>カードコンポーネントの画面表示を検知</li>
<li>GTM経由でGA4にイベントを送信</li>
<li>表示データの分析</li>
</ul>
<h2>実装フロー</h2>
<ol>
<li>Vueコンポーネントで要素の表示を検知</li>
<li>dataLayerにイベントをプッシュ</li>
<li>GTMでイベントを捕捉</li>
<li>GA4にデータを送信</li>
</ol>
<h2>Vueコンポーネントの実装</h2>
<p></p><pre class="crayon-plain-tag">import { ref, onMounted } from 'vue'

interface Props {
  items: Array&lt;{
    id: string
    title: string
  }&gt;
}

const props = defineProps&lt;Props&gt;()
const itemRefs = ref&lt;any[]&gt;([])

onMounted(() =&gt; {
  itemRefs.value.forEach((elementRef, index) =&gt; {
    if (!elementRef) return

    const observer = new IntersectionObserver((entries) =&gt; {
      if (entries[0].isIntersecting) {
        const item = props.items[index]
        // dataLayerにイベントをプッシュ
        window.dataLayer?.push({
          event: 'item_view',
          item_id: item.id,
          item_title: item.title,
          eventModel: {
            send_to: 'G-XXXXXXXX'
          }
        })
        observer.disconnect()
      }
    })

    observer.observe(elementRef.$el || elementRef)
  })
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;div
      v-for="(item, index) in items"
      :key="item.id"
      :ref="el =&gt; itemRefs[index] = el"
    &gt;
      &lt;!-- カードの内容 --&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</pre><p></p>
<h3>ref配列の使用</h3>
<p>itemRefs配列を使って、複数のカードコンポーネントへの参照を管理しています。Vue3のref属性の動的な割り当てを活用しています。</p>
<h3>Intersection Observer APIの設定</h3>
<p>各カードごとに個別のObserverを作成しています。一度表示を検知したらdisconnect()で監視を停止し、重複計測を防止します。</p>
<h3>dataLayerへのデータ送信</h3>
<p>カードのIDとタイトルを送信し、eventModelを使って送信先のGA4を指定しています。</p>
<h2>GTMの設定</h2>
<p>続いて、Google Tag Managerの設定手順です。</p>
<h3>1. 変数の設定</h3>
<p>まずは、dataLayerから送信されたデータを受け取るための変数を設定します。</p>
<p>GTMの変数設定で以下の2つを作成します。</p><pre class="crayon-plain-tag">【Item ID変数の設定】
- 変数名: Item ID
- 種類: データレイヤー変数
- データレイヤー変数名: item_id

【Item Title変数の設定】
- 変数名: Item Title
- 種類: データレイヤー変数
- データレイヤー変数名: item_title</pre><p>これらの変数は、後ほどGA4にデータを送信する際のパラメータとして使用します。</p>
<h3>2. トリガーの設定</h3>
<p>次に、イベントを検知するためのトリガーを設定します。</p><pre class="crayon-plain-tag">【トリガーの設定】
- トリガー名: Item View
- トリガータイプ: カスタムイベント
- イベント名: item_view</pre><p>このトリガーは、dataLayerにプッシュされたitem_viewイベントを検知します。</p>
<h3>3. タグの設定</h3>
<p>最後に、GA4にデータを送信するためのタグを設定します。</p><pre class="crayon-plain-tag">【タグの設定】
- タグ名: GA4 - Item View
- タグタイプ: Google タグ
- 測定ID: G-XXXXXXXX

イベントパラメータ:
- item_id: {{Item ID}}
- item_title: {{Item Title}}</pre><p></p>
<h2>GA4での設定と分析</h2>
<h3>カスタムディメンションの設定</h3>
<p>GA4でデータを分析するために、まずカスタムディメンションを設定する必要があります。</p>
<p>管理画面から以下のディメンションを作成します。</p>
<ol>
<li>Item ID（イベントスコープ）</li>
<li>Item Title（イベントスコープ）</li>
</ol>
<h3>レポートの作成</h3>
<p>探索レポートを使って、以下のように分析が可能です。</p>
<ul>
<li>ディメンション：Item Title</li>
<li>指標：イベントカウント</li>
<li>フィルター：イベント名 = item_view</li>
</ul>
<p>これにより、各カードの表示回数を確認できます。</p>
<h2>デバッグのポイント</h2>
<h3>1. GTMのプレビューモード</h3>
<p>GTMのプレビューモードでは以下を確認します。</p>
<ul>
<li>dataLayerにイベントが正しく送信されているか</li>
<li>変数が期待通りの値を取得できているか</li>
<li>タグが正しいタイミングで発火しているか</li>
</ul>
<h3>2. GA4のデバッグビュー</h3>
<p>GA4のデバッグビューでは</p>
<ul>
<li>イベントが正しく送信されているか</li>
<li>パラメータが正しく設定されているか</li>
</ul>
<p>を確認できます。</p>
<h2>よくあるトラブルと解決方法</h2>
<h3>イベントが発火しない</h3>
<ul>
<li>dataLayerの実装を確認</li>
<li>コンソールでエラーがないか確認</li>
</ul>
<h3>パラメータが取得できない</h3>
<ul>
<li>GTMの変数名が正しいか確認</li>
<li>dataLayerの構造を確認</li>
</ul>
<h3>GA4にデータが反映されない</h3>
<ul>
<li>測定IDが正しいか確認</li>
<li>最大48時間の遅延を考慮</li>
</ul>
<p>→ 実際に24時間くらいデータが反映されず、実装が間違っているのかと見直していましたが時間が経てば反映されてました</p>
<h2>参考リンク</h2>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API">交差オブザーバー API - Web API | MDN</a>
			</p>
							<div class="ys-blog-card__dscr">
					交差オブザーバー API (Intersection Observer API)&hellip;				</div>
										<div class="ys-blog-card__domain">developer.mozilla.org</div>
					</div>
	</div>
</div>


<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/www.gstatic.com/devrel-devsite/prod/v80d9a52eefe4eccd9a262cfbc94473a846b10e2b3115e0674427719fa6f74364/developers/images/opengraph/white.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developers.google.com/analytics/devguides/collection/ga4">Google Analytics for developers &nbsp;|&nbsp; Google for Developers</a>
			</p>
										<div class="ys-blog-card__domain">developers.google.com</div>
					</div>
	</div>
</div>


<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/www.gstatic.com/devrel-devsite/prod/v80d9a52eefe4eccd9a262cfbc94473a846b10e2b3115e0674427719fa6f74364/developers/images/opengraph/white.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://developers.google.com/analytics/devguides/collection/ga4?hl=ja">デベロッパー向け Google アナリティクス &nbsp;|&nbsp; Google Analytics &nbsp;|&nbsp; Google for Developers</a>
			</p>
										<div class="ys-blog-card__domain">developers.google.com</div>
					</div>
	</div>
</div>The post <a href="https://mintaku-blog.net/io-api-ga4/">Intersection Observer APIを使用したGTM/GA4イベントトラッキングの実装</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/io-api-ga4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2565</post-id>	</item>
		<item>
		<title>Nuxt.jsのアプリケーションにVWOのA/Bテストツールを導入する</title>
		<link>https://mintaku-blog.net/nuxt-vwo/</link>
					<comments>https://mintaku-blog.net/nuxt-vwo/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 18 Aug 2024 07:14:46 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2522</guid>

					<description><![CDATA[<p>サイト運営において、ユーザー体験の向上は欠かせない要素となっていますが、改善策を直感的に判断して実施するだけでは、その効果を正確に評価することは難しいと実感 …</p>
The post <a href="https://mintaku-blog.net/nuxt-vwo/">Nuxt.jsのアプリケーションにVWOのA/Bテストツールを導入する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>サイト運営において、ユーザー体験の向上は欠かせない要素となっていますが、改善策を直感的に判断して実施するだけでは、その効果を正確に評価することは難しいと実感しています。</p>
<p>ユーザーの反応を正確に把握し、データに基づいた改善を行うためにも、A/Bテストを導入することにしました。</p>
<p>今回、VWO（Visual Website Optimizer）を利用してA/Bテストを実施する方法などについて説明します。</p>
<h2>A/Bテストツールの導入でユーザー体験を向上させたい</h2>
<p>運用しているサイトにおいて、改善する際にどうしても直感的に頼るところが出てきてしまい、改善した結果が本当に良い影響を与えているかどうかを確証できずにいました。</p>
<p>そこで、実際に改善した結果を元にユーザーフィードバックを収集できるA/Bテストを導入してみることにしました。</p>
<p>A/Bテストを導入することで、ユーザーが最も快適に感じるUI/UXやデザインを特定することができ、サイト全体の使いやすさやエンゲージメント率を高めることができます。</p>
<p>&nbsp;</p>
<h2>Googleオプティマイズの終了</h2>
<p>A/Bテストツールの導入にあたり、広く使われていたGoogleオプティマイズを利用しようとしましたが、調べてみると2023年9月30日をもって廃止されていました。</p>
<p>Googleがこれらのツールが顧客のニーズに対応できていないと判断したためのようで、より包括的なテスト機能を提供するために、Google Analytics 4に注力することを決定したようです。</p>
<p>そのため、別のA/Bテストツールを探し、今回はVWOのA/Bテストツールを導入しました。理由としては、無料で利用できること、使いやすいUIであること、A/Bテストの他にヒートマップやパーソナライゼーションなど利用できるため、選定しました。</p>
<p>&nbsp;</p>
<h2>VWOのA/Bテストツールを導入する</h2>
<p>実際に導入してみます。まずは以下からVWOのアカウントを作成します。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/static.wingify.com/gcp/uploads/sites/3/2022/06/Feature-image_How-To-Use-Hicks-Law-For-CRO-Success-1.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://vwo.com/blog/launching-a-free-plan-of-vwo-testing-a-better-google-optimize/">Launching a Free Plan for VWO Testing, a Better Google Optimize | VWO</a>
			</p>
							<div class="ys-blog-card__dscr">
					VWO offers a free plan for VWO Testing t&hellip;				</div>
										<div class="ys-blog-card__domain">vwo.com</div>
					</div>
	</div>
</div>

<p>ヘッダーの「Start Free Trial」からアカウント作成していきます。</p>
<p><img data-attachment-id="2528" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?fit=1400%2C752&amp;ssl=1" data-orig-size="1400,752" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?fit=300%2C161&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?fit=800%2C430&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?resize=300%2C161&#038;ssl=1" alt="" width="300" height="161" class="alignnone size-medium wp-image-2528" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?resize=300%2C161&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?resize=1024%2C550&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?resize=768%2C413&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_6403ee3d-39eb-8d6c-bdf3-0f87bacb7eeb.jpg?w=1400&amp;ssl=1 1400w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>メールアドレスを入力して、「StartNow」を押します。メールアドレスに確認用のメールが届いているので、そこのURLにアクセスし、必要な情報を埋めたら登録完了です。</p>
<p><img data-attachment-id="2527" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?fit=1370%2C1044&amp;ssl=1" data-orig-size="1370,1044" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?fit=300%2C229&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?fit=800%2C609&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?resize=300%2C229&#038;ssl=1" alt="" width="300" height="229" class="alignnone size-medium wp-image-2527" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?resize=300%2C229&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?resize=1024%2C780&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?resize=768%2C585&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_1852ab4e-4a16-79d2-2c3e-067a1c571c0b.jpg?w=1370&amp;ssl=1 1370w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>アカウント登録が完了したら、自分のサイトにVWOのスクリプトを導入します。WordpressなどのCMSを利用している場合は、プラグインなどで導入できるみたいです。</p>
<p>今回はNuxt3のアプリケーションなので、HTMLコードを&lt;head&gt;の中に追加しました。正しく追加されると、緑枠のようにdetectedになります。</p>
<p><img data-attachment-id="2526" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?fit=1400%2C454&amp;ssl=1" data-orig-size="1400,454" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?fit=300%2C97&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?fit=800%2C259&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?resize=300%2C97&#038;ssl=1" alt="" width="300" height="97" class="alignnone size-medium wp-image-2526" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?resize=300%2C97&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?resize=1024%2C332&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?resize=768%2C249&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_d60b269d-2af4-2298-0d44-be0efba78dac.jpg?w=1400&amp;ssl=1 1400w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>ダッシュボードからA/B Testsを選択します。右上の「Create」から新しいキャンペーンを作成します。</p>
<p><img data-attachment-id="2525" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?fit=1400%2C269&amp;ssl=1" data-orig-size="1400,269" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?fit=300%2C58&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?fit=800%2C154&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?resize=300%2C58&#038;ssl=1" alt="" width="300" height="58" class="alignnone size-medium wp-image-2525" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?resize=300%2C58&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?resize=1024%2C197&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?resize=768%2C148&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_f2f6bc1f-a9ba-b613-427c-89cef350ab61.jpg?w=1400&amp;ssl=1 1400w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>PagesからA/BテストしたいURLを入力します。URLのマッチパターンもいくつかあったり、除外したいページなども設定できます。</p>
<p><img data-attachment-id="2524" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?fit=1400%2C596&amp;ssl=1" data-orig-size="1400,596" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?fit=300%2C128&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?fit=800%2C341&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?resize=300%2C128&#038;ssl=1" alt="" width="300" height="128" class="alignnone size-medium wp-image-2524" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?resize=300%2C128&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?resize=1024%2C436&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?resize=768%2C327&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_eda91b75-8d27-6c0e-e968-7fe3bf290c5e.jpg?w=1400&amp;ssl=1 1400w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>Variationタブでテストパターンを追加できます。また、Traffic spritでどのパターンにどのくらいの割合を流入させたいかの設定ができます。</p>
<p>テストパターンはEditから設定できます。GUIでテストパターンが作れるので、ノーコードでテスト設定ができます。</p>
<p><img data-attachment-id="2523" data-permalink="https://mintaku-blog.net/nuxt-vwo/https___qiita-image-store-s3-ap-northeast-1-amazonaws-com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?fit=1400%2C591&amp;ssl=1" data-orig-size="1400,591" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?fit=300%2C127&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?fit=800%2C338&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?resize=300%2C127&#038;ssl=1" alt="" width="300" height="127" class="alignnone size-medium wp-image-2523" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?resize=300%2C127&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?resize=1024%2C432&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?resize=768%2C324&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2024/08/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_242816_822ea8bf-eb03-689c-b9a6-5331d6f16187.jpg?w=1400&amp;ssl=1 1400w" sizes="(max-width: 300px) 100vw, 300px" data-recalc-dims="1" /></p>
<p>他にもMetricsでどの指標を基準にテストの成功を評価するかを指定したり、Audience and Trafficでどのユーザーにテストを表示するかや、トラフィックの何%にテストを適用するかを設定することができます。</p>
<p>最後に「Start test」のボタンを押したら、A/Bテストが開始されます。</p>
<h2>アプリケーションがSSGの場合</h2>
<p>ちなみにアプリケーションのレンダリング方法がSSGの場合、A/Bテスト導入したのにテストパターンが反映されないなど、うまくいかない場合があります。</p>
<p>SSGの場合事前にビルドして静的ファイルとして生成しているので、サーバーサイドでデータを書き換えることができないかと思います。</p>
<p>その場合は、plugins配下にvwo.client.tsのようなファイルを作成し、クライアントサイドでスクリプトを実行することになるかなと思います。</p>
<p>クライアントサイドで実行するので、オリジナルではないテストパターンの時は値やUIの更新などに少しラグが生じますが、一応導入することは可能です。</p>
<p>&nbsp;</p>
<h2>おわりに</h2>
<p>A/Bテストツールの導入で、特に、直感に頼るだけでは得られない具体的なデータに基づく改善が可能になりました。</p>
<p>VWOのA/Bテストの設定は直感的でわかりやすく、今のところすごく使いやすいと感じています。</p>
<p>これからもA/Bテストを活用して、データに基づく継続的な改善を行い、ユーザーにとって快適で使いやすいサイト作りを目指していきたいです。</p>The post <a href="https://mintaku-blog.net/nuxt-vwo/">Nuxt.jsのアプリケーションにVWOのA/Bテストツールを導入する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt-vwo/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2522</post-id>	</item>
		<item>
		<title>実行環境によってFirebase環境の接続を切り替える</title>
		<link>https://mintaku-blog.net/firebase-dev/</link>
					<comments>https://mintaku-blog.net/firebase-dev/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 06 Dec 2023 04:26:18 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Firebase]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2434</guid>

					<description><![CDATA[<p>今更ながらFirebaseの開発環境を用意して、実行環境によってアプリケーションとの接続を切り替えるようにしたので、メモ代わりに書きました。 開発環境のFi …</p>
The post <a href="https://mintaku-blog.net/firebase-dev/">実行環境によってFirebase環境の接続を切り替える</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>今更ながらFirebaseの開発環境を用意して、実行環境によってアプリケーションとの接続を切り替えるようにしたので、メモ代わりに書きました。</p>
<p>開発環境のFirebaseを構築し、アプリケーション側を設定してきます。今回の目的として、ローカルでは開発環境のFirebaseに接続するようにし、本番では本番環境のFirebaseに接続するようにします。</p>
<h2>実行環境によってFirebaseとの接続を切り替えるアプリケーション側の設定</h2>
<p>CSR・SSRどちらでも利用したいものはpublicで、サーバーサイドのみ利用するものはprivateで環境変数をセットします。今回はSSGで、CSRでも利用するためにpublicで設定しています。</p>
<p>ここでは本番環境の値を定義しています。</p>
<p>・nuxt.config.ts</p><pre class="crayon-plain-tag">...

runtimeConfig: {
  public: {
    fbApiKey: 'xxxxxxxxxxxx',
    fbAuthDomain: 'xxxxxxxxxxxx',
    fbProjectId: 'xxxxxxxxxxxx',
    fbStorageBucket: 'xxxxxxxxxxxx',
    fbMessagingSenderId: 'xxxxxxxxxxxx',
    fbAppId: 'xxxxxxxxxxxx',
    fbMesurementId: 'xxxxxxxxxxxx'
  }
},

...</pre><p>&nbsp;</p>
<p>ローカル用の環境変数を用意して、そこに開発環境用のFirebase接続情報を定義しています。</p>
<p>NUXT_という接頭辞をつけると、ランタイム構成の環境変数を上書きできます。今回はpublic配下の環境変数を上書きするのNUXT_PUBLIC_の接頭辞をつけています。</p>
<p>・.env.local</p><pre class="crayon-plain-tag">NUXT_PUBLIC_FB_API_KEY=xxxxxxxxxxxx
NUXT_PUBLIC_FB_AUTH_DOMAIN=xxxxxxxxxxxx
NUXT_PUBLIC_FB_PROJECT_ID=xxxxxxxxxxxx
NUXT_PUBLIC_FB_STORAGE_BUCKET=xxxxxxxxxxxx
NUXT_PUBLIC_FB_MESSAGING_SENDER_ID=xxxxxxxxxxxx
NUXT_PUBLIC_FB_APP_ID=xxxxxxxxxxxx</pre><p>&nbsp;</p>
<p>最後にpackage.jsonの実行コマンドに&#8211;dotenvで.env.localを読み込むようにして完了です。こうすることで、.env.localの値が上書きされて、ローカル起動時にFirebaseの開発環境に接続して開発することができます。</p>
<p>・package.json</p><pre class="crayon-plain-tag">...

"scripts": {
  "dev": "nuxi dev --dotenv .env.local"
},

...</pre><p>参考：</p>
<div class="ys-blog-card__text-link"><a href="https://nuxt.com/docs/guide/going-further/runtime-config" >https://nuxt.com/docs/guide/going-further/runtime-config</a></div>The post <a href="https://mintaku-blog.net/firebase-dev/">実行環境によってFirebase環境の接続を切り替える</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/firebase-dev/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2434</post-id>	</item>
		<item>
		<title>より高度なSEO対策ができるuseSeoMetaを使ってみる</title>
		<link>https://mintaku-blog.net/nuxt-useseometa/</link>
					<comments>https://mintaku-blog.net/nuxt-useseometa/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sat, 07 Oct 2023 07:51:45 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2411</guid>

					<description><![CDATA[<p>Nuxt.jsの公式ドキュメントを見ていると、useSeoMetaというコンポーザブルがあり、気になったので調べて使ってみました。 そもそもuseHeadと …</p>
The post <a href="https://mintaku-blog.net/nuxt-useseometa/">より高度なSEO対策ができるuseSeoMetaを使ってみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Nuxt.jsの公式ドキュメントを見ていると、useSeoMetaというコンポーザブルがあり、気になったので調べて使ってみました。</p>
<h2>そもそもuseHeadとuseSeoMetaでメタタグ生成するのに違いがあるの？</h2>
<p>メタタグの生成時にはuseHeadを使っているのですが、そもそもuseHeadでメタタグ生成するのとuseSeoMetaで生成するのでは何か違いがあるのか分からず、調べてみました。</p>
<p>結論としては、どちらもSEOに関連するメタタグを生成できますが、useSeoMetaの方がより高度なSEO対策ができるみたいです。</p>
<ul>
<li>useHead：Nuxt の標準的なヘッドコンポーネントでメタタグ、リンクタグ、スタイルタグなどを生成できる</li>
<li>useSeoMeta：SEOに関連するメタタグを生成するための専用コンポーネントでuseHeadよりも多くの機能を提供できる</li>
</ul>
<div class="ys-blog-card__text-link"><a href="https://nuxt.com/docs/api/composables/use-seo-meta" >https://nuxt.com/docs/api/composables/use-seo-meta</a></div>
<p>&nbsp;</p>
<h2>useServerSeoMetaとuseSeoMetaの違いは？</h2>
<p>もう少し調べてみると、useServerSeoMetaというのもあり、useSeoMetaの違いを調べてみました。</p>
<p>useServerSeoMeta は、SSRで使用することを想定されています。サーバー側でメタタグを生成するため、パフォーマンスへの影響が少なく、カスタマイズが容易です。</p>
<p>useSeoMeta は、SPAで使用することを想定されています。クライアント側でメタタグを生成するため、パフォーマンスへの影響があるものの、カスタマイズが難しいというデメリットがあります。</p>
<ul>
<li>useServerSeoMeta：サーバー側でメタタグを生成する。つまり、ページのHTML が生成されるタイミングでメタタグが生成される</li>
<li>useSeoMeta：クライアント側でメタタグを生成する。つまり、ユーザーがページを読み込むたびに、メタタグが生成される</li>
</ul>
<div class="ys-blog-card__text-link"><a href="https://nuxt.com/docs/getting-started/seo-meta#useseometa-and-useserverseometa" >https://nuxt.com/docs/getting-started/seo-meta#useseometa-and-useserverseometa</a></div>
<p>&nbsp;</p>
<h2>useServerSeoMetaを使ってみる</h2>
<p>使い方はuseHeadで使っていたときみたいに、useServerSeoMetaの中に定義されているプロパティに対する値を入れてあげるだけです。</p><pre class="crayon-plain-tag">&lt;script setup lang="ts"&gt;

...

  useServerSeoMeta({
    title: 'test_title',
    description: 'test_description'
  })

...

&lt;/script&gt;</pre><p>パラメータは100以上あり、詳しくはこちらを参照してください。</p>
<div class="ys-blog-card__text-link"><a href="https://github.com/harlan-zw/zhead/blob/main/src/metaFlat.ts" >https://github.com/harlan-zw/zhead/blob/main/src/metaFlat.ts</a></div>
<p>&nbsp;</p>
<h2>SSGでuseServerSeoMetaを使うと</h2>
<p>SSGでどうなるか使ってみたのですが、発見があったのでまとめておきます。</p>
<p>useFetchしてコンテンツを取得して生成するページに関してはuseServerSeoMetaでメタタグ生成されましたが、サービス紹介ページのようなHTMLベタばりのページをSSGした場合は、useServerSeoMetaでメタタグ生成されないことがわかりました。</p>
<p>てっきりSSGの処理的には全ページをサーバー側で静的ページにビルドしていると思っていました。これはSSGでgenerateしてる時に、元々静的なページに関してはサーバー側で呼び出されてないのか分からないのですが、挙動が違ったことがわかりました。</p>
<p>useFetchしない静的ページに関してはuseSeoMetaを使うなどの使い分けが必要なのでしょうか。この辺り詳しい人に裏側でどうなっているのか聞いてみたいです。</p>
<p>&nbsp;</p>The post <a href="https://mintaku-blog.net/nuxt-useseometa/">より高度なSEO対策ができるuseSeoMetaを使ってみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt-useseometa/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2411</post-id>	</item>
		<item>
		<title>改めて運営しているWebサービスのアーキテクチャを整理する</title>
		<link>https://mintaku-blog.net/architecture-2nd/</link>
					<comments>https://mintaku-blog.net/architecture-2nd/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Fri, 19 May 2023 11:35:02 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[アーキテクチャ]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2370</guid>

					<description><![CDATA[<p>最近、NetlifyからFirebase Hostingに移行したり、認証機能を追加実装したりとアーキテクチャがちょっと変わってきたので、改めて整理したいと …</p>
The post <a href="https://mintaku-blog.net/architecture-2nd/">改めて運営しているWebサービスのアーキテクチャを整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>最近、NetlifyからFirebase Hostingに移行したり、認証機能を追加実装したりとアーキテクチャがちょっと変わってきたので、改めて整理したいと思います。</p>
<h2>今のざっくりアーキテクチャ図</h2>
<p><img data-attachment-id="2373" data-permalink="https://mintaku-blog.net/architecture-2nd/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-05-19-11-31-10/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=1418%2C612&amp;ssl=1" data-orig-size="1418,612" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="スクリーンショット 2023-05-19 11.31.10" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=300%2C129&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?fit=800%2C345&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=800%2C345&#038;ssl=1" alt="" width="800" height="345" class="aligncenter size-large wp-image-2373" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=1024%2C442&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=300%2C129&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?resize=768%2C331&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2936e99a19fcbf591587759c99c5011a.jpg?w=1418&amp;ssl=1 1418w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>こちらの記事で書いてありますが、元々Netlifyを使用していましたが、Firebase Hostingに移行しました。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img width="560" height="315" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?fit=560%2C315&amp;ssl=1" class="attachment-large size-large wp-post-image" alt="" loading="lazy" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?w=560&amp;ssl=1 560w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?resize=300%2C169&amp;ssl=1 300w" sizes="(max-width: 560px) 100vw, 560px" data-attachment-id="2363" data-permalink="https://mintaku-blog.net/netlify-to-firebase/help-raise-heart-disease-awareness%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc%e3%81%ae%e3%82%b3%e3%83%92%e3%82%9a%e3%83%bc-9-5/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?fit=560%2C315&amp;ssl=1" data-orig-size="560,315" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Help raise Heart Disease Awarenessのコピーのコピーのコピー (9)" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?fit=300%2C169&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/04/afbf9bcde7cb172c4c5e6bc2b6e660b9.png?fit=560%2C315&amp;ssl=1" />			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://mintaku-blog.net/netlify-to-firebase/">NetlifyからFirebase Hostingに移行した話</a>
			</p>
							<div class="ys-blog-card__dscr">
					満を辞して？NetlifyからFirebase Hostingに移行したので、メ&hellip;				</div>
										<div class="ys-blog-card__domain">mintaku-blog.net</div>
					</div>
	</div>
</div>

<p>基本SSGの構成は変わっておらず、後で説明しますが、一部SPA的なページが追加されました。</p>
<p>流れとしてはmainブランチにプッシュされるとGithub Actionsのパイプラインが動き出し、ビルドが始まります。</p>
<p>ビルド時はContenfulからデータを取得して、静的なページを生成していきます。ページの生成が完了すると、Firebase Hostingにデプロイします。</p>
<p>ページ数は今のところ大体3300ページくらいあり、ビルドしてデプロイするまで20分近くかかっており、長すぎるビルド時間が直近の課題となっています。</p>
<p>今考えているアプローチ方法は、ISGの導入や最近Nuxt3機能追加されたpayloadを使うなど、サーバー通信を減らしていきたいと思っています。</p>
<p>&nbsp;</p>
<h2>認証機能のところ</h2>
<p><img data-attachment-id="2374" data-permalink="https://mintaku-blog.net/architecture-2nd/%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2023-05-19-19-38-33/" data-orig-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=1366%2C910&amp;ssl=1" data-orig-size="1366,910" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="スクリーンショット 2023-05-19 19.38.33" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=300%2C200&amp;ssl=1" data-large-file="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?fit=800%2C533&amp;ssl=1" loading="lazy" src="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=800%2C533&#038;ssl=1" alt="" width="800" height="533" class="aligncenter size-large wp-image-2374" srcset="https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=1024%2C682&amp;ssl=1 1024w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/mintaku-blog.net/mintaku/wp-content/uploads/2023/05/2801c7ada334840d3ce2e425ae897010.jpg?w=1366&amp;ssl=1 1366w" sizes="(max-width: 800px) 100vw, 800px" data-recalc-dims="1" /></p>
<p>今回新しく追加した認証機能はFirebase Authenticationを使いました。Firebaseに統合したいのもあって、ホスティングサービスもNetlifyからFirebase Hostingに移行した感じです。</p>
<p>1stリリースでは、認証のプロバイダはGoogleとTwitterのみにしています。後々、メールアドレスや他のSNS認証も追加する予定です。</p>
<p>ユーザー情報などの格納データはFirestoreを使っています。Firebase Authenticationとあわせて開発しやすかったです。</p>
<p>&nbsp;</p>
<h2>今後のアーキテクチャについて</h2>
<p>今後の開発において、ますます動的な機能が増えていくと思います。今はサーバーレスのSSGで開発していますが、後々はAPIを立てたり、SSRに変更するなど考えています。</p>
<p>新しい技術なども出てきたりすると思うので、臨機応変にその時その時考えて、アーキテクチャや使用技術を決めていきたいと思います。</p>
<p>また、負債を残さないためにもdependabotの導入によるライブラリのバージョンアップやこまめなリファクタリングなど、開発生産性を高く保ちながらより良いサービスを提供していきたいです。</p>The post <a href="https://mintaku-blog.net/architecture-2nd/">改めて運営しているWebサービスのアーキテクチャを整理する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/architecture-2nd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2370</post-id>	</item>
		<item>
		<title>Nuxt3で何となく使っているところをちゃんと理解する</title>
		<link>https://mintaku-blog.net/nuxt3-understand/</link>
					<comments>https://mintaku-blog.net/nuxt3-understand/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 26 Mar 2023 06:46:38 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2348</guid>

					<description><![CDATA[<p>プライベートでNuxt3を使って開発していますが、何となく使っているところがあるのでちゃんと理解するために調べたり動かしたりして理解を深めました。 nuxt …</p>
The post <a href="https://mintaku-blog.net/nuxt3-understand/">Nuxt3で何となく使っているところをちゃんと理解する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>プライベートでNuxt3を使って開発していますが、何となく使っているところがあるのでちゃんと理解するために調べたり動かしたりして理解を深めました。</p>
<h2>nuxt-linkの挙動について</h2>
<p>何となく内部のリンクはnuxt-linkを使っていましたが、改めて挙動について深掘りしてみました。</p>
<p>NuxtのSSRでは初回読み込み時はサーバー側でレンダリングされて返却されます。aタグで遷移した際もサーバー側でレンダリングされますが、nuxt-linkを使った場合は、APIからのデータフェッチなどサーバー側で実行されずにクライアントで実行されています。</p>
<p>そのため、nuxt-linkを使って遷移した先のページは同期的に生成されておらず、変数などちょっと遅れてシュッと入ってくる感じになっています。開発者ツールでnetworkタブを見てみてもnuxt-linkで遷移した時はクライアントサイドでAPIが呼び出されていることが確認できます。</p>
<p>まあたしかに内部リンクへの遷移は特にSEOを気にすることもないので、SSRしてサーバー側への負荷をかける必要がないのでしょう。</p>
<p><span>ちなみに調べてみるとnuxt-linkでto propsに外部リンクを指定することもでき、外部リンクの場合は通常のaタグのように動作するそうです。</span></p>
<p><span>そのため、特に内部リンクと外部リンクでタグを使い分ける必要はないみたいですね。</span></p>
<h3>疑問と仮説</h3>
<p>ただSSGでやっているとnuxt-linkで遷移すると一瞬画面が表示されてそのあと画面が真っ白になりました。</p>
<p>これは遷移後にCSRでAPIコールしており、Netlifyをホスティングサービスとして使っているとサーバーを用意していないためにエラーになっていると思われます。</p>
<p>ローカルでは問題ないのですが、ローカルはSSRモードで動いているからだと思われます。</p>
<p>aタグだと問題ないため、あくまでも仮説ですが、SSGの場合は<span>useFetchをサーバーのみにするかuseAsyncDataにキャッシュキーを指定してあげれば再度呼ばれることもなく正常に動作するのかもしれないと思いました。</span></p>
<h2><code>追記</code></h2>
<p>そもそもSSGでserverディレクトリを使うこと自体想定されていないのかもしれないです。</p>
<p>Webサーバー用意してないのにserverディレクトリ使ってAPIコールする構成自体を見直すべきかもと思いました。</p>
<h2>pluginとmiddleware</h2>
<p>pluguinとmiddlewareの挙動の違いとかも何となく使い分けていたので、改めて調べてみました。</p>
<p>pluguinはアプリケーションの初期化時に実行してくれるもので、アプリケーション全体で利用するものはここでまとめて定義します。ちなみにファイル名の前に数字を付けることで、pluginが登録される順序を制御できます。</p>
<p>middlewareはページ遷移にあたって行う共通処理を記述することができます。middlewareを利用することでページ間の移動、サイトへのアクセス時にページを表示する前に事前に設定していた処理を行うことができます。</p>
<p>どちらもデフォルトではサーバーサイド、クライアントサイド双方で実行されます。ファイル名にclientとかserverとかつけるとそれぞれのみで実行されます。</p>
<p>改めて調べるとそれぞれの挙動を理解できて、ちゃんと使い分けができそうです。</p>
<p>&nbsp;</p>
<div class="ys-blog-card__text-link"><a href="https://nuxt.com/docs/guide/directory-structure/plugins" >https://nuxt.com/docs/guide/directory-structure/plugins</a></div>
<div class="ys-blog-card__text-link"><a href="https://nuxt.com/docs/guide/directory-structure/middleware" >https://nuxt.com/docs/guide/directory-structure/middleware</a></div>
<h2>useFetchとuseAsyncData</h2>
<p>どちらを使えばよいか適切なタイミングがいまいちよくわかってないので調べてみました。</p>
<p>結論から言うとuseFetchは、$fetchを使ったデータ取得に特化した useAsyncDataのラッパー関数なので、基本的にデータを取得するだかならuseFetchを使えば良さそうです。</p>
<p>ここでいう$fetchとはofetchライブラリのことで、これは様々な環境で使用できるFetchAPIです。axiosのようなものみたいです。</p>
<p>useAsyncDataはどうやって使うかというと、以下のように引数を2つ取ります。</p><pre class="crayon-plain-tag">const { data, pending, error, refresh } = await useAsyncData(
  'test',
  () =&gt; $fetch('https://example.com/test')
)</pre><p>第1引数はキーを指定し、内部でキャッシュを保持しています。それによって2回目以降はAPIにリクエストをすることなく、前と同じ結果を返してくれます。このキャッシュはNuxt内部で保持するため、キャッシュを区別するための一意なキーが必要となります。</p>
<p>第2引数は非同期処理を行う関数を指定しています。</p>
<p>useAsyncDataの第二引数が() =&gt; $fetchになるのであれば基本的にuseFetchを使用すれば問題なさそうです。</p>The post <a href="https://mintaku-blog.net/nuxt3-understand/">Nuxt3で何となく使っているところをちゃんと理解する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt3-understand/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2348</post-id>	</item>
		<item>
		<title>Nuxt3 + Firebase Authenticationで認証機能を実装する</title>
		<link>https://mintaku-blog.net/nuxt3-firebase/</link>
					<comments>https://mintaku-blog.net/nuxt3-firebase/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 04 Jan 2023 07:53:31 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[Firebase]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2316</guid>

					<description><![CDATA[<p>前提の話 Firebaseの初期化の設定などはここでは省略します。今回はGoogleアカウントでの認証を例にメモ的な感じでまとめています。 実装としてはGo …</p>
The post <a href="https://mintaku-blog.net/nuxt3-firebase/">Nuxt3 + Firebase Authenticationで認証機能を実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<h2>前提の話</h2>
<p>Firebaseの初期化の設定などはここでは省略します。今回はGoogleアカウントでの認証を例にメモ的な感じでまとめています。</p>
<p>実装としてはGoogleアカウントで認証し、登録情報をFirestoreに登録するといった流れです。</p>
<p>NuxtとFirebaseのバージョンは以下の通りです。</p>
<ul>
<li>Nuxt: 3.0.0</li>
<li>Firebase: 9.15.0</li>
</ul>
<p>&nbsp;</p>
<h2>Googleアカウントで認証する</h2>
<p>Googleアカウントで登録する流れを実装していきます。</p>
<p>まずは登録するための画面をcomponentsに作成します。このコンポーネントはpagesで呼び出すイメージです。</p>
<p>「Googleで登録」のボタンを押下するとgoogleSignUpメソッドが呼び出されます。ここでのuseAuth()とgoogleSignUpメソッドは後ほど実装していきます。</p>
<p>・components/SignUpForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div class="signup-form"&gt;
    &lt;p class="title"&gt;SNSアカウントで登録&lt;/p&gt;
    &lt;ul class="signup-buttons"&gt;
      &lt;li&gt;&lt;button class="google-button" @click="googleSignUp"&gt;&lt;i&gt;&lt;GoogleLogo&gt;&lt;/GoogleLogo&gt;&lt;/i&gt;&lt;span&gt;Googleで登録&lt;/span&gt;&lt;/button&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;
 
&lt;script setup lang="ts"&gt;
  import GoogleLogo from '@/assets/svg/icons/google-logo.svg'
  const googleSignUp = () =&gt; {
    const { googleSignUp } = useAuth()
    googleSignUp()
  }
&lt;/script&gt;</pre><p>&nbsp;</p>
<p>「Googleで登録」のボタンを押下したgoogleSignUpメソッドがここで呼ばれます。GoogleAuthProviderのインスタンスを作成し、signInWithPopupを呼び出すことで、Google認証にポップアップが表示されます。</p>
<p>Google認証が完了したら、そのデータを元にgetUserメソッドでFirestoreのデータを取得しにいきます。</p>
<p>既にFirestoreにデータがある場合は、認証完了済みなのでログイン画面に遷移させます。データがない場合は新規登録なので、新しくFirestoreにuserデータを登録し、mypage画面に遷移させます。</p>
<p>最後にupdateUserメソッドを呼び出し、useState() を使ってuserの状態管理をしています。alertのところはいい感じのポップアップかトーストを実装したいところです。</p>
<p>・composables/useAuth.ts</p><pre class="crayon-plain-tag">import {
  Auth,
  User,
  UserCredential,
  onAuthStateChanged,
  signInWithPopup,
  GoogleAuthProvider,
} from "firebase/auth";
import { computed, ref } from "vue";
import {
  collection,
  where,
  query,
  getDocs,
  addDoc
} from '@firebase/firestore';
 
export function useAuth() {
  const { $auth, $firestore } = useNuxtApp()
  const user = ref&lt;User | null&gt;($auth.currentUser);
  const isAuthed = computed(() =&gt; !!user.value);
  const db = $firestore;
 
  $auth.onIdTokenChanged((authUser) =&gt; (user.value = authUser));
 
  // ユーザー情報取得
  const getUser = async (uid: string): Promise&lt;any&gt; =&gt; {
    const q = query(
      collection(db, 'users'),
      where("uid", "==", uid)
    );
    const querySnapshot = await getDocs(q);
 
    return querySnapshot.docs[0]
  };
 
  // ユーザー作成
  const createUser = async (user: UserCredential) =&gt; {
    await addDoc(collection(db, 'users'), {
      uid: user.user.uid,
      name: user.user.displayName,
      email: user.user.email,
      photo: user.user.photoURL
    })
  };
 
  // Google新規登録
  async function googleSignUp() {
    try {
      const provider = new GoogleAuthProvider();
      const googleUser = await signInWithPopup($auth, provider);
      const user = await getUser(googleUser.user.uid)
      const { updateUser } = await useUser()
      if (user) {
        alert("既にユーザー登録されています。")
        updateUser(user.data())
        navigateTo("/login", { replace: true })
      } else {
        alert("新規登録完了しました")
        await createUser(googleUser)
        const user = await getUser(googleUser.user.uid)
        updateUser(user.data())
        navigateTo("/mypage", { replace: true })
      }
    } catch (error) {
      throw error;
    }
  }
 
  async function currentUser() {
    return $auth.currentUser
  }
 
  return { isAuthed, user, checkAuthState, googleSignUp, currentUser };
}</pre><p>&nbsp;</p>
<p>updateUserメソッドを使ってuserのstateの状態を管理しています。</p>
<p>認証後はuserのstateにUser情報を入れ、ログアウトなどの認証情報が破棄された場合はnullを入れます。</p>
<p>・composables/useUser.ts</p><pre class="crayon-plain-tag">import { User } from "firebase/auth"
import { Ref } from "vue"

export const useUser = () =&gt; {
  const user: Ref&lt;User | null&gt; = useState&lt;User | null&gt;('user')

  const updateUser = (user: Ref&lt;User | null&gt;) =&gt; (value: User | null) =&gt; {
    user.value = value
  }

  return {
    user: readonly(user),
    updateUser: updateUser(user)
  }
}</pre><p>&nbsp;</p>
<h2>認証状態のチェック</h2>
<p>認証後のマイページに認証されていない場合はアクセスできないようにしたい場合、認証状態をチェックするなどの処理が必要になります。</p>
<p>その場合はmiddlewareで認証状態をチェックし、認証されていない場合はログインページに遷移させるなどの処理を実装します。</p>
<p>definePageMetaで指定してあげることで、このmiddlewareの処理が実行されます。</p>
<p>また、ログイン済みの場合にログインページを表示する場合は、マイページに遷移させるなどの処理も必要になるかと思います。</p>
<p>・middleware/auth.global.ts</p><pre class="crayon-plain-tag">export default defineNuxtRouteMiddleware(async () =&gt; {
  if (!process.server) {
    const { isAuthed, checkAuthState } = useAuth();
    await checkAuthState();

    if (!isAuthed.value) {
      window.location.href = "/login";
    }
  }
});</pre><p>&nbsp;</p>
<p>全てのルートで実行したい処理がある場合は、Nuxt3のグローバルミドルウェアを使うことで実現できます。</p>
<p>.globalが付いたファイルは呼び出し処理を書かずに全てのルートで実行されます。</p>
<p>・middleware/auth-check.global.ts</p><pre class="crayon-plain-tag">export default defineNuxtRouteMiddleware(async () =&gt; {
  if (!process.server) {
    const { checkAuthState } = useAuth();
    await checkAuthState();
  }
})</pre><p></p>The post <a href="https://mintaku-blog.net/nuxt3-firebase/">Nuxt3 + Firebase Authenticationで認証機能を実装する</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt3-firebase/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2316</post-id>	</item>
		<item>
		<title>Nuxt3への移行作業時に気になったところをまとめてみた</title>
		<link>https://mintaku-blog.net/nuxt3-rc/</link>
					<comments>https://mintaku-blog.net/nuxt3-rc/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Wed, 12 Oct 2022 02:55:21 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2272</guid>

					<description><![CDATA[<p>Nuxt2で開発したWebサービスをNuxt3 RCに移行作業を行なっているので、その際に気になった機能などをまとめておきます。 公式ページ：https:/ …</p>
The post <a href="https://mintaku-blog.net/nuxt3-rc/">Nuxt3への移行作業時に気になったところをまとめてみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>Nuxt2で開発したWebサービスをNuxt3 RCに移行作業を行なっているので、その際に気になった機能などをまとめておきます。</p>
<p>公式ページ：https://v3.nuxtjs.org/</p>
<h2>Nuxt2との違い</h2>
<p>Nuxt3は、Vue3をベースになっています。Nuxt2との違いとしてSSRの高速化やバンドルサイズの縮小などのパフォーマンスの向上、Composition APIの導入、TypeScript サポートなどが挙げられます。</p>
<p>&nbsp;</p>
<h2>v3.0.0-rc.10でFull SSGモードに</h2>
<p>rc.3で開発していたのですが、SSGしてもクライアント側でデータフェッチしている挙動があり、どうにかならないかと悩んでいました。</p>
<p>Nuxt3のRC情報を見ていたら、SSG後にクライアント側で再フェッチしないFull SSGモードなるものがリリースされており、バージョンを上げたところ無事解決しました。</p>
<div class="ys-blog-card__text-link"><a href="https://github.com/nuxt/framework/discussions/7513" >https://github.com/nuxt/framework/discussions/7513</a></div>
<p>&nbsp;</p>
<h2>Nuxt3のコアパッケージ</h2>
<p>Nuxt3を構成するコアパッケージは以下のようになっています。</p>
<ul>
<li>コアエンジン: nuxt</li>
<li>バンドラー: @nuxt/vite-builder and @nuxt/webpack-builder</li>
<li>コマンドライン: nuxi</li>
<li>サーバーエンジン: nitro</li>
<li>開発キットt: @nuxt/kit</li>
<li>Nuxt 2ブリッジ: @nuxt/bridge</li>
</ul>
<h3>nuxt/bridge</h3>
<p>既存のNuxt 2プロジェクトからNuxt3の機能を使いたい場合はNuxt Bridge を使用するのが良さそうです。変更を最小限に抑えながら、ほとんどの新機能を試すことができます。</p>
<h2>nitro</h2>
<p>nitro(読み方はニトロじゃなくてナイトロ)の基盤は、rollupとh3で、高いパフォーマンスと移植性のために構築された最小限のhttpフレームワークです。特徴は以下の通りです。</p>
<h3>クロスプラットフォームサポート</h3>
<p>様々なプラットフォームにも対応しているエンジンで、JavaScript をサポートする様々なシステムでデプロイできます。</p>
<h3>サーバーレスサポート</h3>
<p>サーバーレス環境でも動作します。</p>
<h3>API routes</h3>
<p>unjs/h3プロジェクトを使用し、h3という軽量のhttp サーバーが使われています。</p>
<h3>自動コード分割</h3>
<p>設定しなくても自動的にコード分割してくれます。</p>
<h3>ホットリロード</h3>
<p>プログラム実行中にコードを変更できます。</p>
<h3>ハイブリッド・モード</h3>
<p>ページのレンダリングを細かくコントロールし、静的サイトとサーバーレスサイトでハイブリッドモードが使用可能です。</p>
<p>ローカル開発時のホットリロードの速度、CICD 上でのビルド実行速度が従来の WebPack とは段違いに速いと感じます。</p>

<div class="ys-blog-card">
	<div class="ys-blog-card__container">
					<figure class="ys-blog-card__image">
				<img src="https://i0.wp.com/nitro.build/_og/_index.png?w=800&#038;ssl=1" alt="" data-recalc-dims="1">			</figure>
				<div class="ys-blog-card__text">
			<p class="ys-blog-card__title">
				<a class="ys-blog-card__link" href="https://nitro.unjs.io/">Nitro - Next Generation Server Toolkit</a>
			</p>
							<div class="ys-blog-card__dscr">
					Create web servers with everything you n&hellip;				</div>
										<div class="ys-blog-card__domain">nitro.unjs.io</div>
					</div>
	</div>
</div>

<p>&nbsp;</p>
<h2>自動インポート</h2>
<p>Nuxt3では、CompositionAPIのsetupで使用する関数のインポートを追加する必要はなく、エンジンによって自動的にインポートされます。また、ディレクトリとファイル名で自動的にComponentもインポートされます。</p>
<h3>components</h3>
<p>インポートしたい Vue コンポーネントを配置するディレクトリ</p>
<h3>composables</h3>
<p>各コンポーネントを記入しなくても、Composables ディレクトリを使って自動的にインポートできる</p>
<p>&nbsp;</p>
<h2>ネスト化されたコンポーネント</h2>
<p>ネスト化されたディレクトリにコンポーネントがある場合は、パス・ディレクトリのコンポーネントの名前を指定するだけで使用できます。</p>
<p>例えば、components/bar/foo.vue の場合、以下のようになります。</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;BarFoo /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</pre><p>アトミックデザインでディレクトリを分けてファイルを作成する際は&lt;AtomButton&gt;みたいな感じになりそうですね。ちょっと冗長になりそうな気もしています。</p>The post <a href="https://mintaku-blog.net/nuxt3-rc/">Nuxt3への移行作業時に気になったところをまとめてみた</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/nuxt3-rc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2272</post-id>	</item>
		<item>
		<title>Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</title>
		<link>https://mintaku-blog.net/about-nuxt3/</link>
					<comments>https://mintaku-blog.net/about-nuxt3/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 06 Mar 2022 12:46:03 +0000</pubDate>
				<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[TypeScript]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2162</guid>

					<description><![CDATA[<p>TypeScriptでフロント開発するにあたって、TypeScriptファーストなNuxt3を使いNuxt2からどう変わったのかなどを簡単にまとめてみました …</p>
The post <a href="https://mintaku-blog.net/about-nuxt3/">Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>TypeScriptでフロント開発するにあたって、TypeScriptファーストなNuxt3を使いNuxt2からどう変わったのかなどを簡単にまとめてみました。</p>
<p>公式ドキュメント：<a href="https://v3.nuxtjs.org/" target="_blank" rel="noopener">https://v3.nuxtjs.org/</a></p>
<h2>Nuxt3でどう変わった</h2>
<p>Nuxt3になったことで、JSX/TSXやSCSSの対応、APIサーバー作成などの機能がデフォルトで含まれています。Nuxt2もセットアップなしで開発を始められるフレームワークでしたが、TypeScriptやSCSSで書く場合は別途追加する必要がありました。</p>
<p>Nuxt3によってそれらが内包され素早いフロント開発に着手できるようになりました。</p>
<p>※ JSXはJavaScripでHTMLっぽいのが書けるようJavaScriptを拡張したものです。TSXはJSXのTypeScript版です。</p>
<h3>サーバー起動とホットリロードが早い</h3>
<p>Nuxt3は開発時のビルドツールにViteが採用されています。そのため開発時のサーバー起動とHMRが早くなりました。</p>
<p>これは実際に使ってみて早さを実感しており、これだけでもNuxt3に乗り換える価値がありそうです。</p>
<h3>Composition APIの採用</h3>
<p>Composition APIは関数ベースでコンポーネントを実装する機能です。関数ベースであるため機能の再利用性を高める役割を持っています。</p>
<p>Vueでの新しい書き方としてプラグインでVue 2にも先行提供されていたComposition APIはNuxt3では標準採用されています。</p>
<p>従来の記法であるOptions APIと比較してTypeScriptの相性がいいことが挙げられます。 Options APIにTypeScript を導入しても thisやdataなどのoptionsの定義が複雑であるため、型定義が難しかったのですが、Composition APIを使用することで型推論でき開発が効率的になりました。</p>
<h3>状態管理としてuseStateとpiniaが追加</h3>
<p>Vue3ではVuexに代わってuseStateとpiniaが推奨されています。</p>
<p>Nuxt 2では主要機能の1つとして組み込まれていたVuexですが、グローバルな状態管理ストアにアプリが依存するリスクやTypeScript、単体テストとの相性の悪さなどがVuexが推奨されなくなった背景とされています。</p>
<p>&nbsp;</p>
<h2>Nuxt3でプロジェクトを作成する</h2>
<p>Nuxt3 からは create-nuxt-app の代わりにNuxt CLIでNuxtプロジェクトを作成します。</p>
<p>npx create-nuxt-appによる質問がなくなったため、すぐにプロジェクトを作成できます。Nuxt3ではTypeScriptはデフォルトになっており、TypeScriptファーストなフレームワークになりました。</p><pre class="crayon-plain-tag">$ npx nuxi init sample-nuxt3-app</pre><p>Nuxt3では生成されるファイルが.tsで作られていることがわかります。</p><pre class="crayon-plain-tag">.gitignore
README.md
app.vue
nuxt.config.ts
package-lock.json
package.json
tsconfig.json</pre><p></p>
<h3>JSX・TSXを使うなら</h3>
<p>JSX・TSXを使う場合は、tsconfig.jsonにcompilerOptionsを追記することが使用することができるようになります。</p><pre class="crayon-plain-tag">{
  // https://v3.nuxtjs.org/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  + "compilerOptions": {
  +   "jsx": "preserve"
  + }
}</pre><p></p>The post <a href="https://mintaku-blog.net/about-nuxt3/">Nuxt3に乗り換えてみてどう変わったか簡単にまとめる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/about-nuxt3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2162</post-id>	</item>
		<item>
		<title>【Nuxt.js】JestでTDDを実践してみる</title>
		<link>https://mintaku-blog.net/jest-tdd/</link>
					<comments>https://mintaku-blog.net/jest-tdd/#respond</comments>
		
		<dc:creator><![CDATA[みんたく]]></dc:creator>
		<pubDate>Sun, 27 Jun 2021 13:42:53 +0000</pubDate>
				<category><![CDATA[Vue.js]]></category>
		<category><![CDATA[Nuxt.js]]></category>
		<category><![CDATA[TDD]]></category>
		<guid isPermaLink="false">https://mintaku-blog.net/?p=2013</guid>

					<description><![CDATA[<p>JestでTDDを実践しながらNuxt.jsで簡単なToDoリストをつくってみました。 ToDoリストのストーリーを考える Nuxt.jsで簡単なToDoリ …</p>
The post <a href="https://mintaku-blog.net/jest-tdd/">【Nuxt.js】JestでTDDを実践してみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></description>
										<content:encoded><![CDATA[<p>JestでTDDを実践しながらNuxt.jsで簡単なToDoリストをつくってみました。</p>
<h2>ToDoリストのストーリーを考える</h2>
<p>Nuxt.jsで簡単なToDoリストをつくりにあたり、ユーザーストーリーを考えます。考えた結果、以下のストーリーができました。</p>
<ul>
<li>ユーザーはフォームと作成ボタンを見ることができる</li>
<li>ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる</li>
<li>ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる</li>
<li>ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない</li>
<li>ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる</li>
<li>ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる</li>
</ul>
<p>ToDoリストの画面は、TaskFormコンポーネントとTaskListコンポーネントに分けて実装します。</p>
<p>またタスクのデータはFirestoreに格納するようにし、REST APIでデータを操作します。</p>
<p>Nuxt.jsやJestのインストールやFirestoreのセットアップなどは省略しています。</p>
<p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」のテストを書く</h2>
<p>まず「ユーザーはフォームと作成ボタンを見ることができる」のテストを書いていきます。</p>
<p>TaskFormコンポーネントを作成し、タスク入力フォームと作成ボタンが存在することをチェックします。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import { mount } from '@vue/test-utils'
import TaskForm from '@/components/TaskForm.vue'

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm)
  })

  describe('template', () =&gt; {
    test('タスク入力フォームが存在すること', () =&gt; {
      const form = wrapper.find('input');
      expect(form.exists()).toBe(true);
    })

    test('タスク作成ボタンが存在すること', () =&gt; {
      const button = wrapper.find('input');
      expect(button.exists()).toBe(true);
   })
  })
})</pre><p>テストを作成したら実際にテストを実行してみます。TDDでいうレッドのところです。</p>
<p>まだ実装はしていないので全部のテストが失敗しますが、期待通りの落ち方をしているかを確認します。</p><pre class="crayon-plain-tag">FAIL  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✕ タスク入力フォームが存在すること (10 ms)
      ✕ タスク作成ボタンが存在すること (1 ms)

  ● components/TaskForm.vue &gt; template &gt; タスク入力フォームが存在すること

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      16 |     test('タスク入力フォームが存在すること', () =&gt; {
      17 |       const form = wrapper.find('input');
    &gt; 18 |       expect(form.exists()).toBe(true);
         |                             ^
      19 |     })
      20 |
      21 |     test('タスク作成ボタンが存在すること', () =&gt; {

      at Object.&lt;anonymous&gt; (components/TaskForm.spec.js:18:29)

  ● components/TaskForm.vue &gt; template &gt; タスク作成ボタンが存在すること

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      21 |     test('タスク作成ボタンが存在すること', () =&gt; {
      22 |       const button = wrapper.find('input');
    &gt; 23 |       expect(button.exists()).toBe(true);
         |                               ^
      24 |     })
      25 |
      26 |   })

      at Object.&lt;anonymous&gt; (components/TaskForm.spec.js:23:31)</pre><p>存在していない、実装されていないなどの落ち方をしていればひとまず完了です。</p>
<p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」を実装する</h2>
<p>テストで書いたシナリオを元にTaskFormコンポーネントを実装してきます。今回は簡易的なHTMLでフォームとボタンを作成しました。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input&gt;
    &lt;button&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームと作成ボタンを見ることができる」をテストする</h2>
<p>実装が終わったら、再度TaskFormのテストを流してみます。TDDでいうグリーンのところです。</p>
<p>テストが全て通ったらこのストーリーの実装は完了です。必要に応じてリファクタリングをします。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (9 ms)
      ✓ タスク作成ボタンが存在すること (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書く</h2>
<p>次に「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書いていきます。</p>
<p>TaskListコンポーネントでToDoリストと完了リストを表示するようにします。</p>
<p>そのため、ToDoリストと完了リストにFirestoreに格納されているデータをfetchしてくるようにします。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue} from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
    test('todoリストが表示されること', () =&gt; {
      const li = wrapper.find('li.todo')
      expect(li.find('span').text()).toBe(todoTask.content)
      expect(li.find('button).text()).toBe('完了')
    })

    test('完了リストが表示されること', () =&gt; {
      const li = wrapper.find('li.done')
      expect(li.find('span').text()).toBe(doneTask.content)
      expect(li.find('button).text()).toBe('ToDoに戻す')
    })
  })

  describe('script', () =&gt; {
    describe('computed', () =&gt; {
      describe('todos', () =&gt; {
        test('storeからtodoTasksが取得できること', () =&gt; {
          expect(wrapper.vm.todoTasks).toEqual(expect.arrayContaining([todoTask]))
        })
        test('storeからdoneTasksが取得できること', () =&gt; {
          expect(wrapper.vm.doneTasks).toEqual(expect.arrayContaining([doneTask]))
        })
      })
    })
  })
})</pre><p>タスクリストの状態管理をするストアのテストを書きます。</p>
<p>ステータスの状態によってToDoタスクと完了タスクを出し分けるようにします。</p>
<p>Storeではaxiosを使って外部サービスにHTTPリクエストしているためaxiosをモック化してテストしています。</p>
<p>async/awaitでaxiosの非同期処理を実現し、結果をgettersから取り出し比較します。</p>
<p>参考：https://github.com/nuxt-community/axios-module/issues/105</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  get: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {
  let store
  let todoTask, doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(_.cloneDeep(index))
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
  })

  describe('getters', () =&gt; {
    let tasks
    let todoTasks
    let doneTasks
    beforeEach(() =&gt; {
      tasks = [todoTask, doneTask]
      todoTasks = [todoTask]
      doneTasks = [doneTask]
      store.replaceState({
        tasks,
        todoTasks,
        doneTasks
      })
    })

    describe('todoTasks', () =&gt; {
      test('statusがtodoのtaskが取得できること', () =&gt; {
        expect(store.getters['todoTasks']).toContainEqual(todoTask)
        expect(store.getters['todoTasks']).not.toContainEqual(doneTask)
      })
    })
    describe('doneTasks', () =&gt; {
      test('statusがdoneのtaskが取得できること', () =&gt; {
        expect(store.getters['doneTasks']).not.toContainEqual(todoTask)
        expect(store.getters['doneTasks']).toContainEqual(doneTask)
      })
    })
    describe('tasks', () =&gt; {
      test('すべてのtasksが取得できること', () =&gt; {
        expect(store.getters['tasks']).toEqual(
          expect.arrayContaining(tasks)
        )
      })
    })
  })

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

    describe('fetchTasks', () =&gt; {
      test('tasksが取得できること', async () =&gt; {
        action = 'fetchTasks'
        mockAxiosGetResult = {
          data: {
            documents: [
              {
                name: `tasks/${todoTask.id}`,
                fields: {
                  content: { stringValue: todoTask.content },
                  status: { stringValue: todoTask.status }
                }
              },
              {
                name: `tasks/${doneTask.id}`,
                fields: {
                  content: { stringValue: doneTask.content },
                  status: { stringValue: doneTask.status }
                }
              }
            ]
          }
        }

        await testedAction({ commit })
        expect(store.getters['tasks']).toEqual([todoTask, doneTask])
      })
    })
  })
})</pre><p>テスト結果を省略していますが、ここでまたテストを実行し期待通りに落ちているかを確認します。</p>
<p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」を実装する</h2>
<p>テストが書いたシナリオを元にTaskListコンポーネントとindexページ、indexストアを実装してきます。</p>
<p>TaskListコンポーネントではToDoリストと完了リストがあり、indexページではTaskListコンポーネントとTaskFormコンポーネントを呼び出してタスクをストアから呼び出すようにします。</p>
<p>indexストアではFirestoreからデータを取ってきてToDoタスクと完了タスクに状態管理するようにします。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoリスト&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了リスト&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {
  computed: {
    ...mapGetters(['todoTasks']),
    ...mapGetters(['doneTasks'])
  }
}
&lt;/script&gt;</pre><p>・pages/index.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;section&gt;
    &lt;TaskForm/&gt;
    &lt;TaskList/&gt;
  &lt;/section&gt;
&lt;/template&gt;

&lt;script&gt;
import TaskForm from '@/components/TaskForm.vue'
import TaskList from '@/components/TaskList.vue'

export default {
  async asyncData({ store }) {
    await store.dispatch('fetchTasks')
  },
  components: {
    TaskForm,
    TaskList
  }
}
&lt;/script&gt;</pre><p><span style="font-weight: 400;">・store/index.spec.js</span></p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,
  todoTasks: state =&gt; state.tasks.filter(task =&gt; task.status === 'todo'),
  doneTasks: state =&gt; state.tasks.filter(task =&gt; task.status === 'done')
}

export const mutations = {
  updateTasks(state, tasks) {
    state.tasks = tasks
  }
}

export const actions = {
  async fetchTasks({ commit }) {
    await this.$axios.get(`/tasks`, {
      baseURL: process.env._AXIOS_BASE_URL_
    })
      .then(res =&gt; {
      let tasks = []
      if (_.has(res.data, 'documents')) {
        tasks = res.data.documents.map(doc =&gt; {
          return {
            id: _.last(doc.name.split('/')),
            content: doc.fields.content.stringValue,
            status: doc.fields.status.stringValue
          }
        })
      }
      commit('updateTasks', tasks)
    })
  }
}</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (13 ms)
      ✓ 完了リストが表示されること (2 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (2 ms)
          ✓ storeからdoneTasksが取得できること (1 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること (1 ms)
    actions
      fetchTasks
        ✓ tasksが取得できること</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」のテストを書く</h2>
<p>先ほどTaskFormコンポーネントに作成したフォームに入力して作成ボタンを押すとToDoタスク一覧に追加されることが確認できるテストを書きます。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as store from '@/store'
import TaskForm from '@/components/TaskForm.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm, {
      store: store,
      localVue
    })
  })

  describe('template', () =&gt; {
    
...

    describe('タスク入力がある場合', () =&gt; {
      beforeEach(() =&gt; {
        const mock = jest.fn()
        wrapper.vm.addTask = mock
        wrapper.find('input').setValue('content')
      })

      test('dataに入力が反映されること', () =&gt; {
        expect(wrapper.vm.taskForm.content).toBe('content')
      })

      test('タスク作成ボタンを押下するとaddTaskが呼ばれること', () =&gt; {
        wrapper.find('button').trigger('click')
        
        expect(wrapper.vm.addTask).toHaveBeenCalled()
      })
    })
  })

  describe('script', () =&gt; {
    describe('data', () =&gt; {
      test('dataの構造が正しいこと', () =&gt; {
        expect(wrapper.vm.$data).toHaveProperty('taskForm.content')
      })
    })
  })
})</pre><p>ストアのテストでは作成ボタンが押下された際にFirestoreに入力されたタスクのデータが保存されることを確認します。</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  
...

  post: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {

...

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

...

    describe('createTask', () =&gt; {
      test('taskが追加されること', async () =&gt; {
        mockAxiosGetResult = {
          data: {
            name: `tasks/${todoTask.id}`,
            fields: {
              content: { stringValue: todoTask.content },
              status: { stringValue: todoTask.status }
            }
          }
        }
        action = 'createTask'
        await testedAction({ commit })
        expect(store.getters['todoTasks']).toContainEqual(todoTask)
      })
    })
  })
})</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」を実装する</h2>
<p>タスク作成ボタンが押されるとaddTaskが呼び出されるように実装していきます。</p>
<p>addTaskメソッドはストアのcreateTaskを呼び出し、タスクが状態管理されるようにします。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input
      v-model="taskForm.content"
    &gt;
    &lt;button
      @click="addTask"&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import _ from 'lodash'

export default {
  data() {
    return {
      taskForm: { content: '' }
    }
  },
  methods: {
    async addTask() {
        await this.$store.dispatch('createTask', _.cloneDeep(this.taskForm))
        this.taskForm.content = ''
    }
  }
}
&lt;/script&gt;</pre><p>addTaskから呼びだされるcreateTaskの実装をストアに書いていきます。</p>
<p>・store/index.js</p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,

...

}

export const mutations = {

  ...

  addTask(state, newTask) {
    state.tasks.push(newTask)
  }

...

}

export const actions = {
  async createTask({ commit }, payload) {
    const req = {
      fields: {
        content: {
          stringValue: payload.content
        },
        status: {
          stringValue: 'todo'
        }
      }
    }
    await this.$axios.post('/tasks', req, {
      baseURL: process.env._AXIOS_BASE_URL_
    }).then(res =&gt; {
      const newTask = {
        id: _.last(res.data.name.split('/')),
        content: res.data.fields.content.stringValue,
        status: res.data.fields.status.stringValue
      }
      commit('addTask', newTask)
    })
  }
 }</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (11 ms)
      ✓ タスク作成ボタンが存在すること (6 ms)
      タスク入力がある場合
        ✓ dataに入力が反映されること (3 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms)
    script
      data
        ✓ dataの構造が正しいこと (1 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること (1 ms)
    actions
      fetchTasks
        ✓ tasksが取得できること (1 ms)
      createTask
        ✓ taskが追加されること (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」のテストを書く</h2>
<p>フォームにタスクが入力していない場合は、作成ボタンを押すことができないことを確認できるテストを書いていきます。</p>
<p>・components/TaskForm.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as store from '@/store'
import TaskForm from '@/components/TaskForm.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskForm.vue', () =&gt; {
  let wrapper
  beforeEach(() =&gt; {
    wrapper = mount(TaskForm, {
      store: store,
      localVue
    })
  })


  describe('template', () =&gt; {

    ...

    describe('タスク入力がない場合', () =&gt; {
      beforeEach(() =&gt; {
        const mock = jest.fn()
        wrapper.vm.addTask = mock
        wrapper.find('input').setValue('')
      })

      test('dataに入力が反映されること', () =&gt; {
        expect(wrapper.vm.taskForm.content).toBe('')
      })

      test('タスク作成ボタンを押下するとaddTaskが呼ばれないこと', () =&gt; {
        wrapper.find('button').trigger('click')
        
        expect(wrapper.vm.addTask).not.toHaveBeenCalled()
      })
    })
  
  })

 ...

})</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」を実装する</h2>
<p>ボタンにdisabled属性を付与し、computedでフォームの入力を監視してボタンの活性非活性を制御するように実装しました。</p>
<p>・components/TaskForm.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;input
      v-model="taskForm.content"
    &gt;
    &lt;button
      :disabled="activateSubmit"
      @click="addTask"&gt;
      タスク作成
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import _ from 'lodash'

export default {
  
...

  computed: {
    activateSubmit () {
      return this.taskForm.content == ''
    }
  },
  
...

}
&lt;/script&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」をテストする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskForm.spec.js
  components/TaskForm.vue
    template
      ✓ タスク入力フォームが存在すること (9 ms)
      ✓ タスク作成ボタンが存在すること (1 ms)
      タスク入力がある場合
        ✓ dataに入力が反映されること (2 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms)
      タスク入力がない場合
        ✓ dataに入力が反映されること (1 ms)
        ✓ タスク作成ボタンを押下するとaddTaskが呼ばれないこと (1 ms)
    script
      data
        ✓ dataの構造が正しいこと (1 ms)</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストを書く</h2>
<p>ToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できるテストを書いていきます。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
   
...

    describe('todoリストの完了をクリックする場合', () =&gt; {
      test('doneTaskが指定の引数で呼び出されること', () =&gt; {
        const mock = jest.fn(todoTask =&gt; todoTask)
        wrapper.vm.doneTask = mock
     
        wrapper.find('li.todo button').trigger('click')
        expect(wrapper.vm.doneTask).toHaveBeenCalled()
        expect(wrapper.vm.doneTask.mock.results[0].value).toBe(todoTask)
      })
    })
  })

  ...

})</pre><p>ストアではタスクのステータスが更新されることが確認できるテストを書いていきます。</p>
<p>・store/index.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import * as index from '@/store'
import { createLocalVue } from '@vue/test-utils'
import _ from 'lodash'
import axios from 'axios'

const localVue = createLocalVue()
localVue.use(Vuex)

let mockAxiosGetResult
jest.mock('axios', () =&gt; ({
  
...

  patch: jest.fn(() =&gt; Promise.resolve(mockAxiosGetResult))
}))

let action
const testedAction = (context = {}, payload = {}) =&gt; {
  return index.actions[action].bind({ $axios: axios })(context, payload)
}

describe('store/index.js', () =&gt; {
  let store
  let todoTask, doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(_.cloneDeep(index))
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
  })


...

  describe('actions', () =&gt; {
    let commit
    beforeEach(() =&gt; {
      commit = store.commit
    })

   ...

    describe('updateTask', () =&gt; {
      beforeEach(() =&gt; {
        store.replaceState({
          tasks: [todoTask]
        })
      })

      test('taskが更新されること', async () =&gt; {
        mockAxiosGetResult = {
          data: {
            name: `tasks/${todoTask.id}`,
            fields: {
              content: { stringValue: 'updatedContent' },
              status: { stringValue: 'done' }
            }
          }
        }
        action = 'updateTask'
        await testedAction({ commit })
        expect(store.getters['doneTasks']).toContainEqual({
          id: todoTask.id,
          content: 'updatedContent',
          status: 'done'
        })
      })
    })
  })
})</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」を実装する</h2>
<p>ToDoタスクの完了ボタンを押すと、ストアのupdateTaskを呼び出しステータスを完了にすることで完了タスク一覧に移動されるように実装します。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoタスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
        &lt;button 
          @click="doneTask(todoTask)"&gt;
          完了
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了タスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {
  
...

  methods: {
    async doneTask(task) {
      const payload = _.cloneDeep(task)
      payload.status = 'done'
      await this.$store.dispatch('updateTask', payload)
    }
  }
}
&lt;/script&gt;</pre><p>・store/index.js</p><pre class="crayon-plain-tag">import _ from 'lodash'

export const state = () =&gt; ({
  tasks: []
})

export const getters = {
  tasks: state =&gt; state.tasks,

  ...

}

export const mutations = {
  
...

  updateTask(state, newTask) {
    const task = state.tasks.find(task =&gt; task.id === newTask.id)
    if (task) {
      task.content = newTask.content
      task.status = newTask.status
    }
  }
}

export const actions = {
 
...

  async updateTask({ commit }, payload) {
    const req = {
      fields: {
        content: {
          stringValue: payload.content
        },
        status: {
          stringValue: payload.status
        }
      }
    }
    await this.$axios.patch(`/tasks/${payload.id}`, req, {
      baseURL: process.env._AXIOS_BASE_URL_
    }).then(res =&gt; {
      const updateTask = {
        id: _.last(res.data.name.split('/')),
        content: res.data.fields.content.stringValue,
        status: res.data.fields.status.stringValue
      }
      commit('updateTask', updateTask)
    })
  }
}</pre><p>&nbsp;</p>
<h2>「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストをする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (17 ms)
      ✓ 完了リストが表示されること (2 ms)
      todoリストの完了をクリックする場合
        ✓ doneTaskが指定の引数で呼び出されること (3 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (1 ms)
          ✓ storeからdoneTasksが取得できること (2 ms)</pre><p></p><pre class="crayon-plain-tag">PASS  store/index.spec.js
  store/index.js
    getters
      todoTasks
        ✓ statusがtodoのtaskが取得できること (3 ms)
      doneTasks
        ✓ statusがdoneのtaskが取得できること (1 ms)
      tasks
        ✓ すべてのtasksが取得できること
    actions
      fetchTasks
        ✓ tasksが取得できること (1 ms)
      createTask
        ✓ taskが追加されること
      updateTask
        ✓ taskが更新されること</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストを書く</h2>
<p>完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できることが確認できるテストを書いていきます。</p>
<p>ストアのタスクステータス更新のテストは先程のストーリーで確認済みなため、新しくストアのテストは書いていません。</p>
<p>・components/TaskList.spec.js</p><pre class="crayon-plain-tag">import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import * as indexStore from '@/store'
import TaskList from '@/components/TaskList.vue'

const localVue = createLocalVue()
localVue.use(Vuex)

describe('components/TaskList.vue', () =&gt; {
  let wrapper
  let store
  let todoTask
  let doneTask
  beforeEach(() =&gt; {
    store = new Vuex.Store(indexStore)
    todoTask = { id: '1', content: 'content_1', status: 'todo' }
    doneTask = { id: '2', content: 'content_2', status: 'done' }
    wrapper = mount(TaskList, {
      store: store,
      localVue
    })
    store.replaceState({ tasks: [todoTask, doneTask] })
  })

  describe('template', () =&gt; {
   
...

   describe('完了リストのToDoに戻すをクリックする場合', () =&gt; {
      test('returnTaskが指定の引数で呼び出されること', () =&gt; {
        const mock = jest.fn(doneTask =&gt; doneTask)
        wrapper.vm.returnTask = mock
     
        wrapper.find('li.done button').trigger('click')
        expect(wrapper.vm.returnTask).toHaveBeenCalled()
        expect(wrapper.vm.returnTask.mock.results[0].value).toBe(doneTask)
      })
    })
 })

  ...

})</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」を実装する</h2>
<p>完了タスクのToDoに戻すボタンを押すと、ストアのupdateTaskを呼び出しステータスをToDoにすることでToDoタスク一覧に移動されるように実装します。</p>
<p>・components/TaskList.vue</p><pre class="crayon-plain-tag">&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;ToDoタスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="todoTask in todoTasks"
        :key="todoTask.id"
        class="todo"
      &gt;
        &lt;span&gt;{{ todoTask.content }}&lt;/span&gt;
        &lt;button 
          @click="doneTask(todoTask)"&gt;
          完了
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
    &lt;hr&gt;
    &lt;p&gt;完了タスク&lt;/p&gt;
    &lt;ul&gt;
      &lt;li
        v-for="doneTask in doneTasks"
        :key="doneTask.id"
        class="done"
      &gt;
        &lt;span&gt;{{ doneTask.content }}&lt;/span&gt;
        &lt;button 
          @click="returnTask(doneTask)"&gt;
          ToDoに戻す
        &lt;/button&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters } from 'vuex'
import _ from 'lodash'

export default {

...

  methods: {

    ...

    async returnTask(task) {
      const payload = _.cloneDeep(task)
      payload.status = 'todo'
      await this.$store.dispatch('updateTask', payload)
    }
  }
}
&lt;/script&gt;</pre><p>&nbsp;</p>
<h2>「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストをする</h2>
<p>実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。</p><pre class="crayon-plain-tag">PASS  components/TaskList.spec.js
  components/TaskList.vue
    template
      ✓ todoリストが表示されること (12 ms)
      ✓ 完了リストが表示されること (3 ms)
      todoリストの完了をクリックする場合
        ✓ doneTaskが指定の引数で呼び出されること (3 ms)
      完了リストのToDoに戻すをクリックする場合
        ✓ returnTaskが指定の引数で呼び出されること (2 ms)
    script
      computed
        todos
          ✓ storeからtodoTasksが取得できること (1 ms)
          ✓ storeからdoneTasksが取得できること (5 ms)</pre><p>&nbsp;</p>
<h2>全部のテストを流す</h2>
<p>全部の実装が終わったら、全てのテストを流します。package.jsonのscriptに&#8221;test&#8221;: &#8220;jest &#8211;config jest.config.js&#8221;を追加し、以下のコマンドで実行します。</p><pre class="crayon-plain-tag">$ npm run test

PASS components/TaskForm.spec.js
PASS components/TaskList.spec.js
PASS store/index.spec.js</pre><p>&nbsp;</p>
<p>全てのテストが通ったら完了です。</p>The post <a href="https://mintaku-blog.net/jest-tdd/">【Nuxt.js】JestでTDDを実践してみる</a> first appeared on <a href="https://mintaku-blog.net">みんたく</a>.]]></content:encoded>
					
					<wfw:commentRss>https://mintaku-blog.net/jest-tdd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2013</post-id>	</item>
	</channel>
</rss>
