20210504のvue.jsに関する記事は14件です。

【独学・未経験】Vue.jsとFirebaseでSPAなイベント系アプリ作ってみた【使ってみての感想募集中】

1.はじめに こんにちわ! NaoHiraと申します。 当記事ではプログラミング完全知識ゼロから独学でポートフォリオアプリ(以下PF)を作成してみましたので、掲載させていただきます。 「こんな機能があればいいのに」「ここを修正したほうがよい」など、アプリを利用してみて感じたことをコメントにいただければ大変助かります。 ◆自己紹介 私は現在30歳で、英会話教室を運営する企業で営業事務として仕事を続けております。 前職の業績が悪くなり会社都合での退社になった経験から、得意な英語以外の専門スキルの必要性を感じプログラミングに興味を持ちました。学習期間は独学で1年ほど。 スクールは使用せず、PF作成時にメンタリングサービスを1ヶ月(週1回1時間)ほど利用したのみです。 今回のPF作成期間は3ヶ月ほどで、現在も都度アップデートを継続しております。 2.アプリの概要 ◆タイトル「FREETALK」 URL: https://freetalk1.web.app/ GitHubリポジトリ ◆トップページ ◆レスポンシブデザイン スマホにも対応したUI設計です。 3.概要 FREETALKは、普段から英語でコミュニケーションする機会がない人たち同士が集まって英会話をすることで、英語でのコミュニケーション力を"無料"で養おうというサービスです。 自分だけの英会話教室を投稿したり、メンバーを募集したり、ユーザーに高評価をつけたりなどの機能を実装しております。 4.開発背景 ● オーストラリアへの語学留学中に、Facebookで外国人の友人を集めて英語でのディベートをしていた経験から、それをアプリを使って日本中の人々でやれば効果的にみんなの英会話力上達に繋がるのではと考えたため。 ● 語学研修企業に入社し、なぜそこまで高額なレッスン費用になるか調査した結果、主に”講師の質”が要因となっており、英語初学者にとって高質な講師から教わるよりもとりあえず英語で喋る機会を多く作ることが大事なのではと思ったため。 5.使用技術 使用箇所 使用技術名 フロント Vue.js/Vuetify データベース Firebase Realtime Database ユーザーアカウント管理 Firebase Authentication アップロードした画像の格納先 Firebase Strage デプロイ先・独自ドメインの接続 Firebase Hosting 開発環境 VueCLI ソース管理 Git/GitHub 6.全体構成図 システム全体の構成は以下の通り、Firebaseによるサーバレスアーキテクチャとなっております。 7.ER図 一貫性のあるテーブル名称とシンプルな構造を意識しました。 7.実装機能 実装機能 説明 CRUD機能 基本機能である Create(生成)、Read(読み取り)、Update(更新)、Delete(削除) ユーザー登録 メールアドレスとパスワードをFirebase Authenticationsにユーザーアカウントとして登録できます。 ログインログアウト Firebase Authenticationsに登録されているメールアドレスとパスワードで認証を行いログイン処理をします。 ゲストログイン メールアドレスやパスワードを登録したりSNSアカウントを使用しなくても気軽にログインできます。 SNSログイン Googleアカウントを使ってログインできます。 出席者登録/出席者一覧表示 参加したい英会話教室へ出席登録ができ、出席登録した英会話教室一覧をプロフィールで表示できます。 プロフィール画像のデフォルト設定/変更 新規でログインしたユーザー向けでデフォルトアイコンが設定されております。編集画面にて変更ができます。 メアド・パスワード変更 Firebase Authenticationsに登録されているメールアドレスやパスワードを変更できます。 バリデーション 新規登録・ログイン・登録情報変更のときにパリデーションが表示されます。 画像アップロード 英会話教室のイメージやユーザーアイコンの設定のときに画像をアップロードしてFirebase Strageへ格納できます。 いいね/いいね一覧表示 英会話教室へお気に入り登録できます。登録した英会話教室一覧をプロフィール画面で表示できます。 投稿一覧表示 ユーザーが投稿した英会話教室一覧をプロフィール画面で表示できます。 ユーザー高評価 評価したいユーザーに向けて高評価できます。 ページネーション 投稿された英会話教室一覧がページネーションで表示されます。 リアルタイム検索 検索したいキーワードに引っかかった英会話教室のみがリアルタイムで表示されます。 コメント投稿/返信投稿 各ユーザーの評判やリクエストを投稿したり返信できます。 8.工夫点 ● 隙間時間にどんな英会話教室があるか頻繁にチェックするという "ユーザーの滞在時間が長い" サービスになる想定だったので、ユーザーをロードする時間などで待機させないSPAで作成することを考えました。 ● ユーザーに使ってもらうには、UI/UXの部分が整っていないといけないと考え、Vuetifyを活用しました。 ● 各ユーザープロフィールにコメント欄や高評価機能をつけることで、参加するユーザーまたは英会話教室を開催するユーザーに「どのようなコメントが寄せられているのか」「何人から高評価されている人物なのか」が事前にわかるようになっております。  9.苦労したところ ◆Vueの親コンポーネントと子コンポーネントのデータ受け渡し props, emitの概念の理解に時間がかかりました。 ◆参考資料があまりネットや書籍になかった Firebase × Vue.jsでのコードがググってもあまり発見できなかったので、Firebase × React/Angularなどの英語でのチュートリアルを参考にしたり、時には紙にデータの流れを書いてゼロからコードを考えたりと手間がかかりました。 なにも考えずにネットのコードをコピペするような手抜きができなかった分、力がついたのではないかとも思っておりますので結果オーライですね。 ◆Realtime DatabaseとVuexストアとの両立 Realtime DatabaseとVuexストアどちらとも辻褄が合うようにCRUD機能を実装する必要性があったため、コードを考える時間を要しました。 ◆Vuetifyの使い方 CSSができれば簡単と聞いてましたが、Vuetify公式サイトのサンプルコンポーネントを確認しながら実装する要領を掴むことに少々苦戦しました。 10.作成の過程で学んだこと ◆データベース設計の大切さ アプリ作成前にER図を書かなかったりデータベース設計をそこまで気にかけずに実装を進めてしまったので、後からRealtime Database内のデータ保存場所を変更したりとかなり時間効率が悪かったなと反省しております。 ◆質問力の大切さ 自身の質問の仕方が悪かったために、エラー解決に結びつくことができずに途方に暮れたことがありました。 Github Issuesに質問事項やデバッグ処理の途中経過をまとめるなど工夫することで、なんとか解決にたどり着くことができました。 ◆プログラミングは暗記ではなかった 義務教育の弊害もあり、気付いたら暗記に走っている自分がいました。(笑) 度重なるデバッグ処理や機能の実装などで苦しんでいるうちに、「暗記ではない」ことを肌感覚で実感することができました。 成長には「苦しむこと」が必須ですね。。(汗) 11.さいごに まだまだITの語彙が乏しく、説明がかなり不十分かと思いますので、他Qiitaの記事や書籍などでIT知識の拡充をしてより皆様の参考になれる記事が書けるように精進します。 資格として「基本情報技術者」も取得予定ですので、もしわかりやすい参考書などあればご教示いただければ大変助かります。 冒頭にも申し上げましたが、追加すべき機能や修正点などもあれば遠慮なく投稿いただければ幸いです。 拙い文章でしたが、最後までご覧いただきありがとうございました! ◆参考にさせていただいた主な教材 媒体 資料名 学習内容 Web Progate HTML, CSS, JavaScript...などの基礎を学習 Web ドットインストール JavaScriptの「ミニアプリを作成しよう」にて、今まで学んだ文法知識がどのように使われて1つのアプリになるのかを学びました。 Web Udemy Vue.jsとFirebaseでのtodoリストを作るチュートリアルを見ながらCRUD機能の実装方法を学びました。 Web Youtube 非同期処理やJSONなどJavaScriptの難しい概念を理解するためや機能実装のヒントを得るために動画でのチュートリアルを参考にしておりました。 Web Firebase公式リファレンス 最初にググるのはやはり公式リファレンスですね。 Web stackoverflow 自分が遭遇しているエラーと似たようなエラーに遭遇した方が質問をしているので、その質問のレスポンス内容を参考にしてエラー解決に繋げてました。 書籍 スラスラわかるJavaScript JavaScriptの基本知識を学びました。サンプル付録は全てjQueryでのコードだったので、web制作寄りの教材だった気がします。 書籍 Vue.jsのツボとコツがゼッタイにわかる本 Vue.jsの基本知識を学習。JavaScriptで書いたコードがVue.jsだとどういいうコードになるかという説明でわかりやすかったです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue 2】Vuexチートシート

