【次の祝日はいつですか】Vue CLIとDjango REST frameworkでREST APIを実装する

公開:2020/10/17
更新:2020/10/17
24 min読了の目安(約22000字TECH技術記事

前置き

はじめてのZenn投稿となります。独学でDjangoを勉強しています。普段は勉強したことなどを自作のブログにアウトプットしていますが、今回は興味本位でZennに投稿してみました。

はじめに

少し前の話ですが、今年のGWの終わりかけに、連休が終わるという絶望感に打ちひしがれながら作ったカレンダーを紹介します。GWを利用してDjango REST framework(DRF)とVue CLIを学び、とりあえずこれらを使ってなにかを作ってみたい!という思いからコードを書いたので、機能性とかUXとかの面では最悪です。しかし、バックエンドのDjangoで登録したデータをフロントエンドのVue.jsに渡す、という流れを体験できたので、いい勉強となりました。

完成版

以下 gifです。これから紹介する内容とファイルや構成など少し違いますが、一応GitHubはこちら(Vue3.0利用)。ちなみに2系を使ったコードは別リポジトリに切っています。

ボタンを押すと次の祝日までの日数が表示されます。

2020年10月は祝日はないので、10月6日時点でこのボタンを押すと次の祝日までは28日もあるのか・・・。と少し暗い気持ちになるかもしれません。
ですが、これを作った今年のGW最後の日には、たしか78日とか表示されてたので、まだマシな方です。

対象

これを作った時、僕はDjangoとVue.jsのCDN版は少し触ったことがあったものの、Vue CLIとDjango REST framework(DRF)はほぼ触ったことがなかったので、そういう方向けです。当時はVueの2系を使っていたのですが、書き換えてVue3.0でも動作を確認しています。これから紹介するコードは一応Vue3.0用のコードです。(正直Vue.jsは今でも勉強不足です。特に3.0はまだまだでComposition APIとかは使ってないです。すみません。)
手順をすべて書くのは多すぎて難しいので、少しかいつまんで記述していきます。

前提知識

そもそも・・・

・Djangoとは
Pythonのフレームワークです。

・Django REST framework(DRF)とは
REST APIバックエンド構築に特化したパッケージです。

・REST APIとは
外部からWebシステムを利用するインターフェースです。今回は、Djangoで登録したデータをVue.jsに渡します。

・Vue.jsとは
JavaScriptのフレームワークです。

・Vue CLIとは
Vue.jsをローカルに落として使えます。いろいろ便利な機能が揃っています。

・コンポーネントとは
直訳するとヘッダー、フッター等のWebページの構成要素のことです。これらを組み合わせてページを作ります。特にVue CLIでは、単一ファイルコンポーネントという、一つのファイルにHTML、JavaScript、CSSを記述されたコンポーネントを扱います。使ってみると直感的でわかりやすく、また開発がしやすいため非常によいです。

・Vuexとは
Vueの状態管理を行うためのライブラリです。端的にいうと、コンポーネント間のデータのやりとりができます。今回導入しています。

・Vue Routerとは
ルーティング制御ができるライブラリです。ページの更新が必要な箇所のみ書き換えが可能です。SPA作成に使えます。今回導入していますが、あまり良さを引き出せていません。

仕様

ざっくり頭の中で考えた流れはこうです。

Djangoで祝日テーブルを作る
→ DRFでREST APIを実装
→ Vue.jsでカレンダー作る
→ Vue.jsからDjangoに祝日のデータをリクエスト
→ DjangoからVue.jsに祝日のデータをレスポンス
→ Vue.jsで処理して表示
  
通常のカレンダーを作り、その上で次の祝日までの日数を表示できるようにします。今回はボタンをクリックするというイベントをトリガーとしています。いちいちDjango側で祝日を登録しなきゃいけないので、はっきりいってカレンダーとしての実用性はないです。あくまで勉強用です。

今回作るカレンダーは単純でURL遷移がなく、文字通り本当のSPA(シングルページアプリケーション)なので、ぶっちゃけDRFとかVue CLIとかVuexとか、特にVue Routerとかの便利なものを使わなくてもおそらくできるのですが、勉強になるので使っています。

確認バージョン

Python 3.8.5
Django 3.0以上
django-cors-headers 3.5.0
djangorestframework 3.12.1
npm 6.14.8
vue-cli 4.5.7
vue 3.0.0
vuex 4.0.0-0
vue-router 4.0.0-0

Vue3.0を使うためにはvue-cliが4.5以上である必要があります。

自分はローカルのnpmとvue-cliが古かったためDocker使いました。GitHubにあげてるコードではnpmのタグはlatestにしてるけど、本来ちゃんとバージョンを指定した方がいいはず。今回Dockerの話は省きます。
docker 19.03.13
docker-compose 1.27.4

構成

だいたい以下のような構成です。

django
├─ manage.py
│
├─ project # Djangoプロジェクト
│   │  settings.py
│   │  urls.py
│   │  wsgi.py 
│   │  asgi.py
│   └─ __init__.py
│
└─ app # Djangoアプリケーション
    │  admin.py
    │  apps.py 
    │  models.py
    │  tests.py 
    │  serializer.py   # デフォルトではないので作成
    │  views.py
    │  urls.py         # デフォルトではないので作成
    │  __init__.py
    ├─ migrations
    │
    └─ vue-calendar # Vueプロジェクト
         │  package.json
         │  package-lock.json
         │  babel.config.js
         │  .eslintrc.js
         │  .browserslistrc
         │    
         ├─ public
         │   └─  index.html
         │  
         └─ src
             │  App.vue
             │  main.js
             │
             ├─  assets
             │
             ├─  components
             │     │   Calendar.vue       # 作成
             │     │   Header.vue         # 作成
             │     └─  Footer.vue         # 作成
             │
             ├─  router
             │     └─  index.js
             │ 
             └─   store
                   │   index.js
                   └─  mutation-types.js  # 作成 

Djangoアプリケーションを作り、その中でVueプロジェクトを作るという構成です。

serializer.pyとviews.pyでREST APIを実装し、Djangoのアドミンページから登録したデータをVue側で受け取り、コンポーネントで処理します。

実装

以下から実装をしていきます。

まずはDjango側からみていきます。言ってしまえば、Djangoの役割は祝日を登録してVueに渡すだけです。なのでそんなに複雑な処理はありません。

とりあえず、必要なライブラリを以下のように落とします。

pip install djangorestframework
pip install django-cors-headers

その後、Djangoプロジェクトとアプリケーションを作成しておきます。settings.pyのISNTALLED_APPSに以下を追加します。

project/settings.py
# 省略

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',             # 追加
    'app.apps.AppConfig',         # 追加 Djangoのアプリケーション
]

