はかせだけど博士じゃない

無職が就活しないでプログラミングとかする

Vue.jsでピアノロールエディタ

お絵かきアプリは考えることが多くてなかなか手を付けられないみたいな感じになったので、趣味でよく触るピアノロールエディタを作ってみようかなと思って作り始めました。

現状

まだ大枠を用意しただけ。

github.com

f:id:s-hakase:20180411060124p:plain

追記

f:id:s-hakase:20180411083507p:plain

TODO

  • 行の高さをそろえる
    • 黒鍵の有無で白鍵の高さを変えてあげる必要がある
    • これを参考に
  • ピアノロールエディタ部分のグリッド表示
  • 前職で使っていたD3.jsも入れようかと思ったけど別にいらん気もする

Vue.jsのチュートリアルのコードを読む2

続きをやっていきます。

ちなみに見出しに付けた「#n」はGitHubからクローンしてきたプロジェクトのディレクトリの番号で、動画の番号と途中から少しずれています。

#7

routerを使うっぽい。冒頭でURL末尾にページ内リンクに使う#から始まる文字をつけるあれを利用してSPAのルーティングを行うんだぜみたいな感じのことを言っているような気がする(英語全然だめ)。

特に説明はなかったけどvue-clirouterをインストールした場合に生成されるプロジェクトで始めているっぽい。

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ファイルはtemplatescriptタグで構成。それぞれの.vueファイルができたらrouter/index.js側でインポートする。

import Account from '@/components/Account'
import Contact from '@/components/Contact'
import Friends from '@/components/Friends'

するとlocalhost:8000/#/contactなどにアクセスできるようになる。

App.vueFriendsへのリンクを追加する。ほかのコンポーネントも同様。

<router-link to="friends">Friends</router-link>

router/index.jsfriendpathを少し変更。

    {
      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.vuescriptに以下を追加。

  props: [
    'id',
    'age',
    'weight',
  ]

router/index.jsfriendprops: 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のオブジェクトとそれをエクスポートする文。オブジェクトはdatamethodを定義。

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インスタンス生成時に渡すオブジェクトにcomputedfiltersというプロパティを定義していて、どちらも関数を定義している。

  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を記述する。文字の色をそれぞれredpinkと記述して確認してみる。

<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-clivue init webpack-simpleではなくvue init webpackとして生成したところ、buildコマンドでindex.htmlを含むものが出力された。index.htmlを出力に含めるにはwebpackのconfigをいじる必要がありそうだ。うーん、ここからはVueじゃなくてwebpackの学習になりそうなので、おとなしくプロジェクト生成からやり直すことにする。GitHubリポジトリどうしようかな。

ホストOSからアクセスできない件

前回の記事でホストOSからアクセスするためにdevServerhostportの設定を書いた。今回生成しなおしたプロジェクトではそれらの設定としてprocess.env.HOSTprocess.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)

qiita.com

上の記事によると今使ってる2.0.0の問題とのことなのでVagrantを2.0.2にアップデートする。アップデートは公式からWindows版を落としてきてインストール済みのフォルダにインストールするだけ。

vagrant --version
Vagrant 2.0.2

アップデートしたらそのままvagrant upして起動を確認。やったぜ。

とかいろいろ弄っていたらSSH接続できなくなった。

vagrant ssh
Permission denied (publickey).

絶望感がやばい。調べた結果、homeディレクトリ以下のパーミッションをまるっと変更したのがまずかったようだ。反省。

元に戻そうにもubuntuに入れないしどうしようかなと思っていたところ

qiita.com

こちらの記事のコメントで、GUIを有効にしてコンソールログインできるという情報を得た。Vagrantfileを弄る。

  config.vm.provider "virtualbox" do |vb|
  #   # Display the VirtualBox GUI when booting the machine
    vb.gui = true

f:id:s-hakase:20180314173223p:plain

おおー、vagrant/vagrantで入れた。

そしたら~/.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を選んでみました。決め手は見た目のキムワイプ感です。

ググって出てきた以下の記事をそのままなぞるだけのつもりなので、気になったところだけ脳内を出力していきます。

qiita.com

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でアクセスできた。やっと進められる。

f:id:s-hakase:20180313163904p:plain

ところでvue-cliでのプロジェクト生成時、webpack-simpleではなくwebpackを指定するとテスティングフレームワークとかLinterとか入ってて便利とのこと。今回はVue.jsの学習のためいったん置いておき、今後何かアプリをつくるときに改めて調べてみる。

ここまででやったこと

  • N予備校の講座でvagrantで作った環境をそのまま使うことにした
  • vue-cliをインストール
  • vue init webpack-simple project-nameでプロジェクトを生成

vagrant上の環境でhot reloadが効かない

解決策

qiita.com

解決できるっぽいが、しばらくは手動でちくちくやる。

ここまでやったこと

  • 画面遷移はないのでrouterは後回しにした
    • なのでindex.html App.vue main.jsの3つだけの最小構成で作る(と思う)
  • .vuetemplate要素の子にdivを二つ作ったら「ルートは一つにしろ」って怒られた
  • formでボタンとかインプットとかチェックボックスとか置いた
    • 最終形の確認のためタスクのリストは一旦手で複数書いてる

ボタンをクリックするとリロードされる

追加・削除のボタンの関数を実装してv-onでそれぞれの関数が実行されるようにしたところ、ボタンをクリックしたらページがリロードされてURLの末尾に?が付くみたいな感じになった。困った。

イベントを購読していない普通のbuttonでも同様の動きをするので、GETメソッドになってクエリパラメータの?がついてしまうみたいなHTMLの動きだと思うたぶん。ひとまずevent.preventDefault()でデフォルトの挙動を無視するようにした。対応がこれで合っているのか分からないがリロードされなくなったので良しとした。

ここまでやったこと

  • リストレンダリングv-for
    • タスクをループで記述
  • 双方向バインディングv-model
    • VueコンポーネントnewTodoを定義してテンプレート側のinput要素のv-model属性に指定すると、入力がnewTodoに反映される
  • イベントを購読v-on:clickしてTodoリストを更新するように実装
    • @clickのような省略も可能らしい
  • v-if v-else属性でフラグによって表示する要素を変更する
  • v-bind:classでフラグによってclassを書き換える

GitHub pagesで公開されない

npm run buildの出力結果が違うので何とかする。ちょっとこれから用事があるので今日はここまで。

Canvasのお絵かきを録画する

トレース練習アプリというかお絵かきアプリのネタになるかなというメモ。

hfuji.hatenablog.jp

MediaRecorder APICanvas の captureStream() を使えば、ブラウザの Canvas 上でお絵描きしている様子を録画できる。

らしい。

ただしTwitterに投稿しようとするとWebMをサーバに送信してgifか何かに変換して投稿する必要がありそう。動画の長さによっては定期的に送信・変換・結合みたいな動きをしなければならない。