Vuexとは コンポーネント間でデータを共有するため状態管理ライブラリ いわゆる、グローバル変数のようなものを定義し操作できる 使い方 これ以降、VueCLIで作成を想定して記載 1. Vuexのインストール Vuexのインストール $ npm install vuex 2. /src直下にstore.jsを作成 このへんの場所に関してはプロジェクトに応じて設定する 図. 今回のプロジェクトのディレクトリ構成 3. main.jsのVueインスタンスにVuexを登録 main.js import Vue from 'vue' import App from './App.vue' import store from './store' // 追加行 Vue.config.productionTip = false new Vue({ store, // 追加行 render: h => h(App), }).$mount('#app') 4. store.jsおよびVueコンポーネントを実装 store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // Vuexインスタンス生成 export default new Vuex.Store({ // state: Vuexで管理する状態を定義 state: { count: 0, // 1 name: 'MouMou', password: 'hoge' }, // getters : stateの値を取得する処理を定義 // 引数のstateは上で宣言した状態管理オブジェクト getters: { count: state => state.count, // 2 name: state => state.name, // 3 password: state => state.password, // 3 }, // mutations : stateの値を変更する処理を定義 // 引数のstateは上で宣言した状態管理オブジェクト // 非同期処理に対応していない mutations: { setCount(state, newCount) { // 4 state.count = newCount }, setName(state, newName) { // 5 state.name = newName }, setPassword(state, newPassword) { // 5 state.password = newPassword } }, // actions : mutationを呼び出す処理を定義 // 非同期も対応している actions: { setCountAction({ commit }, newCount) { // 6 commit('setCount', newCount) }, setNameAction({ commit }, newName) { // 7 commit('setName', newName) }, setPasswordAction({ commit }, newPassword) { // 7 commit('setPassword', newPassword) }, } }) App.vue (他コンポーネントでも同様に記載できる) <template> <div id="app"> <!-- state --> <p>1. count = {{countDirect}}</p> <!-- getters --> <p>2. count = {{count}}</p> <p>3. name = {{name}}</p> <p>3. password = {{password}}</p> <!-- mutations --> <p><button @click="setCount">4. count = {{count}}</button></p> <p><button @click="setName('Qiita')">5. name = {{name}}</button></p> <p><button @click="setPassword('huga')">5. password = {{password}}</button></p> <!-- actions --> <p><button @click="setCountAction">6. count = {{count}}</button></p> <p><button @click="setNameAction('Vuex')">7. name = {{name}}</button></p> <p><button @click="setPasswordAction('huge')">7. password = {{password}}</button></p> </div> </template> <script> // map系を使用する場合importする import {mapGetters} from 'vuex' import {mapMutations} from 'vuex' import {mapActions} from 'vuex' export default { computed: { // [1.state] 直接操作(非推奨) countDirect() { return this.$store.state.count }, // [2.getters] stateの値を取得 count() { return this.$store.getters.count }, // [3.getters] 一括でgettersを設定 ...mapGetters(['name','password']), }, methods: { // [4.mutations] stateの値を変更 setCount() { this.$store.commit("setCount", 1) // 第二引数はmutationに渡す値 }, // [5.mutations] 一括でmutationsを設定 ...mapMutations(['setName','setPassword']) , // [6.actions] mutationsを起動 setCountAction() { this.$store.dispatch('setCountAction', 2) // 第二引数はmutationに渡す値 }, // [7.actions] 一括でactionsを設定 ...mapActions(['setNameAction', 'setPasswordAction']) } } </script> 図. 表示画面 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでi18n対応

