はじめに
こんばんは。
今年もあと残すところわずかですね。
どうにか今年中にこの記事を出したいと思い
大掃除や年の瀬の仕事の合間にひいひい言いながら書いています。
何を作ったのか
今回NuxtとCompositionAPIの組み合わせでポートフォリオサイトを作りました。
サイト→https://portfolio-vng3vvhhsa-an.a.run.app/
Github→https://github.com/tomopict/portfolio-by-nuxt
※1 TopVIewは時間によって背景が変わるんですが、docker内のTZがJSTになっていないらしく、時差が出ています、、、これは今後解決予定。
※2 課金が怖いので小さく運用しています
なぜ作ったのか
自分で個人のサービスを運用していない身としては
ポートフォリオを作ってもコンテンツにできるようなものはあまりなく、
単にサイト作って終わりになりがちです。
ただ色々と試したいことはあるが、通常の業務ではなかなか取り入れるきっかけがない。
なのでポートフォリオサイトそのものを作ることを目的にはせず、
実装によって得た知見を仕事や他のプロダクトに活用したかったというところを目的としました。
試したかった事
まず試したかったことをリストアップして出してみました。
今回試してみたかった事は以下の通り。
- NuxtとcompositionAPIの組み合わせ * 上記の場合での外部APIとの通信
- intersectionObserverを利用したアニメーション
- TweenMaxを利用したアニメーション
- Dockerでのデプロイ
上記のうちTweenMaxを利用したアニメーション以外は実装しました。
アニメーションについてはポートフォリオサイトとしてはリッチにしていきたいですが、
今回は年内にリリースする事にプライオリティを置いていたので、それらについては今後の実装に組み込むことにします。
ますは基本の構成です。
Nuxt v2.10.2
CompositionAPI v0.3.2
yarn v1.13.0
ここからはそれぞれの実装ではまったところ(おもしろ苦しかった所)について触れていきます。
compositionAPIをNuxtで使う
今までNuxtを使いかつTypeScriptを利用する場合はデコレータを利用して開発をしていました。
ただVue3.xからはClassAPIが非推奨になるようです。
https://github.com/vuejs/rfcs/pull/17#issuecomment-494242121
this proposal has been dropped.
それも踏まえて現在Nuxt + TSで実装する場合ClassAPIも含めて3種類の実装方法が考えられます。
- OptionsAPI
- CompositionAPI
- ClassAPI
デコレータ自体はすでに非推奨になっていることもあり新たにNuxtを使うにあたっては、なるべく上2つのどちらかを選びたいところです。
それも受けて今回はcompostionAPI(+一部optioosAPI)をベースにして開発を進めることにしました。
なおcompostionAPIについてはまだ開発中なこともあり、実際のプロダクトで運用するのは
まだ難しい印象です。
もし導入を考えられている方がいたら、あくまで自己責任で利用いただければと思います。
以下に実際に実装してみて所々悩む場面を記載していきます。
asyncDataはsetup内では使えない
Nuxtの場合、サーバーサイドで処理を終わらせたい場面が出てきます。
その場合以前であったらasyncDataというメソッドの中で処理を行うと、サーバー側で呼び出しがされクライアント側からはそれにアクセスすれば良い形になっていました。
ですがsetup内でasyncDataは利用できません。
なので
- 事前にサーバー側でデータをとってきたい
- 該当のコンポーネントでしか利用しない(storeにいれる必要がない)
場合にOptionsAPIと組み合わせることでどうにか実装をしています。
この辺の実装に関してはこちらの記事を非常に参考にさせていただきました。
interface weatherParamModels {
p: string
APPID: string
}
export default Vue.extend({
name: 'Default',
components: {
Footer
},
setup() {},
asyncData() {
const weatherParam = reactive({
q: 'tokyo',
APPID: `${process.env.WEATHER}`
})
const getEvents = usePromise(weatherParam => fetchWeatherData(weatherParam))
getEvents.createPromise(weatherParam)
}
})
</script>
データの取得フロー
外部APIと通信している部分として2箇所あります。
東京の天気予報を取得してきているところと、Qiitaの記事を取得してきているところです。
※前者については実装はしているもののまだ実際にページの中で何か利用しているわけではありません。
今回実装するにあたり考えたパターンは4種類
- 単一のコンポーネントだけで使用される
- storeに入れて複数のページやコンポーネントで使い回す
- サーバー側で取得して単一のコンポーネントで利用する
- クライアント側で取得して単一のコンポーネントで利用する
クライアント側で取得して単一のコンポーネントで利用する
まず前提として
外部APIとのやりとりに関してはservisesディレクトリにまとめています。
axiosをwrap した httpd-client.ts をベースにそれぞれの API の問い合わせを行っています。
storeを利用するものに関しては直接servicesを呼ばずにstoreのなかのactionを参照しています。
storeの参照はgetter経由で行っています。
comonentの内部、あるいは一過性でしか利用しないものに関しては
services内のメソッドをcomposablesディレクトリ内のuse-promise.tsから呼び出して
で利用しています。
これによってloadingであったりエラーハンドリングが非常にやりやすくなりました。
import { AxiosPromise, AxiosRequestConfig } from 'axios'
import httpClient from './http-client'
export const fetchWeatherData = (
parms: {
q: string,
APPID: string
}
): AxiosPromise => {
return httpClient.get(`http://api.openweathermap.org/data/2.5/weather?q=${parms.q}&APPID=${parms.APPID}`)
}
import { ref } from '@vue/composition-api'
export default function usePromise(fn: any) {
const results = ref(null)
const error = ref(null)
const loading = ref(false)
const createPromise = async(...arg) => {
loading.value = true
error.value = null
results.value = null
try {
results.value = await fn(...arg)
} catch {
error.value = null
} finally {
loading.value = false
}
}
return { results, error, loading, createPromise }
}
実際には以下のようにして呼び出しています。
// ファイル内でimportして
import usePromise from '@/composables/use-promise.ts'
asyncData() {
const weatherParam = reactive({
q: 'tokyo',
APPID: `${process.env.WEATHER}`
})
// 使用したい場所で引数にfunctionを渡します
const getEvents = usePromise(weatherParam => fetchWeatherData(weatherParam))
getEvents.createPromise(weatherParam)
}
storeに入れて複数のページやコンポーネントで使い回す
storeにいれる場合はactionsを呼び出して処理を行っています。
具体的には以下のような形。
// コンポーネントでstore内のモジュールをimport
import { qiitaModule } from '@/store/store'
private async getQiitaListsFromApi(): Promise<void> {
try {
// module内のアクションを利用
await qiitaModule.getQiitaUserData({
type: 'user',
user: 'tomopict'
})
} catch (e) {
console.log(e)
}
}
storeの中の該当箇所は以下です。
@Action
public async getQiitaUserData({
type,
user
}: {
type: string
user: string
}): Promise<void> {
const { data } = await fetchQiitaUserData({headers: {
Authorization: `Bearer ${process.env.Qiita}`
}})
this.SET_QIITA_LIST(data)
}
デプロイまで
今回本番環境もDockerを利用してデプロイしたかったので、GCPのCloudRunを利用しました。
AWS上での運用も考えましたが
- 個人で利用するにはオーバースペックである印象を受けた
- 仕事でAWSを使用していることもあり、他サービス上での環境構築を試したい
などの理由からCloudRunを選択しました。
使ってみると非常に使いやすく、楽な時代になったなーと感じています。
GoogleCloudRunに関しては素晴らしい記事がありましたのでそちらを参考にしていただければと思います。
また今回のデプロイまでについては長くなりそうなので、また別の記事にまとめます。
参考にさせていただいた記事
https://www.topgate.co.jp/gcp08-how-to-use-docker-image-container-registry
https://qiita.com/jakushin/items/dd92075f28fba6b083ca
https://inside.dmm.com/entry/2018/11/06/nuxt2-pwa-gae-se
その他
また大きなとこではありませんが以下のような組み合わせでの実装も出来たので、
小粒ではありますがまた改めて記事にできたらと思います。
- Nuxt+FontAwesomeを利用する場合に必要最小限かつコンポーネント化して利用する方法
- CSSGridでビルを描く(topview背景部分)
- compostionAPIでdaysjswp使う際の型定義の方法
上記で参考にさせていただいた記事
- https://qiita.com/flowphantom/items/6d3ffa67c625b76d9f24
- https://github.com/iamkun/dayjs/issues/611
- https://www.youtube.com/watch?v=C1BripDJktc&feature=youtu.be
最後に
最後まで読んでいただきありがとうございます。
なんとか年内に終わらせたいと思い、どうにかまとめた感がありますが
当初やりたかったことはそれなりにできたんじゃないかなぁという所感です。
ポートフォリオサイトとしてはまだまだ味気ないので、
ここからアニメーションをつけていったり、
何よりここに載せられるコンテンツを増やしていきたいと考えています
これは2020年の課題ですね。
冒頭にも書きましたがポートフォリオサイトを作ることが目的ではなくて、
これで得た知見を仕事であったり個人的なプロダクトに活かすのが目的なので、
2020年も勉強しながらやっていく所存です。
それでは今年も残すところあと数時間ですが、皆様良いお年をお過ごし下さい!
筆者はこれから年越しライブへ向かいます!ヒャッホイ!