# 省略

同一オリジンポリシー

オリジンとはスキーム(プロトコル)、ホスト(ドメイン)、ポート番号の組み合わせのことです。
開発時、今回のオリジンは以下のようになると想定しています。

バックエンド(Django) :http://127.0.0.1:8000/
フロントエンド(Vue.js):http://127.0.0.1:8080/

同一オリジンポリシーとはWebブラウザの制御機構のひとつで、オリジンが異なる場合、リソースへのアクセスを制限するものです。リソースの不正利用を防いでくれる重要な仕組みですが、今回は異なるオリジン間でWeb APIを実装したいので、同一オリジンポリシーを限定的に解除する必要があります。

CORS

CORSとは、Cross-Origin Resource Sharing(オリジン間リソース共有)を指し、要は異なるオリジンでもリソースのやりとりをできるようにすることです。先ほどインストールしたdjango-cors-headers というライブラリを用いることで、CORSの設定をすることが可能です。

異なるオリジン間だと通常はセキュリティ上リソースの共有ができませんが、django-cors-headersを導入し、ホワイトリストに追加しておくことでそれが可能となります。

以下のように記述しておきます。
CORS_ORIGIN_WHITELISTに追加されたオリジンには、リソースの共有許可を与えるようになります。

project/settings.py
# setttings.pyの一番下に以下を追加

