エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Vueでレイアウトの切り替えを高階コンポーネントで実装する

この記事はエムスリー Advent Calendar 2019の14日目の記事です。

ご無沙汰しております、エムスリーエンジニアリンググループ 兼 QLife チーフアーキテクトの園田 (@ryoryoryohei) です。

今回はフロントエンドライブラリである Vue.js (というか vue-router)の小ネタです。

Nuxt.js を利用しない場合、vue-router で複数レイアウトを扱うには「ネストされたルート」の利用が公式ドキュメントで案内されています。

今回は「ネストされたルート」を利用せず、複数レイアウトの出し分けを高階コンポーネントで実現してみます。

詳しい人ならこの時点で実装含めてすべてイメージできちゃいますね。

第二の故郷 十和田湖の写真
第二の故郷 十和田湖の写真

高階コンポーネントとは?

高階コンポーネントは、ざっくり言うと、コンポーネントを引数にとって別のコンポーネントを返す関数コンポーネントです。GoF の Decorator パターンなどを実現するためによく利用されるテクニックですね。

よく利用されるパターンとしては、認証やトラッキングなど、コンポーネントの責務とは異なる処理をコンポーネント実装と切り離すために利用します。

本題と実装

SPA を実装していて、ログインページだけはシンプルなレイアウトにしたい、などはよくあると思います。

実現したいレイアウト

2 種類くらいなら問題ないですが、複数レイアウトになってくると vue-router 公式の「ネストされたルート」だとルート定義の見通しが悪くなり、コンポーネント側においても router-view を実装する必要があるため、個人的に好きではありません。

そこで高階コンポーネントを利用してフラットな Route 階層で複数レイアウトを実現します。 見てみた方が早いので、実装です。サンプルでは Vuetify1 を利用しています。

レイアウトコンポーネントの実装

  • mainLayout.tsx 2

トップバーとサイドバー付きのメインレイアウトに変換する高階コンポーネント。

import Vue, { VueConstructor } from 'vue'
import { VApp, VAppBar, VContent } from 'vuetify/lib'
import SideMenu from '@/components/layouts/partials/sidemenu.vue'

export function mainLayout(PageComponent: VueConstructor<Vue>) { // (1)
  return Vue.extend({
    inheritAttrs: false,
    components: {
      PageComponent, // (2)
      SideMenu,
      VApp,
      VAppBar,
      VContent,
    },
    render() {
      return (
        <v-app>
          <v-app-bar app={true} color="primary" dark={true}>
            Title
          </v-app-bar>
          <SideMenu />
          <v-content>
            <PageComponent propsData={this.$attrs} /> // (3)
          </v-content>
        </v-app>
      )
    },
  })
}
  • (1) ラップする対象となるページコンポーネントを引数で受け取る。
  • (2) 引数で受け取ったコンポーネントを利用することをVueに伝える。
  • (3) vue-router から受け取った props をページコンポーネントに渡す。

続いてログインページ用のシンプルなレイアウトです。

  • simpleLayout.tsx
import Vue, { VueConstructor } from 'vue'
import { VApp, VContent } from 'vuetify/lib'

export function simpleLayout(PageComponent: VueConstructor<Vue>) {
  return Vue.extend({
    inheritAttrs: false,
    components: {
      PageComponent,
      VApp,
      VContent,
    },
    render() {
      return (
        <v-app>
          <v-content>
            <PageComponent propsData={this.$attrs} />
          </v-content>
        </v-app>
      )
    },
  })
}

vue-router のルート定義

作成したレイアウト用の高階コンポーネントを vue-router で利用するようにルート定義ファイルを修正します。

  • src/router/index.ts
import Vue from 'vue'
import VueRouter from 'vue-router'
import LoginPage from '@/views/login/index.vue'
import TopPage from '@/views/top/index.vue'
import { simpleLayout } from '@/components/layouts/simple'
import { mainLayout } from '@/components/layouts/main'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'top',
    component: mainLayout(TopPage), // メインのレイアウトを適用
  },
  {
    path: '/login',
    name: 'login',
    component: simpleLayout(LoginPage), // シンプルなレイアウトを適用
  },
]
// ...

エントリーポイントテンプレートの修正

レイアウトを高階コンポーネントで実現するので、エントリーポイントとなるコンポーネントのテンプレートには <router-view /> のみを記載します。

  • App.vue
<template>
  <router-view />
</template>

これで複数レイアウトの出し分けが可能になりました。

まとめ

この方法だとルート定義を見るだけで、どのページがどのレイアウトで表示されるかがわかりやすく、かつページコンポーネントはルートを意識する必要がないので、なかなかいいんじゃないでしょうか?

注意点として、高階コンポーネントは便利ですが、フレームワーク的な要素が強いので、プロダクションコードでは足回りだけ利用するようにしています。 なんでもかんでも高階コンポーネント化すると、処理がブラックボックスになって見通しが悪くなるため、適切な用法・用量で利用しましょう。

We are hiring!

QLife では共にチーム一丸となってより良いものづくりにこだわれる仲間を募集中です! 小さいサービスが多いので新しい技術の利用にも非常に積極的に取り組んでいます! カジュアル面談も行っていますので、興味がある方は entry@qlife.co.jp に「カジュアル面談希望」とメールをください!

www.qlife.co.jp

エムスリーでもエンジニアを随時募集しています! 共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! Vue.js 以外にも React や Nuxt.js なども活用しています! 興味がある方はカジュアル面談やTechtalkにおこしください!!

jobs.m3.com


  1. https://vuetifyjs.com/ja/

  2. tsx は @vue/cli で TypeScript を利用することを選択した場合、特に何も設定しなくても利用できます。すごいぞ @vue/cli