Vue.jsでピアノロールエディタ
お絵かきアプリは考えることが多くてなかなか手を付けられないみたいな感じになったので、趣味でよく触るピアノロールエディタを作ってみようかなと思って作り始めました。
現状
まだ大枠を用意しただけ。
追記
TODO
Vue.jsのチュートリアルのコードを読む2
続きをやっていきます。
ちなみに見出しに付けた「#n」はGitHubからクローンしてきたプロジェクトのディレクトリの番号で、動画の番号と途中から少しずれています。
#7
routerを使うっぽい。冒頭でURL末尾にページ内リンクに使う#
から始まる文字をつけるあれを利用してSPAのルーティングを行うんだぜみたいな感じのことを言っているような気がする(英語全然だめ)。
特に説明はなかったけどvue-cli
でrouter
をインストールした場合に生成されるプロジェクトで始めているっぽい。
App.vue
のテンプレートには<router-view/>
というタグが記述されている。main.js
の方のVueインスタンス生成時に渡すオブジェクトにはインポートしたrouterがrouter
という名前のプロパティとして定義されている。
<template> <div id="app"> <img src="./assets/logo.png"> <router-view/> </div> </template>
import router from './router' 中略 new Vue({ el: '#app', router, template: '<App/>', components: { App } })
router/index.js
にルーティングの設定を記述して、それぞれのコンポーネントとなる.vue
ファイルを作成。
routes: [ { path: '/', name: 'Hello', component: HelloWorld }, { path: '/friends name: 'Friends', component: Friends }, { path: '/contact', name: 'Contact', component: Contact }, { path: '/account', name: 'Account', component: Account } ]
.vue
ファイルはtemplate
とscript
タグで構成。それぞれの.vue
ファイルができたらrouter/index.js
側でインポートする。
import Account from '@/components/Account' import Contact from '@/components/Contact' import Friends from '@/components/Friends'
するとlocalhost:8000/#/contact
などにアクセスできるようになる。
App.vue
にFriends
へのリンクを追加する。ほかのコンポーネントも同様。
<router-link to="friends">Friends</router-link>
router/index.js
のfriend
のpath
を少し変更。
{ path: '/friends/:id/:age/:weight' name: 'Friends', component: Friends },
すると/#/friend/1/2/3
のようなURLにアクセスすることでFriend
コンポーネントが表示される。
Friends.vue
のテンプレートを以下のように変更。
<template> <div> <h1>Friends</h1> {{$route.params.id}} {{$route.params.age}} {{$route.params.weight}} </div> </template>
これでURLの/1/2/3
の部分がそれぞれid
age
weight
として表示される。
Friends.vue
のscript
に以下を追加。
props: [ 'id', 'age', 'weight', ]
router/index.js
のfriend
にprops: true
を追加。
{ path: '/friends/:id/:age/:weight' name: 'Friends', props: true, component: Friends },
Friends.vue
のテンプレートを変更。
<div> <h1>Friends</h1> {{id}} {{age}} {{weight}} </div>
これでさっきの{{$route.params.id}}
のような記述と同じ値が取得できる。
#8
Storeパターンを使うと言っているが最近のフロントエンドの事情に疎いのでStoreパターンを先に少し勉強したい。
適当にググったが、ある程度の規模になるとコンポーネント同士の関係が複雑になってしぬのでStoreっていうグローバルなやつに管理させてコンポーネント同士は疎な結合にしようみたいな話らしい。
各ComponentからはStoreのShared Actionsを呼び出してStoreのShared Stateを更新、各ComponentはShared Stateを監視して表示を変化させる。他にもStoreはShared Eventsを持っていて、共通のイベントで各Componentの何らかの処理を発火させたいみたいなこともするらしい。あと勘違いしてたけどFluxとは別物とのことで、Fluxはさらにいろいろ制約を加えたものみたいなことが書いてあった。雰囲気は?わかった?ような?気が?するのであとは動画で実際のコードを書いて学ぶ。
動画で作るアプリにはNavigationとFriendsとFooterというコンポーネントがありそれぞれがルートのコンポーネントの子として存在している。で、今はFriendsの友達リストが持つ人数をNavigationとFooterに表示したい、という課題を解決したい。(Appに友達リストを持たせることもできるが、規模が大きくなってくるとめっちゃしんどくなりそう)
src
ディレクトリの下にstore
ディレクトリを作成、その中にFriendsStore.js
を作成する。中身はconst
のオブジェクトとそれをエクスポートする文。オブジェクトはdata
とmethod
を定義。
const FriendStore = { data: { friends: ["bobby", "billy"], }, methods: { addFriend(name) { FriendStore.data.friends.push(name); } } }; export default FriendStore;
用意できたらFriendsコンポーネントからストアを呼び出して使う。
import FriendStore from "../stores/FriendStore" export default { data() { return { newFriend: null, FriendStore: FriendStore.data, }; }, methods: { addFriend(name) { FriendStore.methods.addFriend(name) this.newFriend = null; } } }
他のVueコンポーネントからも同様にインポートして使うだけ。思ったよりシンプルだった。
Vue.jsのチュートリアルのコードを読む
はじめに
就活とか趣味のDTMとかしてましたが飽きたので久しぶりに勉強します。
LearnCode.academyのVue.jsチュートリアル
某記事で話題になっていたので動画を見ようかと思いましたが、コードを眺めてみると今まで学んだことでだいたい読めそうだなと思ったのでGitHubのコードをクローンしてきて読んでみようと思います。分からなかったら動画見る。
#1
htmlとjsファイルがそれぞれ1つずつのシンプルな構造。htmlはVue.jsとjsファイルを読み込んでいる以外はIDを振ったdiv
要素を用意しているだけ。
<div id="app"></div>
js側ではVueインスタンスを定義している。
const app = new Vue({ el: "#app", data: { bobby: { name: "Bobby Boone", age: 25 }, john: { name: "John Boby", age: 35, } }, template: ` <div> <h2>Name: {{john.name}}</h2> <h2>Age: {{john.age}}</h2> <h2>Name: {{bobby.name}}</h2> <h2>Age: {{bobby.age}}</h2> </div> ` })
el
で用意したIDと紐付けて、data
を用意してtemplate
がレンダリングされる、みたいなコードだと思う。
#2
ファイル構成は前回と同じ。htmlの方は中身も同じ。
js側はVueインスタンス生成時に渡すオブジェクトにcomputed
とfilters
というプロパティを定義していて、どちらも関数を定義している。
computed: { johnAgeInOneYear() { return this.john.age + 1; } }, filters: { ageInOneYear(age) { return age + 1; }, fullName(value) { return `${value.last}, ${value.first}`; } },
テンプレートにも書けるがこうして関数を用意しておくと共通化できるよ、ということだと思う。
computed
の方は特定のデータに対してロジックを書いているので定数っぽく使い、filters
は汎用的に使うための関数というような感じか。
template: ` <div> <h2>Name: {{john | fullName}}</h2> <h2>Age: {{john.age | ageInOneYear}}</h2> <h2>Name: {{bobby | fullName}}</h2> <h2>Age: {{bobby.age | ageInOneYear}}</h2> </div> `
filters
を使う際には{{ john | fullName }}
のようにパイプっぽく書いて関数に渡している。computed
の方は特に渡す必要がない。
#3
ファイル構成は同じ。html側は見出しが足されているがだいたい同じ。
js側はデータにfriendsという配列が定義されている。またmethods
プロパティに年齢をインクリメント・デクリメントする関数が定義されている。テンプレートではv-for
ディレクティブを使ったループ、v-on
ディレクティブを使ったイベントハンドリングでmethods
で定義した関数の呼び出し、v-model
ディレクティブを使ったバインディング?が書かれている。
const app = new Vue({ el: "#app", data: { friends: [ { first: "Bobby", last: "Boone", age: 25 }, { first: "John", last: "Boone", age: 35, } ], }, filters: { ageInOneYear(age) { return age + 1; }, fullName(value) { return `${value.last}, ${value.first}`; } }, methods: { decrementAge(friend) { friend.age = friend.age - 1; }, incrementAge(friend) { friend.age = friend.age + 1; }, }, template: ` <div> <h2 v-for="friend in friends"> <h4>{{friend | fullName}}</h4> <h5>age: {{friend.age}}</h5> <button v-on:click="decrementAge(friend)">-</button> <button v-on:click="incrementAge(friend)">+</button> <input v-model="friend.first"/> <input v-model="friend.last"/> </h2> </div> ` })
なんかcomputed
無くても全然書けるな?という感じがするが、computed
というくらいだからキャッシュするようになっているんだろうと思うのでいつかパフォーマンスの比較をしてみたい。
#4
Vueコンポーネントを扱っているっぽい。やっと学びたかったところが出てきた。
ファイル構成は同じ。htmlは中身も同じ。
js側はさっそくVueコンポーネントが記述されている。
Vue.component('friend-component', { props: ['friend'], filters: { ageInOneYear(age) { return age + 1; }, fullName(value) { return `${value.last}, ${value.first}`; } }, methods: { decrementAge(friend) { friend.age = friend.age - 1; }, incrementAge(friend) { friend.age = friend.age + 1; }, }, template: ` <div> <h4>{{friend | fullName}}</h4> <h5>age: {{friend.age}}</h5> <button v-on:click="decrementAge(friend)">-</button> <button v-on:click="incrementAge(friend)">+</button> <input v-model="friend.first"/> <input v-model="friend.last"/> </div> ` });
パッと見Vueインスタンスの生成と同じに見える。違うのはel
がないのとprops
というのが定義されていること。
親コンポーネントからデータを受け取るためにprops
を定義している。親のテンプレートは以下のようになっている。
template: ` <div> <friend-component v-for="item in friends" v-bind:friend="item" /> </div> `
friend-component
という要素を記述している。Vue.component
の第1引数と同じ名前だ。v-for="item in friends"
でfriends
の要素数だけ子コンポーネントを生成、v-bind:friend="item"
で子で定義したfriend
とバインドするような感じか。
#5
ファイル構成README.mdが追加された以外は同じ。htmlは中身も同じ。README.mdには今回使用するAPIの仕様が記載されている。
js側は、Vueコンポーネントの記述はなくVueインスタンスの定義が書かれている。新しい部分はmethods
で定義している関数内でfetch
という関数を使っていること、mounted
という関数が定義されていること。
const app = new Vue({ el: "#app", data: { editFriend: null, friends: [], }, methods: { deleteFriend(id, i) { fetch("http://rest.learncode.academy/api/vue-5/friends/" + id, { method: "DELETE" }) .then(() => { this.friends.splice(i, 1); }) }, updateFriend(friend) { fetch("http://rest.learncode.academy/api/vue-5/friends/" + friend.id, { body: JSON.stringify(friend), method: "PUT", headers: { "Content-Type": "application/json", }, }) .then(() => { this.editFriend = null; }) } }, mounted() { fetch("http://rest.learncode.academy/api/vue-5/friends") .then(response => response.json()) .then((data) => { this.friends = data; }) }, template: ` <div> <li v-for="friend, i in friends"> <div v-if="editFriend === friend.id"> <input v-on:keyup.13="updateFriend(friend)" v-model="friend.name" /> <button v-on:click="updateFriend(friend)">save</button> </div> <div v-else> <button v-on:click="editFriend = friend.id">edit</button> <button v-on:click="deleteFriend(friend.id, i)">x</button> {{friend.name}} </div> </li> </div> `, });
mounted
はVueのライフサイクルの一つでそこにフックできる。mounted
ではインスタンスがマウントされてDOM要素にアクセスできるタイミングのようだ。ライフサイクルはほかにもたくさんあるので本格的に作る際には調べる必要がありそう。
fetch
の方はPromiseが返ってるのでよくあるAjaxのやつだと思う。(雑な理解)
#6
ファイル構成が急にしっかりしたやつ(曖昧)になった。vue-cli
を使って環境構築からやっているっぽいのでここからは動画を参照していきたい。
最初にプロジェクトを用意する。いつものvue
コマンドなので省略。
それから生成されたプロジェクトの中身を見て、HelloWorld.vue
のテンプレートから不要な要素(リンク群)をごそっと削除した。
次にHelloWorld
の子コンポーネントModule1.vue
を作成してひな型を用意する。同じものをModule2.vue
として用意する。
<template> <h1>Module1</h1> </template> <script> export default { } </script> <style> </style>
用意したら親(HelloWorld)にこれらのコンポーネントを登録する。
import Module1 from "./Module1" export default { 中略 components { Module1 } }
登録したらテンプレートに記述することでModue1を使用できる。
<template> 中略 <Module1 /> 中略 </template>
<Module1 />
を記述した場所にModule1.vue
のテンプレートh1
要素が表示される。Module2も同様にして表示できる。
次にModule1、Module2それぞれのstyle
を記述する。文字の色をそれぞれred
、pink
と記述して確認してみる。
<style> h1 { color: red } </style>
するとh1
要素の文字がすべてピンクになっていまう。Module2のstyleがModule1のスタイルをオーバーライドしてしまっている。scoped
属性を追加することでそれぞれのコンポーネントごとにスタイルが当たる期待した動作になる。
<style scoped> h1 { color: red } </style>
srcディレクトリの中身は以下の通り。
src src/App.vue src/assets src/assets/logo.png src/components src/components/HelloWorld.vue src/components/Module1.vue src/components/Module2.vue src/main.js
続きはまた後で。
Vagrantのrsyncが遅い件
rsync設定でnode_modulesを除外したら耐えられる速度になった。よかった。
まだ自動で同期されるようにしてないけどvagrant rsync
したらホスト側のブラウザが更新されたのでだいぶ便利になった。
これから高速バスに乗るので短いけど一旦ここまで。
Vue.jsの学習メモ2
前回の続きから。
GitHub Pagesで公開されない件。npm scriptのbuild
コマンドでdocs
ディレクトリに出力するようにしてdocs
を公開するように設定したが、build
コマンドでjsファイルとそのマップファイルと画像だけだったのでPagesのURLにアクセスしても404が帰ってきていた。
試しにvue-cli
でvue init webpack-simple
ではなくvue init webpack
として生成したところ、build
コマンドでindex.html
を含むものが出力された。index.html
を出力に含めるにはwebpackのconfigをいじる必要がありそうだ。うーん、ここからはVueじゃなくてwebpackの学習になりそうなので、おとなしくプロジェクト生成からやり直すことにする。GitHubのリポジトリどうしようかな。
ホストOSからアクセスできない件
前回の記事でホストOSからアクセスするためにdevServer
にhost
やport
の設定を書いた。今回生成しなおしたプロジェクトではそれらの設定としてprocess.env.HOST
、process.env.PORT
が書かれているので起動時に環境変数に指定してあげればよさそう。
HOST="0.0.0.0" PORT=8000 npm run dev
問題なくアクセスできた。
ついでにhot reloadの件も対応してみた
方法は前回貼った記事の通り。
共有ディレクトリにrsync
の設定をするがエラーで起動できなかった。
C:/HashiCorp/Vagrant/embedded/gems/gems/vagrant-2.0.0/lib/vagrant/util/platform.rb:115:in `cygwin_path': undefined method `gsub!' for nil:NilClass (NoMethodError)
上の記事によると今使ってる2.0.0の問題とのことなのでVagrantを2.0.2にアップデートする。アップデートは公式からWindows版を落としてきてインストール済みのフォルダにインストールするだけ。
vagrant --version Vagrant 2.0.2
アップデートしたらそのままvagrant up
して起動を確認。やったぜ。
とかいろいろ弄っていたらSSH接続できなくなった。
vagrant ssh Permission denied (publickey).
絶望感がやばい。調べた結果、homeディレクトリ以下のパーミッションをまるっと変更したのがまずかったようだ。反省。
元に戻そうにもubuntuに入れないしどうしようかなと思っていたところ
こちらの記事のコメントで、GUIを有効にしてコンソールログインできるという情報を得た。Vagrantfileを弄る。
config.vm.provider "virtualbox" do |vb| # # Display the VirtualBox GUI when booting the machine vb.gui = true
そしたら~/.ssh/
を700、~/.ssh/authorized_keys
を600、~/.ssh/id_rsa
を400にしてシャットダウン。落ちたらvagrant up
する。
……接続できない。
SSHのログを確認する。ググった記事にはログの場所は/var/log/secure
と書かれていたが、/var/log/auth.log
という名前だった。
Authentication refused: bad ownership or modes for directory /home/vagrant
ホームディレクトリまで豪快にパーミッション変更していたらしい。反省。755にしてみたところ、やっとSSH接続できるようになった。疲れた……。
環境をいじくるのに時間がかかってVue.jsの学習が全く進んでいないが、そういえばhot reloadを試していた途中だったので確認して今日は休憩する。
vagrant
のプラグインrsync-back
をインストールして再起動。接続してvue-cli
でプロジェクトを作成。それからrsync-back
すると、ものすごいゆっくり同期した。今も同期中。ちょっと遅すぎるので、同期を除外するディレクトリを増やす等対策する必要があるっぽい。
ちょっと疲れたので休憩。
Vue.jsの学習メモ
フロントエンドの勉強もしたいみたいなことを言ってたので、Vue.jsを勉強していきます。ちなみにSPA開発は前職でBackbone.jsベースの内製フレームワークとAngularJS (1.4.x) くらいしか経験がありません。
React.jsと迷いましたが迷うならどっちも触ればいいじゃんということで、まずはVue.jsを選んでみました。決め手は見た目のキムワイプ感です。
ググって出てきた以下の記事をそのままなぞるだけのつもりなので、気になったところだけ脳内を出力していきます。
Vue.jsアプリにホストOSからアクセスできない
vagrantで作った環境を使ってプロジェクトを生成したところ、アプリを起動してホストOSからlocalhost:8080にアクセスしても「サイトにアクセスできません」という画面になった。
アプリケーションのログで「headlessな環境ではopenフラグを使うな」みたいなメッセージが出ていたので、よくわからないがpackage.json
の以下の部分を修正。
修正前
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
修正後
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
メッセージは出なくなったが相変わらずアクセスはできない。
tmuxを起動してウィンドウを二つにして、一方でアプリを起動してもう一方でlocalhost:8080をwgetしてみると、index.htmlの中身が取得できた。
なんか設定が必要なんだろうか。試しにN予備校のアプリケーションを起動してみたところホストOSからアクセスできた。うーむ。
「vagrant webpack dev server」とかでググると出てきた情報をもとにwebpack.config.js
を編集。
編集前
devServer: { historyApiFallback: true, noInfo: true, overlay: true },
編集後
devServer: { historyApiFallback: true, noInfo: true, overlay: true, host: "0.0.0.0", port: 8000 },
私の環境だとhost: "0.0.0.0"
だけではアクセスできず、試しにポートを8000にしたらホストOSからhttp://localhost:8000
でアクセスできた。やっと進められる。
ところでvue-cli
でのプロジェクト生成時、webpack-simple
ではなくwebpack
を指定するとテスティングフレームワークとかLinterとか入ってて便利とのこと。今回はVue.jsの学習のためいったん置いておき、今後何かアプリをつくるときに改めて調べてみる。
ここまででやったこと
vagrant上の環境でhot reloadが効かない
解決策
解決できるっぽいが、しばらくは手動でちくちくやる。
ここまでやったこと
- 画面遷移はないので
router
は後回しにした- なので
index.html
App.vue
main.js
の3つだけの最小構成で作る(と思う)
- なので
.vue
のtemplate
要素の子にdiv
を二つ作ったら「ルートは一つにしろ」って怒られたform
でボタンとかインプットとかチェックボックスとか置いた- 最終形の確認のためタスクのリストは一旦手で複数書いてる
ボタンをクリックするとリロードされる
追加・削除のボタンの関数を実装してv-on
でそれぞれの関数が実行されるようにしたところ、ボタンをクリックしたらページがリロードされてURLの末尾に?
が付くみたいな感じになった。困った。
イベントを購読していない普通のbutton
でも同様の動きをするので、GETメソッドになってクエリパラメータの?
がついてしまうみたいなHTMLの動きだと思うたぶん。ひとまずevent.preventDefault()
でデフォルトの挙動を無視するようにした。対応がこれで合っているのか分からないがリロードされなくなったので良しとした。
ここまでやったこと
- リストレンダリング
v-for
- タスクをループで記述
- 双方向バインディング
v-model
- Vueコンポーネントに
newTodo
を定義してテンプレート側のinput
要素のv-model
属性に指定すると、入力がnewTodo
に反映される
- Vueコンポーネントに
- イベントを購読
v-on:click
してTodoリストを更新するように実装@click
のような省略も可能らしい
v-if
v-else
属性でフラグによって表示する要素を変更するv-bind:class
でフラグによってclass
を書き換える
GitHub pagesで公開されない
npm run build
の出力結果が違うので何とかする。ちょっとこれから用事があるので今日はここまで。