# 以下追加
if DEBUG:
    INSTALLED_APPS += ['corsheaders']
    MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
    CORS_ORIGIN_WHITELIST = (
        'http://127.0.0.1:8080',
        'http://localhost:8080',
    )

モデル

次はDjangoアプリケーション内のmodels.pyです。モデルはめちゃくちゃ簡単にしています。

app/models.py
from django.db import models


class Holiday(models.Model):
    name = models.CharField('祝日', max_length=100)
    date = models.DateField('日付')

    def __str__(self):
        return self.name

マイグレートした後に、アドミンページでデータ追加できるよう、admin.pyを以下のようにします。

app/admin.py
from django.contrib import admin
from .models import Holiday


class HolidayAdmin(admin.ModelAdmin):
    list_display = ('name', 'date')
    ordering = ('date',)

admin.site.register(Holiday, HolidayAdmin)

スーパーユーザー作成後、ローカルサーバーを立ち上げ、アドミンページにアクセスしてデータを追加しておきます。

シリアライザ

DRFではシリアライザを定義します。通常REST APIではJSON形式でデータのやりとりを行いますが、そのJSON文字列と、Djangoのモデルを相互に変換してくれるのがこのシリアライザです。なんというか、forms.pyのAPI版みたいなノリです。

アプリケーション内にserializers.pyというモジュールを作り、以下のように書きます。今回は単一のリソースを扱うだけなのでModelSerializerを使用しています。シリアライザでは、modelで指定したモデルのフィールドに則った形で処理を行ってくれます。fieldsでは利用するフィールド名を指定します。今回はとりあえずallにします。

app/serializers.py
from rest_framework import serializers
from .models import Holiday

class HolidaySerializer(serializers.ModelSerializer):
	class Meta:
		model = Holiday
		fields = '__all__'

ビュー

views.pyではJSON形式のリクエストを受け取り、処理を実行してJSON形式のレスポンスとして返します。
〇〇APIViewというのは汎用APIビューと呼ばれ、他にもたくさん種類があり、用途によって使い分けます。今回やりたいことは一覧の取得だけよいので、ListAPIViewを使います。

app/views.py
from rest_framework import generics
from django.views import generic
from .models import Holiday
from .serializers import HolidaySerializer


class HolidayList(generics.ListAPIView):
    queryset = Holiday.objects.all()
    serializer_class = HolidaySerializer

URL

プロジェクトのurls.pyは以下のようにします。

project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('calendar/', include('app.urls')),   # 追加
]

アプリケーションのurls.pyを作り、DRFのページのパスだけ作ります。ここで指定したパスが、APIのエンドポイントになります。

vcalendar/urls.py
from django.urls import path
from . import views

app_name = 'vcalendar'

urlpatterns = [
    path('api/holiday/', views.HolidayList.as_view(), name='holiday_list'),
]

urls.pyで指定した通り、http://127.0.0.1:8000/api/holiday/ にアクセスすると以下のようなDRFのページに遷移します。ここで遷移するページはBrowsable APIと呼ばれ、GUI上でデータを確認することができます。DRFのウリの一つです。指定したビューによってはここでデータの追加、更新、削除などできるようになります。

このhttp://127.0.0.1:8000/api/holiday/ に対して、Vue.js側からFetchやaxiosなんかを用いることでデータを取得することができます。(今回はFetchを使用します。)

とりあえずこれでDjango側の設定はいったん終了です。

Vueプロジェクト作成

ここからVue CLI側の設定に入ります。

Djangoアプリケーションのフォルダ内に移動し、以下コマンドでvue-calendarという名前のVueプロジェクトを作ります。

vue create vue-calendar

いくつか質問されます。

以下ではManually select featuresを選択します。

? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
❯ Manually select features

次の質問では、プロジェクトの中で何を使いたいか聞かれます。

Check the features needed for your project:

VuexとVue Routerを追加します。デフォルトで入っているBabelとLinterはそのままにしておきます。

