NuxtのコンポーネントをWeb Componentとして利用する
🎍 はじめに
この記事は、QiitaのNuxt.js Advent Calendar2021の11日目の記事であり、記載内容は投稿日時時点の情報です。目下、Nuxt界隈では3系に向けた様々な動きがあるため、最新の情報はNuxtのDiscordの投稿を適宜確認することを推奨します。
本稿では、Nuxt Custom-Elements を用いてNuxtで作成したコンポーネントを使用するケースにおける、実際に案件で使用した所感と導入時の詰まりどころを紹介します。導入方法自体は公式ドキュメントに記載があるため、ここでの紹介は割愛します。単に導入するだけなら非常にシンプルですので、公式ドキュメントを参照してみてください。
✔ 要約
- Nuxt2系のコンポーネントをWeb Componentとして提供する手段として、Nuxt Custom-Elements は利用できる。
- Nuxt Custom-Elements は Vue Custom Elementをラップしたライブラリなので、NuxtのContextは直接利用できない。
- 投稿時現在のPRやIssuesを見る限り、Nuxt Custom-ElementsはNuxt3系(Beta)には対応していない。
🎅 Web Component導入で感じたメリット
私がWeb Componentを導入したユースケースは、「既存Nuxt案件で用いているヘッダーとフッターを、非エンジニアが管理するWebページでも導入する」というケースでした。Nuxt案件側のヘッダーとフッターにはログイン管理・多言語対応・ルーティングが含まれている一方、非エンジニアが管理するWebページはi18nもRouterも含まれていなければVanillaとHTMLとCSSで書かれており、Vueの導入すらありません。その環境にWeb Componentを導入してみた結果、以下のメリットを感じることができました。
- 最初期の導入が非常にスピーディー且つ容易であった。
- Webフロントエンドエンジニアと非エンジニアの責務と技術スタックを完全に分離することができた。
- デプロイフローや決裁のフローも、各々の責務において、別個にWebページを運用することができた。
また、関係者からは以下のフィードバックをいただいています。
- Web開発に精通していなくても、Webエンジニアが作ってくれたコンポーネントを簡単に使えるのは助かる。
🎄 NuxtのContextは使用できない
その導入において、最初に手詰まったのが、$nuxt
やthis.$store
、this.$store
、<nuxt-link>
、<nuxt-image>
、$loading
、などのNuxt的な書き方が、Web Componentとして配信した時に実行時エラーとなったことです。当然、使用できないものには、nuxt/axiosやpluginsなどのNuxtの機能も含まれます。これは、 Nuxt Custom-Elements があくまでも Vue Custom Element のラッパーライブラリであり、NuxtのContextを管理する仕様ではないためです。当たり前といえば当たり前の話ですが、公式ドキュメントにも記載が散見されなかったため、「なんでや」と少し考えてしまいました。
そして、この仕様を認識したあとには、既存コンポーネントの改修とNuxtインスタンスを使用しないためのmixinsの作成、Web Component用のラッパーコンポーネントの作成を行いました。
❗ Web Component化に対応するための既存コンポーネントの改修
今回のユースケースは、あくまでもヘッダーとフッターの転用でしたが、将来的に「他のコンポーネントでも同じことをしたい」とか「コンポーネントライブラリをWeb Component化して配信してほしい」のような要望が出ることは、想定されうる事態であったため、既存のAtomsやMolecules粒度のコンポーネントは、Web Component化を前提とした作りに改修することにしました。「$nuxt
などが使えない時は、Nuxt依存でないフォールバックの処理を充てる」というシンプルな対応です。例えばNuxtLinkのコンポーネントであれば、以下のような感じでしょうか。「Component
タグ使え」と言われればまぁそれはそう。
/* 変更前 */
// <template>
// <nuxt-link
// :to="noLocale ? to : localePath(to)"
// :target="blank ? '_blank' : false"
// >
// <slot></slot>
// </nuxt-link>
// </template>
/* 変更後 */
<template>
<nuxt-link
v-if="!!$nuxt"
:to="noLocale ? to : localePath(to)"
:target="blank ? '_blank' : false"
>
<slot></slot>
</nuxt-link>
<a
v-else
:to="to"
:target="blank ? '_blank' : false">
<slot></slot>
</a>
</template>
こうすれば、$nuxt
が使えない(=Nuxt環境でない)時に、<nuxt-link>
を使わずに、通常のaタグを使わせることができますね。なお、OrganismsやTemplateはドメインが絡む粒度と定義しているため、ドメインが絡む場合の対応は個別対応と定義しました。その粒度のコンポーネントをWeb Componentとして転用するケースが世界に存在しないことを祈るばかりです。
👽 Nuxtインスタンスを使用しないためのmixinsの作成
繰り返しになりますが、Nuxt Custom-Elements では、NuxtインスタンスやpluginsなどのNuxtの機能は使用できません。一方で、Vue-routerやVuei18n、Vuexは使用することができます。そのため、これらを利用するために、次のようなmixinsを作成しました。本当はTSを使っていますが、型定義は省略しています。
// Nuxt依存機能のエスケープを定義
import Vue from 'vue'
// URLなどの定数を扱うためのpluginsを使っている
import { content } from '@/plugins/content'
// Nuxt i18nの代わりにVue i18nを使用する
import i18n from '@/modules/i18n'
// note: Routerは使用していないが、Routerを追加する場合はここに追加する
Vue.prototype._i18n = i18n
export default Vue.extend({
data() {
const content = this?.$nuxt?.context?.$content ?? content
const i18n = this?.$i18n ?? i18n
const $t = this?.$t ?? nuxtI18n.t
return {
content,
i18n,
$t,
}
},
})
なお、Storeの使用は最低限に抑えるため、別のmixinsに切り分けています。
// Vuexを使用する
import Vue from 'vue'
import Store from '@/modules/store'
export default Vue.extend({
data() {
const store = this?.$store ?? Store()
return {
store,
}
},
})
あとは、これらのコンテキストに依存しているコンポーネントに対して、このmixinsを注入して使用すれば、Nuxtインスタンスの依存に伴うWeb Componentの実行時エラーは解消されます。
👕Web Component用のラッパーコンポーネントの作成
配信元となるNuxt側のリポジトリでは、リポジトリファクトリ方式によるAPIコールを行っており、ヘッダーそのものは layout層でコールした内容をpropして描画に利用しています。しかし、Web Componentにはlayoutの機能が含まれないため、今回はWeb Component専用のラッパーコンポーネントを作成し、当該ラッパーコンポーネントでAPIコールをすることにしました。layoutそのものをWeb Component化するアプローチも考えられましたが、影響範囲がコンポーネントに閉じなくなるため、そのアプローチは棄却しました。
// Wrapper Component
<template>
// SSRでの描画はしない
<client-only>
<TheHeader
ref="header"
:pros="pros"
@emit="emitFunction"
/>
</client-only>
</template>
<script lang="ts">
import Vue from 'vue'
import TheHeader from '@/components/organisms/***/TheHeader.vue'
import AvoidNuxtInstance from '@/mixins/avoid-nuxt-instance'
import AvoidNuxtStore from '@/mixins/avoid-nuxt-store'
import { repositoryFactory } from '@/repositories/repositoryFactory'
const ***Repository = repositoryFactory.get('***')
export default Vue.extend({
name: 'WebComponentHeader',
components: {
TheHeader,
},
mixins: [AvoidNuxtInstance, AvoidNuxtStore],
data() {
return {
pros: undefined
}
},
<!-- 以下略 -->
})
</script>
<style lang="scss">
// Baseとなるスタイルもここで読み込む
@use '@/assets/styles/style';
</style>
ここまで用意すれば、あとはbuildしてdeployして、生成されたScriptとカスタムタグを非エンジニアが管理する側のWebページに実装すれば一通りの対応が完了です。APIリクエストを行う場合は、nuxt/axiosが愚直には使えない点と、ドメインが異なる場合のリクエストの許可をお忘れなく。
📝所感
一癖あったものの、Nuxtで作ったコンポーネントをサクッとWeb Component化して他所で使い回せる体験は非常に良かった。これうまく使ってなんかプロダクト作れそう。なお、既存のWeb Componentの良い使われ方としては、AMPをComponentとしてサクッと導入できるBentoがありますね。これは導入したいと思っています。
今回は、突発的に依頼を受けてさっと構築して試験的に運用しただけなので、次以降のケースがあればもっと詳しく調べたいなと思う次第です。
「Nuxt3でのWeb Component開発」と「Web Componentを駆使したコンポーネントライブラリの実装とテストの導入」らへんは調査すればそれなりのリターンが得られる気がしています。あとは、本格運営するならScriptを配信するCDNもどうするかをちゃんと検討する必要がありますね。
末筆ながら、Advent Calendarの公開が遅くなって誠に申し訳ございませんでした。本稿が皆様の一助となれば幸いです。それではよい年末年始を。
✈宣伝(1) Virtual Market2021開催中(2021/12/19迄)
私R.D.Sakamotoは、エストニア法人"OmusBridge OÜ"を趣味半分で経営しながら、VR法人HIKKYでのWeb開発に従事しています。前者では現在エストニアのStartUp Visa申請のための取組中で、後者ではバーチャルマーケット2021が絶賛開幕中です。最近なにかと話題のメタバースで行われているバーチャルマーケット2021のWebサイトはNuxt製ですので、是非ともWebサイトとイベント本体に遊びに来てくださいまし🙏
🎈宣伝(2) 元エンジニアバー"Barloon"のDiscordのご紹介
コロナ渦前は渋谷や中野で営業していたエンジニアバー"Barloon"。当時は500人超のエンジニアがSlackにはいたのですが、今はひっそりと有志メンバーがDiscordに集い、週1の Barloon Meeting でだべったりマイクラをやってたりします。最近は、ブロックチェーンやクリエイティブコーディングについて話していた記憶があります。
なにかと社外の人とのつながりが希薄になりがちな昨今、興味がある人は、是非にググって入り口を探してみてくださいまし。
Discussion