- 投稿日:2020-02-16T23:41:04+09:00
Vue Composition API を利用した段階的なコアロジックからのモダン化
jQueryなどを利用したレガシーコードにVue.jsを導入してコードを書き換える際、雑に分類すると2つの方法があります。
- 気合いで一気にやる
- 段階的に書き換える
全体像が把握できている場合は 「1. 気合いで一気にやる」も全然アリですが、多くの場合はリスク軽減といった観点から「2. 段階的に書き換える」ための手段を模索することになります。
さて、Vue.js 3.x からは Composition API という関数ベースでの新しいAPIが導入されました。
これを利用すると、Vue.js 2.x 系までは主にVue.jsのコンポーネント内部で利用されていたリアクティブシステムについて、Vue.jsの外部でも利用可能となります。また、CompositionAPIを利用する関数は、Vue.jsコンポーネント内部でも利用可能なので、「意味のある機能単位で関数として切り出す」→「テンプレート部をVue.js化していく」といった順番での書き換えも選択肢に入ります。
Vue Composition API を利用した書き換え(実例)
次のようなシンプルなTODOアプリの置き換えを例に考えてみます。
TODOを追加して件数を表示するだけのシンプルなものです。index.html<!DOCTYPE html> <html> <body> <button id="addTodo">Add Todo</button> <div id="todoList"></div> <span id='todoCount'></span> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script> <script src="./legacy.js"></script> </body> </html>legacy.jsfunction addTodo() { var input = $('<input>'); input.attr('type', 'text'); var todo = $('<div>'); todo.addClass('todo'); todo.append(input); $('#todoList').append(todo); } function updateCount() { var count = $('.todo').length; $('#todoCount').text('Total: ' + count); } $(function() { $('#addTodo').on('click', function() { addTodo(); updateCount(); }); updateCount(); });ここからCompositionAPIによる切り出しを試してみましょう。
(ビルドの設定などに関する説明はこのエントリでは省略します)TODOの管理・追加を
useTodo.js
というファイルに切り出すと次のようなイメージになります。useTodo.jsimport { reactive, computed, toRefs } from "@vue/composition-api"; export const useTodo = () => { const state = reactive({ todoList: [] }); const count = computed(() => state.todoList.length); const addTodo = () => { state.todoList.push({ todo: "" }); }; return { ...toRefs(state), count, addTodo } };この関数を適切な箇所で呼び出すことで、レガシーコードと並行して状態管理を構築することができます。
legacy.js(useStore.jsを呼び出し)import Vue from 'vue'; import VueCompositionApi from '@vue/composition-api'; import { useTodo } from './useTodo'; Vue.use(VueCompositionApi); // Vue2.x系で利用する場合は必須 const todoState = useTodo(); // CompositionAPIを呼び出し function addTodo() { var input = $('<input>'); input.attr('type', 'text'); var todo = $('<div>'); todo.addClass('todo'); todo.append(input); $('#todoList').append(todo); } function updateCount() { var count = $('.todo').length; $('#todoCount').text('Total: ' + count); } $(function() { $('#addTodo').on('click', function() { addTodo(); updateCount(); todoState.addTodo(); // 状態管理に書き込み }); updateCount(); });レガシーコードを動作させたまま、Vue.jsで必要となる状態(データ)のみを収集可能となりました。
こういったコードを増やしていき、状態管理が整った時点でVue.jsに一気に置き換えることで、見通しが良い状態で書き換えに着手することができます。watchによるDOMからの段階的な切り離し
ここまでに説明した内容は、Vue Composition API 以外の方法でも実現可能です。
外部でVuex/Redux/MobXといった状態管理ライブラリで状態を集約しても良いですし、
Vue2.x系でもVue.observable
といったAPIを利用することでもほぼ同様のことが可能です。しかし、Composition API を利用した書き換え時の利点として
watch
APIの存在が挙げられます。
watch
APIは Composition API で生成されたリアクティブな値を監視し、変更時に任意の処理を実行できます。これにより、従来はDOMに依存していた箇所を、少しずつデータを中心とする形に書き換えていけます。
legacy.js(データ依存での動作に変更)import Vue from 'vue'; import VueCompositionApi, { watch } from '@vue/composition-api'; import { useTodo } from './useTodo'; Vue.use(VueCompositionApi); const todoState = useTodo(); // 件数の変更をトリガーにDOMを更新 watch(todoState.count, (count) => { $('#todoCount').text('Total: ' + count); }); // 件数の増分だけTODOを追加 watch(todoState.count, (count, prevCount = 0) => { [...Array(count - prevCount)].map(() => { var input = $('<input>'); input.attr('type', 'text'); var todo = $('<div>'); todo.addClass('todo'); todo.append(input); $('#todoList').append(todo); }); }); $(function() { $('#addTodo').on('click', function() { todoState.addTodo(); }); });件数の表示やTODOの追加の処理が、データの変更をトリガーに動作するようになりました。
何が嬉しいのか
コアとなる処理のみを先に切り出せる
古き良きDOM依存コードは、カオスなDOM操作コードの影に隠れている 「アプリケーション全体はどういうデータを必要とするのか?」「結局何をやってるのか?」という点を洗い出すのに非常に苦労します。
今回の方法の場合、予めコアとなるロジック部をデータを含めて切り出すことができ、既存の挙動を維持したままコア部分のみテストを書き足して動作を保証しておく、といったことも可能となります。
DOM依存箇所を徐々に減らしていける
DOMに依存しているコードを削っていく場合、影響箇所を把握するのが一番骨の折れる作業になります。
watch
の利用は人によっては抵抗がありそうですが、データを中心とした世界に徐々にシフトしていくことで、Vue.jsコンポーネントに切り出す際も、どういった単位で進めれば良いかの判断がしやすくなります。切り出したものがVue.jsコンポーネント内部でそのまま使える
最終的にVue.jsへの置き換えを想定する場合、大きい恩恵になるのがコレだと予想されます。
Vue Composition APIを利用している関数は、当然ですが最終的にテンプレートごとVue.jsコンポーネント化した際にそのまま利用できます。事前にレガシーコード時に切り出したコアロジックが、そのまま持っていけるのです。
上記サンプルコードの場合、最終的に次のような形で利用可能です。
App.vue<template> <div> <button @click="addTodo">Add Todo</button> <div> <div v-for="(todo, index) in todoList" :key="index"> <input type="text" :value="todo.todo" @input="updateTodo(index, $event.target.value)" /> </div> </div> <span>Total: {{count}}</span> </div> </template> <script> import { createComponent } from "@vue/composition-api"; import { useTodo } from "./useTodo"; export default createComponent({ name: 'App', setup() { return { ...useTodo() // 事前に作成したコアロジックを利用 } } }); </script>useTodo.js(※updateTodoのみ新たに追加)import { reactive, computed, toRefs } from "@vue/composition-api"; export const useTodo = () => { const state = reactive({ todoList: [] }); const count = computed(() => state.todoList.length); const addTodo = () => { state.todoList.push({ todo: "" }); }; const updateTodo = (index, todo) => { state.todoList[index].todo = todo; } return { ...toRefs(state), count, addTodo, updateTodo } };app.jsimport Vue from 'vue'; import VueCompositionApi from '@vue/composition-api'; import App from "./App.vue"; Vue.use(VueCompositionApi); new Vue({ render: (createElement) => createElement(App) }).$mount("#app");テストを先に書いておけば、コアロジックは壊していないことを担保したままで、Vue.jsテンプレートとの連携だけを意識して書き換えていくことが可能となり、段階的な移行時には安心して作業を進めていけそうです。
まとめ
というわけで、Vue Composition APIを利用した段階的なモダン化の考察でした。
正直冗長な部分もあるので、どんなシチュエーションでも使える手法ではないとは思います。
(さっさとVue.jsでガッと書き換えた方が速いでしょ、と思う気持ちもありますし、そもそもゼロから全部書き換えたい気持ちになることも多そう)ひとまず、手札のひとつとして持っておくと便利かもな〜と思いました。
- 投稿日:2020-02-16T23:27:26+09:00
【Vuejs】axiosを使ってみる
公式ドキュメントを参考に試してみました。
公式ドキュメント#基本的な例
https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BE%8Bスタブ
スタブを用意するのが面倒なので公式ドキュメントにも載っている以下を利用します。
https://api.coindesk.com/v1/bpi/currentprice.json
こんな感じのJSONが返ってきます。
作ったもの
sample.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test</title> </head> <body> <style> .bpi.header { background-color: beige; } .bpi.even { background-color: azure; } .bpi.odd { background-color: beige; } </style> <div id="app"> <div id="rawdata"> <strong>rawdata :</strong><br>{{coindesk}} </div> <br> <div id="update_time"> <strong>update_time :</strong>{{coindesk.time.updated}} </div> <div id="desc"> <strong>disclaimer :</strong> {{coindesk.disclaimer}} </div> <div id="chartName"> <strong>chartName :</strong>{{coindesk.chartName}} </div> <table> <tr class="bpi header"> <th>No.</th> <th>code</th> <th>symbol</th> <th>rate</th> <th>description</th> <th>rate_float</th> </tr> <tr v-for="(e,key,index) in coindesk.bpi" v-bind:class="[index % 2 == 0 ? 'even' : 'odd']" class="bpi"> <td>{{index+1}}</td> <td>{{e.code}}</td> <td>{{decode(e.symbol)}}</td> <td>{{e.rate}}</td> <td>{{e.description}}</td> <td>{{e.rate_float}}</td> </tr> </table> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> var app = new Vue({ el: "#app", data: { coindesk: null, }, mounted() { axios .get('https://api.coindesk.com/v1/bpi/currentprice.json') //.then(response => (console.log( response.data))) .then(response => (this.coindesk = response.data)) }, methods: { decode: function (str) { return str;// TODO デコード結果を返したい } } }) </script> </body> </html>実行結果
ポイント
- 配列ではなくオブジェクトをv-forする場合はkey,indexの2つ必要だった。
- 公式ドキュメントだと
response => (this.info = response)
となっているけどAPI叩いた時のレスポンスはresponse => (this.info = response.data)
の中にある。- ライフサイクルはmountedでなくてもcreatedでも多分OK。
TODO
次はaxiosを使って非同期でデータ取得を試したい。
- 投稿日:2020-02-16T22:58:33+09:00
うんちメーカーを支える技術 ~ 状態遷移はうんちの夢を見るか?
うんちメーカーの誕生
「うんちメーカー」というブラウザゲームを作りました。
うんちの長さを競い合うゲームで、オンラインのランキング機能があります。
使用した技術はDjango, Vueなどです。
今回はこのうんちメーカーで使っている技術について書いてみたいと思います。主役はVue
フロントエンドのフレームワークはVueを使いました。
Vueはコンポーネント志向のフレームワークで、SFC(Single File Component)という単位でモジュールを定義できます。
例えば↓のようにです。<script> export default { data () { return { } } } </script> <template> <div class="">Hello, World!</div> </template> <style lang="scss" scoped> </style>SFCはJavaScript, HTML, CSSを1つのファイルにまとめたコンポーネントで、これらを1つにまとめることで効率よく開発することが出来ます。
↓のようにコンポーネントを利用すると、属性にデータを渡すことができます。<my-component :my-data="1" />属性に渡されたデータはSFCでは
props
というオブジェクト内の変数に共有されます。
この変数はたとえば↑のmy-data
の値を変更すると、その変更がリアルタイムでSFCにも反映されます。
このprops
の機能を使うことで、状態を持つコンポーネントを簡単に定義することが出来ます。コンポーネントの状態遷移
ゲームは状態遷移のかたまりだと、今回の開発で思いました。
状態遷移とは、状態を定義した変数をひとつ用意して、その変数の値を次々に変化させて、モジュールなどの振る舞いを変える技術を指します。たとえば↓のような変数があるとして、
const status = 'first'この変数は↓のように参照されます。
switch (status) { case 'first': /* TODO */ break case 'running': /* TODO */ break case 'waiting': /* TODO */ break }
switch
文のcase
に状態に応じた処理を書くことで、状態遷移を実現させることが出来ます。
状態を持つ変数自体は、各状態の処理の中で変更していきます。先ほどのコンポーネントの
props
にこのような状態を持たせることで、コンポーネントに状態遷移を行わせることが出来ます。
例えば↓のようにです。<my-component :status="myComponentStatus" />コンポーネント内では、
setInterval
でループを回し、この状態を監視させます。mounted () { this.iid = setInterval(this.update, 16.66) }, methods: { update () { switch (this.status) { case 'first': /* TODO */ break case 'running': /* TODO */ break case 'waiting': /* TODO */ break } }, },うんちメーカーのオブジェクトは複雑な状態を持っていますが、基本的にはこのような状態遷移で動作しています。
ゲーム開発における状態遷移の有用性
私は状態遷移は文字列のパースなどで学んだのですが、今回の開発でゲームにも応用できることがわかりました。
ゲームは複雑な状態を持っていますが、それらを一度に処理しようとすると高い確率で肛門がパンクします。
しかし、状態という単位にゲーム全体の振る舞いを分割統治することで開発が容易になります。ゲーム開発における状態遷移の有用性は確かなもので、これを知っていると知らないとでは作れるものが異なってくると思いました。
知らないとクソゲーを作ることになってしまいますが、知っていればうんちメーカーのようなクソゲーを作ることが出来ます。デザインパターンへの応用
状態を管理するデザインパターンに有名なGoFのStateパターンがありますが、今回はこれは使いませんでした。
うんちメーカーはかなり規模の小さいアプリだったので利用する必要もなかったのですが、規模の大きさによってはこれらのデザインパターンの利用を検証する必要があるかもしれません。
おわりに
ストレスの多い世の中ですが、うんちメーカーでいっぱい出してすっきりしましょう。
以上です。
- 投稿日:2020-02-16T22:26:57+09:00
Vue.js初心者がチャットアプリケーションの開発を通じて勉強した話
はじめに
Vue.js初心者がチャットアプリケーションを作りながら勉強した話です。
題材はBuild a Real-time Chat App with Pusher and Vue.js(*1)です。(ありがとうございます)このアプリケーション全てを理解するのは初心者には大変だったので
細かい部分の理解は置いておき、全体の理解に努めたつもりです。ちなみに私のスペックは
- Vue.jsの知識は全くない
- 開発経験はある(Reactとか)
という感じです。
(*1) 一部日本語訳と私のコメントですm(_ _)m
手順
では、本題です。
製作物の確認(1/9)
Slackのようなチャットアプリケーションを作ります。
要件は
- 複数の部屋がある
- ルームメンバーをリストし、ログイン状態を表示する
- 他のユーザが入力開始するタイミングを検出し表示する
です。(本格的ですね)
バックエンドはChatKitというサービスを利用し、フロントエンドをVue.jsで実装します。事前準備として、Vue CLIをグローバルにインストールしておきます。
(Nodeがマシンにインストールされている必要があります)$ npm install -g @vue/cliバックエンドの準備(2/9)
PusherのChatKitというサービスを利用するので、まずは右上からサインアップします。
(Githubでサインアップしてエラーが出た場合、 パスワードのリセットで解消する可能性があります)サインインするとポップアップが出ますが、一旦スキップでも大丈夫です。
「CHATKIT」を選び、「CREATE」を押します。
INSTANCE NAMEはVueChatTut
でOKです。インスタンスができたら「Console」タブの「CREATE USER」からユーザを作成します。
"John"(User Identifier)と“John Wick"(Display Name)とします。
同じ要領で
- salt, Evelyn Salt
- hunt, Ethan Hunt
というユーザも作りましょう。
続いて部屋を作り、ユーザを割り当てます。
「Create and join a room」をクリックし、"John Wick"(Select a user to create the room)を選択、"General"(Room Name)と記入し「CREATE ROOM」をクリックします。部屋ができたら「Add user to room」でsaltとhuntを追加します。
同様に
- Weapons (john, salt)
- Combat (john, hunt)
という部屋も作り、メンバーを追加しましょう。
次に、「Add message to room」からテストメッセージを送っておきます。例えば、"General"で"John Wick"(Select a message author)を選択、"test"(Messge)と記入し「CREATE MESSAGE」をクリックします。
プロジェクトの作成(3/9)
Vue CLIでプロジェクトを作ります。何個か質問されますが、全てEnterしました。
$ vue create vue-chatkit不要なファイルの削除と、今回使うファイルの作成を行います。
$ mkdir src/assets/css $ mkdir src/store $ mkdir src/views $ touch src/assets/css/{loading.css,loading-btn.css} # ↑はhttps://github.com/sitepoint-editors/vue-chatkit/tree/master/src/assets/cssから内容をコピペしておきます $ touch src/components/{ChatNavBar.vue,LoginForm.vue,MessageForm.vue,MessageList.vue,RoomList.vue,UserList.vue} $ touch src/store/{actions.js,index.js,mutations.js} $ touch src/views/{ChatDashboard.vue,Login.vue} $ touch src/{chatkit.js,router.js} $ rm src/components/HelloWorld.vuesrcディレクトリ以下はこんな感じになります。
$ tree -L 2 --matchdirs src src ├── App.vue ←大元のビュー ├── assets ←CSSや画像 │ ├── css │ └── logo.png ├── chatkit.js ←ChatKitと接続する ├── components ←コンポーネント。コンポーネントを集めてビューを作る │ ├── ChatNavBar.vue │ ├── LoginForm.vue │ ├── MessageForm.vue │ ├── MessageList.vue │ ├── RoomList.vue │ └── UserList.vue ├── main.js ←Vueアプリケーションを起動する ├── router.js ←ルーティング ├── store ←状態管理 │ ├── actions.js │ ├── index.js │ └── mutations.js └── views ←ビュー ├── ChatDashboard.vue └── Login.vue 5 directories, 16 files次に、依存関係をインストールします。
$ npm i @pusher/chatkit-client bootstrap-vue moment vue-chat-scroll vuex-persist vue-router vuex
- @pusher/chatkit-client、ChatKitのリアルタイムクライアントインターフェイス
- bootstrap-vue、CSSフレームワーク
- moment、日付と時刻のフォーマットユーティリティ
- vue-chat-scroll、新しいコンテンツが追加されると自動的に下にスクロールする
- vuex、Vue.jsアプリケーションのための状態管理ライブラリ。Reactで言うReduxのイメージ
- vuex-persist、ブラウザのローカルストレージにVuexの状態を保存する
- vue-router、Vue.jsのルータ
では、Vue.jsプロジェクトの設定をしましょう。
src/main.js
を開き、以下のように更新します。src/main.jsimport Vue from 'vue' import BootstrapVue from 'bootstrap-vue' import VueChatScroll from 'vue-chat-scroll' import App from './App.vue' import router from './router' import store from './store/index' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' import './assets/css/loading.css' import './assets/css/loading-btn.css' Vue.config.productionTip = false // trueにすると開発者向けメッセージがコンソールに出る Vue.use(BootstrapVue) // ライブラリを利用する宣言 Vue.use(VueChatScroll) new Vue({ router, store, render: h => h(App) }).$mount('#app') // Vueインスタンスを作成し、#appにマウント(idがappであるDOMを置換) // 詳しく知りたい方はネットで調べてみてください。私のような初心者の内はおまじないという認識で良さそう
src/router.js
を以下のように更新します。src/router.jsimport Vue from 'vue' import Router from 'vue-router' import Login from './views/Login.vue' import ChatDashboard from './views/ChatDashboard.vue' Vue.use(Router) export default new Router({ mode: 'history', // ページのリロードなしにURL遷移を実現する(SPAのためという理解) base: process.env.BASE_URL, routes: [ // 「/」というパスのルートをLoginコンポーネントにマップする { path: '/', name: 'login', component: Login }, { path: '/chat', name: 'chat', component: ChatDashboard, } ] })
src/store/index.js
を更新します。src/store/index.jsimport Vue from 'vue' import Vuex from 'vuex' import VuexPersistence from 'vuex-persist' import mutations from './mutations' import actions from './actions' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' const vuexLocal = new VuexPersistence({ storage: window.localStorage }) export default new Vuex.Store({ state: { }, // ストアの状態 mutations, // ストアの状態を変更するメソッド群(ミューテーションをコミットすることで変更できる) actions, // ミューテーションをコミットするメソッド群 getters: { }, // ストアの状態を加工して取得するメソッド群 plugins: [vuexLocal.plugin], // LocalStorageを使うよ strict: debug // 開発時に状態変更をデバッキングツールで追跡できるようにする(ChromeかFireFoxの方は「Vue.js devtools」という拡張機能が便利) })UIインターフェイスの構築(4/9)
ここからちょっとハードになって行きますよ〜まずは
src/App.vue
を更新します。src/App.vue<template> <div id="app"> <router-view/> // 先ほどのrouter.jsによってマッチしたコンポーネントが描画される </div> </template>次に、
src/store/index.js
のstateとgettersセクションを更新します。
loading
で「CSSローダーを実行する必要があるか」やerror
で「エラー情報」を管理していますが、一個一個分かっていなくてOKです。// ... state: { loading: false, sending: false, error: null, user: [], reconnect: false, activeRoom: null, rooms: [], users: [], messages: [], userTyping: null }, getters: { hasError: state => state.error ? true : false }, // ...ログイン画面
src/view/Loing.vue
を作ります。構成はこんな感じで、フォームがコンポーネントとなっています。
src/view/Loing.vue<template> // テンプレートはtemplateタグで囲む <div class="login"> // b-xxはbootstrapのコンポーネント <b-jumbotron header="Vue.js Chat" lead="Powered by Chatkit SDK and Bootstrap-Vue" bg-variant="info" text-variant="white"> <p>For more information visit website</p> <b-btn target="_blank" href="https://pusher.com/chatkit">More Info</b-btn> </b-jumbotron> <b-container> <b-row> <b-col lg="4" md="3"></b-col> <b-col lg="4" md="6"> <LoginForm /> </b-col> <b-col lg="4" md="3"></b-col> </b-row> </b-container> </div> </template> <script> import LoginForm from '@/components/LoginForm.vue' export default { name: 'login', components: { // Loginのテンプレート内でLoginFormを使えるようにする LoginForm } } </script>ログイン画面で利用するコンポーネント
src/view/LoginForm.vue
を更新します。src/view/LoginForm.vue<template> <div class="login-form"> <h5 class="text-center">Chat Login</h5> <hr> // ↓preventDefaultを実行し、onSubmitメソッドを呼ぶ <b-form @submit.prevent="onSubmit"> // ↓showの頭にコロンをつけているので、値としてscriptタグ内のhasErrorという算出プロパティを使える。また、変数を表示する場合は{{}}で囲う <b-alert variant="danger" :show="hasError">{{ error }} </b-alert> <b-form-group id="userInputGroup" label="User Name" label-for="userInput"> <b-form-input id="userInput" type="text" placeholder="Enter user name" // ↓双方向データバインディングを作成する。入力に応じてuserIdが更新される v-model="userId" autocomplete="off" :disabled="loading" required> </b-form-input> </b-form-group> <b-button type="submit" variant="primary" class="ld-ext-right" v-bind:class="{ running: loading }" // v-bind:classでクラスを動的に切り替えられる。loadingがtrueのときrunningクラスをつける :disabled="isValid"> Login <div class="ld ld-ring ld-spin"></div> </b-button> </b-form> </div> </template> <script> import { mapState, mapGetters } from 'vuex' export default { name: 'login-form', data() { // データ定義 return { userId: '', } }, computed: { // 算出プロパティ。テンプレートからロジックを切り出せる isValid: function() { const result = this.userId.length < 3; return result ? result : this.loading }, ...mapState([ 'loading', 'error' ]), // stateを返す ...mapGetters([ 'hasError' ]) // getterの評価後の値を返す } } </script>ここまで書くと表示確認ができます
npm run serve
でVue devサーバを起動し、http://localhost:8080 を開いてみましょう。続いて、チャット画面
src/view/ChatDashBoard.vue
を作ります。
チャット画面は
- ChatNavBar、ナビゲーションバー
- RoomList、ログインしたユーザがアクセスできる部屋の一覧表示
- UserList、選択したルームのメンバーをリスト
- MessageList、選択したルームに投稿されたメッセージをリスト
- MessageForm、選択したルームにメッセージを送信するためのフォーム
src/view/ChatDashBoard.vue<template> <div class="chat-dashboard"> <ChatNavBar /> <b-container fluid class="ld-over" v-bind:class="{ running: loading }"> <div class="ld ld-ring ld-spin"></div> <b-row> <b-col cols="2"> <RoomList /> </b-col> <b-col cols="8"> <b-row> <b-col id="chat-content"> <MessageList /> </b-col> </b-row> <b-row> <b-col> <MessageForm /> </b-col> </b-row> </b-col> <b-col cols="2"> <UserList /> </b-col> </b-row> </b-container> </div> </template> <script> import ChatNavBar from '@/components/ChatNavBar.vue' import RoomList from '@/components/RoomList.vue' import MessageList from '@/components/MessageList.vue' import MessageForm from '@/components/MessageForm.vue' import UserList from '@/components/UserList.vue' import { mapState } from 'vuex'; export default { name: 'Chat', components: { ChatNavBar, RoomList, UserList, MessageList, MessageForm }, computed: { ...mapState([ 'loading' ]) } } </script>全てのコンポーネントを表示するため、ボイラープレートコードを挿入します。
ここはコピペで大丈夫です。src/view/ChatNavBar.vue<template> <b-navbar id="chat-navbar" toggleable="md" type="dark" variant="info"> <b-navbar-brand href="#"> Vue Chat </b-navbar-brand> <b-navbar-nav class="ml-auto"> <b-nav-text>{{ user.name }} | </b-nav-text> <b-nav-item href="#" active>Logout</b-nav-item> </b-navbar-nav> </b-navbar> </template> <script> import { mapState } from 'vuex' export default { name: 'ChatNavBar', computed: { ...mapState([ 'user', ]) }, } </script> <style> #chat-navbar { margin-bottom: 15px; } </style>src/components/RoomList.vue<template> <div class="room-list"> <h4>Channels</h4> <hr> <b-list-group v-if="activeRoom"> <b-list-group-item v-for="room in rooms" :key="room.name" :active="activeRoom.id === room.id" href="#" @click="onChange(room)"> # {{ room.name }} </b-list-group-item> </b-list-group> </div> </template> <script> import { mapState } from 'vuex' export default { name: 'RoomList', computed: { ...mapState([ 'rooms', 'activeRoom' ]), } } </script>src/components/UserList.vue<template> <div class="user-list"> <h4>Members</h4> <hr> <b-list-group> <b-list-group-item v-for="user in users" :key="user.username"> {{ user.name }} <b-badge v-if="user.presence" :variant="statusColor(user.presence)" pill> {{ user.presence }}</b-badge> </b-list-group-item> </b-list-group> </div> </template> <script> import { mapState } from 'vuex' export default { name: 'user-list', computed: { ...mapState([ 'loading', 'users' ]) }, methods: { statusColor(status) { return status === 'online' ? 'success' : 'warning' } } } </script>src/components/MessageList.vue<template> <div class="message-list"> <h4>Messages</h4> <hr> <div id="chat-messages" class="message-group" v-chat-scroll="{smooth: true}"> <div class="message" v-for="(message, index) in messages" :key="index"> <div class="clearfix"> <h4 class="message-title">{{ message.name }}</h4> <small class="text-muted float-right">@{{ message.username }}</small> </div> <p class="message-text"> {{ message.text }} </p> <div class="clearfix"> <small class="text-muted float-right">{{ message.date }}</small> </div> </div> </div> </div> </template> <script> import { mapState } from 'vuex' export default { name: 'message-list', computed: { ...mapState([ 'messages', ]) } } </script> <style> .message-list { margin-bottom: 15px; padding-right: 15px; } .message-group { height: 65vh !important; overflow-y: scroll; } .message { border: 1px solid lightblue; border-radius: 4px; padding: 10px; margin-bottom: 15px; } .message-title { font-size: 1rem; display:inline; } .message-text { color: gray; margin-bottom: 0; } .user-typing { height: 1rem; } </style>src/components/MessageForm.vue<template> <div class="message-form ld-over"> <small class="text-muted">@{{ user.username }}</small> <b-form @submit.prevent="onSubmit" class="ld-over" v-bind:class="{ running: sending }"> <div class="ld ld-ring ld-spin"></div> <b-alert variant="danger" :show="hasError">{{ error }} </b-alert> <b-form-group> <b-form-input id="message-input" type="text" v-model="message" placeholder="Enter Message" autocomplete="off" required> </b-form-input> </b-form-group> <div class="clearfix"> <b-button type="submit" variant="primary" class="float-right"> Send </b-button> </div> </b-form> </div> </template> <script> import { mapState, mapGetters } from 'vuex' export default { name: 'message-form', data() { return { message: '' } }, computed: { ...mapState([ 'user', 'sending', 'error', 'activeRoom' ]), ...mapGetters([ 'hasError' ]) } } </script>データがない状態なので、stateにモックデータを入れてみます。
src/store/index.js// ... state: { loading: false, sending: false, error: 'Relax! This is just a drill error message', user: { username: 'Jack', name: 'Jack Sparrow' }, reconnect: false, activeRoom: { id: '124' }, rooms: [ { id: '123', name: 'Ships' }, { id: '124', name: 'Treasure' } ], users: [ { username: 'Jack', name: 'Jack Sparrow', presence: 'online' }, { username: 'Barbossa', name: 'Hector Barbossa', presence: 'offline' } ], messages: [ { username: 'Jack', date: '11/12/1644', text: 'Not all treasure is silver and gold mate' }, { username: 'Jack', date: '12/12/1644', text: 'If you were waiting for the opportune moment, that was it' }, { username: 'Hector', date: '12/12/1644', text: 'You know Jack, I thought I had you figured out' } ], userTyping: null }, // ...stateは元に戻しておきます。
/src/store/index.js// ... state: { loading: false, sending: false, error: null, user: null, reconnect: false, activeRoom: null, rooms: [], users: [], messages: [], userTyping: null } // ...パスワードレス認証(5/9) ※公開するサービスでは適切で安全な認証システムにしましょう
プロジェクトのルートに以下のファイルを作成し、環境変数を設定します。
.env.localVUE_APP_INSTANCE_LOCATOR= VUE_APP_TOKEN_URL= VUE_APP_MESSAGE_LIMIT=10ChatKitの「Credentials」タブに移動し、「TEST TOKEN PROVIDER」の「ENABLED?」にチェックをつけます。
そして、
VUE_APP_INSTANCE_LOCATOR
に「Instance Locator」
VUE_APP_TOKEN_URL
に「Your Test Token Provider Endpoint」
の値を書きます。
VUE_APP_MESSAGE_LIMIT
は取得するメッセージの数を制限しているだけです。次に、
src/chatkit.js
に行き、ChatKitへ接続する土台を作ります。src/chatkit.jsimport { ChatManager, TokenProvider } from '@pusher/chatkit-client' const INSTANCE_LOCATOR = process.env.VUE_APP_INSTANCE_LOCATOR; const TOKEN_URL = process.env.VUE_APP_TOKEN_URL; const MESSAGE_LIMIT = Number(process.env.VUE_APP_MESSAGE_LIMIT) || 10; let currentUser = null; let activeRoom = null; async function connectUser(userId) { const chatManager = new ChatManager({ instanceLocator: INSTANCE_LOCATOR, tokenProvider: new TokenProvider({ url: TOKEN_URL }), userId }); currentUser = await chatManager.connect(); return currentUser; } export default { connectUser }続いて、stateにデータをセットする処理を追加します。
長いですが、ただのセッターなのでがっつり見る必要はありません。src/store/mutationsexport default { setError(state, error) { state.error = error; }, setLoading(state, loading) { state.loading = loading; }, setUser(state, user) { state.user = user; }, setReconnect(state, reconnect) { state.reconnect = reconnect; }, setActiveRoom(state, roomId) { state.activeRoom = roomId; }, setRooms(state, rooms) { state.rooms = rooms }, setUsers(state, users) { state.users = users }, clearChatRoom(state) { state.users = []; state.messages = []; }, setMessages(state, messages) { state.messages = messages }, addMessage(state, message) { state.messages.push(message) }, setSending(state, status) { state.sending = status }, setUserTyping(state, userId) { state.userTyping = userId }, reset(state) { state.error = null; state.users = []; state.messages = []; state.rooms = []; state.user = null } }次に、
src/store/actions.js
を更新します。src/store/actions.jsimport chatkit from '../chatkit'; // Helper function for displaying error messages function handleError(commit, error) { const message = error.message || error.info.error_description; commit('setError', message); } export default { // ChatKitに接続し、stateを更新する async login({ commit, state }, userId) { try { commit('setError', ''); commit('setLoading', true); // Connect user to ChatKit service const currentUser = await chatkit.connectUser(userId); commit('setUser', { username: currentUser.id, name: currentUser.name }); commit('setReconnect', false); // Test state.user console.log(state.user); } catch (error) { handleError(commit, error) } finally { commit('setLoading', false); } } }今、作ったメソッドを
src/components/LoginForm.vue
から実行します。src/components/LoginForm.vueimport { mapState, mapGetters, mapActions } from 'vuex' //... export default { //... methods: { ...mapActions([ 'login' ]), async onSubmit() { const result = await this.login(this.userId); if(result) { // ログインできたらチャット画面に遷移する。loginメソッドがbooleanを返していないからまだ動かない this.$router.push('chat'); } } } }
.env.local
をロードするために、Vueサーバを再起動します。
間違ったユーザ名を入力するとエラーになることが確認できます。チャットに参加する(6/9)
チャット入室時に
RoomList
とUserList
が反映されるようsrc/chatkit.js
を更新します。src/chatkit.js//... import moment from 'moment' import store from './store/index' //... function setMembers() { const members = activeRoom.users.map(user => ({ username: user.id, name: user.name, presence: user.presence.state })); store.commit('setUsers', members); } async function subscribeToRoom(roomId) { store.commit('clearChatRoom'); activeRoom = await currentUser.subscribeToRoom({ roomId, messageLimit: MESSAGE_LIMIT, hooks: { onMessage: message => { store.commit('addMessage', { name: message.sender.name, username: message.senderId, text: message.text, date: moment(message.createdAt).format('h:mm:ss a D-MM-YYYY') }); }, onPresenceChanged: () => { setMembers(); }, onUserStartedTyping: user => { store.commit('setUserTyping', user.id) }, onUserStoppedTyping: () => { store.commit('setUserTyping', null) } } }); setMembers(); return activeRoom; } export default { connectUser, subscribeToRoom }ChatKitサービスのイベントハンドラの意味はこちらです。
- onMessage、メッセージを受信する
- onPresenceChanged、ユーザがログインまたはログアウトした時にイベントを受け取る
- onUserStartedTyping、ユーザが入力しているイベントを受け取る
- onUserStopperTyping、ユーザが入力を停止したというイベントを受け取る
ログイン後、チャット画面にリダイレクトするよう更新します。
src/store/actions.js//... try { //... (place right after the `setUser` commit statement) // Save list of user's rooms in store const rooms = currentUser.rooms.map(room => ({ id: room.id, name: room.name })) commit('setRooms', rooms); // Subscribe user to a room const activeRoom = state.activeRoom || rooms[0]; // pick last used room, or the first one commit('setActiveRoom', { id: activeRoom.id, name: activeRoom.name }); await chatkit.subscribeToRoom(activeRoom.id); return true; } catch (error) { //... }これで正しいユーザ名でログインした時チャット画面に遷移するようになりました。
部屋を変更する(7/9)
RoomList
のクリックで部屋を変更できるようにします。
まず、src/store/actions.js
のlogin
メソッドの後にこちらを追加します。
stateのactiveRoom
を更新するものです。src/store/actions.jsasync changeRoom({ commit }, roomId) { try { const { id, name } = await chatkit.subscribeToRoom(roomId); commit('setActiveRoom', { id, name }); } catch (error) { handleError(commit, error) } },次に、
src/componenents/RoomList.vue
のscriptタグ内に以下を追加します。
すでにルーム名をクリックするとonChange
を呼び出す実装はされている@click="onChange(room)"
ので、先ほど作ったchangeRoom
をonChange
メソッドから実行します。src/componenents/RoomList.vueimport { mapState, mapActions } from 'vuex' //... export default { //... methods: { ...mapActions([ 'changeRoom' ]), onChange(room) { this.changeRoom(room.id) } } }ブラウザで部屋を切り替えると、
MessageList
とUserList
が更新されることが確認できますもし
Cannot read property 'subscribeToRoom' of null
というエラーが出たらログインし直してみてください。
次のセクションで対応します。ページの更新後のユーザの再接続(8/9)
前セクションでご紹介したエラーはページをリロードした際に、ChatKitサーバに接続する参照がnullにリセットされるために起こるものです。
これを修正するには再接続操作を実行する必要があります。
src/components/ChatNavBar.vue
のscriptタグ内を以下のように更新します。src/components/ChatNavBar.vue<script> import { mapState, mapActions, mapMutations } from 'vuex' export default { name: 'ChatNavBar', computed: { ...mapState([ 'user', 'reconnect' ]) }, methods: { ...mapActions([ 'logout', 'login' ]), ...mapMutations([ 'setReconnect' ]), onLogout() { this.$router.push({ path: '/' }); this.logout(); }, unload() { if(this.user.username) { // User hasn't logged out this.setReconnect(true); } } }, mounted() { window.addEventListener('beforeunload', this.unload); if(this.reconnect) { this.login(this.user.username); } } } </script>
- upload。ページの更新が発生すると呼び出される。
user.username
が設定されている=ログインしていると、stateのreconnectにtrueを設定する。- mounted。ChatNavBarビューのレンダリングが完了するたびに呼び出される。最初に、ページがアンロードされる直前に呼び出されるイベントリスナーにハンドラーを割り当てる。また、stateのreconnectがtrueであれば、ログイン手順が実行され、ChatKitサービスに再接続される。
また、ログアウト機能も追加されていますが、これは次のセクションで。
これらの更新を行った後、ページをリロードして部屋を切り替えてもエラーにならないはずです
メッセージの送信、ユーザの入力の検出、ログアウト(9/9)
ラストです。
src/chatkit.js
を更新します。src/chatkit.js//... async function sendMessage(text) { const messageId = await currentUser.sendMessage({ text, roomId: activeRoom.id }); return messageId; } export function isTyping(roomId) { currentUser.isTypingIn({ roomId }); } function disconnectUser() { currentUser.disconnect(); } export default { connectUser, subscribeToRoom, sendMessage, disconnectUser }
src/atore/actions.jsに
移動し、changeRoom
メソッドの直後に以下を挿入します。src/atore/actions.jsasync sendMessage({ commit }, message) { try { commit('setError', ''); commit('setSending', true); const messageId = await chatkit.sendMessage(message); return messageId; } catch (error) { handleError(commit, error) } finally { commit('setSending', false); } }, async logout({ commit }) { commit('reset'); chatkit.disconnectUser(); window.localStorage.clear(); }
logout
メソッドでは、セキュリティのためストアのリセットとローカルストレージのクリアも行います。次に、
src/components/MessageForm.vue
のinputディレクティブを更新します。src/components/MessageForm.vue<b-form-input id="message-input" type="text" v-model="message" @input="isTyping" placeholder="Enter Message" autocomplete="off" required> </b-form-input>
src/components/MessageForm.vue
のscriptタグ内を以下の通り更新します。src/components/MessageForm.vue<script> import { mapActions, mapState, mapGetters } from 'vuex' import { isTyping } from '../chatkit.js' export default { name: 'message-form', data() { return { message: '' } }, computed: { ...mapState([ 'user', 'sending', 'error', 'activeRoom' ]), ...mapGetters([ 'hasError' ]) }, methods: { ...mapActions([ 'sendMessage', ]), async onSubmit() { const result = await this.sendMessage(this.message); if(result) { this.message = ''; } }, async isTyping() { await isTyping(this.activeRoom.id); } } } </script>そして、
src/MessageList.vue
を更新します。src/MessageList.vueimport { mapState } from 'vuex' export default { name: 'message-list', computed: { ...mapState([ 'messages', 'userTyping' ]) } }メッセージ送信機能が動作するはずです。
別のユーザが入力しているという通知を表示するには、
src/MessageList.vue
のmessage-group
直後にこのスニペットを追加します。src/MessageList.vue<div class="user-typing"> <small class="text-muted" v-if="userTyping">@{{ userTyping }} is typing....</small> </div>最後に、
src/components/ChatNavBar.vue
にログアウト処理をつけます。src/components/ChatNavBar.vue<b-nav-item href="#" @click="onLogout" active>Logout</b-nav-item>完成です٩( 'ω' )و
まとめ
知識ない状態でアプリケーションをイチから作るのは(記事があるとは言え)不安もありましたが、この辺が良かったのでまたやりたいと思います
- ドキュメントをただ読むより楽しい!
- ファイル構成や、ライフサイクル、機能といったVue.jsの特徴を知ることができた
- チャットのライブラリやUIライブラリなど実用的なツールを知ることができた
- 投稿日:2020-02-16T22:08:32+09:00
Vue.jsでTodoアプリ作ってみよう!
今回はVue.jsでTodoアプリを作ってみます
早速やっていきましょう~!
なお超基礎的な知識として下記の記事に書いてることを前提でお話します!
https://qiita.com/takepon_it/items/f89e0e3023a3070dbce61.画面を作ろう
今回はTodoリストの機能を作りたいだけなので見た目は簡素な物でいきます!
https://unpkg.com/vue
を読み込むのを忘れないでください!index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Todoアプリ</title> <link rel="stylesheet" href="style.css"> <script src="https://unpkg.com/vue"></script> </head> <body> <div class="container"> <h1>Todoアプリ</h1> </div> <div id="app"> <div> <input type="text" placeholder="テキスト入力してね"> <button type="button">追加</button> <ul> </ul> </div> </div> <script src="main.js"></script> </body> </html>こんな感じですね!
2.コンポーネントを作ろう!
早速タスクを追加する機能を追加していきましょう!
まずコンポーネントを定義していきます
コンポーネントはVue.componentを使うことで定義することできます!ToDo の要素を todo-item というコンポーネントとして定義してみましょう!
それと合わせて既にここでタスクを追加した際のhtmlも作っておきましょう!
テンプレートをtemplateオプションに渡せばできます!main.jsVue.component('todo-item', { props: { todo: { type: Object, required: true } }, //htmlの挿入 template: '<div>' + '<input type="checkbox" v-model="todo.completed">' + '<span>{{todo.text}}</span>' + '</div>' });取り合えずこれでtodo-itemというコンポーネントを作れました!
3.タスクを保持するデータを作ろう!
次にテキストボックスの値をinputという名前で保存しよう!
では今からdataにinputを入れていくよ~main.jsvar vm = new Vue({ el: '#app', data: { input: '', //タスクを管理する配列 todos: [] },タスクを管理する配列として todos:[] も作っとこう!
4.テキストとデータをくっつけよう!
早速テキストボックスとinputをv-modelを使ってくっつけよう!
v-modelはinput要素にv-modelという属性をつけることで、データとinput要素の入力値をくっつけることができます!
追加ボタンの上のテキストボックスの所を
index.html<input type="text" v-model="input" placeholder="テキスト入力してね">にしてみましょう! 早速次にいきましょう!
5.タスクを追加するインスタンスメソッドを作ろう!
本題のTodoを追加するための機能を作っていきましょう!
addTodoというメソッドを作ろう!
インスタンスメソッドを作るにはmethodsオプションを使用したらできるよmain.jsmethods: { //Todo追加関数 addTodo: function () { this.todos.push( { completed: false, text: this.input }) this.input = '' } }これを追加してください!
配列へのpushやそういったものは割愛させて頂きます6.タスクを追加する処理を書いていこう!
そしたらボタンをクリックしたらタスクを追加できるようにしたいので
ボタンとメソッドを紐づける必要があります!
これをするには
v-on:click
これを使って、ボタンが押されるのを監視し、ボタンが押された時に先ほどのaddTodoメソッドを実行できるようにしよう!index.html<button type="button" v-on:click="addTodo">追加</button>こうですね!
7.タスクを削除する機能を作ろう!
タスクを追加したら、それを遂行した時の為に当然削除っていう機能も欲しいですよね?
こちらを今から実装していきたいと思います!冒頭で作成したtempleteオプションに続けて記述したいと思います
main.js//htmlの挿入 template: '<div>' + '<input type="checkbox" v-model="todo.completed">' + '<span>{{todo.text}}</span>' + '<button type="button" v-on:click="onclickRemove">削除</button>' + '</div>',最終的にはこうですね!
しかしこれだけではなv-onオプションを使っている所を見て頂きたいのですが
onclickRemoveとあります
実際に削除ボタンを押されたときの処理をまだ書いていないので今から書いていきましょう。main.jsmethods: { onclickRemove: function () { this.$emit('remove') } }こちらを始めのインスタンスメソッドに追記します
$emitでカスタムイベントのremoveを発動します!
そして最後にindex.html<li v-for="(todo, index) in todos"> <todo-item v-bind:todo="todo" v-on:remove="todos.splice(index,1)"></todo-item> </li>ul要素の中に上記を追記してください
v-forで要素を繰り返し表示します、この場合はtodos配列の中に格納されているタスクを表示させる為ですね
v-bindでデータを属性の値に紐付けします。以上をもってTodoアプリの完成です!!!!!!
8.最後に
プログラミング初心者の割には頑張れた気がします
この調子でQiitaとか使ってアウトプットしていきたいな・・・
- 投稿日:2020-02-16T20:19:34+09:00
Vue.js:アロー演算子を使うタイミング
Vue.jsで「アロー演算子」を使うタイミングが分からないなと思ったので、
このパターンが来たら、アロー演算子の出番だよ。と言うタイミングをメモっておいた。https://jp.vuejs.org/v2/guide/components.html
data: function () { //←function() return { //←return count: 0 } }出た!このパターンがきたら、アロー演算子を使う。
data: () => { count: 0 }スッキリする!
computed:{ hoge:{ get: function(){ //←function() return this.$store.getters.hoge //←return } set: function(val){ this.$store.dispatch('setHoge',val) } } }このパターンが来たらアロー演算子
computed:{ hoge:{ get() => ( this.$store.getters.hoge ) set(val){ this.$store.dispatch('setHoge',val) } } }スッキリする。
getFilteredCity:{ get: function(){ //←function & return return this.citys.filter(function(City){ //←function & return return (City.pref.indexOf(this.prefCode) !== -1 && (City.pref == this.prefCode || this.prefCode == '')) } } } },フィルタ関数のこのパターンも
getFilteredCity:{ get() => ( this.citys.filter( (City) => (City.pref.indexOf(this.prefCode) !== -1 && (City.pref == this.prefCode || this.prefCode == '') ) ) },こんなにスッキリする。
watch:{ meta(){ this.$emit('updateHead') setTimeout( funtion(){ this.getStoreLoading(false) }, 1000); }, },setTimeoutも。
watch:{ meta(){ this.$emit('updateHead') setTimeout(() => { this.getStoreLoading(false) }, 1000); }, },ちょっとスッキリする。
でもPrettierとかESLintで--fixを設定してると…
勝手に直してくれるから、勝手にイケてる感じにしてくれる。
Babelのプラグイン
自動で直す設定にしておくと、ドンドンイケてる感じに直って行くので、
babel.config.js
に、プラグインを入れておいたほうが良いと思います。module.exports = { presets: ["@babel/preset-env"], overrides: [ { presets: [ ["@babel/preset-env", { targets: { ie: "11" } }], ], plugins: [ '@babel/plugin-transform-arrow-functions', //←これ ] } ], }以上です。
- 投稿日:2020-02-16T20:17:32+09:00
Vue.js:アロー演算子を使うタイミング
Vue.jsで「アロー演算子」を使うタイミングが分からないなと思ったので、
このパターンが来たら、アロー演算子の出番だよ。と言うタイミングをメモっておいた。https://jp.vuejs.org/v2/guide/components.html
data: function () { //←function() return { //←return count: 0 } }出た!このパターンがきたら、アロー演算子を使う。
data: () => { count: 0 }スッキリする!
computed:{ hoge:{ get: function(){ //←function() return this.$store.getters.hoge //←return } set: function(val){ this.$store.dispatch('setHoge',val) } } }このパターンが来たらアロー演算子
computed:{ hoge:{ get() => ( this.$store.getters.hoge ) set(val){ this.$store.dispatch('setHoge',val) } } }スッキリする。
getFilteredCity:{ get: function(){ //←function & return return this.citys.filter(function(City){ //←function & return return (City.pref.indexOf(this.prefCode) !== -1 && (City.pref == this.prefCode || this.prefCode == '')) } } } },フィルタ関数のこのパターンも
getFilteredCity:{ get() => ( this.citys.filter( (City) => (City.pref.indexOf(this.prefCode) !== -1 && (City.pref == this.prefCode || this.prefCode == '') ) ) },こんなにスッキリする。
watch:{ meta(){ this.$emit('updateHead') setTimeout( funtion(){ this.getStoreLoading(false) }, 1000); }, },setTimeoutも。
watch:{ meta(){ this.$emit('updateHead') setTimeout(() => { this.getStoreLoading(false) }, 1000); }, },ちょっとスッキリする。
でもPrettierとかESLintで--fixを設定してると…
勝手に直してくれるから、勝手にイケてる感じにしてくれる。
Babelのプラグイン
自動で直す設定にしておくと、ドンドンイケてる感じに直って行くので、
babel.config.js
に、プラグインを入れておいたほうが良いと思います。module.exports = { presets: ["@babel/preset-env"], overrides: [ { presets: [ ["@babel/preset-env", { targets: { ie: "11" } }], ], plugins: [ '@babel/plugin-transform-arrow-functions', //←これ ] } ], }以上です。
- 投稿日:2020-02-16T20:13:28+09:00
Techpitの教材「Vue.js と Firebase ではてなブックマーク風なブックマークサービスを作ってみよう!」を終えての感想
説明
TechCommitさんのお年玉企画で、Techpitさんの教材「Vue.js と Firebase ではてなブックマーク風なブックマークサービスを作ってみよう!」が当選したので使ってみた感想を書きました。
前提
筆者のレベル
言語・ツール レベル HTML・CSS 基礎レベル JavaScript 業務で少し触ったことがある Vue.js はじめて Firebase はじめて 扱った技術
- Vue.js
- Bulma
- Firebase Authentication
- Firestore
- Firebase Storage
- Firebase Cloud Function
- Firebase Hosting
作った機能
- ブックマークサービス
- トップページ(全ユーザーのブックマーク一覧)
- サインアップ
- サインイン
- 認証情報変更
- ブックマークへのコメント
- ブックマークコメントのスター登録
- プロファイルページ
完成品
公開したアプリケーション。
https://pitmark-prod-9c6b2.firebaseapp.com/トップページ
サインアップ
サインイン
認証情報変更
ブックマークへのコメント&スター登録
プロファイルページ
感想
手順通りに進めるとアプリができあがるので、Progate等で基礎を学んだあとに何か成果物を作りたい人に良いと思いました。Vue.jsとFirebaseの使い方を丁寧に説明してくれるので詰まる箇所も少なく進めていけました。ただし説明が少ない箇所もあるので自分で調べる必要もあります(Vueの公式ページがわかりやすいので助かる)。
HTML・CSS、JavaScriptの基礎を学んでいないと消化不良になりそうです。自分はJavaScriptをしっかり学んでいなかったのでJavaScriptとVue.jsの両方を調べながら教材を進めていきましたが、それだとVue.jsの内容が頭に入りきらないことがあったのでちょっともったいなかったと感じました。
Firebaseは初めて触りましたが、基礎的な内容を学ぶことができ勉強になりました。アプリを簡単に公開までもっていけます。学んだ内容は
- Firebase Authentication:認証機能
- Firestore:データベース機能
- Firebase Storage:ファイルストレージ機能
- Firebase Cloud Function:データが作成・更新・削除されたときに特定の処理を裏側で行わせる等で使用
- Firebase Hosting:アプリの公開に使用
Bulmaが便利。Flexboxベースの柔軟なレイアウトを簡単に構築できる。教材に沿って使っただけなので自分で色々試してみたい。
- 投稿日:2020-02-16T20:12:40+09:00
【Vuejs】v-forで配列を出力する時に1つ目の要素とそれ以降でstyleを変える
テーブルとかもそうですが、最初の要素だけスタイルを切り替えたい時にどうするんだろうと調べていたら3項演算子が使えました。
class属性を配列の要素番号で切り替える
sample.html<style> .first{ color: aquamarine; } .two-or-more{ color: burlywood; } </style> <div id="app"> <ul> <li v-for="(e,index) in mylist" v-bind:class="[index == 0 ? 'first' : 'two-or-more']">{{e.name}}</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { mylist: [ { name: "1つ目" }, { name: "2つ目" }, { name: "3つ目" }, { name: "4つ目" }, ], } }) </script>結果
参考
公式ドキュメント#配列構文
https://jp.vuejs.org/v2/guide/class-and-style.html#%E9%85%8D%E5%88%97%E6%A7%8B%E6%96%87
- 投稿日:2020-02-16T19:38:11+09:00
Vue.jsならPugとStylusが良い
「vue.js」での「pug」と「stylus」は相性が良いです。
インストール
$ npm install -D pug $ npm install -D pug-loader $ npm install -D stylus $ npm install -D stylus-loaderファイル準備
/stylus/_reset.styl
/stylus/_variable.styl
/stylus/_mixin.styl
元々あるやつをファイル変換しておく。Component.vue
コンポーネントの中で使っていく
<template lang="pug"></template> <style lang="stylus" scoped></style> <script></script>外部ファイルは先に読み込んでおく。コンポーネントからの
相対パスで指定する必要があると思います。ここはsassより不便。<template lang="pug"></template> <style lang="stylus" scoped> @import "../../../../scss/_reset.styl" @import "../../../../scss/_variable.styl" @import "../../../../scss/_mixin.styl" </style> <script></script>中身を書いていく
実際に中身を書いてみる
<template lang="pug"> .component .wrapper .container .inner .item .text a.link ボタン </template> <style lang="stylus" scoped> @import "../../../../scss/_reset.styl" @import "../../../../scss/_variable.styl" @import "../../../../scss/_mixin.styl" .component component() //mixinを読み込む margin 0 .wrapper wrapper() //mixinを読み込む margin 0 .container container() //mixinを読み込む width $default .inner inner() //mixinを読み込む margin-bottom 0 .item item() //mixinを読み込む display flex jusitify-content space-between .text text() //mixinを読み込む font-size $normal a.link link() //mixinを読み込む color $link </style> <script></script>どうですか?
_variable
と_mixin
から呼び出して使って書いても
めっちゃ書きやすくないですか?{:};@include
書かなくて良いんです。以上です。
- 投稿日:2020-02-16T18:58:56+09:00
【Nuxt.js】Vue.filter()で文字列整形
Fiter()で文字列の整形
グローバルな Vue.filter() を使用してカスタマイズしたフィルタを登録します。
コードは'/plugins'ディレクトリの中に作成。
今回は日付をフィルターにかけて文字列を整形してみます。plugins/filter.jsimport Vue from 'vue' //定数monthsに月名を文字列で用意 const months = [ "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" ]; const dateFilter = value => { return formatDate(value); }; function formatDate(inputDate) { const date = new Date(inputDate); const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); const formattedDate = year + "年" + " " + months[month] + " " + day + "日"; return formattedDate; } Vue.filter('date', dateFilter)'date'という名前で、定数'dateFilter'の値をコンポーネント側に渡します。
これでどのコンポーネントからでも使用できるようになりました。config.jsに登録
nuxt.config.jsplugins: [ '~/plugins/filter.js', ]filterを適用
コンポーネント側で利用してみましょう。
'date'という名前で使用できます。
他にも書き方はありますが今回はmustache型で記述してみました。
'updateDate'はStoreで別途用意しています。index.vue<template> // {{ フィルタに渡すデータ | フィルタ }} <div>更新日:{{ updateDate | date }}</div> </template>ありがとうございました。
- 投稿日:2020-02-16T18:11:08+09:00
Vue.jsでFontAwesomeを使う
普通にFontawesomeを読み込んで使っても大丈夫と思うんですが、
あえてvue.js専用にラップしてある物の使い方(備忘録)です。インストール
$ npm install --save @fortawesome/fontawesome-svg-core $ npm install --save @fortawesome/free-solid-svg-icons $ npm install --save @fortawesome/free-brands-svg-icons $ npm install --save vue-fontawsomemain.js
import Vue from 'vue' import App from './App.vue' //(省略) import { library } from '@fortawesome/fontawesome-svg-core' //基本セットを読み込んで、使いたいアイコンを読み込みます。 import { faHome , faSearch} from '@fortawesome/free-solid-svg-icons' import { faFacebook , faLine } from '@fortawesome/free-brands-svg-icons' //使うものはあらかじめ読み込んで「library」に追加しておきます。 library.add(faHome) library.add(faSearch) library.add(faFacebook) library.add(faLine) //さらに「vue-fontawesome」を読み込みます。 import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' //カスタムタグ「font-awesome-icon」として使えるようにします。 Vue.component('font-awesome-icon', FontAwesomeIcon)Component.vue
<template lang="pug"> .btn a font-awesome-icon( :icon="['fas','home']" ) | Home a font-awesome-icon( :icon="['fas', 'search']" ) | Search a font-awesome-icon( :icon="['fab','facebook']" ) | Facebook a font-awesome-icon( :icon="['fab', 'line']" ) | Line </template>これでアイコンが使えるようになります。
慣れた人ならCSSで定義したほうが速いような気がしますが、
このやり方もスッキリしていて、割と良い気がします。参考にした記事
- 投稿日:2020-02-16T17:50:25+09:00
BootstrapVueで、さらっと表を作る
はじめに
Vue.jsというと、RouterやVuexを併用してSPAを作るものと思う方もいるかもしれませんが、
PHPなどでの同期処理やちょっとした非同期通信だけでも、いろいろな事ができます。
例えば、ユーザーのアクション(クリックやタップ婦)で動的に切り替わるUIなどです。Vue単体でもいろいろ作れますが、BootstrapVueのようなフレームワークを追加すると、より簡単に色々なUIが作成できます。
今回は、バックエンドから割と好評だった表のサンプルを、備忘を兼ねて掲載します。
使用するもの
Vue.js (https://jp.vuejs.org/)
BootstrapVue (https://bootstrap-vue.js.org/)*より簡単にやるために、BootstrapVue を使用しています。
また、元はBootstrapなので、デザイナーさんがstyleを設定しやすいという事もあります。作り方
Buildの環境をつくる
自分でwebpackで設定したり、VueCLI などで簡単に作っても良いです。
https://cli.vuejs.org/ブラウザで読み込む(script要素利用)
とりあえずこのサンプルでは、ブラウザで読み込んでしまいます。
(一番手っ取り早い方法でもあります)参考はこちら。
https://bootstrap-vue.js.org/docs#browser
上記のサンプルだとIntersectionObserver
のpollyfillをloadしていますが、大抵は無くても大丈夫です。表を作る
表のコンポーネント
表を作るコンポーネントはこちらになります。
BootstrapVueのコンポーネントになります。
b-table
https://bootstrap-vue.js.org/docs/components/table/b-tableのプロパティ
プロパティはいろいろありますが、まずはこの2つです。
データを表示する表であれば、これだけで十分です。
items
表に表示するのデータ本体です。
このプロパティにデータをArrayで渡します。
またデータのArrayは、Key:ValueのObjectになります。// sample [ { age: 40, name: 'admin1', email: 'hoge@for.jp' }, ... ]
fields
fields
は「見出し」になります。HTMLでのth
です。
これもArrayで渡します。// sample [ { key: 'name', label: 'Name' }, ]
key
はitems
のObjectのKeyになります。
label
は見出しとして表示するデータになります。
fields
のkeyの値と、items
内のObjectのkeyがマッチすると見出しとして表示されます。具体例
下記を見て頂くとわかりやすいかと思います。
<b-table striped hover caption-top label="Table Options" :items="[ { age: 40, name: 'admin1', email: 'hoge@for.jp' }, { age: 21, name: 'admin2', email: 'huga@for.jp' }, { age: 89, name: 'admin3', email: 'piyo@for.jp' }, { age: 38, name: 'admin4', email: 'aaaaa@for.jp' } ]" :fields="[ { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, { key: 'age', label: 'Old' } ]" > <template v-slot:table-caption>表サンプル</template> </b-table>
ちょっとした応用
特定のデータを非表示にしたい。でもitemsを変更したくない
こんな時は、
fields
から表示したくないkey:value
を削除する事で対応できます。サンプル
https://codepen.io/H_Naito/pen/RwPaKxJ
(年齢非表示サンプルを参照してください)ソートつけたい、一部にHTMLを追加したい
まずソートの場合は、fieldsに
sortable
の追加で対応できます。
sortable
を追加した列がソート対象になります。一部のセルにHTMLを入れたい、例えばリンクしたい場合などはもう少しコードの追加が必要になります。
下記のコードで、セルを指定して表示したいデータとHTMLを設定します。
下記では、emailのセルにmailtoのリンクを設置しています。<template v-slot:cell(email)="data"> <a :href="`mailto:${data.item.email}`">{{ data.item.email}}</a> </template>サンプル
<b-table striped hover caption-top :items="[ { age: 40, name: 'admin1', email: 'hoge@for.jp' }, { age: 21, name: 'admin2', email: 'huga@for.jp' }, { age: 89, name: 'admin3', email: 'piyo@for.jp' }, { age: 38, name: 'admin4', email: 'aaaaa@for.jp' } ]" :fields="[ { key: 'name', label: 'Name', sortable: true }, { key: 'email', label: 'Email', sortable: true }, { key: 'age', label: 'Old', sortable: true } ]" > <template v-slot:table-caption>リンクとソート機能付き</template> <template v-slot:cell(email)="data"> <a :href="`mailto:${data.item.email}`">{{ data.item.email}}</a> </template> </b-table>サンプルの全コード
https://codepen.io/H_Naito/pen/RwPaKxJ
参考
Tables
https://bootstrap-vue.js.org/docs/components/table/Sorting
https://bootstrap-vue.js.org/docs/components/table/#sortingCustom data rendering
https://bootstrap-vue.js.org/docs/components/table/#custom-data-rendering
普通にtableを書くと、table要素からtr、ht、tdを記述して、そこにloop設定してとなりますが、結構簡単に作れると思います。
他にもいろいろなプロパティがありますので、何かのプロジェクト等で利用してみてください。閲覧ありがとうございました。
- 投稿日:2020-02-16T17:17:13+09:00
【Laravel 7 リリース】向け、 Laravel7のインストール方とUpgrade Guideの日本語訳
Laravel 7系
Laravel公式サイトにより、Laravel 7のリリースは2020年の3月3日と発表されました?
皆さんは6系にしましたか?7系もう目の前ですよ!?
ただし、7はLTS(Long-Term Support)じゃなさそうなので、商用なら6の方が良いかな。Laravel 6
- 2021年9月3日までのバグ修正
- 2022年9月3日までのセキュリティ修正
Laravel 7
- 2021年9月3日までのバグ修正
- 2022年9月3日までのセキュリティ修正
現時点Laravel 7のインストール方
packagistのLaravel 7.0すでに
dev-develop
ブランチに更新したので、
以下のコマンドはxyyoというLaravelアプリが作成できます。
composer create-project --prefer-dist laravel/laravel xyyo dev-develop
7系と遊びたい方はぜひ!!
変わっているものは、以下に記載されているようにそんなに多くありませんが、5.x系前半からなど、だんだんキツくなるでしょう。個人プロジェクトは早くアップデートしましょうね。では、早速見てみましょう?
Upgrade Guide
6.xから7へ
PHPバージョン
- 7.2.5 以上は必須
Symfony 5 関連のアップグレード
- 影響性 : 高
Laravel7 は Symfony5系のコンポネートを使用しているので、Symfony5系の変更によって、変更が必要になってきます。
App\Exceptions\Handler
クラスのreport
とrender
メソードはException
インスタンスを格納されなくて、インターフェイスThrowable
のインスタンスを格納します。// Laravel 6 public function report(Exception $exception); public function render($request, Exception $exception)⬇︎
// Laravel 7 use Throwable; public function report(Throwable $exception); public function render($request, Throwable $exception);コンフィグファイルの
session
のoption
のデフォルト値の変更// Laravel 6 'secure' => env('SESSION_SECURE_COOKIE', false). 'same_site' => null,⬇︎
// Laravel 7 'secure' => env('SESSION_SECURE_COOKIE', null), 'same_site' => 'lax',Authenticationー認証
- 影響性 : 高
全ての認証系のscaffoldingは
laravel/ui
に移動されました。もし5系のphp artisan make:auth
か、6系php artisan ui:auth
利用しているなら、laravel/ui
を~2.0
にする必要があります。composer require laravel/ui "~2.0"
ちなみに、scaffoldingのフロント側デフォルトは
vue
使用ですが、パラメータ指定でArguments:type ー The preset type (bootstrap, vue, react)
変更することもできます。Date Serializationー日付のシリアライズ化
- 影響性 : 高
Eloquentの
toArray
とtoJson
メソードを使う際、Laravel7は新たな日付のシリアライズフォーマットを使用します。Laravel7は日付のシリアライズをする時に、タイムゾン秒の小数部に対応しているISO-8601に適用されるCarbonのtoJson
メソードを使用します。プラス、この変更はより良いフロント側の日付の対応を提供します。7系前の日付フォーマット
2019-12-02 20:01:00
は7系から2019-12-02T20:01:00.283041Z
に更新されます。もし7系以前のフォーマットを使用したければ、モデルに
serializeDate
をoverrideしてください。/** * Prepare a date for array / JSON serialization. * * @param \DateTimeInterface $date * @return string */ protected function serializeDate(DateTimeInterface $date) { return $date->format('Y-m-d H:i:s'); }この変更はモデルレベルの影響しかないです。データベースに保存されているデータに影響しません。
Blade
The component Method
- 影響性 : 中
Blade::component
メソードをBlade::aliasComponent
に変更したので、使用している場合は変更してください。Blade Components & "Blade X"
- 影響性 : 中
Laravel7 は公式的にBladeの"tag components"をサポートします。もし続いて
spatie/laravel-blade-x
パッケージ利用の場合は、Blade::withoutComponentTags
メソードを使って、Bladeの"tag components"を無効にしてください。
もしspatie/laravel-blade-x
パッケージ使用てなければ、これを無視してよろしいです。
使用している場合は、withoutComponentTags
メソードをAppServiceProvider
のboot
に追加してください。use Illuminate\Support\Facades\Blade; Blade::withoutComponentTags();Factory Types
- 影響性 : 中
7系から"factory types"機能を削除します。この機能は2016年の10月からドキュメントに下げられて、もし使っている場合はよりフレキシブルなfactory states機能に移行してください。
Eloquent
The addHidden / addVisible Methods
- 影響性 : 低
ドキュメントされていない
addHidden
とaddVisible
メソードが削除されました。使用している場合はmakeHidden
とmakeVisible
を使ってください。The booting / booted Methods
- 影響性 : 低
モデルの"boot"の時にロジックを追加するメソード
booting
とbooted
をEloquentに追加しました。もし同じ名前のメソードを使っている場合は名前を変更してください。Route Binding
- 影響性 : 低
Illuminate\Contracts\Routing\UrlRoutable
インターフェイスにあるresolveRouteBinding
メソードは$field
パラメータを使います。もしこのインターフェイスを使用していれば、更新してください。更に、
Illuminate\Database\Eloquent\Model
クラスのresolveRouteBinding
メソードも$field
パラメータを使いますので、もしこのメソードをoverrideしたら、更新してください。最後、
Illuminate\Http\Resources\DelegatesToResources
traitのresolveRouteBinding
メソードも$field
パラメータを使いますので、もしこのメソードをoverrideしたら、更新してください。The
getOriginal
Method
- 影響性 : 低
$model->getOriginal()
メソードはキャストされたモデールを参考にします。もし以前のキャストされない、生の属性を使用したい場合はgetRawOriginal
を使ってください。
TokenRepositoryInterface
クラス
- 影響性 : 低
Illuminate\Auth\Passwords\TokenRepositoryInterface
にrecentlyCreatedToken
メソードを追加したので、もしこのインターフェイスを使用している場合は、recentlyCreatedToken
メソードを追加してください。HTTPーPSR-7の適用性
- 影響性 : 低
PSR-7レスポンス生成ためのZend Diactorosライブラリが推奨されません。もしPSR-7の適用性のために、使用している場合は
nyholm/psr7
composerパッケージを使用してください。更に、composerパッケージのsymfony/psr-http-message-bridge ^2.0
も一緒にインストールしてください。MailーMarkdownメールテンプレートの更新
デフォルトのMarkdownメールテンプレートのデザインなどはより良くなります。プラス、ドキュメントに書かれていない
promotion
テンプレートは削除されます。Queueー非推奨される
--daemon
削除
- 影響性 : 低
queue:work
コマンドのフラグ--daemon
が削除されます。daemonとして実行されるのはデフォルトになります。Resourcesー
Illuminate\Http\Resources\Json\Resource
クラス
- 影響性 : 低
非推奨される
Illuminate\Http\Resources\Json\Resource
は削除されます。リソースはIlluminate\Http\Resources\Json\JsonResource
クラスを継承にします。Sessionー
array
セッションドライバ
- 影響性 : 低
array
セッションドライバのデータはカレントリクエストに持続的にアクセスできるようになります。7系以前は使えません。Testingー
assertSee
アサーション
- 影響性 : 中
TestResponse
クラスのassertSee
アサーションは自動的にエスケープされます。もし手動的にエスケープしてきたら、しないでください。Validationー
different
ルール
- 影響性 : 中
different
ルールはリクエストに特定のパラメータが足りない場合に通りません。
個人的に7系に非常に楽しみにしています?
特にこれ、元々はモデルにgetRouteKeyName
メソード使って、primary keyをoverrideしないといけないこと以下のように記述すれば、終わりみたい!便利そう!Route::get('/posts/{post:slug}', function (Post $post) { // ... });
リソース:
https://laravel.com/docs/master/upgrade
https://laravel-news.com/implicit-route-model-binding現在2020年2月16日の最新情報です。リリースノートなど新たな情報が出たら、更新したいと思います。
- 投稿日:2020-02-16T16:33:35+09:00
Vue.jsとDjango-Rest-Frameworkで神経衰弱アプリを作ってみる【その6】~ユーザ認証編2~
ログインを実装
ログイン画面作成
Login.vue<template> <v-form> <v-container> <v-row> <v-col cols="12"> <v-text-field v-model="username" label="Username" outlined ></v-text-field> </v-col> </v-row> <v-row> <v-col cols="12"> <v-text-field v-model="password" outlined label="Password" style="min-height: 96px" type="password" > </v-text-field> </v-col> </v-row> <div class="text-right"> <v-btn depressed large color="primary">ログイン</v-btn> </div> </v-container> </v-form> </template> <script> export default { name: 'Login', data: function () { return { username: null, password: null } }, mounted () { // } } </script>router/index.tsを修正
router/index.tsimport Vue from 'vue' import Router from 'vue-router' import PlayScreen from '@/views/PlayScreen.vue' import Login from '@/views/Login.vue' Vue.use(Router) export default new Router({ mode:'history', routes: [ { path: '/', redirect: 'login' }, { path: '/login', name: 'login', component: Login }, { path: '/playscreen', name: 'playscreen', component: PlayScreen }, ] })
npm run dev
すると作成したログイン画面が初期表示されるようになります。vuexのインストール
npm install vuex --saveaxiosをインストール
axiosは「Promise ベースの HTTP クライアント」です(公式より)。
npm install axios --saveaxiosでget, postリクエストを投げる共通関数を用意
src
直下にapi
フォルダを用意し、またその直下にcommon.tsを作成してaxiosの共通関数を用意concentratio # プロジェクトルートディレクトリ ├── config │ ├── ... │ ├── frontend │ ├── ... │ ├── .. │ ├── . │ └── src │ └──api # 作成 │ └──common.ts # 作成 └── manage.pysrc/api/common.tsimport axios from "axios"; const baseRequest = axios.create({ baseURL: "http://localhost:8000", headers: { "Content-Type": "application/json", } }); /** * GETリクエスト * @param url URL */ function get(url) { return baseRequest.get(url); } /** * POSTリクエスト * @param url URL */ function post(url, payload) { return baseRequest.post(url, payload); } export default { get, post };認証用のvuexモジュールを用意
単一のgettersやactionsで管理するとソースコードが膨れ上がるだけだなあと思って機能毎にモジュール分割してみようと思って挑んだのですが、見事に半日ほどハマりました・・・。
concentratio # プロジェクトルートディレクトリ ├── config │ ├── ... │ ├── frontend │ ├── ... │ ├── .. │ ├── . │ └── src │ └──store # 作成 │ └──authenticates # 作成 │ ├──actions.ts # 作成 │ └──index.ts.ts # 作成 │ └──mutations.ts # 作成 └── manage.pystore/authenticates/actions.tsimport commonAxios from "../../api/common"; export default { LOGIN: function({ commit }, payload) { return new Promise((success, error) => { commonAxios.post("/api-token-auth/", payload).then( response => { // 成功時の処理 commit("SET_TOKEN", response.data.token); // mutations.tsのSET_TOKEN関数にトークンを渡す commonAxios.baseRequest.defaults.headers.common['Authorization'] = `JWT ${response.data.token}`; success(); } ); }); } }
上記、urlの最後に"/(スラッシュ)"を付け忘れるとCORSではじかれてエラーになります。
これにも気づくのに時間がかかって小一時間ほどハマりました・・・。
store/authenticates/index.tsimport actions from './actions'; export const state = {}; export default { namespaced: true, // 必ずつける、外すとvueのmapActionsで読み込む時にエラーになるよ。 state, actions, mutations };store/authenticates/mutations.tsexport default { SET_TOKEN: function(state, token) { state.token = token } };
src/store
直下にindex.ts
を作成store/index.tsimport Vue from "vue"; import Vuex from "vuex"; import authModules from "./authenticates"; // authenticates配下のモジュールをauthModulesという名前で読み込む Vue.use(Vuex); export default new Vuex.Store({ modules: { authModules: authModules, } });
src/main.ts
を修正src/main.ts. .. ... import store from "./store"; // 追加 ... .. . new Vue({ el: '#app', router, components: { App }, template: '<App/>', vuetify: new Vuetify(), store // 追加 });Login.vueを修正
Login.vue<template> <v-form> <v-container> ... .. . <div class="text-right"> <v-btn depressed large color="primary" @click="login"> ログイン </v-btn> </div> </v-container> </v-form> </template> <script> import { mapActions } from "vuex"; export default { name: "Login", data: function() { return { username: null, password: null }; }, methods: { ...mapActions("authModules", ["LOGIN"]), // モジュール分割したaction.tsからLOGIN関数を読み込み login: function() { const data = { username: this.username, password: this.password }; this.LOGIN(data).then(res => { console.log(res); }); } }, mounted() { // } }; </script>responseが返ってくるか確認
正しいusernameとpasswordを入力後ログインボタン押下でresponseが返ってくる確認します。
返ってきました!
認証成功後、トップページに遷移するようにする。
Login.vue
のconsole.log(res);
をthis.$router.push("/playscreen");
に書き換えるだけ。以上
無事、ログイン画面からトップページに遷移することができました。
- 投稿日:2020-02-16T15:19:37+09:00
SIerの僕、プログラミング始めてみた(10月スタート・初投稿)
簡単な自己紹介
SIerに2年間在籍しております、かなたです。
誰も興味がない趣味とか仕事とか、本記事のきかっけを初めに。
- 好き①:服(Margielaとか)
- 好き②:映画(おすすめは Into the Wild)
- 業務①:DataSpiderを用いたデータ連携基盤を構築
- 業務②:C#デスクトップ系業務アプリ
10月からWEBアプリケーションの勉強を独学で始めまして、よくよくエラー等ひっかかります。
昨日の自分自身に対して、教えるならどのように伝えるかを、ここに表現できればと思っています。
(あとモチベーションアップに繋がれば…という思いです)作っているもの
SIer(以外もそうだと思うんですが)は基本的に、実績ベースで予定・工数を積み上げ、スケジューリングします。
ですが、実績を記録しているPJ/チームを僕は見たことがありません。
そのため、予実データ化し改善までつながるアプリケーションを開発しよう!と始めてみました。成果
さてさて、10月からおよそ4ヶ月の成果はざっくり以下の通りです。
プログラミング基本
- HTML/CSS/JavaScript/Ruby/Railsの基本(Progate)
- Railsチュートリアル2周(GitHub~Herokuデプロイ)
- Vueサンプル複写(基礎から学ぶ Vue.js)
本
- リーダブルコード
- 初めてのRuby
- Webを支える技術
- Web API: The Good Parts
- テスト駆動開発
- リファクタリング
- エンジニアファースト
- エンジニアの知的生産術
- コンピュータはなぜ動くのか
- デザイン系の本を数冊
アプリケーションの実装状況
- フロントエンド ・・・ Vue.js + Vuex + VueRouter + Vuetify
- サーバサイド ・・・ RubyonRails
- 実装:メールアドレスを利用したユーザの登録〜ログインおよびユーザ情報の更新
- 詳細:ユーザログイン時には、WebStorageを利用。ログイン永続化を実装。
ざっくりこんな状況です。頑張らないと・・・
さいごに
また、目標・スケジュールをこちらに記載できればと思っています(もしかして、そういった場でない?)
そして、次回からは、冒頭で述べたとおり、自分自身の備忘録として、
学んだことを表現させていただきます。よろしくお願いいたします。
- 投稿日:2020-02-16T15:13:06+09:00
Vue-cli3、Webpack4、babel7で、IE11向けの「@babel/preset-env」を使いたい
Vue.jsで作っていて、IE11が真っ白になる時。
SCRIPT1003 Expected ':'このエラーが何か分からなくて、困る時がありますよね?
おそらくbabel
の設定が、うまくできてないだけだと思います。IE11は...
{ value }というようなshort-handが使えず、
{ key: value }にしていないと駄目だったり、hoge(){}というような関数の省略した書き方は使えず、
hoge: function(){}という書き方にしたり、data:() => {}というようなアロー演算子は使えないので、
data: function(){ return }に直してみたり...そもそもコレ
babel
で解決するべき事ですよね。。
babel.config.js
って、難しいですよね。
babel-polyfill
は非推奨ですって言うけど、じゃあどうしろとか説明ないし。
babel7
以降で、IE11向けに@babel/preset-env
を使いたい時。とりあえずインストール
今回参考にした記事が
vue2-dropzone
のissue
なので、ほぼ関係ないと思いますが、
vue2-dropzone
以外でも使える書き方みたいです。
まあとりあえず、参考記事の通り入れます。$ npm install -D vue2-dropzonevue.config.js
module.exports = { //(※通常の設定は省略しています) //↓webpack.config.jsの内容を変更する時の書き方 configureWebpack: { module: { rules: [ { test: /\.js$/, exclude: (file) => ( /node_modules/.test(file) && !/vue2-dropzone/.test(file) ), use: [ { loader: "babel-loader" }, ], } ], }, } };babel.config.js
module.exports = { presets: ["@babel/preset-env"], overrides: [ { test: ["./node_modules/vue2-dropzone"], presets: [ ["@babel/preset-env", { targets: { ie: "11" } }], ], plugins: [ '@babel/plugin-transform-arrow-functions', '@babel/plugin-transform-shorthand-properties', ] } ], }
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-shorthand-properties',
これがうまく動いていれば、IE11で見えるはずです。以上です。
参考記事
- 投稿日:2020-02-16T15:06:00+09:00
[Vue.js]外側をクリックすると閉じるメニュー(複数の場合)
はじめに
- メニューボタン押すと開く
- メニューボタンもう一度押すと閉じる
- メニューの外側押しても閉じる
この操作はこちら↓の記事を読んでもらえればできます。
[Vue.js]外側をクリックすると閉じるドロップダウンメニューですが、複数ボタンがある場合、この処理だけだと操作性の問題が生じたため、いろいろ改善しました。それを紹介します。
注意 : 本記事は上で紹介した記事の技術がわかった上でお話をします。
[問題 1] メニューが閉じない場合がある
複数のボタンがある時、メニューを一度開き、別のボタンを押すと先に開いていたメニューが閉じない。
デモ (JSFiddle)以下の画像をご覧ください。
Info 1
から順番にボタンを押していくと、Info 2
を押した時にInfo 1
のメニューが閉じない。
構造
親コンポーネント↓ :
<msg-comp>
を複数呼び出して並べる。<div id="app"> <div class="containter"> <msg-comp v-for="msg in msgList" :msg="msg" ></msg-comp> </div> </div>
<msg-comp>
コンポーネント↓ : ボタンを押すと<info-menu>
が開く。<div class="msg"> <button v-on:click.stop="showInfo = !showInfo">Info {{msg}}</button> <info-menu v-if="showInfo" v-on:close="showInfo = false"> </info-menu> </div>
<info-menu>
コンポーネント↓ : クリックイベントが発生した際、クリックした要素がこのコンポーネント内の要素ではない場合に閉じる。<div class="info"> <div>AAA</div> <div>BBB</div> <div>CCC</div> </div>問題1 の原因
<!-- <msg-comp> コンポーネント内 --> <button v-on:click.stop="showInfo = !showInfo">Info {{msg}}</button>
.stop
modifierでバブリングの停止を行っているのが原因。
Info 2
を押してメニューを開ける時に、自分自身のメニューを閉じるイベントが発火しないようになっているのはいいんだが、Info 1
のメニューを閉じるイベントも発火しない。問題1 解決策
.stop
modifierをやめて自前で実装。- クリックした要素が開けるボタンだった時に、閉じる処理を発火させないようにする。
まず、
<msg-comp>
コンポーネント内のbuttonから.stop
を削除。
ref="msgBtn"
でボタンのDOMを取得。取得したDOM要素を<info-menu>
コンポーネントへpropsで投げる。<!-- <msg-comp> コンポーネント内 --> <div class="msg"> <button ref="msgBtn" v-on:click="showInfo = !showInfo" > Info {{msg}} </button> <info-menu v-if="showInfo" v-on:close="showInfo = false" :msgBtn="$refs.msgBtn" > </info-menu> </div>
<info-menu>
コンポーネントで、ボタンのDOM(msgBtn)を受け取る。
イベントリスナーの発火条件を追加。
これで判定できる→!this.msgBtn.contains(e.target)
→ 意味:クリックしたDOMが、ボタンのDOM内にあればfalseになるVue.component('info-menu', { mixins:[listener], template: '#info', props: { msgBtn: { type: HTMLButtonElement } }, created:function(){ this.listen(window, 'click', function(e){ if (!this.$el.contains(e.target) && !this.msgBtn.contains(e.target)){ this.$emit('close'); } }.bind(this)); } }注意点:propsにtype指定する際、msgBtnのDOMの型(type)を
HTMLButtonElement
としている。a要素とかを使うなら変わる。以上です。
Info 1
を押して開いた状態でInfo 2
を押すとInfo 1
のmenuが閉じる! 解決できました。[問題 2] Tab移動でInfo内部から抜けた時に勝手に閉じて欲しい
これは問題というより、そういう設計にしたかったという願望です。
Infoのサイズが大きい場合にはUX的にもそうしたいですよね。問題2 解決策
これはめちゃ簡単。新しくイベントリスナー追加するだけ。
created:function(){ // ~~~ this.listen(window, 'keyup', function(e){ if (!this.$el.contains(e.target) && e.key === 'Tab'){ this.$emit('close'); } }.bind(this)); }注意点としては
keydown
ではなくkeyup
にするところ。
keydown
にしてしまうと、まだbuttonにいて、Infoに移ろうとする時に発火してしまい、すぐ閉じてしまう。
keyup
にしておくと、Tabのkeydown
時にInfoに移るので、keyup
が発火する時にはInfoにいる。よって閉じない。[おまけ] Nuxt.jsでやりたい!
Nuxt.jsでやる際に参考になりそうな話をします。
mixinをNuxtでやる
Nuxt.jsの場合、mixinは
plugins
フォルダにmixin-common-methods.js
的なものを作り、
nuxt-config.js
内にplugins: ['@/plugins/mixin-common-methods']
とすれば良い。
各コンポーネントからわざわざ呼び出しとかもなし、これだけで使える。参考 : Nuxt.jsで異なるコンポーネントから共通で利用できる関数を定義する(mixin編)
plugins/mixin-common-methods.jsimport Vue from 'vue' Vue.mixin({ destroyed() { if (this._eventRemovers) { this._eventRemovers.forEach(function(eventRemover) { eventRemover.remove() }) } }, methods: { listen(target, eventType, callback) { if (!this._eventRemovers) { this._eventRemovers = [] } target.addEventListener(eventType, callback) this._eventRemovers.push({ remove() { target.removeEventListener(eventType, callback) } }) } } })~/components/info-menu.vueexport default { props: { msgBtn: { type: HTMLButtonElement } }, //~~~ created:function(){ this.listen(window, 'click', function(e){ if (!this.$el.contains(e.target) && !this.msgBtn.contains(e.target)){ this.$emit('close'); } }.bind(this)); }, //~~~ }
- 投稿日:2020-02-16T14:35:12+09:00
#Vue の初心者向けチュートリアルが面倒なので、1個のHTMLファイルにまとめて動作確認する ( はじめに - Vue.js )
チュートリアル
手順
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <body style="font-size: 150%;"> <div id="app"></div> <hr> <div id="app-2"> <span v-bind:title="message"> Hover your mouse over me for a few seconds to see my dynamically bound title! </span> </div> <hr> <div id="app-3"> <span v-if="seen">Now you see me</span> </div> <hr> <div id="app-4"> <ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol> </div> <hr> <div id="app-5"> <p>{{ message }}</p> <button v-on:click="reverseMessage">Reverse Message</button> </div> <hr> <div id="app-6"> <p>{{ message }}</p> <input v-model="message"> </div> </body> <script> new Vue({ template: '<p></p>', data:{ msg:'hello world!' } }).$mount('#app') var app2 = new Vue({ el: '#app-2', data: { message: 'You loaded this page on ' + new Date().toLocaleString() } }) var app3 = new Vue({ el: '#app-3', data: { seen: true } }) var app4 = new Vue({ el: '#app-4', data: { todos: [ { text: 'Learn JavaScript' }, { text: 'Learn Vue' }, { text: 'Build something awesome' } ] } }) var app5 = new Vue({ el: '#app-5', data: { message: 'Hello Vue.js!' }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join('') } } }) var app6 = new Vue({ el: '#app-6', data: { message: 'Hello Vue!' } }) </script>これをてきとうなパスに保存して開くこと。
Macならテキストをコピーした後に
pbpaste > ~/vue-start.html && open ~/vue-start.htmlとか。
動作例
Original by Github issue
- 投稿日:2020-02-16T14:18:12+09:00
#Vue js を環境構築なし、サーバーなし、HTMLファイル1個だけで一瞬で始める、動かす。 (初心者向けチュートリアルを始めたい)
local に html ファイルを作る
自分のPCでファイルを作成、編集してアクセスする
~/start.html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"></div> <script> new Vue({ template: '<p></p>', data:{ msg:'hello world!' } }).$mount('#app') </script>
open ~/start.html
これだけで動くみたいだ。
npm とか 色々用意しないと動かせないのかと思いこんでいたが、なんと
チュートリアルの二個目
ちゃんと動いている。
テキストにマウスを乗せるとポップアップが出てくるぜ。
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app-2"> <span v-bind:title="message"> Hover your mouse over me for a few seconds to see my dynamically bound title! </span> </div> <script> var app2 = new Vue({ el: '#app-2', data: { message: 'You loaded this page on ' + new Date().toLocaleString() } }) </script>オンラインサイトを使う場合
公式でもおすすめされているサイト
https://jsfiddle.net/chrisvfritz/50wL7mdz/
にアクセスすると既にHTML / Javascriptが入力されており、 Run を実行するだけで動作させられるようだ!
参考
「Vue js はじめに」にこのことが書いてあるのだが、そもそも、膨大なドキュメントのどれが「はじめのはじめ」なのかが、初学者には分からないのですよ。
https://jp.vuejs.org/v2/guide/index.html
この「はじめに」にインストールボタンがあって、そこから npm の話とか出てくるじゃないですか…。
Original by Github issue
- 投稿日:2020-02-16T13:58:30+09:00
ReactとVueとAngularの戦い。
日本ではReactとVueが競い合っているように思えますが、実際はどうなのかを調べてみました。
npm trends
npm trendsという場所がありまして、そこではnpmパッケージのダウンロード数をグラフに表示してくれます。
使い方は簡単、パッケージ名を一つづつ入力していくだけです。
調査結果
react vs @angular/core vs vue vs jquery vs typescript | npm trends
reactとvueと@angular/coreとjqueryとtypescriptの図
Reactの圧勝ですね。Vueは日本だと記事を色々見かけて人気なのはなぜなのでしょうか?
The State of JavaScript 2019
ついでにこちらでも調べてみます。
The State of JavaScript 2019でフレームワークを見てみます。Front End Frameworksカテゴリ
React
https://2019.stateofjs.com/front-end-frameworks/
Vue
https://2019.stateofjs.com/front-end-frameworks/
Angular
https://2019.stateofjs.com/front-end-frameworks/angular/濃いピンクが、以前使用したことがあり、また利用したい。
薄いピンクが、以前使用したことがあり、もう使わない。
濃い緑が、聞いたことがあり学びたいと思っている。
薄い緑が、聞いたことはあるが興味はない。満足度(=濃いピンク/(濃いピンク+薄いピンク))はReactとVueは同じくらいのようです。
SvelteがVueを抜き第二位に入っていますね。
The State of JavaScript 2019: Front End Frameworks
- 投稿日:2020-02-16T12:48:32+09:00
Vuetify使おうと思ってwebpack3からwebpack4への移行した時のメモ
概要
タイトル的なことをやったらハマったけど最終的に出来たっぽいのでまとめてみた。
経緯
Vuetifyのクイックスタートガイドに以下のような記述があり、自分のvue init webpackで作成したプロジェクトを確認したらwebpack3系で作られていた。
ツリーシェーキングに vuetify-loader を使用する場合は、Webpackのバージョン >=4であることを確認してください。
バージョン色々
- vue: 2.5.22 -> 2.6.11
- vue-cli: 3.3.0
- webpack: 3.12.0 -> 4.41.6
移行手順
- 依存関係の更新
- こちらを参考にさせて頂いた。
- ソースコードの編集
- 上記のリンクにも記載がありましたが、より詳細がわかりやすかったのでこちらを参考にさせて頂いた。
- 個別にやったこと
- 上記を実施した際に、なぜかwebpack-dev-serverのバージョンだけ上がってなかったので、とりあえず
npm isntall --save-dev webpack@latest
した。ハマったとこ
上記の移行手順をやり切って、
npm run build
は無事成功。
お次はnpm run dev
でローカル動作確認を、と思ったらdevサーバは立つもブラウザに以下のエラーが出て画面は描画されず。。。
とりあえずググるとこのような記事が。なるほど、そんなんもうわからん…解決
ひたすらググり続けて同事象のissueに以下のコメントを発見。
use
webpack-dev-server@3.5.1
instead of 3.9.0 solved the problemほう?そういえばさっきwebpack-dev-serverを適当に最新化したぞ…
package.json"webpack-dev-server": "^3.10.3"やっぱり。
npm isntall --save-dev webpack-dev-server@3.5.1
してみよう。package.json"webpack-dev-server": "^3.5.1"
npm run dev
実行
おー、できた!(画面も描画された)そんなわけでひとまずwebpack3 -> webpack4の移行は完了。Vuetifyの導入に戻ります。
参考文献