次は、どのバージョンのVueを使うか選択します。vue-cliが4.5以上だとVueの3系が選択できます。こちらを選びます。

? Choose a version of Vue.js that you want to start the project with 
   2.x
❯  3.x (Preview)

それ以外は、とりあえずデフォルトにします。最後の設定の保存のとこだけnにしても良いかと思います。
これでVueのプロジェクトが作成できました。

作成されたvue-calendarの中に移動し、以下のコマンドを打つとローカルサーバーが起動されます。Djangoのpython manage.py runserverのようなものです。

npm run serve

http://127.0.0.1:8080/ にアクセスすると、ページが表示されています。確認できたらCtrl+Cでいったん停止します。

コンポーネント作成(その1)

上記で表示したページですが、デフォルトではvue-calendar/src/components/の中のHelloWorld.vueというコンポーネントが表示されています。

この呼び出し元はvue-calendar/src/App.vueです。このApp.vueを以下のようにします。

vue-calendar/src/App.vue
<template>
<body>
  <div id="app">
    <!-- ここでコンポーネントをタグで配置 -->
    <Header />
    <router-view />
    <Footer />
  </div>
</body>
</template>

<script>
// ここでコンポーネントを呼び出し
import Header from "./components/Header.vue";
import Footer from "./components/Footer.vue";
export default {
  name: "App",
  // コンポーネントを設定
  components: {
    Header,
    Footer
  }
};
</script>

<style>
* {
  margin: 0;
}
#app {
  text-align: center;
  color: #2c3e50;
}
</style>

別に大事じゃないのでなくてもいいんですが、とりあえず今回はヘッダー用、フッター用としてHeader.vueとFooter.vueというコンポーネントを作って呼ぶようにします。HelloWorld.vueは不要なので削除します。

Header.vueとFooter.vueを以下のように作成します。

vue-calendar/src/components/Header.vue
<template>
  <header>
    <div>
      <h1>Calendar</h1>
    </div>
  </header>
</template>

<script>
export default {
  name: "site-header"
};
</script>

<style scoped>
h1 {
  margin: 8px;
}
</style>
vue-calendar/src/components/Footer.vue
<template>
  <footer>
    <ul>
      <li>
        <!-- リンクを自由に設定 -->
        <a href="https://github.com/selfsryo/vue3_django_calendar" target="_blank">Github</a>
      </li>
      <li>
        <a href="https://twitter.com/selfsryo" target="_blank">Twitter</a>
      </li>
    </ul>
  </footer>
</template>

<script>
export default {
  name: "site-footer"
};
</script>

<style scoped>
footer {
  display: flex;
  position: absolute;
  bottom: 0;
  background: #000;
  width: 100%;
  color: #fff;
}
ul {
  list-style: none;
  display: flex;
}
li {
  padding: 0 10px;
  text-align: center;
}
a {
  color: #fff;
}
</style>

Vue Router

vue-calendar/src/App.vueのtemplateタグの中にrouter-viewというタグがありますが、ここには Vue Routerで設定されたコンポーネントが入ります。

Vue Routerはvue-router/src/router/の中のファイルで設定します。index.jsを以下のようにします。Vue3.0の記法になっているので注意してください。

vue-douter/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Calendar from '@/components/Calendar.vue'

