- 投稿日:2020-12-12T23:22:01+09:00
Vueでフォームを表示せずに画像の更新を行う方法
結論
See the Pen image submit with vue by msickpaler (@msickpaler) on CodePen.
clearImage() { this.image = "https://i.gyazo.com/459b578735367db587a1193f9afe84da.png" document.getElementById('input_image').value = '' //忘れやすい }inputの値を初期化しないと、
画像選択→クリア→同じ画像選択
したときに2回目の画像選択でinputの値が変わらず@changeで発火しないのでちゃんと初期化しましょう。
- 投稿日:2020-12-12T23:00:51+09:00
2020年もそろそろ終わるし今年出た Nuxt.js の機能をおさらいしようぜ
この記事はNuxt.js Advent Calendar 2020の14日目の記事です。
はじめに
2020年9月19日、Vue.jsのバージョン3.0「One Piece」がリリースされました。
Nuxt.js も Vue3 の素敵な機能 (Fragments や Multiple v-model binding など) にいち早く対応してほしいと願うものです。
Vue3 からサポートされるようになる機能は他にもありますが、一番盛り上がったと思うのは Composition API だと思います。
まだまだ発展途上の機能ではありますが、ロジックごとにコードを分割し、再利用が簡単な書き方ができるために注目を浴びています。
FragmentsやMultiple v-model bindingにはまだ対応していない Nuxt.js ですが、Composition APIは Nuxt.js の公式がモジュールを提供しています。ただ、Nuxt.js ×
Composition APIについて調べても、公式モジュールを使用した記事があまり見つからないため、意外と認知されていないのかも? と思いました。そこで、今年もそろそろ終わりということもあり、2020年にNuxt.jsがリリース・サポートした内容をいくつかピックアップし、紹介するのが今回の目的です。
機能一覧
- Composition API
- Fetch Function
- SSG (Static Site Generation)
- Content
- RuntimeConfig
- Auto import component
- NuxtConfig Context
Composition API
ソース
モジュールのリポジトリ: nuxt-community/composition-api
概要
Vue3 の目玉機能でもある
Composition APIを、Nuxt.js 独自の機能が扱えるよう拡張したモジュールがあります。
Vue Composition APIの説明は他の記事でもたくさんありますが、Nuxt Composition APIの拡張もなかなかなもので、そのうちのいくつかを説明します。ここでは特に使いそうな
useAsync,useFetch,useContextの三つを簡単に紹介します。先に例となるコードを記載します。export default defineComponent({ setup() { const { $axios, app, params } = useContext() const response = useAsync(() => { return $axios.$get<string>('') }) // response: ref<string | null> const foo = ref<string | null>(null) const { $fetchState } = useFetch(async () => { foo = await $axios.$get<string>(params.value.id) if (!foo) { app.router!.push('/') } }) return { isLoading: computed(() => $fetchState.pending) } } })
useAsyncは従来の asyncData に該当する関数です。返り値はRef型です。データをプリレンダリングしたい際に使用するので fetch と使い分けになるとは思います。個人的には、1ページ内で何回も呼び出すケースでは fetch を、一回のみ読んでプリレンダリングのためだけにほしい場合には asyncData を使うことが多いです。
その fetch を Composition API の中で使うための関数が
useFetchです。返り値は{ fetch, fetchState, $fetch, $fetchState }の四つを含んだオブジェクトです (先端の$マークの有無の差はないです)。export const useFetch = (callback: Fetch) => { ... function result() { return { fetch: vm!.$fetch, fetchState: vm!.$fetchState, $fetch: vm!.$fetch, $fetchState: vm!.$fetchState, } } ... return result() }
useContextは、従来のContext に加え、routeやquery,from,paramsを返します。query,from,paramsは単にrouteから引っ張ってきているだけです。export const useContext = (): UseContextReturn => { ... return { ...(vm[globalNuxt] || vm.$options).context, route: computed(() => vm.$route), query: computed(() => vm.$route.query), from: computed(() => vm.$route.redirectedFrom), params: computed(() => vm.$route.params), } }その他にも、
vue-metaが内包されているため、defineComponentの中でheadを定義できたり、useMetaでメタ情報を取得できたりもします。Fetch Function
ソース
対応バージョン: 2.12.0
該当のPR: #6880
ドキュメント: Understanding how fetch works in Nuxt 2.12
概要
Composition API の説明でも述べましたが、Nuxt には fetch という関数があります。これが v2.12.0 以前と以降で、大きく役割が変わっています。
v2.12.0 以前に搭載されていた fetch は、引数に context を持ち、Vue インスタンスが生成される前に呼ばれるライフサイクルでした。すなわち、ほとんど asyncData と同じ機能でした。
v2.12.0 以降の fetch は、created のあとに呼ばれるライフサイクルフックの一つです。Vue インスタンスが生成されたあとなので、従来の書き方における
thisが使えます。さらに、
this.$fetch()と書くことで、任意のタイミングで fetch 関数を発火させることができます (これがすごい便利)。さらに、コンテキストに
$fetchStateが追加され、エラー、fetch の状態、fetch が呼ばれた時刻、にアクセスできます。declare module 'vue/types/vue' { interface Vue { ... $fetch(): void $fetchState: { error: Error | null pending: boolean timestamp: number } } }そのため、ロード中は非表示にする、などの処理は
<div v-if='$fetchState.pending'> <!-- ロード中に表示させて置きたい項目 e.g. ローディングアニメーション --> </div> <div v-else> <!-- ローディング後に表示させたい項目 --> </div>のようにして、簡単に実装できます。
SSG (Static Site Generation)
ソース
対応バージョン: 2.13.0
該当のPR: #6159 など
概要
2020年の3月10日に Reactベースのフレームワークである Next.js が SSG (Static Site Generation) に対応しました。
Nuxt.js が SSG に対応したのは、そこから3ヶ月経った6月19日のことでした。
SSGは、SPAよりもSEOに優れ、SSRよりも高速なページレンダリングが可能なレンダリング方式です。
SSGとしてレンダリングするためには、
nuxt.configに以下のような記述をします。export default { mode: 'ssr', target: 'static' ... }そして、
nuxt exportを行うことでファイルを生成することができました。……ところが、2020年12月現在では、この方法では二つの
deprecatedが含まれています。今は次のように行いましょう。まず
nuxt.configに以下のように書きます。export default { ssr: true, target: 'static' }Nuxt v2.14.5 以降、
modeプロパティはdeprecatedになり、ssrを使うことが推奨されています。それから、
nuxt generateを叩きます。もともと
nuxt generateが存在し、v2.13.0 でnuxt exportがそれを置き換えるように登場したのですが、哀れにも数ヶ月でnuxt exportはdeprecatedに降格しました。@jagaapple さんの記事『Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな』 にもある通りですが、Nuxt.js でも SSR を行うよりは SSG を行ったほうがほとんどのケースで優れていると思います。
Content
ソース
モジュールのリポジトリ: nuxt/content
概要
Empower your NuxtJS application with @nuxt/content module: write in a content/ directory and fetch your Markdown, JSON, YAML, XML and CSV files through a MongoDB like API, acting as a Git-based Headless CMS.
@nuxt/content は、content ディレクトリ以下に入れた Markdown とか JSON とかのファイルをいい感じに書き出してくれるGitベースのヘッドレスCMSです (意訳)。
content というディレクトリを作成し、その中に Markdown などのファイルを入れると、ページから読み込んで表示することが可能です。
Markdown のなかでコンポーネントを使えるため、従来の Markdown で書けるブログよりも遥かに拡張性が高くなります。
以下のようなディレクトリ構造を考えます。
content ├ 2020 │ └ 12 │ ├ hogehoge.md │ └ fugafuga.md └ 2021 └ 1 └ foobar.md pages └ _year └ _month └ _slug.vue
_slug.vueの中身はこのようになっています。<template> <h1>{{ article.title }}</h1> <nuxt-content :document="article" /> </template>export default Vue.extend({ async asyncData ({ $content, params }) { const { year, month, slug } = params const article = await $content(year, month, slug).fetch() return { article } }, })ここで、
/pages/2020/12/hogehogeにアクセスすると、year が 2020, month が 12, slug が hogehoge になるため、content/2020/12/hogehoge.md を取得してくれます。Json なども扱えるので、会社のブログなどで、従業員全員のデータを Json 形式で content 直下にいれておき、名前やアイコンなどをそこから取得する、ということもできますね。
SSG にも対応しているので、超高速なコーポレートブログが描画できそうです。
RuntimeConfig
ソース
対応バージョン: 2.13.0
該当のPR: #7312など
概要
nuxt.configにはもともとenvというプロパティがあり、ここで環境変数を扱うこともできました (僕は使ったことがないので仔細はわかりません)。あるいは、@potato4d さんの記事『Nuxt.js + TypeScript のアプリケーションで環境変数を安全に管理する』のように、
environments.tsを作成するケースもあるかと思います。どちらを採用するにせよ、ページやコンポーネント、プラグインなどで
process.envから直接値を引っ張ってくるのは型安全でなく、タイポの危険性も孕むため推奨されないでしょう。v2.13.0 で追加された
RuntimeConfigという機能は、環境変数をNuxt.jsで扱いやすくするためのものです。
envプロパティ、environments.ts、process.env直アクセス、いずれの方法にしても、クライアント側から閲覧できてしまうため、うっかりシークレットを入れてしまうと漏れうるリスクがあります。このたび
nuxt.configにはprivateRuntimeConfigとpublicRuntimeConfigという二つのプロパティが追加されました。
privateRuntimeConfigで定義した値はサーバーサイドでのみ利用が可能で、クライアント側からアクセスしようとするとundefinedになります。さらに、RuntimeConfig に定義した値は Nuxt.js の
context.$configからアクセスすることができます。API_SECRET=hogehoge BASE_URL=fugafugaexport default { ssr: true, privateRuntimeConfig: { apiSecret: process.env.API_SECRET }, publicRuntimeConfig: { baseUrl: process.env.BASE_URL } }export default Vue.extend({ asyncData ({ $config }) { console.log($config.apiSecret) // hogehoge console.log($config.baseUrl) // fugafuga }, fetch ({ $config }) { console.log($config.apiSecret) // Server: hogehoge, Client: undefined console.log($config.baseUrl) // Server: hogehoge , Client: fugafuga } })※ SSR時、
asyncDataはサーバーサイドでのみ、fetchはサーバーサイドとクライアントサイドの両方で呼ばれる。型拡張すれば型安全に、シークレットをクライアントに公開することなく、環境変数を使いやすくすることができます。
ちなみに、Next.jsでは同様の機能が2018年3月27日にリリースされています……
Auto import component
ソース
対応バージョン: 2.13.0
該当のPR: #7374
モジュールのリポジトリ: nuxt/components
概要
従来は
<template> <the-button /> </template>import TheButton from '@/components/TheButton.vue' export default Vue.extend({ components: { TheButton } })のように、明示的にインポートし、components プロパティの中に定義してあげる必要がありました。
nuxt.configにcomponents: trueと記載すると、ページ内で使われているコンポーネントを自動でインポートしてくれるようになります。すなわち、
<template> <the-button /> </template>だけで十分ということです。
難点は、Veturによるコンポーネントのpropsのサジェストやバリデーションが走らないということですかね……
NuxtConfig Context
ソース
対応バージョン: 2.12.0
該当のPR: #6855
概要
NuxtConfig Context は僕が勝手に命名しました。正直、これを使うケースはあまり多くないとは思いますが、こんな機能があったのか、ということを広めるためにここで紹介します。
Nuxt の設定を書く
nuxt.configで関数を export すると、その関数の引数にcontextが渡されるようになりました。export default (context) => ({ ...Config })ここでいう
contextは、Nuxt で通常使われるcontextとは異なります。Note: using the term context might be confusing as this is ofc not the normal Nuxt context people are used to. Not sure how to call it differently though
contextには
{ commant: 'dev' | 'build' | 'generate' ... dev: boolean }が含まれるようになっています。
export default ({ command, dev }) => {}のように分割代入を用いて取得することも可能です。
nuxt-ts(通常npm run devかyarn dev) が呼ばれたときには{ command: 'dev', dev: true }が返り、
nuxt-ts build(通常npm run buildかyarn build) が呼ばれたときには{ command: 'build', dev: false }が返ってきます。
ちょっとニッチな機能ですが、どう使うケースがあるんですかね?
process.env.NODE_ENV !== 'production'の替わりに使う以外の道が思い浮かばないです。PRに関連付けられていたIssueを見ると、
nuxt-apolloに使えるのではないか? という機能でしょうか。おわりに
以上、7機能を紹介しました。
最近は、React および Next.js の市場シェア率の大きさや、Svelte および Sapper といった新進気鋭のフレームワークが台頭など、若干 Vue や Nuxt.js に対する向かい風が強くなった気がします。
特に、runtimeConfig は Next.js でできていた機能がやっとできるようになった、という印象が拭えません。
しかし、React や Next.js に対する初速の速さや学習コストの低さ、自由度の高さなどを考えると、まだまだ棲み分けができる状況と考えています。
最新の技術をキャッチアップしつつ、Vue や Nuxt.js を使いこなせるといいなと思います。この記事を見て、「こんな機能あったんだ」と、新たな発見につながれば幸いです。
- 投稿日:2020-12-12T22:38:13+09:00
【写真とコード付き】Vue.jsで書くイベント処理の方法【4選】
はじめに
この記事はVue.jsの基本は抑えられている程で、話を進めていきます。もしVue.jsを初めて触ると言う方は、こちらの記事を参照していただければと思います。
⬇️【写真とコード付き】Vue.jsの構築から基本的な書き方まで1から解説【超初心者向け】
https://qiita.com/yuki4839/items/62f40564e3f4c8dbfc51
では今回は、Vue.jsでイベントの発火によく使用されるであろう、
v-onディレクティブについて、詳しく解説していきます。早速いきましょう。
実行環境
使用ツール、デバイスはこちら
- Google chrome
- Mac OS Catalina
- Visual Studio Code
また今回使用するディレクトリ階層はこちら。
ディレクトリ階層─ root(任意のディレクトリ) │ ├─ index.html │ ├─ css │ └ style.css │ └─ js └ main.js
各ファイルの初期値はこちら。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="./css/style.css"> </head> <body> <div id="web"> <p> {{ context }} </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="./js/main.js"></script> </body> </html>style.css/* 出力結果を見やすくするためのスタイルです */ body { background-color: #add8e6; } #web { background-color: #fff; margin: 20px; padding: 20px; width: 300px; }main.jsconst web = new Vue({ el: '#web', data: { context: `Hello Vue.js!` } })
現時点での出力結果はこちら。
v-on
まず
v-onディレクティブについて簡単に説明すると、ボタンなどがクリックされた時に、何らかのイベントを発生させる事ができるディレクティブになります。
クリック処理
最初は
v-onディレクティブで、最も基本的な処理についてです。文章で伝えるよりも、コードを見て理解してもらうのが早いと思いますので、まずは実際のコードをご覧ください。index.html<!-- headタグ省略 --> <body> <div id="web"> <button v-on:click="click"> Click! </button> <p> {{ time }} </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="./js/main.js"></script> </body>main.jsconst web = new Vue({ el: '#web', data: { time: '' }, methods: { click: function() { this.time = new Date().toLocaleString(); } } })
ポイントは、buttonタグに
v-on:clickの属性がある事と、js側では属性値に対応したfunction()が定義されている点です。動作内容としては、ボタンがクリックされると、
click function()が動作し、中の処理が実行されます。(今回なら現在時刻を出力。)
1度のみの処理
続いて
v-onディレクティブで、画面読み込み後に1度だけ発火させたいイベント処理の方法です。まずコードはこちら。index.html<!-- headタグ省略 --> <body> <div id="web"> <button v-on:click.once="click"> Click! </button> <p> {{ time }} </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="./js/main.js"></script> </body>main.jsconst web = new Vue({ el: '#web', data: { time: '' }, methods: { click: function() { this.time = new Date().toLocaleTimeString() } } })ポイントは
v-on:clickの後に.onceを記述している点です。これにより、画面読み込み後は1度イベントが発火すると、そのあとは発火しなくなります。
一見通常のクリック処理と同じですが、このあとボタンを押しても、画面の読み込みを行わない限り更新はされません。(先ほどの
.onceなしの記述の場合は、何度も更新する事ができます。)
引数を利用した処理
続いて
v-onディレクティブを使用した属性値に、引数を渡した際の処理の方法です。まずはコードはこちら。index.html<!-- headタグ省略 --> <body> <div id="web"> <button v-on:click="clickHandler('Hello Vue.js!')"> Click! </button> <p> {{ context }} </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="./js/main.js"></script> </body>main.jsconst web = new Vue({ el: '#web', data: { context: '' }, methods: { clickHandler: function(message) { this.context = message } } })ポイントは
v-on:click属性の属性値に、関数名と引数を設定。そしてボタンがクリックされたら、js側で用意されていたイベントが発火と言う仕組みです。上記の場合は引数で受け取った値を、そのままcontextに代入しているので、出力結果は引数の値になります。
省略記法
最後に
v-onの省略した書き方についてです。まずコードはこちら。(jsは先程と同様です。)index.html<!-- headタグ省略 --> <body> <div id="web"> <button @click="clickHandler('Hello Vue.js!')"> Click! </button> <p> {{ context }} </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script src="./js/main.js"></script> </body>main.jsconst web = new Vue({ el: '#web', data: { context: '' }, methods: { clickHandler: function(message) { this.context = message } } })ポイントは
v-on:の部分が、@に変わった点です。このように省略して書く事が可能です。もしプロジェクトで使用する際は、どちらかに統一しておくと、可読性が上がると思われます。
先程と全く同じ結果が得られました。
まとめ
以上が
v-onディレクティブで良く使われるであろう、イベント処理の記述方法になります。もう一度まとめておくと、以下の通りです。
- クリック処理
- v-on:click="click"
- 1度だけの処理
- v-on:click.once="click"
- 引数を利用した処理
- v-on:click="clickHandler('Hello Vue.js!')
- 省略記法
- @click="clickHandler('Hello Vue.js!')"
また補足として、
v-onディレクティブは他にも多数の使用方法があります。多くは「これいつ使う?」って感じだったので、今回は紹介しておりませんが、もし興味がある方は公式リファレンスをご参照ください。イベントハンドリング(Vue.js公式リファレンス)
https://jp.vuejs.org/v2/guide/events.html最後まで読んでいただき、ありがとうございました!
筆者:yuki|IT業界のリアルな転職事情など発信|元トップ営業マン(訪販)→未経験からエンジニア転職へ
Qiita:https://qiita.com/yuki4839
Twitter:https://twitter.com/yukifullstack
- 投稿日:2020-12-12T20:13:34+09:00
vue3でバリデーションする
vue3でバリデーションについて調べたのでメモ
参考(https://www.npmjs.com/package/@vuelidate/core)環境
vue3
typescriptvuelidateを使う
現時点でアルファ版ぽい
自分の環境ではバージョンの互換性がなかったので--legacy-peer-depsで強制的にインスコnpm install @vuelidate/core @vuelidate/validators --save --legacy-peer-depsmain.tsimport { createApp } from 'vue' import App from './App.vue' import { VuelidatePlugin } from '@vuelidate/core' const app = createApp(App) app.use(VuelidatePlugin) app.mount('#app')App.vue<template> <div> <input v-model="emailAddress" label="メールアドレス" @blur="$v.emailAddress.$touch()" > <p v-for="(error, index) of $v.$errors" :key="index"> {{ error }} </p> </div> </template> <script lang="ts"> import { ref } from 'vue' import { useVuelidate } from '@vuelidate/core' import { email, required } from '@vuelidate/validators' export default { setup () { const name = ref('') const emailAddress = ref('') const rules = { name: { required }, emailAddress: { required, email } } const $v = useVuelidate(rules, { name, emailAddress }) return { name, emailAddress, $v } } } </script>こんな感じです
- 投稿日:2020-12-12T19:49:37+09:00
create-nuxt-appでvuetifyが導入できなかった話
初めに
create-nuxt-appでオプションを選択する際に、vuetifyの導入に手こずってしまいました。何とか解決できたので、メモとして残しておきます。
どんなエラーだったか
? Choose the package manager Yarn ? Choose UI framework Element ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules Axios ? Choose linting tools ESlint ? Choose test framework None ? Choose rendering mode Universal (SSR)以上のオプションで作成をしようとしたら
throw new SAOError(`Failed to install ${packageName} in ${cwd}`)このようなエラーが発生してしまいました。
原因は?
ESlintを選択していたのが不味かったみたいです。代わりにPrettierを選択したら大丈夫そうです。
またもやエラー
調べた通りの方法で解決かと思いきや、さらにエラーが発生。
gyp info it worked if it ends with ok gyp info using node-gyp@5.1.0 gyp info using node@14.8.0 | darwin | x64 gyp info find Python using Python version 2.7.16 found at "/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python" gyp info spawn /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python gyp info spawn args [ gyp info spawn args '/usr/local/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py', gyp info spawn args 'binding.gyp', gyp info spawn args '-f', gyp info spawn args 'make', gyp info spawn args '-I', gyp info spawn args '/Users/grinch1252/nuxt-blog/node_modules/fibers/build/config.gypi', gyp info spawn args '-I', gyp info spawn args '/usr/local/lib/node_modules/npm/node_modules/node-gyp/addon.gypi', gyp info spawn args '-I', gyp info spawn args '/Users/grinch1252/Library/Caches/node-gyp/14.8.0/include/node/common.gypi', gyp info spawn args '-Dlibrary=shared_library', gyp info spawn args '-Dvisibility=default', gyp info spawn args '-Dnode_root_dir=/Users/grinch1252/Library/Caches/node-gyp/14.8.0', gyp info spawn args '-Dnode_gyp_dir=/usr/local/lib/node_modules/npm/node_modules/node-gyp', gyp info spawn args '-Dnode_lib_file=/Users/grinch1252/Library/Caches/node-gyp/14.8.0/<(target_arch)/node.lib', gyp info spawn args '-Dmodule_root_dir=/Users/grinch1252/nuxt-blog/node_modules/fibers', gyp info spawn args '-Dnode_engine=v8', gyp info spawn args '--depth=.', gyp info spawn args '--no-parallel', gyp info spawn args '--generator-output', gyp info spawn args 'build', gyp info spawn args '-Goutput_dir=.' gyp info spawn args ] No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'.原因は?
こちらの場合はXcodeの再インストールで直りました。DeveloperToolsCLI周りで問題が有ったのですかね?
xcode-select --install以上のコマンドで再インストール。
$ yarn add -D @nuxtjs/vuetify success Saved lockfile. success Saved 9 new dependencies. info Direct dependencies └─ @nuxtjs/vuetify@1.11.2 info All dependencies ├─ @nuxtjs/vuetify@1.11.2 ├─ clone-deep@4.0.1 ├─ detect-libc@1.0.3 ├─ fibers@4.0.3 ├─ sass-loader@8.0.2 ├─ sass@1.30.0 ├─ shallow-clone@3.0.1 ├─ vuetify-loader@1.6.0 └─ vuetify@2.3.21 ✨ Done in 44.55s.無事にvuetifyの導入ができました。
参考にさせていただいた記事
- 投稿日:2020-12-12T17:16:48+09:00
オブジェクトに新しいプロパティの追加・削除(Vue.js)
今回はアドベントカレンダーっぽく、面白い記事を書きたい!と思っていましたが、
ネタが出てこず(笑 )だったので、いつも自分が忘れてしまうVue.jsでオブジェクトに新しくプロパティを追加・削除する方法をアウトプットします。ご了承くださいませはじめに
Vue.jsは、dataやpropsの変更を検知して、自動的にDOMに反映してくれます。ですが、変更を検知できない追加方法があり、その方法でオブジェクトの追加をしてしまうとDOMに反映されません。
変更検出の注意事項
Vue はプロパティの追加または削除を検出できません。Vue はインスタンスの初期化中に getter/setter の変換を行うため、全てのプロパティは Vue が変換してリアクティブにできるように data オブジェクトに存在しなければなりません
オブジェクトの中に新しいキーと値を直接代入しても、 Vue.jsはそれを監視できない為、値の更新や再描写ができません。下記のサンプルコードをご覧ください。
Hoge.vue<template> <div> <button @click="onClickedButton">追加</button> <p v-for="value in object" :key="value.id"> {{ value }} </p> </div> </template> <script> export default { data() { return { object: { id: 1, name: 'hoge', }, }; }, methods: { onClickedButton() { this.object.age = 30 //この追加方法では、検知機構がないプロパティになってしまう }, }, }; </script>ボタンを押して後にVue.js devtoolsでdataの内容を確認してみても、オブジェクトの中にageというキーと30の値が入っていない状態です。
解決方法
・一つのキーを追加する場合は、this.$setを使用する
this.$set(this.object, 'age', 30)第一引数はキーを追加したいオブジェクト、第二引数はキー名、第三引数は値を指定してあげます。vm.$setインスタンスメソッドを使用してあげることで、リアクティブなプロパティを追加することができます。
上記のサンプルコードの記述を変更してVue.js devtoolsで確認してみると、
はい、オブジェクトの中にageというキーと30の値が入っていることが確認できます。また、ブラウザ画面でも反映されています。
・1.複数のキーを追加する場合は、新しいオブジェクトを作成する(スプレッド構文を使用)
const newObject = { age: 30, country: 'Japan' } this.object = { ...this.object, ...newObject }上記は、スプレッド構文を使用してobjectとnewObjectのそれぞれを展開して、マージした後に再度、objectの代入をしています。こちらで複数のプロパティが追加できます。
ageとcountryのキーと値が無事に追加できていることが確認できます。スプレッド構文について知りたい方がいましたら、他の方が書かれた分かりやすい記事がありましたので、参考文献としてリンクを貼っております↓
JSのスプレッド構文を理解する・2.複数のキーを追加する場合は、新しいオブジェクトを作成する(Object.assignを使用)
スプレッド構文を使用しない場合は、
const newObject = { age: 30, country: 'Japan' } this.object = Object.assign({}, this.object, newObject)第一引数は空のオブジェクト、第二引数と第三引数には既存のオブジェクトと新しいオブジェクトを指定してあげます。
こちらでもスプレッド構文を使用した時と同様の結果になります。個人的には、下記の理由からスプレッド構文を使用する方が良いかなと思っています。
・スプレッド構文の方がモダンかつスッキリと書ける
・Object.assignはIEではサポートしていないまた、下記にはスプレッド構文とObject.assignの違いについて書かれてある記事のリンクを貼っておきますので、ご興味ある方はぜひどうぞ?♂️
Object.assign({},obj)と{...obj}の違いでは、最後にプロパティを削除する方法です。
・プロパティの削除は、this.$deleteを使用する
this.$delete(this.object, 'name')第一引数はオブジェクト、第二引数は削除したいプロパティのキー名
を指定してあげます。
こちらでプロパティの削除ができます。はい、以上になります。
おわりに
見てくださった方ありがとうございます!
もし誤字・脱字等ありましたらご指摘頂けると嬉しいです
よろしくお願いいたします。
来年こそは、アドベントカレンダーらしいネタ記事が書けたらいいな〜?
- 投稿日:2020-12-12T17:06:45+09:00
Vue.jsの使い方【まとめ】
この記事は「超Vue.js 2 完全パック - もう他の教材は買わなくてOK!
」で学んだ内容を忘れない程度にメモしているものになります。
ディレクティブ
vue.jsにおける特別な属性。
記法
v-○○v-once
一度だけしか描写したくない。(後からの処理によって変更されない。)
<p v-once> {{ once }} </p>v-html
エスケープされずに表示可能になる。
XSS脆弱性の危険。<p v-html="html(プロパティ名)"></p>v-bind
htmlの属性にプロパティを入れたい時に使う。
<a v-bind:href="href">リンク</a> 以下省略形。 <a :href="href">リンク</a>さらにオブジェクト型で記述もできる。
<a v-bind="{href:プロパティ,id:プロパティ}">リンク</a>v-on
DOMが変化した時に処理を実行したい時に使える。
[index.html] //クリックはイベント名を指定すればOK。(例:hover) <div id="app"> <button v-on:click="n += 1(処理を書く)"></button> </div> あるいは <div id="app"> //()は省略可 <button v-on:click="countUp()"></button> </div> 省略形 <div id="app"> <button @click="countUp()"></button> </div> ------------------- [index.js] new Vue({ el:'#app', data:{ number:0 }, methods:{ countUp:function(){ this.number +=1 } } })イベントオブジェクトの取得
[index.html] <div id="app"> <p id="sampleText" v-on:mousemove="mousePosition">ここにマウスを載せると下のX、Yの値が変わるよ</p> <p id="result">X:{{x}}, Y:{{y}}</p> </div> ------------------- [index.js] new Vue({ el: '#app', data: { x: 0, y: 0 }, methods: { mousePosition: function(event) { this.x = event.clientX; this.y = event.clientY; // eventの中を見てみると、全てのイベントオブジェクトが入っている console.log(event); } } })引数を与える
イベントオブジェクトを使いたい時は
$eventを使う。[index.html] <div id="app"> <p id="sampleText" v-on:mousemove="mousePosition($event,10)">ここにマウスを載せると下のX、Yの値が変わるよ</p> <p id="result">X:{{x}}, Y:{{y}}</p> </div> ------------------- [index.js] new Vue({ el: '#app', data: { x: 0, y: 0 }, methods: { mousePosition: function(event,hikisu) { this.x = event.clientX / hikisu; this.y = event.clientY / hikisu; // eventの中を見てみると、全てのイベントオブジェクトが入っている console.log(event); } } })v-model
双方向バインディングを使用するためのディレクティブ。
[index.html] <div id="app"> <input type="text" v-model="message"> <p>{{message}}</p> </div> ------------------- [index.js] new Vue({ el: '#app', data: { message:'こんにちは' }, })イベント修飾子
.stop
伝播させないようにする。
<p id="sampleText" v-on:mousemove.stop>ここにマウスを載せると下のX、Yの値が変わるよ</p>prevent
リンクなどをクリックしても反応しなくなる。
キー修飾子
特定のキーの時のみ反応させる。
例えば、エンターキーを押した時は下記の通り。
```
//繋げられる
```知識的な部分
・dataには動的プロパティは使わない。基本的にプロパティの初期値をつけるもの。
・動的なプロパティを使いたい場合はcomputedをつかう。
- 投稿日:2020-12-12T16:50:20+09:00
Vue 3でPresentational and Container Componentパターンを使用する方法
こちらはGoodpatch Advent Calendar 2020 12日の記事です。
Presentational and Container Componentパターンとは
Presentational and Container Componentパターンとは、@dan_abramovさんが提唱したコンポーネントの分割方法です。
大きく分類すると、Presentational ComponentはDOMやスタイルに対する関心を持ち、コンテンツはpropsによってのみ与えられます。一方でContainer Componentはデータや振る舞いを自身以外へ供給する役割を担う他、状態の参照や管理を行います。
主にPresentational ComponentはContainer Componentから参照して用います。また、稀にContainer Componentを別のContainer Componentからの参照によって用いるケースも存在します。逆に、Presentational ComponentがContainer Componenを参照することはありません。
これらを分割することによってインタフェースが共通のコンポーネントを複数種作成する際に、DOMとスタイル(Presentational Component)を何度も記述することを防ぎ、それぞれのコンポーネントのデータや振る舞い・状態の管理を各Container Componentに分離することができるようになります。
元記事はこちら: Presentational and Container Components - Dan Abramov | Medium
今回リファクタリングするコンポーネント
今回はComposition APIで記述したこちらのモーダルコンポーネントをPresentational ComponentとContainer Componentに分割します。
モーダルの機能⚙️
- モーダル展開時に、ラジオボタンの要素(valueとlabelのセット)をComputedでStoreから参照してラジオボタンに反映。
- 「バツアイコン」と「キャンセルボタン」を押下すると、Storeで管理しているモーダルの表示フラグを
falseにcommitしてモーダルを閉じる。- 「決定」を押下すると、選択中のラジオ要素のvalueをStoreに格納した上で、上記同様にモーダルを閉じる。
ソースコード?
selectedSlopeModal.vue<template> <div class="overlay"> <div class="modal"> <div class="modal__header"> <h2 class="modal__header__title"> <Title>好きな坂道を選ぼう!</Title> </h2> <Icon name="times" @click="handleClickClose" aria-label="閉じる" class="modal__header__close" /> </div> <div class="modal__main"> <Radios :value="state.value" name="slope" :options="slopeOptions" :selectedValue="slopeValue" @input="handleUpdateValue($event)" /> </div> <div class="modal__footer"> <Button @click="handleClickCancel" class="modal__footer__button" >キャンセル</Button > <Button theme="primary" @click="handleClickSubmit" class="modal__footer__button" >決定</Button > </div> </div> </div> </template> <script lang="ts"> import { defineComponent, computed, reactive } from "vue"; import store from "@/store"; import Title from "@/components/Title.vue"; import Icon from "@/components/Icon.vue"; import Button from "@/components/Button.vue"; import Radios from "@/components/Radios.vue"; export default defineComponent({ name: "SelectSlopeModal", components: { Title, Icon, Button, Radios }, setup() { //data const state = reactive({ value: "" }); //methods const handleUpdateValue = (event: Event) => { if (event.target instanceof HTMLInputElement) { state.value = event.target.value; } }; const handleClickClose = () => { store.commit("updateModalViewVisible", false); }; const handleClickCancel = () => { handleClickClose(); }; const handleClickSubmit = () => { store.commit("updateSelectedSlope", state.value); handleClickClose(); }; //computed const slopeValue = computed(() => { return store.state.slope.value; }); const slopeOptions = computed(() => { return store.state.slope.options; }); return { state, handleUpdateValue, handleClickClose, handleClickCancel, handleClickSubmit, slopeValue, slopeOptions }; } }); </script> <style lang="scss"> .overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; background: rgba(#000, 0.3); } .modal { display: flex; flex-direction: column; max-width: 400px; width: 100%; max-height: 640px; border: solid 1px #aaa; border-radius: 8px; background-color: #fff; overflow: hidden; &__header { display: flex; align-items: center; justify-content: space-between; border-bottom: solid 1px #eee; background-color: #ebf5fe; padding: 16px 16px 12px; &__close { width: 16px; height: 16px; } } &__main { height: 100%; padding: 16px; } &__footer { display: flex; justify-content: flex-end; padding: 12px 16px 12px; border-top: solid 1px #ddd; &__button { &:not(:first-child) { margin-left: 8px; } } } } </style>store/index.tsimport { createStore } from "vuex"; export default createStore({ state: { modalViewVisible: false, selectedSlope: "", slopeOptions: [ { value: 0, label: "乃木坂(東京都港区赤坂)" }, { value: 1, label: "鳥居坂(東京都港区麻布)" }, { value: 2, label: "けやき坂(東京都港区六本木)" }, { value: 3, label: "日向坂(東京都港区三田)" }, { value: 4, label: "さくら坂(東京都港区六本木)" } ] }, mutations: { updateModalViewVisible(state, payload) { state.modalViewVisible = payload; }, updateSelectedSlope(state, payload) { state.selectedSlope = payload; } } });コンポーネントを分割する方法
方針?
Presentational and Container Componentパターンの方針と同様に、DOMやスタイルを管理する役割(Presentation)と、データや振る舞いを供給する役割(Container)の2種類にファイルを分割します。Containerから供給される値はPresentationのPropsにて受け取ります。
ソースコード(成果物)?
ModalView.vue<template> <div class="overlay"> <div class="modal"> <div class="modal__header"> <h2 class="modal__header__title"> <Title>{{ title }}</Title> </h2> <Icon name="times" @click="handleClickClose" aria-label="閉じる" class="modal__header__close" /> </div> <div class="modal__main"> <Radios :value="value" :name="radioName" :options="options" :selectedValue="value" @input="handleUpdateValue($event)" /> </div> <div class="modal__footer"> <Button @click="handleClickCancel" class="modal__footer__button" >キャンセル</Button > <Button theme="primary" @click="handleClickSubmit" class="modal__footer__button" >決定</Button > </div> </div> </div> </template> <script lang="ts"> import { defineComponent, reactive } from "vue"; import Title from "@/components/Title.vue"; import Icon from "@/components/Icon.vue"; import Button from "@/components/Button.vue"; import Radios from "@/components/Radios.vue"; export default defineComponent({ name: "ModalView", components: { Title, Icon, Button, Radios }, props: { title: { type: String, required: false }, radioName: { type: String, required: false }, value: { type: String, requiired: false }, options: { type: Array, required: false }, cancel: { type: Function, required: true }, close: { type: Function, required: true }, submit: { type: Function, required: true } }, setup(props) { const state = reactive({ value: "" }); const handleUpdateValue = (event: Event) => { if (event.target instanceof HTMLInputElement) { state.value = event.target.value; } }; const handleClickClose = () => { props.close(); }; const handleClickCancel = () => { props.cancel(); }; const handleClickSubmit = () => { props.submit(state.value); }; return { state, handleUpdateValue, handleClickClose, handleClickCancel, handleClickSubmit }; } }); </script> <style lang="scss"> .overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; width: 100vw; height: 100vh; background: rgba(#000, 0.3); } .modal { display: flex; flex-direction: column; max-width: 400px; width: 100%; max-height: 640px; border: solid 1px #aaa; border-radius: 8px; background-color: #fff; overflow: hidden; &__header { display: flex; align-items: center; justify-content: space-between; border-bottom: solid 1px #eee; background-color: #ebf5fe; padding: 16px 16px 12px; &__close { width: 16px; height: 16px; } } &__main { height: 100%; padding: 16px; } &__footer { display: flex; justify-content: flex-end; padding: 12px 16px 12px; border-top: solid 1px #ddd; &__button { &:not(:first-child) { margin-left: 8px; } } } } </style>SelectSlopeModal.tsimport { defineComponent, computed, reactive, h } from "vue"; import store from "@/store"; import ModalView from "@/components/ModalView.vue"; export default defineComponent({ name: "SelectSlopeModal", setup() { const state = reactive({ title: "好きな坂道を選ぼう!", radioName: "slope" }); const close = () => { store.commit("updateModalViewVisible", false); }; const cancel = () => { close(); }; const submit = (value: string) => { store.commit("updateSelectedSlope", value); close(); }; const slopeValue = computed(() => { return store.state.slope.value; }); const slopeOptions = computed(() => { return store.state.slope.options; }); return { state, close, cancel, submit, slopeValue, slopeOptions }; }, render() { return h(ModalView, { title: this.state.title, radioName: this.state.radioName, options: this.slopeOptions, value: this.slopeValue, cancel: this.cancel, onCancel: this.cancel, close: this.close, submit: this.submit }); } });ポイント?
Vue v2で用意されていた仮想DOM構築用の関数
createNodeDescription()(createElement())は、v3からはcreateVNode()(h())にリプレイスされました。
createElement()ではstyleやprops、slot、on(イベントハンドラ)などをオブジェクトで与えることができたのですが、h()ではpropsとslotのみを与えられるという仕様に変わりました。この変更によって、2系ではPresentational Componentで発火したクリックイベントをemitで透過させてCointainer Componentのイベントハンドラを実行するように記述していたのですが、3系ではCointainer Componenからイベントハンドラをpropsで渡した上でPresentational Componentに渡ってきたpropsを実行するように修正しました。
当初はemitでイベントを透過させる記法に慣れていたので思考を切り替える作業に苦労しましたが、emitよりもpropsの方がハンドラ名の厳密性が保たれるので、3系の仕様は妥当性があるのではないかと感じています。
createElement()とh()の詳細仕様は以下のドキュメントからどうぞ!
- 仮想DOM - 描画関数とJSX - Vue.js (v2.x)
- 仮想DOMツリー - Reder関数 | Vue.js (v3.x)
まとめ
コンポーネントを2つの役割に分割することで、モーダルのDOMとスタイルの再利用性を高め、特定のオプションに依存しないコンポーネントにリファクタリングすることができました。
今後、新たなモーダルを追加したい場合はPrensentational Componentは今回分割した物を流用し、Container Componentのみを新規作成するだけで完結するようになります。また、今回リファクタリングしたコンポーネントはラジオボタンで特定の要素を選択するという役割に限定した物ですが、モーダルの中身をSlot化することで、様々なエレメントの表示やアクションを実現することも可能です。
もちろんモーダルだけではなく、様々なコンポーネントにおいてこのパターンを適用することができます。この記事が皆様のコンポーネントを適切に分割し、再利用性を高めるためのお役に立てたら嬉しいです。
最後までお付き合いいただき、ありがとうございました!
- 投稿日:2020-12-12T16:43:43+09:00
【導入まで】Laravel x Vue.js(laravel/ui)
Laravel x Vue.js (laravel/ui)
はじめに
Laravelの導入については、DockerやXAMPPなどを利用するなど様々な方法がありますが、ここではLaravelの導入が完了しているという体で話をします。
本記事で使用する主要なcomposerのパッケージは、laravel/uiでありサポート状況は以下の様になっています。
laravel/uiサポートバージョン(2020/12/11時点 *1参照)
laravel/uiのバージョン Laravelのバージョン 1.x 5.8,6.x 2.x 7.x 3.x 8.x 動作環境
・OS: Mac OS Catalina
・PHP: 7.4.13
・Laravel: 8.18.1
・composer
・npm導入手順
laravel/uiパッケージをインストール
composer require laravel/ui上記コマンドを実行し、laravel/uiをインストールします。
基本的なvueの構成を生成する
php artisan ui vue*おまけ:この時に php artisan ui laravel/ui --authと打ち込むとログイン/レジスター機能を盛り込むことが出来ます
npmパッケージ
npm install上記コマンドを実行する。
アセットをコンパイル
npm run dev参考
- 投稿日:2020-12-12T16:04:40+09:00
vue3をvue-i18nを使って多言語化対応してみる
vue3に移行している際に多言語化対応について調べたのでメモ
参考(https://vue-i18n-next.intlify.dev)
環境
Vue3(cliで環境作成)
Typescript多言語化プラグイン vue-i18nを使う
インストール
# npm install vue-i18n@nextmain.tsimport { createApp } from 'vue' import { createI18n } from 'vue-i18n' const messages = { en: { message: { hello: 'hello world' } }, ja: { message: { hello: 'こんにちは、世界' } } } const i18n = createI18n({ locale: 'ja', // set locale messages, // set locale messages) }) const app = createApp({ // something vue options here ... }) app.use(i18n) app.mount('#app')App.vue<template> <div> {{$t("message.hello")}} </div> </template>多言語化しなくても、メッセージを切り分けることで後々メンテナンスが楽になると思います。
- 投稿日:2020-12-12T15:11:38+09:00
NuxtアプリをSSGでビルドしてCI/CDをお手軽に設定する【Github Actions × Firebase Hosting】
はじめに
今回はNuxt.jsのSSG(静的サイトジェネレーター)モードで作成したアプリのCI/CDを設定する方法を紹介します!
Github ActionsとFirebase Hostingを連携させるとかなり手軽に設定できたのでNuxt/Vue初心者の方も実践できる内容だと思います。対象読者
・実務でCI/CDを使った事が無い人
・CI/CDという言葉は聞いた事があるがよく分からない人
・Vue/Nuxtを触った事はあるがレンダリングの仕方までは気にした事がない (SSGってナニ?な人)
・firebaseがなんとなく分かる人説明しない事
・firebase各種サービスの細かな説明
・テスト(Jest)の書き方SSG (静的サイトジェネレーター)とは?
個人的にはこの記事の説明が分かりやすいと思います
Nuxt.jsを使うときに、SPA・SSR・SSGのどれがいいか迷ったら -QiitaNuxtをSSGモードで始めると、ビルド時(nuxt generate実行時)にvueファイル内で呼ばれているAPIからデータを取得し、それに基づいたHTMLが生成されます。
ビルド時点のDBから取得した値がHTMLへ直書きされるので、DBの値が変わったとしても再度デプロイするまではHTML内の値が変わる事はありません。
特徴
・サーバーサイドとデータのやりとりをする必要が無いので
1) レスポンスが速い
2) セキュリティ面のリスクが軽減される・データの更新をする為には都度ビルドする必要があるので、頻繁に更新するサイトには向かない (LPやポートフォリオには向いている)
CI/CDとは
ニフクラさんによると
「CI」とは「Continuous Integration(継続的インテグレーション)」の略で、ソフトウェア開発におけるビルドやテストを自動化し、継続的に行うアプローチのことです。
「CD」とは「Continuous Delivery(継続的デリバリー)」の略で、CIによってテストされたコードのマージや、本番環境向けのビルドの作成を自動的に行い、本番環境にデプロイが可能な状態を整えるプロセスのことです。簡単に言えば、CI/CDとは継続的にビルド/テスト/デプロイを行う事で、いつでも本番環境を更新できるような状態に保つ為の仕組みの事です。
近年の開発手法の主流である「アジャイル開発」の中で、リリース回数が多くなっても品質を下げない様にする為の施策であるとも言えます。今回実装するCI/CDの動き
今回作成するシステムは以下の様になります
mainブランチはリリース用のブランチなので、そこで作業することはありません。手順
1. 機能を実装し、実装箇所のテストを書いたら、mainブランチへコードをpushします。
2. Github Actionsはmainブランチへのpushを検知して、ビルド、テストを実行します。
3. 2で問題がなければそのままFirebase Hostingへコードをデプロイします。
4. 問題があった場合(テスト/ビルドに失敗)は通知がされ、本番への反映はされません。本編
※ Node.jsがインストールされていること、Githubが使える事が前提
環境
Node v12.20.0 Nuxt v2.14.6 firebase v8.1.1
それでは実際に環境構築していこうと思います!
まず最初にgithubでリポジトリを作成して、cloneしておきます。$ git clone[作成したリポジトリ名] $ cd[作成したリポジトリ名]
Nuxtアプリの作成
次にnuxtアプリを作成します。
npx create-nuxt-app [アプリ名]いくつか質問に答えます。
create-nuxt-app v3.4.0 ✨ Generating Nuxt.js project in '[アプリ名]' ? Project name: '[アプリ名]' ? Programming language: JavaScript ? Package manager: Npm ? UI framework: None ? Nuxt.js modules: Axios ? Linting tools: ESLint, Prettier ? Testing framework: Jest ? Rendering mode: Universal (SSR / SSG) ? Deployment target: Static (Static/JAMStack hosting) ? Development tools: jsconfig.json ? Continuous integration: None ? Version control system: Nonecreate-nuxt-appが完了したら、生成したファイルをルートディレクトリに移動します。
# ファイルをルートに移動 mv [アプリ名]/{*,.*} . # 空になったディレクトリを削除 rm -rf [アプリ名]最後にfirebaseのパッケージをインストールします。
npm i firebasefirebaseのセットアップ
プロジェクトの作成
https://console.firebase.google.com
firebaseコンソールにてプロジェクトを作成してください。
初めての方はGmailのアカウントが必要になります。
無事作成できると、コンソールページに入れるようになります。Cloud Firestoreの設定
次にCloud Firestoreのページへ移動し、データベースを作成します。
今回はテストモードで開始しますが、運用の際にはルールを設定してください。
データベースが作成されたら、適当なレコードを登録しておきます。
自分はusersコレクションにnameを持ったドキュメントをいくつか作成しました。これが今回扱うデータになります。
アプリにFirebaseを追加
自分のアプリ(今回はNuxt)でfirebaseを使うための設定をしていきます。
「プロジェクトを設定」から「ウェブアプリに Firebase を追加」を選択します。手順が表示されるので、この通りに進めていきます。
1.「このアプリのFirebase Hostingも設定します」にチェックを入れてください。
2. モジュールバンドラを使用するので「FirebaseSDKの追加」は飛ばしてください。
4.firebase initでいくつかの質問を答える事になります。以下の様に回答してください。? Which Firebase CLI features do you want to set up for this folder?: > firestore/hosting ? Please select an option: > Use an existing project Select a default Firebase project for this directory: > [先ほど作成したプロジェクト] ? What file should be used for Firestore Rules? > firestore.rules ? What file should be used for Firestore indexes? > firestore.indexes.json What do you want to use as your public directory? > dist ? Configure as a single-page app (rewrite all urls to /index.html)? > No ? Set up automatic builds and deploys with GitHub? > Yes # Githubの認証が必要になります ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) > [今回使っているリポジトリ] ? Set up the workflow to run a build script before every deploy? > Yes ? What script should be run before every deploy? > npm ci & npm run build ? Set up automatic deployment to your sites live channel when a PR is merged? > Yes ? What is the name of the GitHub branch associated with your sites live channel? > main後半はCI/CDの設定についてです (後述)
最後にfirebase deploy --only hosting:[アプリ名]を実行とありますが、これはデプロイ用のコマンドであり、今のままではうまくいきません。後ほど設定をします。テストの確認
まずデプロイの前に、実装したコードに不備が無いかテストを実行します。
デフォルトでtestディレクトリにLogo.spec.jsがあると思います。
今回は説明の為にこのファイルのみテストします。
Logo.vueがvue instanceであるかのチェックです。
npm run testを実行すればテストが実行されます。
jestを使ってテストをしています。
1 passed, 1 totalとあるように、合計1つのテストケースのうち、1つのテストがpass(成功)しました。
コードorテストに不備があるとエラーが起きます。このようにしてデプロイ(本番反映)前にテストを実行する事でバグを未然に防げます。ただし、テストを実行し忘れてデプロイしてしまう可能性もあるので、CI/CDを使ってデプロイの度に自動テストをする仕組みを作成します。
本番環境へデプロイ
テストが成功した事を確認したら、本番環境へデプロイしていきます。
まずビルド用のコマンド、
npm run generateを実行します。
すると
dist配下に静的ファイルが作成されます。
このdistの中身が、今回Firebase Hostingによってホスティングされる静的ファイル群です。ビルドが成功したら、デプロイ用のコマンド、
firebase deploy --only hostingを実行してみてください。
完了すると
Hosting URL:[URL]と最後に表示されるので、アクセスしてみます。
無事アクセスできました!
Nuxt × firestoreでSSGを試す
それではNuxt側でfirestoreにアクセスし、データを取得/表示してみます。
firebaseSDKを使う
まずはfirebaseSDKをNuxtで使うために、firebaseを初期化するためのpluginファイルを作成します。
plugins配下にfirebase.jsを作成します。
firebaseConfigの部分は自分のものに変えてください。
plugins/firebase.jsimport firebase from 'firebase' const firebaseConfig = { apiKey: 'Your apiKey', authDomain: 'Your authDomain', projectId: 'Your projectId', storageBucket: 'Your storageBucket', messagingSenderId: 'Your messagingSenderId', appId: 'Your appId', } if (!firebase.apps.length) { firebase.initializeApp(firebaseConfig) } export default firebase
これ以降はNuxt側でimport firebase from '~/plugins/firebase'と書く事で、先ほど作成したfirebaseプロジェクトを利用できるようになります!
asyncDataとmountedを比較
SSGでレンダリングする場合には、
asyncData内でAPIへリクエストを送り、レスポンスのPromiseを{key:value}形式で返します。
keyにはtemplate内で使いたい名前を、valueにはPromiseを入れます。実際にindex.vueを書き換えて、SSGとSPAでの挙動の違いを確認してみます。
index.vue<template> <div class="container"> <div> <h2>Users with asyncData (SSG)</h2> <p v-for="(user, i) in asyncUsers" :key="'user-async' + i"> {{ user.name }} </p> <h2>Users with mounted</h2> <p v-for="(user, i) in mountedUsers" :key="'user-mounted' + i"> {{ user.name }} </p> </div> </div> </template> <script> import firebase from '~/plugins/firebase' export default { data() { return { mountedUsers: [], } }, async asyncData() { const res = await firebase .firestore() .collection('users') .get() .then((querySnapshot) => { const array = [] querySnapshot.forEach((doc) => { array.push(doc.data()) }) return array }) .catch((e) => console.log(e)) return { asyncUsers: res } }, async mounted() { await firebase .firestore() .collection('users') .get() .then((querySnapshot) => { querySnapshot.forEach((doc) => { this.mountedUsers.push(doc.data()) }) }) .catch((e) => console.log(e)) }, } </script> <style> .container { margin: 0 auto; min-height: 100vh; display: flex; justify-content: center; align-items: center; text-align: center; } h2 { margin: 16px 5px; } p { margin: 5px 0px; } </style>
npm run devで確認してみます。
asyncDataはビルド時に実行されていて、DBから取得した値がHTMLに直書きされている状態なので、その都度取得するmountedより表示速度が速い事が分かります。SSGを利用している部分をデプロイ
今回新しい実装をしたので、再度本番環境へ反映させます。
手順としてはざっくり、
・テストの実行 (本来なら変更箇所のテストを追加作成) →
npm run generate
・ビルドの実行 →npm run generate
・デプロイの実行 →firebase deploy --only hostingとなると思います。
ただし、
毎回これらの手順を人の手で実行するのは面倒である事はもちろん、テストを実行し忘れたままデプロイしてしまう等、本番環境での不具合にも繋がります。本番環境への反映をシステム化する為にもCI/CDを導入します。
以上の点を踏まえて、CI/CDを設定して変更内容をデプロイします!
CI/CDで自動テスト&自動デプロイをする
デプロイの設定は既に完了しています。
firebase initを実行した時にいくつか質問に答えたと思います。
この質問の後半での回答がCI/CDの設定になります。? Set up automatic builds and deploys with GitHub? > Yes ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) > [今回使っているリポジトリ] #Github actionsを設定するリポジトリ ? Set up the workflow to run a build script before every deploy? > Yes ? What script should be run before every deploy? > npm ci & npm run build #デプロイ時に実行するコマンドを指定 ? Set up automatic deployment to your sites live channel when a PR is merged? > Yes # マージされた時も自動デプロイを有効にするかどうか ? What is the name of the GitHub branch associated with your sites live channel? > main #ここで指定したブランチへpushした時にGithub actionsが走りますmainブランチへのpushをGithub actionsが検知し、指定したJobを実行してくれるようになります。
ビルド、テストの設定についてはもう少し手を加える必要があります。
では、実行するJobを編集していきます。
CI/CDの定義はymlファイルに記述します。既存の
npm ci & npm run buildを削除して、以下のように編集します。.github/workflows/firebase-hosting-merge.yml- - run: npm ci & npm run build + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci + + - name: Run test + run: npm test + + - run: npm run generateそれでは、変更内容を
mainブラントへpushしてみましょう!pushしたらgithub actionsをみてみます。
リポジトリのActionsタブにワークフローが表示されていると思います。
詳細をみてみると、先ほど定義したjobが実行されている事が分かります。
ステータスが緑のチェックになっていれば成功です!
これ以降はmainブランチに変更内容をpushするだけで、本番環境へ反映されるようになります。
ここでテストが通らなければデプロイに失敗します。例えば以下のように変更すればテストが通らず、デプロイにも失敗します。
- expect(wrapper.vm).toBeTruthy() + expect(wrapper.vm).toBeFalsy()まとめ
今回扱った技術については理解が曖昧な部分があったので、自分としてもまとめられて良かったです!
何よりSSGが使いやすくて好きになりました!
ポートフォリオサイトに最適だと思ったので、今回の構成で作り直してみようと思います。
SSR/ISRも試してみよう。
22卒の学生エンジニアです。
Twitterもやってるのでフォローお願いします!
https://twitter.com/1keiuu
- 投稿日:2020-12-12T14:23:48+09:00
npmパッケージ検索サイトからの使い方を初心者が解説してみた!
node.jsのパッケージが無料で使えるサイト
npmという無料で使えるnode.jsのパッケージまとめサイト
npm build amazing things
このサイトから必要な情報を取り出し実装するまでを、node初心者が解説します!
初心者なのでスムーズには扱えなかった
node.jsのパッケージを無料で提供してくれているとは言っても、使い方がわからなかったので
覚書として記録しておこうと思います。使ってみたい!と思ったのはvue.jsのパッケージであるdraggbale
デモサイトは
https://david-desmaisons.github.io/draggable-example/vue.jsを利用して、ドラッグ&ドラッグが簡単に実装できるみたいです。
これを使ってタスク管理をしてみたい!と思ったので使ってみる事にしました。ラッキー!サンプルコードがgithubに上がってる
Hello.vue
というフォルダに入っていました。<template> <div class="fluid container"> <div class="form-group form-group-lg panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Sortable control</h3> </div> <div class="panel-body"> <div class="checkbox"> <label><input type="checkbox" v-model="editable">Enable drag and drop</label> </div> <button type="button" class="btn btn-default" @click="orderList">Sort by original order</button> </div> </div> <div class="col-md-3"> <draggable class="list-group" tag="ul" v-model="list" v-bind="dragOptions" :move="onMove" @start="isDragging=true" @end="isDragging=false"> <transition-group type="transition" :name="'flip-list'"> <li class="list-group-item" v-for="element in list" :key="element.order"> <i :class="element.fixed? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'" @click=" element.fixed=! element.fixed" aria-hidden="true"></i> {{element.name}} <span class="badge">{{element.order}}</span> </li> </transition-group> </draggable> </div> <div class="col-md-3"> <draggable element="span" v-model="list2" v-bind="dragOptions" :move="onMove"> <transition-group name="no" class="list-group" tag="ul"> <li class="list-group-item" v-for="element in list2" :key="element.order"> <i :class="element.fixed? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'" @click=" element.fixed=! element.fixed" aria-hidden="true"></i> {{element.name}} <span class="badge">{{element.order}}</span> </li> </transition-group> </draggable> </div> <div class="list-group col-md-3"> <pre>{{listString}}</pre> </div> <div class="list-group col-md-3"> <pre>{{list2String}}</pre> </div> </div> </template> <script> import draggable from "vuedraggable"; const message = [ "vue.draggable", "draggable", "component", "for", "vue.js 2.0", "based", "on", "Sortablejs" ]; export default { name: "hello", components: { draggable }, data() { return { list: message.map((name, index) => { return { name, order: index + 1, fixed: false }; }), list2: [], editable: true, isDragging: false, delayedDragging: false }; }, methods: { orderList() { this.list = this.list.sort((one, two) => { return one.order - two.order; }); }, onMove({ relatedContext, draggedContext }) { const relatedElement = relatedContext.element; const draggedElement = draggedContext.element; return ( (!relatedElement || !relatedElement.fixed) && !draggedElement.fixed ); } }, computed: { dragOptions() { return { animation: 0, group: "description", disabled: !this.editable, ghostClass: "ghost" }; }, listString() { return JSON.stringify(this.list, null, 2); }, list2String() { return JSON.stringify(this.list2, null, 2); } }, watch: { isDragging(newValue) { if (newValue) { this.delayedDragging = true; return; } this.$nextTick(() => { this.delayedDragging = false; }); } } }; </script> <style> .flip-list-move { transition: transform 0.5s; } .no-move { transition: transform 0s; } .ghost { opacity: 0.5; background: #c8ebfb; } .list-group { min-height: 20px; } .list-group-item { cursor: move; } .list-group-item i { cursor: pointer; } </style>これだけでは動きませんでした。
当たり前ですよね。githubからファイルをインストール
使用するツールはVisual Studio Code
Visuak Studio Codeのターミナルを立ち上げ
以下のコマンドを打ちました。
git clone https://github.com/David-Desmaisons/draggable-example.gitcloneが完了するとまるっと一式追加されていました。
必要npmインストール / run serv / build
が必要でした。最後まできちんと見ないといけないですね。
また、githubからcloneしたファイルの【README.md】にもコマンドの手順が記載されていました。https://github.com/David-Desmaisons/draggable-exampleの最下部にコマンドが書かれています。
順番にコマンドを打っていきます
draggableを動かすために必要な、他のnpmを一括インストールします。
npm installcd draggable-exampledraggable-exampleのフォルダに移動してからinstallをしないといけなかったです。
沢山インストールされたようです。
node_moduleフォルダが生成され、大量のnmpがインストールされました。
次はブラウザで動きが見れるようにサーバーを立ち上げます。
npm run serve
ローカルサーバー立ち上がりました!localhost:8080でデモサイトとまったく同じものが出来上がりました。
一端【Ctrl + C】コマンドでサーバーを終了します。
最後にビルドをして、webサーバーへアップロードしても動くようにします。npm run buildindex.htmlを始めとする必要ファイルがdocsフォルダの中に入っていました。
docsフォルダの中身を全てサーバーへアップロードするとサーバーで稼働します。カスタマイズする時
src > components
の中にあるHello.vueを修正
修正をしたら
npm run serveでブラウザで確認。
本番へアップする時だけ最後にnpm run buildをすれば良いです。毎回buildまでする必要はありません!
まとめ
・githubの説明は最後まできちんと読む事
・ダウンロードしたファイルにあるREADME.mdを見に行く
・ファイルの階層に注意してnpmのコマンドをうつ自分で調べたnpmを初めて使いました。
もっとnpmを自由に操れるように修業が必要だなと感じました。
- 投稿日:2020-12-12T14:04:39+09:00
【実践】フロントエンド開発でちょっとずつドメイン駆動設計してみた
この記事で書いていることを要約すると
どうすればドメイン駆動設計を段階的に取り入れられるのかVueのサンプルプロジェクトを作りながら考えてみた
ということになります。
考えるに至った経緯
最近 NUXT(typescript)とFirebaseでファイヤーエンブレム風ゲーム作ってみた で自分のコードが汚いということに気付き、ドメイン駆動設計を学んでみることにしました。
そして学び、実践してみると確かにコードが綺麗で読みやすくなったなという実感がありました。
そこでせっかくなので、仕事でもぜひ使ってみたい!と思ったのですが
- ドメイン駆動設計(コードのみの話なので正確には軽量DDD。詳しくは後述)ってどうやってフロントエンドフレームワークに適応したらいいんだろう?
- 自分が仕事で関わっているプロジェクトではドメイン駆動設計を導入できないけど、そのエッセンスを部分的にでも活用することはできないのだろうか?
といった疑問を抱えることとなりました。
今回は下記教材を用いてドメイン駆動設計を学びましたが、教材内のサンプルプロジェクトの言語はどれもC#なので、フロントエンドのフレームワークにどのように適応すればいいのかもよく分かりませんでした。
- C#でドメイン駆動開発パート1【C#でドメイン駆動開発とテスト駆動開発を使って保守性の高いプログラミングをする方法】
- ドメイン駆動設計の入門 ボトムアップでわかる!ドメイン駆動設計の基本
- エリック・エヴァンスのドメイン駆動設計
『エリック・エヴァンスのドメイン駆動設計』に関しては半分ぐらいしか読めておりません。。
そんな中、下記の記事は非常に参考になりました。
→ Vuex + DDDのアーキテクチャを考えるこれでなんとなくフロントエンドでもどうやってドメイン駆動設計を適応していけばいいのかイメージがつかめたものの、だからといってすぐに自分の案件に適応できるのかというとそうではなく。
なぜ自分の関わっているプロジェクトにドメイン駆動設計が適応できないのかについてですが、その前に簡単に自分が関わっている案件の説明をしておくと、
- 主にフロントエンド開発(ReactとかVueとかAngularとか)
- 常駐エンジニア
- 数ヶ月〜半年レベルのプロジェクトがほとんど
- プロジェクトに関わる人数は基本的に少人数(1〜3人ぐらい)
- 開発フェーズとしてスクラッチから機能追加までいろいろ
という感じです。要するに私が関わる案件はスクラッチ開発だったり機能追加することが多いのですが
- スクラッチ開発→プロジェクトがどの程度大きくなるのか分からず、いきなりドメイン駆動設計を100%適応しようという気にならない
- 機能追加→既存のプロジェクトにドメイン駆動設計が適応されていることはほぼ無く、これまでの書き方を無視していきなりドメイン駆動的には書きづらい
と、どちらにせよドメイン駆動設計をいきなり完璧に適応することはできません。特に大規模な既存のフロントエンドプロジェクトでドメイン駆動設計が適応されていないケースに関しては、途中から適応しようとすると相当なリソースが必要になるためハードルは非常に高いです。(てか無理)
人によっては「いや、そんな状況だったら導入しない方がいいのでは?」と思う人もいるかもしれませんが、個人的には亜型であったとしてもドメイン処理がviewから引き剥がされてた方が分かりやすく、変更が簡単なコードになると考えています。
なので、今回はまだ希望のあるスクラッチ開発で、どうすれば(完璧でなくても)ドメイン駆動設計を適応することができるのか、考えてみました。
具体的にはサンプルプロジェクトとしてTODOアプリを作成し、その規模が大きくなるにつれてよりドメイン駆動設計が適用されていき、どのようなメリットがあるのか書いてみました。
ドメイン駆動設計とは?
「そもそもドメイン駆動設計ってなに?」という方向けに。
下記の記事がドメイン駆動設計の概要を学ぶのに分かりやすかったです。
→わかった気になるDDD入門記事まとめ先にお伝えしておくと、当記事の内容は基本的なドメイン駆動開発の内容しか書いていません。ただ、エンティティ、アプリケーションサービス、リポジトリーの三つは理解しておいた方が分かりやすいかと思います。
また、ここまでドメイン駆動設計という言葉を使ってきましたが、この記事で書いていることは正確に言うと軽量DDDと呼ばれる、ドメイン駆動設計の中でも詳細設計以降の技術的な実装パターンの話になります。(タイトルややこしくてすみません)
ちなみに、そもそも戦略的なドメイン駆動設計の部分を省き戦術的な軽量DDDだけやっても意味はないという意見もあるようです。
ですがドメイン駆動設計を学び、実際に軽量DDDを実践してみた結果、コードが綺麗になったなという実感がありましたので、個人的には軽量DDDだけでも実践する価値はあると思っています。
フロントエンドでどうやってちょっとずつドメイン駆動設計を適応するのか
それでは実際にサンプルコードを交えつつTODOリストアプリをみんな大好きVueで作成しながら解説していきます。
まずは最初のフォルダー構成がこちら。
src/ ├ App.vue └ components/ └ Top.vue(必要なファイル構成のみ表示しています)
App.vue<template> <div id="app"> <Top /> </div> </template> <script> import Top from "./components/Top.vue"; export default { name: "App", components: { Top, }, }; </script>Top.vue<template> <div class="container"> <ul> <li v-for="(todo, index) in todoList" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> </div> </template> <script> export default { name: "HelloWorld", data: function () { return { todoList: [], text: "", }; }, methods: { registerTodo() { this.todoList.push(this.text); this.text = ""; }, }, }; </script>ドメイン駆動開発の話にフォーカスしたいので、UIとか処理は適当です。
現状としてサーバー無し、ローカルストレージも使っておらず、リフレッシュしたら登録したタスク全部消えるような状態です。
エンティティ
初めにTodoタスクに下記のような仕様が加わったとします。
- 文字は3文字以上で20文字以内
- 特定の言葉を省く(「アホ」とか「バカ」とか)
- 登録できるTodoは10個まで
ここでやりがちなのが、「利口なUI」ということで下記のような実装でしょう。
Top.vue<template> <div class="container"> <ul> <li v-for="(todo, index) in todoList" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> </div> </template> <script> export default { name: "HelloWorld", data: function () { return { todoList: [], text: "", }; }, methods: { registerTodo() { // 追加 if ( this.todoList.length >= 10 || /(アホ|バカ)/.text(this.text) || this.text.length < 3 || this.text.length > 20 ) return; this.todoList.push(this.text); this.text = ""; }, }, }; </script> <style scoped> .container ul { list-style: none; } </style>でもこうすると、このvueファイルには
- UIに関する処理
- ドメインに関する処理
の二つが混ざってしまい、他のページでもTodoに関する処理を書こうとすると同じ処理をもう一度書かなければならなくなります。
ということでドメイン駆動設計でいうEntity単位でファイルを分けていきます。
helper/todo.js// 追加 var TodoList = function () { this.value = [] } TodoList.prototype.add = function(text) { if ( this.value.length >= 10 || /(アホ|バカ)/.test(text) || text.length < 3 || text.length > 20 ) return false; this.value.push(text) return true } export default TodoListTop.vue<template> <div class="container"> <ul> <li v-for="(todo, index) in todoList.value" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> </div> </template> <script> import TodoList from "../helper/todo.js"; export default { name: "HelloWorld", data: function () { return { // 修正 todoList: new TodoList(), text: "", }; }, methods: { registerTodo() { // 修正 if (this.todoList.add(this.text)) this.text = ""; }, }, }; </script>現在のフォルダー構成はこんな感じ。
src/ ├ App.vue ├ components/ │ └ Top.vue └ helper/ └ todo.jsここで「なんでフォルダー名helperなの?」と違和感もたれた方もいるかもしれませんが、正直フォルダー名にどのような名前をつけるかというのが一番悩みました。。。
もうプロジェクト単位でガッツリとドメイン駆動設計導入してるなら、DomainってフォルダーにEntityとかValueObjectとかって名前のフォルダーを突っ込んでいったらいいと思います。
ただ、それだとドメイン駆動設計のドの字も知らない人からしたら「Domainってなに(´;Д;`)」ってなりかねません。
なので、妥協案として「helperぐらいなのかな..」って感じで付けてます。(ベターな名前がある人ぜひご共有頂ければ。。)
こうしてドメインを別ファイルに分けることで、いざユーザーからの要望で複数ページでTodoの管理を行いたくなったとしてもサクッと対応することができます。
もしTodoの管理を別々に行いたいのであれば各ページで
new TodoList()すればOK。Sub.vue<template> <div class="container"> <div>サブページ</div> <ul> <li v-for="(todo, index) in todoList.value" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> </div> </template> <script> import TodoList from "../helper/todo.js"; export default { name: "subpage", data: function () { return { // Topページと同じ todoList: new TodoList(), text: "", }; }, methods: { registerTodo() { if (this.todoList.add(this.text)) this.text = ""; }, }, }; </script>src/ ├ App.vue ├ components/ │ └ Top.vue ├ pages/ │ └ Sub.vue │ └ helper/ └ todo.jsもし、画面間で共通の状態を保持したいという場合、vuexを使うことになりますが、その場合はvuexの形式にあった形にドメイン処理を変換してやる必要があります。
store/todoList.jsexport default ({ namespaced: true, state: { value: [], }, getters: { value(state) { return state.value }, }, mutations: { add(state, payload) { state.value.push(payload) }, }, actions: { add({ commit, state }, payload) { if ( state.value.length >= 10 || /(アホ|バカ)/.test(payload) || payload.length < 3 || payload.length > 20 ) return false commit('add', payload) return true }, }, })store/index.jsimport Vue from 'vue'; import Vuex from 'vuex'; import todoList from './todoList.js' Vue.use(Vuex); export default new Vuex.Store({ modules: { todoList } });Top.vue<template> <div class="container"> <div>トップページ(<router-link to="/sub">サブページへ</router-link>)</div> <ul> <li v-for="(todo, index) in todoList" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> </div> </template> <script> export default { name: "top", data: function () { return { text: "", }; }, computed: { todoList: function () { return this.$store.getters["todoList/value"]; }, }, methods: { async registerTodo() { if (await this.$store.dispatch("todoList/add", this.text)) this.text = ""; }, }, }; </script> <style scoped> .container ul { list-style: none; } </style>src/ ├ App.vue ├ components/ │ └ Top.vue ├ pages/ │ └ Sub.vue └ store/ ├ index.js └ todoList.jsちなみにですが、Todo一つ一つをValueObjectととして表現することもでき、その場合は3文字以上、20文字以内という制限はValueObject側に移行することになります。
ただ、個人的にはこの段階でValueObjectを適応するのはあまりにやり過ぎかなと思いやっていません。
あと、javascriptがオペレーターのオーバーロードに対応していないので、そもそもjavascirptでValueObjectって表現しにくいと考えてあまり使ったことがありません。
const task1 = new Todo('家の掃除') const task2 = new Todo('お買い物') task1 === task2 // これをするための機能が提供されていないなんとかオーバーロードしようとしている方の記事も読みましたが、ここまでやらないといけないなら無理する必要ないんじゃないだろうか?(そもそもオーバーロードせずにValueObjectを表現できる方法もあるのかもしれないけど)
果たしてjavascriptで演算子オーバーロードは可能なのか
アプリケーションサービス
さて、ここまでで複数のページにまたがるドメイン関連の処理を良い感じに一つにまとめることができましたが、今度はユーザーから下記のような要件が出てきたとします。
- 家事とその他のTodoリストは別々に表示して欲しい
- 家事のTodoには優先順位を付けて確認できるようにしたい
そこで、下記のようにUIを実装することにしました。
- 家事のTodoかその他のTodoかを選択するセレクトボックスを新たに追加
- 家事のTodoの時、優先度を0〜10の数字で入力する入力欄を追加
- 追加されたタスクはそれぞれの種類別に表示
(セレクトボックスを追加。通常のTodoと家事のTodoを表示する場所を分ける)
(セレクトボックスより通常のTodoか、家事のTodoかを選択可能)
(セレクトボックスで家事を選択すると優先順位を入力する項目が表示)では実装面についてですが、先ずは家事のTodoを前回作ったTodoと同じようにstoreに実装していきたいと思います。
houseworkTodoList.jsexport default ({ namespaced: true, state: { value: [], }, getters: { value(state) { return state.value }, }, mutations: { add(state, payload) { state.value.push(payload) state.value = state.value.slice().sort((todo1, todo2) => todo2.priority - todo1.priority) }, }, actions: { add({ commit }, payload) { if(payload.priority < 0 || payload.priority > 10) return false commit('add', payload) return true }, }, })家事のTodoは優先度に合わせて順番を入れ替える必要がありますので、addした後にsortします。
また、優先度は1〜10という決まりなので、それ以外の数字だと弾くようにしています。
ここまでは良いのですが、「家事のTodoの場合はhouseworkTodoListに、普通のTodoの場合はtodoListにタスクを追加する」という処理はどこに書くべきでしょう。
view側に書くことももちろん可能です。
Top.vuemethods: { async registerTodo() { let result; if (this.todoType === "housework") { result = this.$store.dispatch("houseworkTodoList/add", { priority: this.priority, text: this.text, }); } else { result = this.$store.dispatch("todoList/add", this.text); } if (result) this.text = ""; }, },ですが、こうしてしまうと前回と同様、似たような処理が他の画面で出てきた時に同じようなコードを書く羽目になり、これはよくありません。
ということで、ドメイン駆動設計でいうアプリケーションサービスを作成し、そちらに処理を任せたいと思います。
store/applicationService/todoList.jsexport default ({ namespaced: true, actions: { add({ dispatch }, payload) { if (payload.type === 'housework') { return dispatch('houseworkTodoList/add', payload, {root: true}) } else { return dispatch('todoList/add', payload.text, {root: true}) } }, }, })アプリケーションサービスは基本的に状態を保持しないので、storeではなく普通のjsファイルで実現したいところですが、それだとstoreにアクセスできない(はず)のでstore内にアプリケーションサービス層のファイルを作りました。
こうすることでview側が知っているのは、どのアプリケーションサービスに値を渡す必要があるのか、ということだけです。
Top.vue<template> <div class="container"> <div>トップページ(<router-link to="/sub">サブページへ</router-link>)</div> <p>通常</p> <ul> <li v-for="(todo, index) in todoList" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo }}</label> </li> </ul> <p>家事</p> <ul> <li v-for="(todo, index) in houseworkTodoList" v-bind:key="index"> <input type="checkbox" /> <label for="checkbox">{{ todo.text }}</label> <span> 優先度:{{ todo.priority }} </span> </li> </ul> <select v-model="todoType"> <option value="normal">通常</option> <option value="housework">家事</option> </select> <input v-model="text" type="text" /> <button @click="registerTodo">register</button> <div v-if="todoType === 'housework'"> 優先順位を入力 <input v-model="priority" type="number" max="10" min="0" /> </div> </div> </template> <script> export default { name: "top", data: function () { return { text: "", todoType: "normal", priority: 0, }; }, computed: { todoList: function () { return this.$store.getters["todoList/value"]; }, houseworkTodoList: function () { return this.$store.getters["houseworkTodoList/value"]; }, }, methods: { async registerTodo() { if ( await this.$store.dispatch("todoListService/add", { text: this.text, type: this.todoType, priority: parseInt(this.priority, 10), }) ) this.text = ""; }, }, }; </script>現状の実装だと、view側からtodoListとhouseworkTodoListという二つのドメイン層のstoreに直接アクセスしてしまっていますが、それが気持ち悪いなら(ちょっと面倒ですが)todoListServiceのgetterを通してアクセスするように変えてしまうのもいいかもしれません。
Top.vuecomputed: { todoList: function () { return this.$store.getters["todoListService/value"]; }, houseworkTodoList: function () { return this.$store.getters["todoListService/houseworkListValue"]; }, },store/applicationService/todoList.jsgetters: { value(_, _1, _2, rootGetters) { return rootGetters['todoList/value'] }, houseworkListValue(_, _1, _2, rootGetters) { return rootGetters['houseworkTodoList/value'] } },次にフォルダー全体の構成図ですが、ドメイン駆動よりに寄せるのであれば下記のような構成になるかと思います。
src/ ├ App.vue ├ components/ │ └ Top.vue ├ pages/ │ └ Sub.vue └ store/ ├ index.js │ ├ entity/ │ ├ houseworkTodoList.js │ └ todoList.js │ └ applicationService とか service とか/ └ todoList.jsこれは勝手な想像ですが、entityはER図なんかでも出てくる概念ですし、ドメイン駆動設計を知らなくてもまだ何をしているか分かるフォルダー名なのかなって思います。
ですがapplicationServiceについてはドメイン駆動設計を知らないとなかなか分からないのではないでしょうか。
なのでもし、ドメイン駆動っぽさを出したくないのであれば、viewModelやpagesなどにして下位のファイル名は src/pages/ 以下のファイル名と同じにしておけば各ページ関連の処理がこのstoreファイルに入っている、と分かりやすいのではないでしょうか。
src/ ├ App.vue ├ components/ │ └ Top.vue ├ pages/ │ └ Sub.vue └ store/ ├ index.js │ ├ entity/ │ ├ houseworkTodoList.js │ └ todoList.js │ └ viewModel/ └ Top.js名前をviewModelにしてしまうと、1画面につき1つのファイルでアプリケーション機能を表現することになり、その処理を他の画面でも使い回すのは難しくなります。(Top.js という名前のstoreファイルを src/pages/Sub.vue で使うのは違和感があるため)
結果的にvueファイルでアプリケーションサービス層のロジックを実装しているのと同じようにみえます。
Top.vue// 結局これと同じ? methods: { async registerTodo() { let result; if (this.todoType === "housework") { result = this.$store.dispatch("houseworkTodoList/add", { priority: this.priority, text: this.text, }); } else { result = this.$store.dispatch("todoList/add", this.text); } if (result) this.text = ""; }, },ただ、それでも後述するAPIの呼び出し(Repository)とか、(もしあるなら)ドメインサービスやDTOの作成なども必要な場合にこのアプリケーションサービス層に追加できますので、やはりこっちの方が変化に強いコードと言えそうです。
ドメイン駆動設計をちょっとずつ実践しようとして実感していますが、アプリケーションサービスあたりが出てきた段階で、フォルダー構成がちょっとずつドメイン駆動設計ぽくなっていってしまうのは仕方ないのかなと。
あと、今回はエンティティとアプリケーションサービスをstoreとして表現しましたが、例えば家事のTodoListとhouseworkTodoListの振る舞いが似ており、共通化したいとなった場合。
ベースクラスを作ってそれを拡張してというのがオブジェクト思考の定番のやり方ですが、storeを使っているとこれがやりづらいので、冒頭で紹介した Vuex + DDDのアーキテクチャを考える のようにエンティティをstoreではなく生のjs(ts)で表現した方がいいでしょう。
そもそも私がエンティティをstoreで表現することになったキッカケがこちらのゲームを作った時ですが、この時使ったのがvuexfireというライブラリです。これを使用してfirestoreとvuexを同期させるために仕方なくそうしたという経緯があります。
ただ、storeで表現していたDomain関連の処理を生のjs(ts)で書き直せない、したくない(時間がない、コードスタイルをvuexで統一したなど)あると思うので、ここは状況に合わせて対応すればいいのではと思います。(すいません、ここら辺はまだあまり知見がないので分からない部分でもあります)
リポジトリ
ここまで複数のドメインを組み合わせてアプリケーションの機能を実現するところまではできましたが、今度はプロダクトマネージャーから「TODOのデータをサーバー側に保存したい!」となったとします。
バックエンドでデータベースやらAPIの作成やらが終わっているという前提ですが、APIの呼び出しは基本的に画面描画とは関係ない処理です。なので、ここの処理はアプリケーションレイヤーに追加していきます。
store/applicationService/todoList.jsexport default ({ namespaced: true, getters: { value(_, _1, _2, rootGetters) { return rootGetters['todoList/value'] }, houseworkListValue(_, _1, _2, rootGetters) { return rootGetters['houseworkTodoList/value'] } }, actions: { add({ dispatch }, payload) { let result switch (payload.type) { case 'housework': result = await dispatch('houseworkTodoList/add', payload, { root: true }) break default: result = await dispatch('todoList/add', payload.text, { root: true }) break } if (result.isSucceeded) { // HTTPの処理を記述 } return result.isSucceeded }, }, })store/entity/todoList.jsexport default ({ namespaced: true, state: { value: [], }, getters: { value(state) { return state.value }, }, mutations: { add(state, payload) { state.value.push(payload) }, }, actions: { add({ commit, state, getters }, payload) { if ( state.value.length >= 10 || /(アホ|バカ)/.test(payload) || payload.length < 3 || payload.length > 20 ) return { isSucceeded: false, value: null } commit('add', payload) return { isSucceeded: true, value: getters.value } }, }, })(store/entity/houseworkTodoList.js も同じようにactionの返り値を変更しています。)
リポジトリに関しては以上。超ザックリ。
フロントエンドに適応できるドメイン駆動設計の要素は少ない?
ここまでで、
- エンティティ
- アプリケーションサービス
- リポジトリ
という三つのドメイン駆動設計にて出てくる技術的要素を取り上げましたが、これ以外にも
- バリューオブジェクト
- ドメインサービス
- 仕様オブジェクト
- その他のドメイン駆動設計だけでない要素(インターフェースとか集約だとか...)
などなど色々あります。ですが、全部ちゃんと適応しないと分かりづらいプロジェクトって相当規模の大きいものになるのかなと思っていて、それこそ導入するんだったら中途半端にやるのではなくガッツリ導入すべきだと思います。
なので、プロジェクトが小〜中規模だけど「ドメイン駆動設計のいい部分も意識しながら作りたい」という場合、エンティティとアプリケーションサービスとレポジトリ+α必要になったものを少々、適宜適応していくというスタイルで十分なのかなと思ったりしてます。
あと、そもそもバックエンドをしっかり構築し、フロントエンドで必要なのは描画処理のみという状況にできていたならば、フロントエンドで行わなければならないドメイン関連の処理は少なくなるはず。
なので、プロジェクトの規模が大きくないにも関わらず、ドメイン関連の処理がフロントエンド側にあまりに漏れ出している場合、バックエンドの設計を見直すべきなのかもしれません。
私もまだまだドメイン駆動設計学び始めたばかりなので、あくまで参考にしていただければ幸いです。
以上。ドメイン駆動設計学んで実践してみた、でした。
- 投稿日:2020-12-12T13:59:15+09:00
Vue.jsとhamliteを組み合わせてオセロ定石集を作ってみた
はじめに
オセロ関係の方は以下のような盤面表示ツールを見たことがあるのではないでしょうか。(※以下は画像なので動かせません)
これはhamliteというオセロ棋譜再生のブログパーツです。かなり昔からある有名なツールなので、Web上に棋譜を公開するのに使われている方も多いと思います。今回はこのhamliteをVue.jsから使うことで、オセロの定石集を作ってみたのでその技術説明をします。
目標
オセロの定石集では、定石名をクリックするとその定石の盤面が表示されるようになっています。このサイトではさらに順番に定石を見られるような矢印ボタンをつけたりとか、vuetifyも使って少し画面体裁を整えたりとかしていますが、今回はシンプルに定石名をクリックすると盤面に定石が表示される部分のみ抜粋して説明します。
hamliteの使い方
まずは前提としてhamliteの使い方を簡単に説明しておきます。
詳細はhamliteの公式サイトに書かれていますが、例えば虎定石であれば以下のようにすると表示することができます。<iframe ref="hamlite" width="325" height="390" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" allowtransparency="true" src="https://reversi-ai.appspot.com/v1.62/hamlite.html?&size=EL&kifu=f5d6c3d3c4&start_move=6"> </iframe>
kifuのパラメータに棋譜(f5d6c3d3c4)を書いて、start_moveのパラメータに開始局面(次が6手目)を入れると表示されるというわけです。簡単に使えますね。Vue.jsで棋譜と開始局面を差し替える
オセロの定石集のように、定石名をクリックしてその盤面を表示するためには、先ほどの
kifuやstart_moveのパラメータを変えてあげれば良いわけです。Vue.jsのリアクティブな仕組みを使うと、これは驚くほど簡単に書けます。
ソースコードは以下の通りです。たったこれだけですが、このコードをHTMLで保存してブラウザで開くとそれだけで動きます。<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>オセロの定石</title> </head> <body> <div id="openings"> <h1>{{items[current].name}}</h1> <iframe ref="hamlite" width="325" height="390" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" allowtransparency="true" :src="`https://reversi-ai.appspot.com/v1.62/hamlite.html?&size=EL&kifu=${items[current].moves}&start_move=${items[current].moves.length / 2 + 1}`"></iframe> <ul> <li v-for="(item, i) in items" :key="i"> <a href="#" @click.prevent.stop="current = i">{{item.name}}</a> </li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script> new Vue({ el: '#openings', data: () => ({ current: 0, items: [ {name:"虎", moves:"f5d6c3d3c4"}, {name:"兎", moves:"f5d6c5f4e3"}, {name:"牛", moves:"f5f6e6f4e3"}, {name:"ネズミ", moves:"f5f4e3f6d3"}, ], }), }) </script> </body> </html>ちょっとした解説
データの準備
見てすぐわかるように、Vueのデータ部分に
itemsとして定石一覧を用意しています。今回は4つだけにしました。同じくデータ内にあるcurrentは現在表示中の定石番号です。最初は0なので虎定石です。定石一覧の表示
一覧リストを作っているのは以下の部分です。
<a>タグでクリックしたときにcurrent = iが実行されます。つまり表示中の定石を自身の番号にするということです。今回のソースでロジックっぽいところは実はこの代入文だけです。<li v-for="(item, i) in items" :key="i"> <a href="#" @click.prevent.stop="current = i">{{item.name}}</a> </li>
@click.prevent.stopとしているのは、<a>タグの本来のクリック動作を抑止するためです。定石盤面の表示
いよいよ核心部分である定石盤面の表示です。Vue.jsのリアクティブな仕組みを使えば、以下のように書いておくだけで、
currentの値が変わると勝手に表示が切り替わります。<iframe ref="hamlite" width="325" height="390" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" allowtransparency="true" :src="`https://reversi-ai.appspot.com/v1.62/hamlite.html?&size=EL&kifu=${items[current].moves}&start_move=${items[current].moves.length / 2 + 1}`"></iframe>
kifuパラメータにitems[current].movesを設定しています。start_moveがちょっとややこしいですが、棋譜部分は2文字で1手なので、文字列の長さを2で割って、その次の手が最終局面なので1を足しています。おわりに
以上、Vue.jsからhamliteを使う方法について説明してきました。
今回のオセロ定石集は、原型は去年jQueryベースで作っていたのですが、最近Vue.jsを使うようになったので今年Vue.jsベースでできないかと思って作り替えてみたものです。想像以上に簡単に作ることができて驚きました。
棋譜集や定石集作りに役立てていただければと思います。
- 投稿日:2020-12-12T12:54:40+09:00
共同開発で学んだこと
私立探求学園というオンラインサロンで技求祭が行われました。
今回は共同でのアプリ開発となりました。1.技求祭とは
技術を探求するお祭り(ハッカソン)です。
テーマ・・・技術祭のテーマはコロナの中で便利に使えるアプリ
期間・・・5月初旬から8月末
白組、青組、紅組に分かれあるテーマをもとにアプリを共同開発しました。
私が所属している白組ではLaravel,Vue.jsでアプリを開発しました。2.メンバー構成(白組)
PM 1人
Laravelチーム 4人
Vue.jsチーム 4人3.開発工程
①アプリ作成案
②画面遷移図
③デザインカンプ
④環境構築(Laravel 5.8 Vue.js 2.6)
⑤ER図
⑥開発■4.アプリ作成案
白組ではOTOSHIという料理アプリを開発することにしました。■5.OTOSHIの機能
・秒単位から10分以内で作れる料理のみに厳選
・オンライン飲み会でたべるおつまみ
・皆で同じメニューをつくり、いいねの数で競い合う6.画面遷移図
どんな画面があって、どのようにいったりきたりできるのか表す図
7デザインカンプ
8.ER図
どんな画面があって、どのようにいったりきたりできるのか表す図
9.開発
Laravelチーム、Vueチームに分かれて開発。
勉強会を6月末まで1週間に1回行い、7月からタスクを割り当て本格的に開発に着手。10.発表
8月末に探求学園内(オンライン)て各組発表を行いました。
白組はそこで見事優勝することができました!発表で使われた資料
OTOSHIアプリ完成品の動画
https://www.youtube.com/watch?v=nVFrVWLM-fg
11.技求祭で学んだこと
・開発工程
・アイディアを形にする楽しさ
・共同開発することの難しさ今回を通して自分たちでアイディアをだし、ものを作ることの楽しさを知ることができたのが一番の学びです。
ああでもない、こうでもないといいながらアイディアを形にするのはとても楽しかったです!
エンジニアになれて本当に良かったと思いました。12.反省点
・チームとしてタスクの進捗管理の見える化をしていなかった
・誰がどれくらいの時間開発に時間を割くことができるのか事前に把握していなかった
・皆で共有できるきちんとした議事録がなかった13.総括
まだまだ未熟もので、なかなか要件定義の部分から仕事で関わることが難しい中、環境と機会を与えてくれた私立探求学園は控えめに言っても最高だと思いました!
これからも私立探求学園で技術を探求し続けていきたいと思いました。
今回チームとしてアプリを作りましたが、技求祭を通して学んだことを活かして個人でアプリ草案から開発まで行いたいと思います。
- 投稿日:2020-12-12T12:05:51+09:00
【Vue.js】computedとmethodsの違いまとめ
はじめに
UdemyのVue.js講座で学習していたところ、computedとmethodsの違いがよく分からなかったので、備忘のためにアウトプットしたいと思います。
computedとmethodsの違い
computedがリアクティブな依存関係が更新されたときにだけ再評価されるのに対し、methodsは再描画が起きると常に関数が実行されます。
methods computed オブジェクト内での役割 メソッド プロパティ 処理結果のキャッシュ キャッシュされない キャッシュされる テンプレートからの呼び出し方 {{ call() }} {{ call }} getter, setter getterのみ定義できる getter, setter両方定義できる 実際の動き
以下はcomputedが実行されると
computed発火!、methodsが実行されるとmethods発火!がコンソールに出力される処理です。
See the Pen
abmBreG by TokioTakamiya (@tktaka)
on CodePen.
methodsの場合
methodsは再描画の度に関数が実行されるため、computed実行時でも
computed発火!とともにmethods発火!も表示されています。computedの場合
一方、methodsの実行時は
computed発火!は画面リロード時の1度しか表示されておらず、methods発火!のみ増加しています。
次回の記事では、
computedとwatchの違いについて触れたいと思います。参考
- 投稿日:2020-12-12T11:04:34+09:00
Suspenseを自作する
始めに
Vue.jsも3系からSuspenseが実装されましたが、今一な点がいくつかありました。
- コンポーネントとして作らないといけない
- ページングなど、複数使用する場合はできなそう
参考
Vue.js 3.0 の新機能を試す。 〜 Suspense 編〜そこで自分がやりたいことを満たすようなコンポーネントを自作してみたので、それの備忘録として記事に残します。
自作コンポーネント(SuspensePromise)の機能
Promiseを渡して状態を監視する
以下のようにPromiseをコンポーネントに渡し、状態に応じて表示内容を切り替えられるようにしています。promiseを渡すだけなので、新しくしたらまたローディングの表示ができるようになります。状態の切り替えの際にresolve, rejectで送ったデータも受け取って表示させることができます。
SuspensePromiseの使用例<template lang="pug"> div SuspensePromise( :promise="$data.promise" :waitTime="100" ) template(#pending) p pending... template(#resolved="{ data }") p resolved, {{ data }} template(#rejected="{ data }") p rejected, {{ data }} </template>pending状態の表示待ち時間を指定できる
こういうローディング系で一番気にしているのが「一瞬ローディングが出てくる」ことです。resolveされるタイミングが早い場合はこういうローディングを出さずにしたいと思って調整しようと思うと、結構大変だと思います。
それをこのコンポーネントでは解消してくれて、
waitTimeにpending状態を表示するまでの待ち時間を指定できます。この時間がくる前にresolveされれば、pendingを表示させずにすぐ結果を出すことができます。終わりに
以上が自作した機能の紹介でした。詳細の実装はCodePenに書いていますので、興味がある方は是非みてください。
See the Pen Suspenseの自作(SuspensePromise) by wintyo (@wintyo) on CodePen.
- 投稿日:2020-12-12T08:34:58+09:00
【ESLint】error clear vue/comment-directiveって怒られる(その場凌ぎ)
ご無沙汰してます、おおのんです。
ワケあってnuxtのnode-module入れなおしたら、ESLintで怒られてビルドできない。
[Vue] ESLintを更新すると謎のエラー clear が表示される件
こちらの記事で少し解説されています。プラグイン制作者本人様も登場されています。これが複数表示されるerror clear vue/comment-directive参考:ESLint Vue plugin showing false positives for vue/comment-directive
ルートディレクトリ配下の.eslintrc.jsmodule.exports = { ...省略... // add your custom rules here rules: { 'vue/comment-directive': 0 } }これで解決します。
その場凌ぎです。
- 投稿日:2020-12-12T03:02:43+09:00
Nuxt.jsでvue-slickを使う
前提条件
これを設定している
インストール
$ npm i -S vue-slick jquery slick-carousel使う
slider.vue<template lang="pug"> .slider client-only Slick(:options="slickOptions") .slider-item(v-for="i in 3" :key="i") img(src="https://placehold.jp/150x150.png") </template> <script> import '~/node_modules/slick-carousel/slick/slick.css' export default { components: { Slick: () => process.browser ? import('vue-slick') : null, }, data: () => ({ slickOptions: { swipe: true, // スワイプできるか slidesToShow: 1, // スライドを何個表示するか infinite: true, // ループさせるか vertical: false, // 縦に表示するか verticalSwiping: false, // 縦にスワイプできるか draggable: true, // マウスでドラッグできるか arrows: true, // アローを使うか prevArrow: '<div class="slide-prev"></div>', // 戻るアローのdomを指定 nextArrow: '<div class="slide-next"></div>' // 次へアローのdomを指定 } }) } </script> <style lang="stylus" scoped> .slider img width 100% </style>Nuxt.jsのそのほか
- 投稿日:2020-12-12T01:49:06+09:00
Nuxt.jsでスムーススクロールする
インストール
$ npm i -S vue-scrolltoプラグイン作成
$ touch plugins/vue-scrollto.jsplugins/vue-scrollto.jsimport Vue from 'vue' import VueScrollTo from 'vue-scrollto' Vue.use(VueScrollTo)設定
nuxt.config.jsexport default { plugins: [ '~/plugins/vue-scrollto' ] }使用
<nuxt-link v-scroll-to="'#target'" to>スムーススクロール</nuxt-link>Nuxt.jsのそのほか
- 投稿日:2020-12-12T01:36:13+09:00
Nuxt.jsでスクロールに応じてフェード出現
プラグインファイルを作成・設定したら、スクロールに応じてイベントが走る
作成
$ touch plugins/v-scroll.jsplugins/v-scroll.jsimport Vue from 'vue' Vue.directive('scroll', { inserted: function (el, binding) { let f = function (evt) { if (binding.value(evt, el)) { window.removeEventListener('scroll', f) } } window.addEventListener('scroll', f) } })設定
nuxt.config.jsmodule.exports = { plugins: ['~/plugins/v-scroll'] // 追記 }使用
test.vue<template lang="pug"> .test(:class="[{'is-active': flg}]" v-scroll="handleScroll") test </template> <script> export default { data: () => ({ flg: false }), methods: { handleScroll(evt, el) { const top = el.getBoundingClientRect().top const wh = window.innerHeight if (top < wh / 2) this.flg = true } } } </script> <style lang="stylus"> .test opacity 0 transition all 1s &.is-active opacity 1 </style>Nuxt.jsのそのほか
- 投稿日:2020-12-12T01:29:07+09:00
Nuxt.jsでLoadingページを設定する
loadingコンポーネントを作るcomponents/Loading/index.vue<template lang="pug"> .loading-page(v-if="loading") p Loading... </template> <script> export default { data: () => ({ loading: false }), methods: { start () { this.loading = true }, finish () { setTimeout(() => { this.loading = false }, 3000) } } } </script>設定ファイルに変更を加える
nuxt.config.jsexport default { loading: '~/components/Loading/index.vue' // 追記 }Nuxt.jsのそのほか
- 投稿日:2020-12-12T01:26:02+09:00
Nuxt.jsでエラー用ページを作る
- 投稿日:2020-12-12T01:22:27+09:00
Nuxt.jsにgtm入れる
- 投稿日:2020-12-12T01:18:34+09:00
Nuxt.jsでデフォルトで使うメタ情報を設定する
設定ファイルの
head欄に変更を加えるnuxt.config.jsexport default { head: { title: 'test', htmlAttrs: { lang: 'ja' }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: 'test' }, { hid: 'og:title', property: 'og:title', content: 'test' }, { hid: 'og:description', property: 'og:description', content: 'test' }, { hid: 'og:url', property: 'og:url', content: 'https://test.com/' }, { hid: 'og:image', property: 'og:image', content: 'https://test.com/assets/image/share/og.png' }, { hid: 'twitter:card', name: 'twitter:card', content: 'summary_large_image' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] }, }Nuxt.jsのそのほか
- 投稿日:2020-12-12T01:01:49+09:00
Windows10環境IntelliJでVue.jsの開発環境を作成する
はじめに
Mac版の開発環境手順を「IntelliJ IDEAでVue.jsを動かす」というタイトルでmiyamotok0105さんが公開されていました。
本記事を参考に、こちらではWindows10版の手順をまとめてみました。なお、IntelliJのVue.jsプラグインですが、 単一ファイルコンポーネントによる開発 を前提にしたテンプレートが生成されます。
そのため本手順で構築される環境も、単一ファイルコンポーネントによる開発を行うための環境となります。
通常(?)の下記のようなhtmlファイルにscriptタグでvue.jsを指定してjsをゴリゴリ書くような開発向けではないので、ご注意ください。こういうのは!!対象外!!<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> </head> ~省略~ </html>※ 参考:「Vue.js公式:単一ファイルコンポーネント」
環境
ソフトウェア バージョン Windows 10 Professional - IntelliJ Ultimate 2020.3 nodejs (※) 14.15.1 ※ nodejsはZIP版をC:\Programs\nodejs\node-v14.15.1-win-x64に展開。
手順
PluginsからVue.jsをインストールします。
New ProjectからJavaScriptを選択し、リストからVue.jsを選択してNextをクリック。
Project Name:適当にプロジェクト名を入力
Project location:デフォルトのまま
Node interpreter:インストールしたnodejsの絶対パスを設定する(※)
Vue CLI:デフォルトのまま
Finishをクリック。
※ なお、Node interpreterの選択肢にDownload Node(うろ覚え)という選択肢があったので、こいつを選択すれば
事前にNode.jsをインストールしなくてもIntelliJが自動的に最新のNode.jsをインストールしてくれるかもしれません。
Finishをクリックすると、IntelliJが自動的にVueプロジェクトの初期化を行います。
そこそこ重い処理なのか、僕の環境では10分くらいかかりました。(10年くらい前のノートパソコン)
Vueプロジェクトの初期化が完了すると、以下のようなテンプレートが生成されます。
動作確認
単一ファイルコンポーネントのためビルドする必要があります。
今回生成されたテンプレートには、package.jsonのなかに下記が用意されています。
- server ビルドを行い確認用のサーバーを起動する
- build ビルドを行う
- lint リント(たぶん構文チェック。。。)
動作確認のため、「server」を実行します。
IntelliJ上で、「Ctrl + tab」を押してSwitcherを立ち上げ、0:npmに選択を合わせてEnterキーを押してください。
npmのWindowが表示されるようになります。
serverを右クリックし、メニューから「Run 'server'」を選択しクリックしてください。







































