Nuxt.js簡単にサイト構築ができてとても便利ですね。 今回はNuxt.jsでi18n対応をやりましたのでその手順を記載します。 i18n対応とは 国際化対応とも言われる。 ソフトウェアやインターネットサイトを複数の言語で使用、閲覧可能な状態にすること。 「Internationalization」の省略表記で、最初と末尾の文字、及びその間の文字数を記載して「i18n」と呼ぶらしい。 今回はnuxt-i18nのパッケージを使用。 nuxt-i18nのインストール i18n対応可能とするパッケージnuxt-i18nをインストール yarn add nuxt-i18n package.jsonに追加されていることを確認 "dependencies": { ... "nuxt-i18n": "^6.26.0" } 言語翻訳ファイルの作成 ./locales/[laguage] 配下に言語ファイルを作成 以下は、./locales/ja/index.jsonの作成例 ※必要に応じて、他言語のファイルも作成 ./locales/ja/index.json { "welcome": "ようこそ!" } nuxt.config.jsファイルの設定 nuxt.config.jsファイルのmudulesの箇所に以下の文言を追加 ./nuxt.config.js modules: [ [ 'nuxt-i18n', { // 使用する言語の設定 locales: [ { code: 'ja', name: 'Japanese', iso: 'ja_JP', file: 'ja/index.json' }, { code: 'en', name: 'English', iso: 'en-US', file: 'en/index.json' } ], defaultLocale: 'ja', // デフォルトの言語 langDir: 'locales/', // 翻訳ファイルのディレクトリパス strategy: 'prefix_and_default', // URLに言語のプレフィックスを追加するかの指定 vueI18n: { // 翻訳ファイルが見つからなかった場合の言語を指定 fallbackLocale: 'ja' }, vueI18nLoader: true, lazy: true } ] ], Vueファイル内での使用 コンテンツ部となるVueファイル内で、jsonのキー情報を埋め込む $t('key-name')を指定することで、翻訳ファイルのキーと一致させることが可能 ./pages/index.vue <v-card-text> <p>{{ $t('welcome')}}</p> ... </v-card> 言語切替ボタンの追加 言語切替ボタンを追加するにはnuxt-linkを使用する v-btnを使用してボタン表示(UIフレームワークとしてvuetifyを使用) ./pages/index.vue <nuxt-link :to="switchLocalePath('ja')"><v-btn color="primary">Japanese</v-btn></nuxt-link> <nuxt-link :to="switchLocalePath('en')"><v-btn color="primary">English</v-btn></nuxt-link> 実行確認 一連の修正を実施して動作確認 npm run dev 実際表示された画面 真ん中の部分の「Japanese」及び「English」のボタンで言語切替が可能となりました。 ※ボタンすぐ上の「welcome!」の箇所が多言語対応されています。 最後に UIフレームワークとしてVuetifyを使用しましたが、そもそもVuetifyがi18n対応済でvue-i18nとは連携可能とことです。 この辺り、もう少し深堀りすることでもっと効率の良いやり方があるかも知れません。 また分かり次第、投稿したいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js~イベント処理~

概要 フォームの入力の読み取り、送信するまでのプログラムについての記事です。 v-on,v-modelについて主に取り上げています。 イベント発生 今回のフォームの実装では、ユーザーが入力して送信ボタンを押すのがイベント発生となります。 イベントを設定します。 HTML index.html <form @submit.prevent ="addItem"> <input type="text" v-model="newItem"> <input type="submit" value="add"> </form> イベントはsubmitです。 イベント処理を実装するのはv-onです。(上記では@sbmit)となっています。 更に今回のイベントは、ユーザーからの入力内容を受け取り反映したいので、input type="text"から値を受け取ります。 v-model v-model="データオブジェクトプロパティ" v-on v-on="...." (@イベント名="") sbmitされたときのメソッドを指定します。今回は、addItemです。 更に今回はボタンタグにsubmitを使用しています。それにより、ページ変移が起きてしまうのでpreventDefaultメソッドを使用します。 index.html @submit.prevent と記述します。 Vue つづいて、Vueの実装内容です。 index.js data:{ newItem:'', lists:[ 'list1', 'list2', 'list3' ] }, 双方向データバインディング 先程、v-modelで指定したプロパティはnewItemです。 現在こちらのプロパティは空に設定しています。こちらに文字列を入れてみました。 すると、テキストボックスに初期値が反映されていました。 これは、双方向データバインディングといいます。Vue.jsのフォーム開発で大切になります。 データオブジェクトからテキストボックスの反映はもちろん。テキストボックスからの反映も可能にします。(データオブジェクトとテンプレート状態を同期する) ちなみに、v-modelを利用した場合はテキストボックスでHTMLから指定されたValue属性は無効になります。 index.js methods:{ addItem: function() { this.lists.push(this.newItem); this.newItem = ''; } } 次に送信されたときの処理についてです。methodsというキーでイベントオブジェクトを作ります。 先程のHTMLで指定したaddItemで処理を書いていきます。 今回の処理内容は、submitしたときにlistsという配列にプッシュします。 data内のlistにはthisでアクセスすることが出来ます。this.lists.pushとすることで追加することが出来ます。何をプッシュするのかv-modelで取得したプロパティですね。 値を送信した後に値がテキストボックスからなくなるようにthis.newItem = ''空文字にします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】自動ルーティング設定

はじめに nuxt では vue-router を自分で設定していいみたい。 pages ディレクトリ配下に作った vueファイルは自動的にルーティングが行えるように設定してくれるようなので、ドキュメント見ながら簡単にまとめたいと思います。 自動的にルーティングを生成 下記のような .vue ファイルを pages/ 配下に作成すると、、 pages/ --| product/ -----| index.vue -----| name.vue --| index.vue 自動的に以下が生成されます router: { routes: [ { name: 'index', path: '/', component: 'pages/index.vue' }, { name: 'product', path: '/product', component: 'pages/product/index.vue' }, { name: 'product-name', path: '/product/name', component: 'pages/product/name.vue' } ] } ここで定義されているルートは、こんな意味です。 { name: 'product', path: '/product', //このパスにアクセスしたら component: 'pages/product/index.vue' //このコンポーネントを呼ぶ } 動的な場合 Qiitaなどの記事を API から取得する場合、記事の名前や記事IDのようなものがURIに入ることになります。そうなると、ルートの名前を決定することができないので、動的に設定する必要があります。 その場合は、アンダーバーをファイル名の頭に付けます。 pages/ --| article/ -----| _articleid.vue 下記が生成されます { name: 'articleid', path: '/article/:articleid', component: 'pages/article/_articleid.vue' } これにより、https://itblog.com/article/943095435 このようなURIで記事にアクセスできます。 記事IDを取得する 動的な記事ページ article/_articleid.vue があるとします。この articleid にアクセスするには、 this.$route.params.articleid でいけるみたいです。 URIを指定してページ遷移するには NuxtLink コンポーネントを使ってください。 <template> <NuxtLink to="/article">記事一覧</NuxtLink> </template> さいごに vue はポートフォリオつくる際に触りましたが、nuxt は初。何が便利なんだろうってところから一つ一つ理解。 Nuxt に用意された router プロパティで色々カスタマイズできるみたいです。 参考文献 Nuxt.js 公式 - ルーティングの基礎
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js~v-for~

概要 JSでは、イベント(ユーザーがクリックしたときなど)に処理(イベントハンドラーと言います)が始まります。 その際に、クリックイベントを取得することにより実行することができました。Vue.jsでも記述は多少異なりますが仕組みは変わりません。 繰り返し処理についての記事です。 繰り返し処理 Vue.jsでは、繰り返し処理の際に[v-for]を使用します。 HTML index.html <ul> <li v-for="list in lists">{{ list }}</li> </ul> 構文は以下のようになります。 v-for="仮変数 in 任意の配列" 配列listsから順に要素を取り出します。 マスタッシュ構文に変数listを格納します。 このlistの中にlistsの配列の中身がなくなるまで繰り返し処理されます。 Vue index.js data:{ lists:[ 'list1', 'list2', 'list3' ] } index番号 インデックス番号を取得します。 index.html <ul> <li v-for="(list,i) in lists">{{ list + i }}</li> </ul> //list1 0 //list2 1 //list3 2 配列要素+インデックス番号で取得することが出来ます。 番外編for文 index.js <span v-for="i in 10">{{i }}</span> //12345678910
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebaseとフロントエンドのみでオンラインゲームを作った話(ソースコード有り)

はじめに なんか急にゲームが作りたくなった!!! →よし作ろう!!! もっと詳しい動機 ・手軽にできるオンラインゲームがやりたい! →ないじゃん作りたい ・わがままなせいか、現状面白いと思えるゲームがない →自分で作ればいいじゃん ・頭の中では大規模な設計図がある →いきなりそんなの無理だよね(実際大規模ゲーム作ろうとして挫折した経験ありツクールやUnityだったが・・・。)  →まずはオンラインゲームのベースを探そう(お金掛けたくないしFirebaseがいいな)   →探したけど無い!!ならば俺が! 使った技術 Firebase(RealtimeDatabase)とVue.jsで作っています Cloud FireStoreでも同じことが出来ると思います RealtimeDatabase/Cloud FireStoreを使えばフロントエンドのみで、オンラインゲームが出来ますよー!ということですね 作ったもの ただのじゃんけんゲームです(期待させてすんません) 一応、多人数対応しています!!! https://onlinejyanken.web.app 目次 ソースコード 解説 ソースコード 実際は単一コンポーネント(.vue)ですがQiitaではハイライトがつかないため、HTMLとJavascriptを分割しています index.vue <template> <div> <p>じゃんけんリスト</p> <div v-for="(res,index) in jyankenList" :key="index"> <div class="d-flex"> <div style="width:50px; height:50px;"> <img class="fill" src="/assets/dasumae.png" alt /> </div> <b>{{res.name}}</b> </div> </div> <p>参加者リスト</p> <div v-for="(res,index) in userList" :key="index"> <div> <b>{{res.name}}</b> </div> </div> <!-- <input type="text" id="txb-room" v-model="roomId" class="form-control" placeholder="ルーム番号" /> --> <input type="text" id="txb-name" v-model="name" class="form-control" placeholder="名前" /> <button v-on:click="join(name)" id="btn-join" type="button" class="btn waves-effect waves-light orange darken-1" >参加</button> <div id="btn-list" class="d-flex justify-content-around"> <button disabled="true" class="btn waves-effect waves-light white" style="width:100px; height:100px; padding:0;" v-on:click="putOut(0,name)" > <img class="fill" src="/assets/gu-.png" alt /> </button> <button disabled="true" class="btn waves-effect waves-light white" style="width:100px; height:100px; padding:0;" v-on:click="putOut(1,name)" > <img class="fill" src="/assets/tyoki.png" alt /> </button> <button disabled="true" class="btn waves-effect waves-light white" style="width:100px; height:100px; padding:0;" v-on:click="putOut(2,name)" > <img class="fill" src="/assets/pa-.png" alt /> </button> </div> <p>結果!</p> <div v-for="(res,index) in jyankenListResultView" :key="index"> <div class="d-flex"> <div style="width:50px; height:50px;"> <img class="fill" :src="res.img" alt /> </div> <b>{{res.name}}</b> </div> </div> <h2 style="color:red;">{{result}}</h2> </div> </template> index.vue function uuid() { var uuid = "", i, random; for (i = 0; i < 32; i++) { random = (Math.random() * 16) | 0; if (i == 8 || i == 12 || i == 16 || i == 20) { uuid += "-"; } uuid += (i == 12 ? 4 : i == 16 ? (random & 3) | 8 : random).toString(16); } return uuid; } const myId = uuid(); import firebase from "firebase"; import { defineComponent } from "vue"; export default defineComponent({ name: "App", props: { roomId: String }, data() { return { name: "", //DBと紐付けるじゃんけんリスト jyankenList: [], //DBと紐付ける参加ユーザー一覧 userList: [], result: "", //結果表示用じゃんけんリスト jyankenListResultView: [] }; }, mounted() { let vm = this; //ユーザーのリスナー let key = "joinUser/" + this.roomId; firebase .database() .ref(key) .on("child_added", snap => { this.userList.push(snap.val()); if (this.userList.length >= 2) { this.changeBtnDisable(false); this.deletePutOut(this.roomId); } }); //ユーザーのリスナー firebase .database() .ref(key) .on("child_removed", snap => { let data = snap.val(); let temp = this.userList.filter(x => x.userId != data.userId); this.userList = []; this.userList = temp; }); //ジャンケンポンのリスナー let key2 = "jyanken/" + this.roomId; firebase .database() .ref(key2) .on("child_added", snap => { this.jyankenList.push(snap.val()); if (this.judge(this.jyankenList, this.userList)) { this.viewResult(this.jyankenList); this.result = this.culcJankenMain(this.jyankenList); this.jyankenList = []; this.changeBtnDisable(false); } }); }, methods: { //ジャンケンポンの計算をするか judge: function(jyankenList, userList) { let jankenUserIdList = jyankenList.map(x => x.userId); for (let user of userList) { if (jankenUserIdList.includes(user.userId) == false) { return false; } } return true; }, //じゃんけんのメイン(計算メソッドを呼んで結果を表示) culcJankenMain: function(jyankenList) { let theyPutOut = jyankenList.filter(x => x.userId != myId); let myPutOut = jyankenList.filter(x => x.userId == myId)[0]; let res = this.culcJanken( myPutOut.value, theyPutOut.map(x => x.value) ); switch (res) { case 1: return "勝ち!"; case -1: return "負け!"; case 0: return "あいこ"; } }, //結果表示(じゃんけん一覧) viewResult: function(jyankenList) { for (let jyanken of jyankenList) { let temp = jyanken; temp["img"] = this.getImg(temp.value); this.jyankenListResultView.push(temp); } }, getImg: function(num: number) { switch (num) { case 0: return "/assets/gu-.png"; case 1: return "/assets/tyoki.png"; case 2: return "/assets/pa-.png"; } }, //ユーザー参加 join: function(name: string) { document.querySelector("#btn-join").disabled = true; document.querySelector("#txb-name").disabled = true; let key = "joinUser/" + this.roomId + "/" + myId; var ref = firebase.database().ref(key); let obj = { userId: myId, datetime: new Date().toLocaleString(), name: name }; ref.set(obj); }, //じゃんけんぽん putOut: function(your: number, name: string) { this.jyankenListResultView = []; let key = "jyanken/" + this.roomId; var ref = firebase .database() .ref(key) .push(); let obj = { jyankenId: uuid(), value: your, datetime: new Date().toLocaleString(), userId: myId, //RDB脳な方はおかしいだろって思うかもしませんが、RealtimeDatabaseでは助長化が常識です name: name }; this.changeBtnDisable(true); ref.set(obj); }, //じゃんけんのデータをDBから消す deletePutOut: function(roomId) { let key = "jyanken/" + roomId; var updates = {}; updates[key] = null; firebase .database() .ref() .update(updates); }, //じゃんけんの計算 //ホスト・クライアントのシステムであれば、ホストのみで計算をさせるべきだが //ホストが計算して各クライアントに返すと通信量が増えたり、動作が遅くなったりすることを考えると //各クライアントが計算するのでもいっか!という感じ //二人以上のじゃんけんにも対応 culcJanken: function(your: number, opponentList: number[]) { let allList = []; allList.push(your); allList = allList.concat(opponentList); let kindCount = 0; let opponent = -1; if (allList.includes(0)) { if (your != 0) { opponent = 0; } kindCount++; } if (allList.includes(1)) { if (your != 1) { opponent = 1; } kindCount++; } if (allList.includes(2)) { if (your != 2) { opponent = 2; } kindCount++; } if (kindCount != 2) { return 0; } if (your == 0 && opponent == 1) { return 1; } else if (your == 1 && opponent == 2) { return 1; } else if (your == 2 && opponent == 0) { return 1; } return -1; }, changeBtnDisable: function(disabled) { Array.prototype.map.call( document.querySelector("#btn-list").querySelectorAll("button"), function(i) { i.disabled = disabled; } ); }, //ページ遷移時のハンドラ //参加者から自分を消す transitionPage(event) { let key = "joinUser/" + this.roomId + "/" + myId; var ref = firebase.database().ref(key); var updates = {}; updates[key] = null; firebase .database() .ref() .update(updates); } }, created() { window.addEventListener("beforeunload", this.transitionPage); }, destroyed() { window.removeEventListener("beforeunload", this.transitionPage); } }); 解説 ソースコード上にがっつりコメント書いたので、あまり言うことはないのですが ルートpropでルームIDを受け取って、そこのルームでじゃんけんするって感じです 以下、簡単な流れです 参加する 参加者が二人以上でじゃんけんボタン有効化 じゃんけん! 各クライアントのじゃんけんをイベントでリッスン 参加者全員がじゃんけんを出し終わったら計算 勝ち負け表示! その他にも、途中脱離した場合は参加者一覧から消すなどもしています (そうでないと、全員出し終わらない状態が続いてどうにもならなくなるため) 色々ガバガバですが、一応オンラインゲームのベースになるんではないでしょうか・・・ ガバガバリスト ・各クライアントで結果計算しているため齟齬が発生する場合がある。(本来、ホスト・クライアントでは、ホストのみが計算してクライアントに返すべき。ただこの方法だと無駄な通信が発生するので嫌だなと) ・じゃんけんの結果を生値で返しているので、開発者ツールで値覗けば相手がだしたのが何かわかる ・ゲーム終わった後DB消す処理がないので、ゴミデータが残る(参加者が一人の場合ページ遷移したらルームデータ全消しでいいのかも。) ・セキュリティがあれ(RealtimeDatabaseのルール設定ちゃんとすればいくらかは・・・) 等。ほかにもあるかも
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js出力

概要 Vue.jsでこんにちわを出力するまでについて、まとめました。 HTML 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="stylesheet" href="css/styl.css"> <title>Document</title> </head> <body> <div id="app"> <p>皆さん{{ message }}</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> <script src="js/main.js"></script> </body> </html> Vue.jsは、HTMLのテンプレート構文があります。 今回の実装ではHTMLファイルを使用します。 Vue.jsの機能を適用させるためにbody要素の直下にdiv要素を作ります。id属性を使用します。名前は特に決まりはありません。 JS側で、このidをターゲットにVue.jsが提供するデータや、メソッドを使用します。 このidターゲットがない場合、Vue.jsを使用することが出来ません。 {{...}}これをマスタッシュ構文と言います。これを書くことで、dataオブジェクトのmesaggeプロパティの値を引用することが出来ます。 scriptタグには、Vue本体のCDNを組み込みます。ローカル環境にダウンロードしたVue本体のファイルを読み込み、ユーザーが作成したファイルを実行する方法です。 2つ目のscriptタグにJSファイルを読み込みます。 *注意:2つのスクリプトタグを反対に記述すると、CDNが読み込まれる前にJSファイルが読み込まれてしい、errorになります。 JS index.js const app =new Vue({ el:'#app', data:{ message:'こんにちわ' } }); Vue.jsは、Vueクラスをインスタンス化して使用します。 new Vueは、Vueクラスからインスタンスを生成する場合の命令です。 {}の中には、オブジェクトリテラルを書きます。 el ▶Vueインスタンスを結びつけるHTML要素を指定します。 data▶Vue.jsで扱うデータを入れておく場所です。 このデータをHTMLファイルから読み込み画面に表示することが出来ます。テンプレートHTMLから参照できる値を参照できる値を格納したオブジェクトです。なお、マスタッシュ構文に格納できるのは式だけです。代入、条件分岐は使用することは出来ません。 Vue.jsでは、アプリを利用する値をデータオブジェクトで用意して、テンプレートが参照することをデータバインディングと言います。 {マスタッシュ構文}では属性の値などは指定することは出来ません。 属性の変更、条件分岐などはディレクティブを使用します。ディレクティブは[v-]から始まります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独学・未経験者がポートフォリオを作った【Rails / Vue.js / Docker / AWS】

はじめに はじめまして! 未経験者が独学でポートフォリオを作成しましたので、紹介させてください。 About Me 私は33歳で現在、看護師として病院に勤務しています。 看護師として働く前は、テレコミュニケーターとして某プロバイダーの派遣社員として働いていました。 そういった経歴でパソコンを触るのが好きだったこともあり、独自で作成した動画を使って、部署内で勉強会を開催するなどしていました。 昨今の新型コロナウイルスの影響で、従来の「集まって教育・学習・研修をする」ということができなくなり、IT機器やオンラインの重要性を個人的にも感じていました。 「もっと看護業界、医療業界が加速するようなことがしたい」と考えるようになり、以前から興味のあった「Webエンジニア」への転職を目指しました。 2020年5月からプログラミング学習を開始しました。 紹介させていただく、ポートフォリオの作成期間は約3ヶ月半(2021年1月〜4月はじめ) ポートフォリオ紹介 アプリ名「With Nurse」 URL:https://www.withnurse.net/ Githubリポジトリ TOPページ レスポンシブ対応 ログイン後TOPページ 概要 看護師専用の記事投稿+学習サービスです。 記事投稿機能やVue.jsを使った一部SPAのゲーム機能を実装しています。 なぜ開発したか About Meでもお話したように、私は看護師として働きながら、プログラミング学習をしています。 その中で感じた、以下の理由で開発しようと考えました。 1.同業者との情報共有でスッと理解できる 現場では先輩から看護について教育を受けます。その中でテキストや文献を読むだけではなかなか理解できなかったことを、その先輩の説明でスッと理解できる場面がたくさんあります。 それは先輩だけでなく、同期や後輩の言葉が理解を深めるということも多々あります。 もちろん二次情報になるので、勘違いや見当違いなこともあるわけなので、盲信できるわけではありませんが、現場で起きている「スッと理解できる」という場面をネット上でもできないかと考え、このアプリを開発しました。 2.Qiitaに感動した プログラミング学習を初めて、一番驚いたのがたくさんの人が情報発信をしていたということです。 ポートフォリオを作成しながら、わからない・できないところはググって実装していきました。 ほとんどのことをネット上で調べることができました。 たくさんの情報発信をエンジニアの方たちがされていること、そして同じエンジニアの人たちと知識を共有しているエンジニア業界の仕組みに感動したからです。 公式ドキュメントなどで一次情報を調べることは必須ですが、理解しづらいことが、ある人の情報発信で「スッと理解できる」Qiitaのようなサービスがあればいいなと考えたからです。 3.学習サイト、スキルトレーニングサイトの重要性 私は物事を学習する上でアウトプットに勝るものはないと考えています。 私自身も勉強会に参加するときよりも、開催するほうが自分の知識やスキルが向上することを実感していたからです。 自分の得意な分野や学習を始めた分野のアウトプットをする場所がインターネット上にあればとても有用だと思い、記事投稿サービスを作りました。 また看護師として10年勤務して、「このスキルを鍛えておくと、非常に有用である」と思った内容をゲームとして提供しています。 タイピングゲームを実装しているのですが、それもその一つです。 使用技術 HTML/CSS Bootstrap SASS javascript jQuery Vue.js Ruby 2.6.3 Ruby on Rails 6.0.3 Rubocop (コード解析ツール) RSpec (テスト) Docker/Docker-compose(ローカル開発環境からデプロイまで) AWS(VPC, EC2, Route53, ELB, S3, RDS, ACM) 実装機能 基本機能 新規会員登録・ログイン機能 Googleアカウントログイン機能 パスワードリセット機能 記事一覧機能(ページネーション) 記事ソート機能(タグで絞り込み) 記事検索機能(テキスト検索) タグ機能 記事詳細画面閲覧機能 タイピングゲーム機能(データー登録はしない) いいね数表示機能 お問い合わせ機能 ログイン後機能 ユーザーマイページ表示機能( フォロー・フォロワー表示・My記事一覧表示・My記事投稿数表示機能 ) ユーザー情報変更機能(アイコン画像・ユーザーネーム・メールアドレス・パスワード) MYユーザー削除機能 タイピングゲーム機能(スコア、プレイ回数表示、保存) 記事いいね機能(非同期通信) ユーザーフォロー機能 記事投稿機能(ActionText) 記事編集機能 画像投稿機能(ActiveStorage経由でS3バケットに保存) 管理者機能 ユーザー一覧表示 ユーザー削除機能 インフラ構成図 ER図 ポートフォリオの工夫したところ ユーザー対象は看護師で、業務上よく使う、officeのWordのUIに比較的近い「Action Text」を採用 課題 技術選定、DB設計など、初期の段階で構想がめちゃくちゃ甘かった 未経験からのWEB系エンジニアへの転職が当面の目標であったため 多く採用されている 学習がしやすい(公式ドキュメント、実装事例が多い、ユーザーが多いため、質問しやすい) 上記の理由からRuby on Rails、Vueを使うとは決めていました。 しかしAWSやDockerの採用などはなぜこの技術なのかという技術的判断よりも、デファクトスタンダードであろう技術であり、ポートフォリオ作成時点でキャッチできた情報で手探りで作りました。 なぜその技術が必要なのかという判断をするための知識もまだまだ不足しているため、ポートフォリオの要件に合わせて、ブラッシュアップしていく必要があります。 体系的に技術の学習が今後も必要 これはポートフォリオ作成ではなく、プログラミングを行う上での課題です。 よく言われている「プログラミングは暗記しなくていい」「まず手を動かしながら覚えていく」精神でポートフォリオ作成をしてきました。 わからないところはすぐにググって、都度実装してきたが、「実装したコードをちゃんと理解しているか」と自分に問うと、非常に怪しい・・・ コードを見直せば、何をしているのかはなんとなくわかるが、しっかり言語化するまでに時間がかかる。 体系的に理解できていないことがその原因だと考えています。 独学にこだわりすぎたのも問題で、メンターサービスなどの活用もするべきでした。 まとめ 当初考えていた機能は実装できたので、完成としました。 リファクタリングや機能の追加は今後も継続していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

elについて

Vueインスタンス内のelというオプションについて確認しておきましょう。 elというオプションが指定した要素がVue.jsが作用を及ぼす範囲になります。 elは「element」の略です。 '#app' のように、DOM要素を指し示すセレクタが指定されます。 このプロパティの値は、Vueアプリーケーションの範囲、Vueの管轄領域を表します。 タグ内部がVueアプリケーションとして認識されるということです。 https://jsstudy.hatenablog.com/entry/What-is-el-in-Vuejs elやdataは、Vue.jsのAPIと呼ばれるもので、Vue.jsを扱う上での規格のようなものです。 用意されているAPIを使って、さまざまな処理を記述します。 new Vue({})の直下にあるものはAPIと思ってだいたい間違いないです。 初期化 初期化 Vueインスタンスの生成の際に el オプションが与えられた場合、Vueインスタンスは直ちにコンパイルの段階に入ります。コンパイルの段階の間に、VueはDOMをひと通り見て、遭遇したディレクティブを収集します。一旦紐付けられると、それらのDOMノードはVueインスタンスによって管理されていることになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】インスタンス内のプロパティーにアクセスする

先ほどの関数をつかってい同じインスタンス内にあるメッセージプロパティーを呼ぶには「this.」をつける必要がある。 <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <div id="app"> <p>{{sayYes()}}</p> <!-- <button v-on:click="reverseMessage">メッセージ反転</button> --> </div> <script> new Vue({ el: '#app', data: { message: "おはよう"}, methods: { sayYes: function() { return this.message; } } }) </script> </body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsの勉強がてらにエンジニアポートフォリオサイト作ってみた

はじめに Vue.jsの勉強がてらポートフォリオサイト作って公開しました。せっかく作ったので忘れないように技術的な内容をまとめようと思います。 Vue.jsやアニメーションなど効果的には使えていないと思うので、温かい目で見てもらえると幸いです。 Web開発の経験について 5,6年前にとある企業の社内報サイトと人材管理Webシステムの開発に携わり、下記のWeb系の技術経験が1~2年ほどあります。(5年以上経つのでかなり忘れていました。。。) HTML, CSS, AngularJS, SpringBoot, MySQLなど モチベーション たまたまWebデザインの最新トレンド記事を読んで、自分のデザイン感覚がアップデートできていないことに気づいた。最低限の感覚はアップデートしたい せっかくなのでフロントエンドの人気のフレームワークに触れてみたい フロントエンドの忘れた知識を取り戻したい 今まで技術的な発信してこなかったので何か発信したい 作成したポートフォリオサイトについて 作成したサイトはこちら。Ike's Portfolio 技術要素 Vue.js 単一ファイルコンポーネント Vue Router / SPA Element UI デザイン パララックス アニメーション マウスカーソルの動きに合わせて動く背景(パララックスの一部かも?) その他 Firebase Hosting 画面構成 TOP アニメーション マウスカーソルの動きに合わせて動く背景 Profile Skill Set アニメーション(スライドイン) パララックス Works アニメーション(フェイドイン) element ui (プログレスバー) Contact 実装 全体 各セクションを単一ファイルコンポーネントで作成し、大枠のvueから呼び出すような構成にしました。 App.vue <template> <div id="app"> <Header /> <!-- ルーティング時の表示領域を準備 --> <router-view/> <!-- Home.vueが呼ばれる --> <Footer /> </div> </template> Home.vue <template> <div class="home"> <Splash /> <Profile /> <Skill /> <Works /> <Contact /> </div> </template> アニメーション(フェードイン、スライドイン) アニメーションは複数ファイルで利用できるようコンポーネント化しました。「slot」を利用してアニメーションさせたい要素を別ファイルから差し込めるようにしています。 参考記事:[Vue.js]スクロールでコンテンツがフェードインする実装 また、アニメーション処理については、まずスクロール位置を管理するため「addEventListener()」を用いてスクロールイベントをリスナー登録します。特定の位置までスクロールしたら変数visibleをtrueにし、アニメーションを指定するcssのclassが付与されるようにしています。 下記はフェードインのソースコードですが、スライドインも同様に作成しています。 FadeInComponent.vue <template> <div ref="fade" :class="{fadein: visible, hidden: !visible}"> <slot v-show="visible"></slot> </div> </template> <script> export default { name: 'FadeInComponent', data () { return { visible: false } }, created () { window.addEventListener('scroll', this.handleScroll) }, destroyed () { window.removeEventListener('scroll', this.handleScroll) }, methods: { handleScroll () { if (!this.visible) { var top = this.$refs.fade.getBoundingClientRect().top this.visible = (top + 100) < window.innerHeight } } } } </script> <style scoped> .hidden { opacity: 0; } .fadein { animation: fadeIn 1s; opacity: 1; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY(50px); } 100% { opacity: 1; transform: translateX(0px); } } </style> アニメーションさせたい要素を上記コンポーネントで挟み込むことで動作するようにしています。 Skill.vue <FadeIncomponent> <h1 class="title">Skill Set</h1> </FadeIncomponent> パララックス わかりにくいですが、Skill Setの背景にパララックスを適用しています。 パララックスはdirectivesを使い、HTMLタグに指定できるようにしました。 scroll量に合わせて背景画像のポジションをbackground-position-yで変化させることで実現しています。 参考記事:https://note.com/rottenmarron/n/nb7a23b1483ca Skill.vue(directives箇所を抜粋) <script> ~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~ directives: { parallax: { inserted (el, binding) { el.setAttribute( // 初期状態のスタイル 'style', ` background-position: center top; background-size: initial; backgroung-repeat: no-repeat; background-position-y: ${window.scrollY / binding.value.denominator}px; ` ) window.addEventListener('scroll', () => { el.setAttribute( // スクロール量に応じて動的に変化するスタイル 'style', ` background-position: center center; background-size: initial; backgroung-repeat: no-repeat; background-position-y: ${window.scrollY / binding.value.denominator + 300}px; ` ) }) } } }, ~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~ </script> 下記のようにHTMLタグにdirectivesを指定することで要素にパララックス効果を適用しています。 Skill.vue(directiveの指定) <div id="skill" class="bg-blar" :class="{safari: safari}" v-parallax="{ height: '100vh', denominator: 1.8 }"> マウスカーソルの動きに合わせて動く背景 parallax.jsライブラリを使用しました。 視差効果を付与したい要素に対してnew Parallaxでオプションを指定するだけでマウスカーソルの位置に合わせて要素が移動します。 Splash.vue(HTML) <div id="parallax"> <div data-depth="0.2"> <h1 class="title"> Ike's Portfolio </h1> <span>Full Stack Engineer</span> </div> </div> Splash.vue(script) <script> export default { ~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~ mounted () { this.$nextTick(function () { var layer = document.getElementById('parallax') const parallax = new Parallax(layer, { scalarX: 10.0, // X軸の移動速度と範囲を増減 scalarY: 10.0, // Y軸の移動速度と範囲を増減 frictionX: 0.1, // X軸のレイヤーの速度。0.00〜1.00の範囲内で指定可能 frictionY: 0.1, // Y軸のレイヤーの速度。0.00〜1.00の範囲内で指定可能 }) }) window.addEventListener('scroll', this.onScroll) }, ~~~~~~~~~~~~ 省略 ~~~~~~~~~~~~~~ </script> プログレスバー Element UIライブラリを利用しました。 Elementライブラリ導入後に、<el-progress>タグを利用することで簡単にプログレスバーを描画することができます。 Skill.vue <div class="progress-wrapper"> <span>Swift / Objective-C</span> <el-progress :percentage="80"></el-progress> </div> Web公開 Firebase Hosting ホスティングにはFirebase Hostingを利用しました。以下の理由で選定しました。 将来的に他のFirebaseサービス(Cloud Functions for Firebaseなど)と組み合わせる可能性あり サーバ費用、ドメイン費用が無料 CDNエッジサーバのSSDキャッシュに保存されるので、コンテンツを高速に配信できる 導入方法はFirebase Hosting公式サイト記載の導入手順を実行すればお手軽に導入できます。 今後の展望 内容が薄いので厚くしたい。特に個人開発の欄を増やしたい 検索しても出て来ないのでSEO対策する CSSを整理したい ブログ作って発信する量を増やしたい 色々効果的に利用できていないので、UI/UXデザインを学びたい 移り変わりが早いので定期的に新しいトレンドを学ぶ 最後まで閲覧いただきありがとうございました。もしコメントがあればいただけますと嬉しいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【vue.js】{{  }}二重中括弧

<body> <script src="vue.js"></script> <div id="abc"> <p>{{message}}</p> </div> <script> new Vue({ el: '#abc', data: { message: 'HelloWorld!' }, }) </script> </body> {{}}二重中括弧はnew Vueでコードされたdataの中の同じキー[message]を探してその値[HelloWorld]を代入し表示してくれる。 (発展) {{}}の中身はRuby on Railsの式展開#{}のように変数や構文が使えます。 ・データの中身を5に変えて data: { number: 5} <p>{{number}}</p> ・その数を使って計算させてみる <p>{{number + 3}}</p> ・関数を使ってメソッドを作って表示 同じ処理を定義し何度も使い回しができる形にしたもの。 関数とは <script> function 関数名(){ 処理 } </script> 無名関数とは関数名なしのもの。 <script> function(){ 処理 } </script> 今回は <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <div id="app"> <p>{{sayYes()}}</p> </div> <script> new Vue({ el: '#app', // data: { number: 5} methods: { sayYes: function() { return 'Yes'; } } }) </script> </body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsはじめます

(1) へアクセスします。 (2) CDNをコピペしてください。ダウンロードへダウンロードします。 (3) <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Vue TEST</title> <!-- Vue.js を読み込む --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> </head> <body> <div id="app-1">{{ message }}</div> <!-- {{ message }} が Vueデータに置換される --> <script> var app1 = new Vue({ el: '#app-1', /* #app-1 要素に対して Vue を適用する */ data: { message: 'Hello world!' } /* message という名前のデータを定義する */ }) </script> </body> </html> Vue.js入門 - とほほのWWW入門 https://www.tohoho-web.com/ex/vuejs.html#how_to_use
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む