const routes = [
  {
    path: '/',
    name: 'django-calender',
    components: {
      default: Calendar,
    }
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

これで、ルートのURLにアクセスするとCalendar.vueというコンポーネントが表示されるようになります。本来なら、URLによって表示させるコンポーネントを変えることができますが、今回はコンポーネントが1つしかないのでVue Routerにはこれだけしか設定しません。

main.js

Vue Routerで設定したCalendar.vueを作成する前に、DRFからデータを受け取るための設定をします。/vue-calendar/src/main.jsを以下のようにします。

vue-calendar/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'


const app = createApp(App).use(store).use(router)

// fetchを定義
app.config.globalProperties.$http = (url, opts) => fetch(url, opts)
// DRFのURL(API用)
app.config.globalProperties.$httpHoliday = 'http://127.0.0.1:8000/api/holiday/'

app.mount('#app')

Vue3.0では、createAppでアプリケーションを作成し、そのアプリケーションに.useで使いたいプラグインを追加していきます。
また、appに対して上記のようにglobalPropertiesを定義しておくと、すべてのコンポーネントで参照できるようになります。$httpとしてfetchメソッド、$httpHolidayとしてAPIのエンドポイント(Djangoで設定したパス)を指定しておきます。これによって、コンポーネント上でthis.$http(this.$httpHoliday)・・・の形でfetchメソッドを呼び出すことができます。

Vuex

続いて、/vue-calendar/src/store/index.jsを以下のようにします。このstoreフォルダの中で、Vuexの設定を行います。ゲッターを通してデータを受け取るようにします。ここで定義したストアのオブジェクトはコンポーネントから自由に呼び出すことができます。DRFから受け取ったデータをストアオブジェクトに入れる処理はここでは行わず、コンポーネントで行います。

/vue-calendar/src/store/index.js
import { createStore } from 'vuex'
import { UPDATE_HOLIDAY } from './mutation-types'


export default createStore({
  strict: process.env.NODE_ENV !== 'production',
  state: {
    holiday: {},
  },
  getters: {
    holidayList(state) {
      return state.holiday
    }
  },
  mutations: {
    [UPDATE_HOLIDAY](state, payload) {
      state.holiday = payload
    },
  },
  actions: {
    [UPDATE_HOLIDAY]({ commit }, payload) {
      commit(UPDATE_HOLIDAY, payload)
    },
  },
  modules: {
  }
})

上記では、UPDATE_HOLIDAYという定数をmutation-types.jsから呼び出しています。
このように、ミューテーションのメソッド名を定数化して一つのファイルにまとめると、操作を簡単に把握できます。(今回は1つしかないのでいらなくね?という気もしますが)
mutation-types.jsを以下のようにします。

/vue-calendar/src/store/mutation-types.js
// 処理を把握しやすくするため定数化
export const UPDATE_HOLIDAY = 'updateHoliday'

コンポーネント作成(その2)

いよいよ先ほどRouterで呼び出したCalendar.vueを記述していきます。

長いので非表示にします。結構スパゲティコードですがすみません。

カレンダーの仕様、デザイン自体はほぼこちらを参考にさせていただいております。

/vue-calendar/src/components/Calendar.vue
vue-calendar/src/components/Calendar.vue
<template>
  <div>
    <!-- カレンダーヘッダー -->
    <div id="cal-header">
      <span class="header-arrow" @click="setLastMonth"></span>
      <span class="selected-month">{{ year }}.{{ month }}</span>
      <span class="header-arrow" @click="setNextMonth"></span>
    </div>
    <!-- カレンダーテーブル -->
    <table id="cal-main">
      <thead>
        <th v-for="dayname in weekdays" :key="dayname">{{ dayname}}</th>
      </thead>
      <tbody>
        <tr class="cal-day" v-for="(weekData, index) in calData" :key="index">
          <td v-for="(dayNum, index) in weekData" :key="index" @click="dateClick(dayNum)" :class="{'cal-today': isToday(dayNum), active: day === dayNum, isSaturday: index === 6, isSunday: index === 0, 'cal-holiday': isHoliday(dayNum)}">
            <span v-if="isToday(dayNum)">
              <strong>today</strong>
            </span>
            <span v-else>{{ dayNum }}</span>
          </td>
        </tr>
      </tbody>
    </table>
    <!-- 日付表示部分 -->
    <div class="holiday">
      <p>Today:{{ today }}</p>
      <button @click="setNextHoliday">Let's check next holiday!</button>
      <ul v-show="checkHoliday">
        <li>Next Holiday:{{nextHolidayDate}} ({{nextHolidayName}})</li>
        <transition>
          <li v-show="checkHoliday" class="check">
            There's
            <strong>{{termUntilNextHoliday}} days</strong> to next holiday...
          </li>
        </transition>
      </ul>
    </div>
  </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
import { UPDATE_HOLIDAY } from '../store/mutation-types'
export default {
  data() {
    return {
      weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Tur', 'Fri', 'Sat'],
      year: 2020,
      month: 10,
      day: -1,
      today: '',
      checkHoliday: false,
      nextHolidayObj: '',
      nextHolidayName: '',
      nextHolidayDate: '',
      termUntilNextHoliday: 0
    };
  },
  computed: {
    ...mapGetters(['holidayList']),
    calData() {
      const calData = []
      const firstWeekDay = new Date(this.year, this.month - 1, 1).getDay()
      const lastDay = new Date(this.year, this.month, 0).getDate()
      let dayNum = 1
      while (dayNum <= lastDay) {
        const weekData = []
        // 日曜~土曜の日付データを配列で作成
        for (let i = 0; i <= 6; i++) {
          if (calData.length === 0 && i < firstWeekDay) {
            // 初週の1日以前の曜日は空文字
            weekData[i] = ''
          } else if (lastDay < dayNum) {
            // 最終日以降の曜日は空文字
            weekData[i] = ''
          } else {
            // 通常の日付入力
            weekData[i] = dayNum
            dayNum++
          }
        }
        calData.push(weekData)
      }
      return calData
    }
  },
  created() {
    // 祝日データ取得
    this.$http(this.$httpHoliday)
      .then(response => {
        return response.json()
      })
      .then(data => {
        this[UPDATE_HOLIDAY](data)
      })
  },
  mounted() {
    // 今日の日付を文字列で算出
    const date = new Date()
    const y = date.getFullYear()
    const m = ('0' + (date.getMonth() + 1)).slice(-2)
    const d = ('0' + date.getDate()).slice(-2)
    this.year = y
    this.month = Number(m)
    this.today = y + '-' + m + '-' + d
  },
  methods: {
    ...mapActions([UPDATE_HOLIDAY]),
    setLastMonth() {
      if (this.month === 1) {
        this.year--
        this.month = 12
      } else {
        this.month--
      }
      this.day = -1
    },
    setNextMonth() {
      if (this.month === 12) {
        this.year++
        this.month = 1
      } else {
        this.month++
      }
      this.day = -1
    },
    dateClick(dayNum) {
      if (dayNum !== '') {
        this.day = dayNum
      }
    },
    isToday(day) {
      const date = this.setDateToString(day)
      if (this.today === date) {
        return true
      }
      return false
    },
    setDateToString(day) {
      // 日を年月日の文字列にして返す
      const date =
        this.year +
        '-' +
        String(this.month).padStart(2, '0') +
        '-' +
        String(day).padStart(2, '0')
      return date
    },
    isHoliday(day) {
      // Ajaxで取得した日と一致しているかチェック
      const date = this.setDateToString(day)
      for (const holiday in this.holidayList) {
        if (this.holidayList[holiday].date === date) {
          return true
        }
      }
      return false
    },
    calcTermDays(day1, day2) {
      // Dateオブジェクトの日数の差分を計算
      return (day2 - day1) / 86400000
    },
    calcNextHoliday() {
      // 次の祝日までの日数、および次の祝日のオブジェクトを返す
      for (const holiday in this.holidayList) {
        const holidayToDate = new Date(this.holidayList[holiday].date.split('-'))
        const todayToDate = new Date(this.today.split('-'))
        const termDays = this.calcTermDays(todayToDate, holidayToDate)
        if (termDays >= 0) {
          return [termDays, this.holidayList[holiday]]
        }
      }
    },
    setNextHoliday() {
      // 次の祝日の名前、日数をセット
      [this.termUntilNextHoliday, this.nextHolidayObj] = this.calcNextHoliday()
      this.nextHolidayName = this.nextHolidayObj.name
      this.nextHolidayDate = this.nextHolidayObj.date
      this.checkHoliday = true
    }
  }
};
</script>


<style scoped>
body {
  margin: 0;
}
#cal-header {
  font-size: 24px;
  padding: 0;
  text-align: center;
  margin-bottom: 10px;
  background-color: green;
  border-bottom: 1px solid #ddd;
  display: flex;
  justify-content: space-between;
}
#cal-header span {
  padding: 15px 20px;
  color: white;
  display: inline-block;
}
#cal-header .header-arrow {
  cursor: pointer;
}
#cal-main {
  font-size: 14px;
  line-height: 20px;
  table-layout: fixed;
  width: 100%;
  margin-bottom: 1rem;
  color: #212529;
  border-bottom: 1px solid #ddd;
  border-collapse: collapse;
}
#cal-main th {
  padding: 0;
  text-align: center;
  vertical-align: middle;
  font-weight: normal;
  color: #999;
}
#cal-main td {
  padding: 8px;
  text-align: center;
  vertical-align: middle;
  border-top: 1px solid #ddd;
}
.cal-today {
  background-color: #fcf8e3;
}
.cal-holiday {
  color: red;
}
.cal-day .active {
  background-color: #ffc9d7;
}
.cal-day .isSaturday {
  color: blue;
}
.cal-day .isSunday {
  color: red;
}
.holiday {
  position: absolute;
  top: 400px;
  right: 0;
  left: 0;
}
p {
  font-size: 30px;
}
li.check {
  font-family: "Eater", cursive;
}
button {
  margin: 30px auto 10px;
  width: 300px;
  font-size: 20px;
  padding: 5px;
  border: solid 1px #000;
  line-height: 30px;
  background: darkblue;
  color: #fff;
}
button:hover {
  cursor: pointer;
}
ul {
  list-style: none;
}
li {
  margin: 15px;
}
li > strong {
  color: red;
}
li {
  font-size: 30px;
}
.v-enter-active {
  transition: opacity 3s;
}
.v-enter-from {
  opacity: 0;
}
.v-enter-to {
  opacity: 1;
}
@media (min-width: 900px) {
  #cal-main,
  #cal-header {
    width: 900px;
    margin: auto;
  }
}
</style>

ここでは簡単に、DRFから受け取ったデータの処理についてのみ記述します。やっていることとしては、DRFから受け取った祝日の日付と、該当の日付を比較して、一致したら祝日と認定されるクラスを付与しています。また、祝日と本日の日付を比較し、次にやってくる一番近い祝日を特定しています。そしてボタンが押された際に、イベントで表示するようにしています。
データの処理としては、まず、computedでmapGetters、methodsでmapAtsionsを定義して、ストアのプロパティ名で呼び出せるようにしています。
createdでは、先ほどmain.jsのグローバルプロパティで定義したfetchを呼び出し、DRFから受け取った祝日のデータを最終的にストアのholidayプロパティから参照できるようにしています。

index.html

あとは/vue-calendar/public/index.htmlのタイトルを修正し、Webフォント呼び出しのリンクをつけると完了です。
Google Fontsで、ホラーチックなフォントを探しました。 (当時ここに一番時間がかかった記憶)

vue-calenadar/public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- フォントのリンク -->
    <link href="https://fonts.googleapis.com/css2?family=Eater&display=swap" rel="stylesheet" />
    <!-- タイトル修正 -->
    <title>django_calendar</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

その後、Djangoのローカルサーバーを立ち上げ、かつfrontend直下でnpm run serveを実行してhttp://127.0.0.1:8080/ にアクセスすると、カレンダーが表示されます。Djangoで登録した祝日が赤くなっており、ボタンを押したら直近の祝日が表示されるようになっています。

終わりに

2020年9月にVue3がリリースされ、過去に作ったアプリケーションをVue3に更新してみたのですが、今回その成果物を記事に紹介させていただきました。以前からZennに何か投稿してみたいと思っていたので、いいきっかけになりました。

自分はアウトプットしながら吸収していく派なので、知識が断片的になってしまいがちですが、いったん立ち返って復習しつつ、もっと理解を深めていきます。