20200721のvue.jsに関する記事は10件です。

Angular, knockoutjs, React, Vue.js の使用感とメモリ使用量と速度の記録(2020年7月)

いわゆるUIライブラリ(Angular, knockoutjs, React, Vue.js)の使用感とメモリ使用量と速度の記録(2020年7月)

プログラマのみなさまこんにちは?。ハーツテクノロジーの山崎です。この記事は仕事の中で得たられた知見によって書かれています。

で、いきなり冒頭から脱線してお詫びから入りますが、「UIライブラリ」という言葉はそのものズバリではないですよね。すみません。
「UIライブラリ」と書くと Bootstrap のようなCSS系ライブラリがメインのような気がしますし、「JSライブラリ」と書くと、広すぎて、UIとは無関係のライブラリが含まれてしまいます。「フレームワーク」も同様に広すぎるし、なんというか、データバインド系のJSライブラリの総称があるとよいのですが、いまのところ見つけられないので「UIライブラリ」を使わせていただいています。ピッタリな言葉がありましたらコメントください。よろしくお願いいたします。?‍♂️

0. 背景「UIライブラリをどういう基準で選んだらよいのだろうか??」

フロントエンドUIライブラリってたくさんあって、どれを、どういう基準で選んだらよいのだろうか? いろいろな意見があるけど「実際の数値はどうなんだろう?」と気になった。うわさや感覚だけではなく、実際の数値を見て判断材料にしたいと思った。

今回調べたUIライブラリとそのバージョン

方針としては、同じ課題を4種類のUIライブラリで実装してみて、以下のポイントを比べてみることとした。

・コード量、書きやすさ、可読性。
・Chrome で起動したときの使用メモリ(KB)と速度(fps) 。

Chrome 以外のブラウザでの調査は保留。コードはすべてまるっと CodePen に置いたので、気になるひとは各自の環境で実行してみてくださいませ。

で、なぜこの4つを選んだのか?というと、特に理由はなくて、なんとなく有名どころを順に選んでみました。

Riot.js が入っていないのは「その昔、Riot.js で SVG を表示しようとしてひどい目にあった記憶が蘇るから」という個人的な理由だったりします。いまは改善されているのでしょうか?(詳しい方のフォローのコメントをお待ちします?)

1. ミッション→各UIライブラリで table と svg をたくさん表示してみる?

なるべく、コンパクトでわかりやすく、処理性能がわかるような課題を選んだ。
先に、動いているところを見てもらったほうが理解が早いと思うので、アニgifを貼ります。こんな感じ?
2020-07-22.gif

ざっと説明すると、「矩形領域の中を跳ねまわる円を SVG で描いて、その座標を table に書く。それを setInterval() を使って 60fps でリアルタイムに動かす!おまけに object の数もリルタイムに変えられるようにする」というもの。なので、以下のコードがおおむね共通になります。

const AREA_W = 333
const AREA_H = 111

class Point {
  constructor( _no ) {
    this.no = _no
    this.color_h = _no * 53 % 360
    this.x = Math.random() * AREA_W
    this.y = Math.random() * AREA_H
    this.movespeed_x = (Math.random() - 0.5)
    this.movespeed_y = (Math.random() - 0.5)
  }
  move() {
    this.x += this.movespeed_x
    this.y += this.movespeed_y
    if ( this.x < 0      ) { this.x = 0; this.movespeed_x *= -1 }
    if ( this.y < 0      ) { this.y = 0; this.movespeed_y *= -1 }
    if ( this.x > AREA_W ) { this.x = AREA_W; this.movespeed_x *= -1 }
    if ( this.y > AREA_H ) { this.y = AREA_H; this.movespeed_y *= -1 }
  }
}

const vm = {
    p_list:   [],
    p_size:   0,
    max_size: 200,
}

setInterval( ()=>
{
    if ( vm.p_size != vm.max_size ) { // 再構築
        vm.p_list = []
        for ( let i=0 ; i<vm.max_size ; ++i )
            vm.p_list.push( new Point( i+1 ) )
        vm.p_size = vm.max_size
    }

    for ( let p of vm.p_list ) p.move()  // 移動

}, 1000/60 ) // 60fps

Qiita を読んでいるひとには解説は不要なくらいシンプルに書いたつもり。

  1. class Point に座標(x,y), 色(color_h), 移動速度(movespeed_x, movespeed_y)を持って。move() メソッドで座標を移動し、範囲(AREA_W, AREA_H)からはみ出さないように位置と速度を調整しています。

  2. max_size が object 数の入力値で、 p_list に Point object を入れています。

  3. 最後に、setInterval() を使って、60fps で p_list 内のすべての object を移動します。

常に変化するデータ p_list を、どのくらいリアルタイムに表示できるか? また、どのくらいのコード量で記述&表現できるのか?を調べるのがこのエントリのミッションとなります。

この共通コードはおおむね 40行なので、表示のために増えたコード量が UIライブラリ特有の記述量と考えてよいはず。

(念の為に、以下に実行環境の情報を貼って置きます。基本、スルーで。)

使用した Chrome のバージョン

image.png

使用した PC のスペック

image.png

image.png

ちなみに、ディスプレイのリフレッシュレートは 60Hz です。計測した日は 2020-07-21 です。

2. コード量と書きやすさ

ここでのポイントは、なるべく1行の意味が同じになるように(UIライブラリごとの不公平感が少なくなるように)注意しながら書いたところ。
また、読みやすさ、理解しやすさにも注意をはらいました。

ブラウザを2つ左右に並べてこのエントリを表示&比較することで、それぞれのUIライブラリの書き方の特徴(どのあたりが同じで、どのあたりが異なるのか)がわかると思います。

先に、全体のコードを書いた感想を書かせていただくと、4つのUIライブラリで書き方に大きな違いは感じませんでした。ですので、書き方で比較するとなるとどうしても重箱の隅を突く傾向が避けられない感じです。

Angular 1.8.0 のコード(73行)

コードを書いてみた感想は、$scope, $interval という引数 object に追記していく書き方が特徴的でした。
あと、グローバルな変数は参照できないようで $scope.AREA_W = AREA_W のように、ローカルな位置に転送する必要が見られた。

Angularは、svg が苦手」という話をどこかで聞いたような気もするけど、特に、気にせず書きたいように書いて動いてしまいました。(昔の話だったのかな?)

全体的に難しいこともなく、悩むこともなく、以外に普通だった。

<a href="https://angularjs.org/" >AngularJS 1.8.0</a> SVG example

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.0/angular.min.js"></script>
<div ng-app="app" ng-controller="AngularSVG">
    <div style="width: 333px; height: 111px; overflow: auto;" >
        <table style="width: 300px; border: solid 1px;" >
            <thead>
                <tr><th>No</th><th>Color</th><th>X</th><th>Y</th></tr>
            </thead>
            <tbody>
                <tr ng-repeat="p in p_list">
                    <td style="text-align: center;">{{ p.no }}</td>
                    <td style="text-align: right;">{{ p.color_h }}</td>
                    <td style="text-align: right;">{{ p.x.toFixed(2) }}</td>
                    <td style="text-align: right;">{{ p.y.toFixed(2) }}</td>
                </tr>
            </tbody>
        </table>
    </div>
    <hr/>
    <svg ng-attr-width="{{AREA_W + 5 * 2}}" ng-attr-height="{{AREA_H + 5 * 2}}" stroke="#111" fill="#ddd" >
        <rect x="0" y="0" ng-attr-width="{{AREA_W + 5 * 2}}" ng-attr-height="{{AREA_H + 5 * 2}}" ></rect>
        <circle ng-repeat="p in p_list" ng-attr-cx="{{p.x + 5}}" ng-attr-cy="{{p.y + 5}}" r="5px" ng-attr-fill="{{'hsl('+ p.color_h +', 75%, 75%)'}}" ></circle>
    </svg>
    <div> max_size: <input type="number" ng-model="max_size" /> {{ max_size }}</div>
</div>
<script>

const AREA_W = 333
const AREA_H = 111

class Point {
  constructor( _no ) {
    this.no = _no
    this.color_h = _no * 53 % 360
    this.x = Math.random() * AREA_W
    this.y = Math.random() * AREA_H
    this.movespeed_x = (Math.random() - 0.5)
    this.movespeed_y = (Math.random() - 0.5)
  }
  move() {
    this.x += this.movespeed_x
    this.y += this.movespeed_y
    if ( this.x < 0      ) { this.x = 0; this.movespeed_x *= -1 }
    if ( this.y < 0      ) { this.y = 0; this.movespeed_y *= -1 }
    if ( this.x > AREA_W ) { this.x = AREA_W; this.movespeed_x *= -1 }
    if ( this.y > AREA_H ) { this.y = AREA_H; this.movespeed_y *= -1 }
  }
}

angular.module('app', [])
.controller('AngularSVG', function($scope, $interval) {

    $interval( ()=>
    {
        if ( $scope.p_size != $scope.max_size ) { // 再構築
          $scope.p_list = []
          for ( let i=0 ; i<$scope.max_size ; ++i )
              $scope.p_list.push( new Point( i+1 ) )
          $scope.p_size = $scope.max_size
        }

        for ( let p of $scope.p_list ) p.move()  // 移動
    }, 1000/60 ) // 60fps

    $scope.p_list   = []
    $scope.p_size   = 0
    $scope.max_size = 200

    $scope.AREA_W = AREA_W
    $scope.AREA_H = AREA_H
})
</script>

knockoutjs 3.5.1 のコード(82行)

コードのコメントにも書いたけど、配列 p_list の更新を knockoutjs に知らせる方法がわからなかった。setInterval() で vm.p_list は更新されているのだけど表示が変わらないという。仕方が無いので、removeAll() でいったん削除して、vm.p_list を作り直しています。速度が出ないのはこの書き方も理由だと思っています。(詳しいひとがおられましたらコメントいただけるとたすかります。?)

書き方が、data-bind="hoge: fuga" という記述で統一されているので、そこさえ理解してしまえば、難しいところは無い感じ、かな。

<a href="https://knockoutjs.com/" >Knockout.js 3.5.1</a> SVG example

<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"></script>
<div style="width: 333px; height: 111px; overflow: auto;" >
    <table style="width: 300px; border: solid 1px;" >
        <thead>
            <tr><th>No</th><th>Color</th><th>X</th><th>Y</th></tr>
        </thead>
        <tbody data-bind="foreach: p_list">
            <tr>
                <td data-bind="text: no" style="text-align: right;"></td>
                <td data-bind="text: color_h" style="text-align: right;"></td>
                <td data-bind="text: x.toFixed(2)" style="text-align: right;"></td>
                <td data-bind="text: y.toFixed(2)" style="text-align: right;"></td>
            </tr>
        </tbody>
    </table>
</div>
<hr/>
<svg data-bind="attr: { width: AREA_W + 5 * 2, height: AREA_H + 5 * 2 }" stroke="#111" fill="#ddd" >
    <rect x="0" y="0" data-bind="attr: { width: AREA_W + 5 * 2, height: AREA_H + 5 * 2 }" ></rect>
    <g data-bind="foreach: p_list" >
        <circle data-bind="attr: { cx: x + 5, cy: y + 5, fill: 'hsl('+color_h+', 75%, 75%)' }" r="5px" ></circle>
    </g>
</svg>
<div> max_size: <input type="number" data-bind="value: max_size" /><span data-bind="text: max_size"></span</div>

<script>

const AREA_W = 333
const AREA_H = 111

class Point {
  constructor( _no ) {
    this.no = _no
    this.color_h = _no * 53 % 360
    this.x = Math.random() * AREA_W
    this.y = Math.random() * AREA_H
    this.movespeed_x = (Math.random() - 0.5)
    this.movespeed_y = (Math.random() - 0.5)
  }
  move() {
    this.x += this.movespeed_x
    this.y += this.movespeed_y
    if ( this.x < 0      ) { this.x = 0; this.movespeed_x *= -1 }
    if ( this.y < 0      ) { this.y = 0; this.movespeed_y *= -1 }
    if ( this.x > AREA_W ) { this.x = AREA_W; this.movespeed_x *= -1 }
    if ( this.y > AREA_H ) { this.y = AREA_H; this.movespeed_y *= -1 }
  }
}

const vm = {
    p_list:   ko.observableArray([]),
    p_size:   0,
    max_size: ko.observable( 200 ),
}

setInterval( ()=>{

    if ( vm.p_size != vm.max_size() ) { // 再構築
        vm.p_list.removeAll()
        for ( let i=0 ; i<vm.max_size() ; ++i )
            vm.p_list().push( new Point( i+1 ) )
        vm.p_size = vm.max_size()
    }

    // 移動
    let nw = []
    for ( let p of vm.p_list() )
    {
        p.move()
        nw.push( p )
    }

    vm.p_list.removeAll()   // KO.js はいったん空にしないと、Object の中が変わっていても再描画対象にはしない模様。なので、全部入れ替えているのだけど、、、もっとよい方法がありましたら教えて下さい。
    for ( let p of nw )
        vm.p_list.push( p )

}, 1000/60 ) // 60fps

ko.applyBindings( vm )
</script>

React 16.13.1 のコード(97行)

JSX を通すのに、babel.min.js を使いました。JSX を使わないと、コードが長くなって、他のライブラリとの比較が困難になるからです。

さて、現在、一番人気と噂される React ですが、4つの中で、一番コードが長くなったのがこの React です。

React は class を継承するのがポイントでしょうか。他の書き方もあるかもしれませんが、短時間でそこまで調べることはできませんでした。必ず、state を更新するのもお約束なのでしょうか?こちらも調べている時間がありませんでした。いろいろ作法があるようで、短期間にちゃちゃっと学習して動かすのはちょっとハードル高めに感じました。

4つのUIライブラリ中で、記述に一番苦労した(悩んだ)のが React でした。みなさん、本当に React が使いやすい(書きやすい)と思って選んでいるのでしょうか? ちょっと疑問に思えてきました。

<a href="https://reactjs.org/docs/add-react-to-a-website.html" >React 16.13.1</a> SVG example 

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js" ></script><!-- https://cdnjs.com/libraries/babel-standalone -->
<div id='ReactSVG-example'></div>
<script type="text/babel" >

const AREA_W = 333
const AREA_H = 111

class Point {
  constructor( _no ) {
    this.no = _no
    this.color_h = _no * 53 % 360
    this.x = Math.random() * AREA_W
    this.y = Math.random() * AREA_H
    this.movespeed_x = (Math.random() - 0.5)
    this.movespeed_y = (Math.random() - 0.5)
  }
  move() {
    this.x += this.movespeed_x
    this.y += this.movespeed_y
    if ( this.x < 0      ) { this.x = 0; this.movespeed_x *= -1 }
    if ( this.y < 0      ) { this.y = 0; this.movespeed_y *= -1 }
    if ( this.x > AREA_W ) { this.x = AREA_W; this.movespeed_x *= -1 }
    if ( this.y > AREA_H ) { this.y = AREA_H; this.movespeed_y *= -1 }
  }
}

class ReactSVG extends React.Component {
  constructor(props) {
      super(props)

      this.state = {
          p_list:   [],
          p_size:   0,
          max_size: 200,
      }

      this.p_list_renewal = this.p_list_renewal.bind(this)   // つい忘れがち
  }

  p_list_renewal( ev ) {
      const mx = parseInt(ev.target.value)
      this.setState( st => { st.max_size = mx ; return st } )
  }

  componentDidMount() {
    setInterval(() =>
    {
        if ( this.state.p_size != this.state.max_size ) { // 再構築
          this.state.p_list = []
          for ( let i=0 ; i<this.state.max_size ; ++i )
              this.state.p_list.push( new Point( i+1 ) )
          this.state.p_size = this.state.max_size
        }

        for ( let p of this.state.p_list ) p.move() // 移動

        this.setState( st => st ) // KO.js と同じように、Object の中の値が変化しても再描画対象にはならない模様。
    }, 1000/60 )
  }

  render() {
    return (
      <div>
      <div style={{ width: "333px", height: "111px", overflow: "auto" }} >
        <table style={{ width: "300px", border: "solid 1px" }} >
          <thead>
            <tr><th>No</th><th>Color</th><th>X</th><th>Y</th></tr>
          </thead>
          <tbody>
            { this.state.p_list.map( (p,idx) => ( <tr key={ idx } >
              <td style={{textAlign: "center"}}>{ p.no }</td>
              <td style={{textAlign: "right"}}>{ p.color_h }</td>
              <td style={{textAlign: "right"}}>{ p.x.toFixed(2) }</td>
              <td style={{textAlign: "right"}}>{ p.y.toFixed(2) }</td>
            </tr> ) ) }
          </tbody>
        </table>
      </div>
      <hr/>
      <svg width={ AREA_W + 5 * 2 } height={ AREA_H + 5 * 2 } stroke="#111" fill="#ddd" >
          <rect x="0" y="0" width={ AREA_W + 5 * 2 } height={ AREA_H + 5 * 2 } ></rect>
          { this.state.p_list.map( (p,idx) => (
            <circle key={ idx } cx={ p.x + 5 } cy={ p.y + 5 } r="5px" fill={ 'hsl('+ p.color_h +', 75%, 75%)' } ></circle>
          ) ) }
      </svg>
      <div> max_size: <input type="number" value={ this.state.max_size } onChange={ this.p_list_renewal }/> { this.state.max_size }</div>
      </div>
    )
  }
}

ReactDOM.render( <ReactSVG />, document.querySelector( 'div#ReactSVG-example' ) )
</script>

Vue.js 2.6.11 のコード(75行)

4つのなかでは、比較的書きやすく、また読みやすくも感じました。ストレスが少ないです。

template: を使ったので、HTML tag を文字列として指定したためシンタックスハイライト機能が働かないので、目視チェックで頑張りました。

<template> タグを使えば回避できるかも?とちょっと思ったのだけど、動かす方を優先してしまいました。

<a href="https://jp.vuejs.org/v2/guide/">Vue.js 2.6.11</a> SVG example 

<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js'></script><!-- 2020-05-08 https://cdnjs.com/libraries/vue -->
<div id='Vue_SVG'></div>
<script>

const AREA_W = 333
const AREA_H = 111

class Point {
  constructor( _no ) {
    this.no = _no
    this.color_h = _no * 53 % 360
    this.x = Math.random() * AREA_W
    this.y = Math.random() * AREA_H
    this.movespeed_x = (Math.random() - 0.5)
    this.movespeed_y = (Math.random() - 0.5)
  }
  move() {
    this.x += this.movespeed_x
    this.y += this.movespeed_y
    if ( this.x < 0      ) { this.x = 0; this.movespeed_x *= -1 }
    if ( this.y < 0      ) { this.y = 0; this.movespeed_y *= -1 }
    if ( this.x > AREA_W ) { this.x = AREA_W; this.movespeed_x *= -1 }
    if ( this.y > AREA_H ) { this.y = AREA_H; this.movespeed_y *= -1 }
  }
}

var vm = new Vue( {
    el: 'div#Vue_SVG',

    template: `<div>
        <div style="width: 333px; height: 111px; overflow: auto;" >
            <table style="width: 300px; border: solid 1px;" >
                <thead>
                    <tr><th>No</th><th>Color</th><th>X</th><th>Y</th></tr>
                </thead>
                <tbody>
                    <tr v-for="p in p_list" >
                        <td style="text-align: center;">{{ p.no }}</td>
                        <td style="text-align: right;">{{ p.color_h }}</td>
                        <td style="text-align: right;">{{ p.x.toFixed(2) }}</td>
                        <td style="text-align: right;">{{ p.y.toFixed(2) }}</td>
                    </tr>
                </tbody>
            </table>
        </div>
        <hr/>
        <svg v-bind:width="AREA_W + 5 * 2" v-bind:height="AREA_H + 5 * 2" stroke="#111" fill="#ddd" >
            <rect x="0" y="0" v-bind:width="AREA_W + 5 * 2" v-bind:height="AREA_H + 5 * 2" ></rect>
            <circle v-for="p in p_list" v-bind:cx="p.x + 5" v-bind:cy="p.y + 5" r="5px" v-bind:fill="'hsl('+ p.color_h +', 75%, 75%)'" ></circle>
        </svg>
        <div> max_size: <input type="number" v-model:value="max_size" /> {{ max_size }}</div>
    </div>`,

    data: {
        p_list:   [],
        p_size:   0,
        max_size: 200,
    },
} )

setInterval( ()=>
{
    if ( vm.p_size != vm.max_size ) { // 再構築
        vm.p_list = []
        for ( let i=0 ; i<vm.max_size ; ++i )
            vm.p_list.push( new Point( i+1 ) )
        vm.p_size = vm.max_size
    }

    for ( let p of vm.p_list ) p.move()  // 移動

}, 1000/60 ) // 60fps
</script>

2-1, コード量のまとめ

コード量を行数でソートすると以下の順になりました。

・(1) AngularJS 1.8.0 のコード(73行)
・(2) Vue.js 2.6.11 のコード(75行)
・(3) KnockoutJS 3.5.1 のコード(82行)
・(4) React 16.13.1 のコード(97行)

image.png

Angular が意外にもすんなり書けていて、好感触?
knockoutjs はもうちょっと短く書けるかもしれません?‍♂️

全体としては、React のハードルが高めであることが再確認された感触です。

(コード量を「行数」のようなアバウトな定義で数値化することに抵抗を感じるひともおられると思いますが、そもそも課題を選ぶ時点で向き不向きもありコードへの影響もあります。コードの読みやすさ、書きやすさといった感覚もアバウトですし、この行数も、その程度のアバウトな数値であると理解していただきたいと思います。よろしくお願いいたします。)

2-2, データバインディング

ところで、4つのUIライブラリで書いてみて、大きな違いに気がついたのですが、データのバインディングのされ方に違いが見られました。データをどのように監視するか?といった思想的な仕様の部分です。

たとえば、Vue.js は setInterval() などで、外部からデータを非同期的に更新しても、なんの問題もなく表示が追従するのですが、
React は明示的に、setState() で更新したことを知らせる必要がありました。knockoutjsReact と同じように更新を明示しないとデータの内部までは監視していない動きでした(書き方に注意が必要なのかもしれない)。
AngularVue.js と同じように非同期の更新に追従して表示されますが、スコープの外からデータを更新できるのか?までは不明でした。

ですので、データバインディングという観点からは Vue.js が一番素直な動きだったので、書きやすかったです。

2-3, UIライブラリそのもののサイズ

念の為、JSファイルの byte 数も調べて記録しておきます。それぞれのUIライブラリの JSファイルをダウンロードして byte 数を調べました。

angular.min.js  172 KB (176,531バイト)

knockout-latest.min.js  65.6 KB (67,224 バイト)

react.production.min.js 12.1 KB (12,463 バイト)
react-dom.production.min.js 115 KB (118,656 バイト)
babel.min.js    772 KB (791,236 バイト)

vue.min.js  91.4 KB (93,670 バイト)

表にするとよくわかりますが、React がダントツです。babel が足かせになっていますね。

image.png

Vue.js が小さいと思っていましたが、実際に一番小さかったのは knockoutjs でした。

ファイルが小さい方がネットに負荷を与えず素早く起動できそうです。しかし、ネットが高速になった昨今ではあまり気にしなくてよいのかもしれません。

3. メモリ使用量

Chrome の DevTool を開いて、Memory タブの 「Allocation instrumentation on timeline」を使って、20秒間計測し、結果を「statistics」で表示し、キャプチャしました。

キャプチャには fps も表示されていますが、DevTool の Rendering タブの「FPS meter」をONにして表示しています。

上で書いた4つのコードをそれぞれ Chrome で実行し、表示する object の数を 1, 10, 100, 1000, 10000 と変えて、メモリ使用量を確認しました。

まずは、Object 1つから。

React だけ 3MB 超えていますが、babel を使っているので仕方がないかと。
他の3つは同じくらい。Vue.js は軽いイメージの通りで、React も重いイメージの通り。
knockoutjs は、まぁ、そのくらいかなぁという印象で。
Angular は予想に反して軽め、ちょっと予想外。

Object 10 個。

予想通り、object 1つのときとあまり変わらないですね。変わったら怖いですけど。

Object 100 個。

さすがに、object 100個になると使用するメモリも増え始めます。

Object 1,000 個。

object 1000 個は、なかなか厳しいのではないでしょうか。object も描ききれず、背景が隠れています。
Angular のメモリ使用量が増えてきました。ここで、Reactを抜いてトップです。

Object 10,000 個。?

  • Angular 1.8.0 -> 60037KB
    2020-07-21--AN-10000-60037KB.png

  • knockoutjs 3.5.1
    計測不能(残念)ってか、他の3つがすごすぎ。

  • React 16.13.1 -> 37354KB
    2020-07-21--Re-10000-37354KB.png

  • Vue.js 2.6.11 -> 42547KB
    2020-07-21--Vu-10000-42547KB.png

さて、無謀な object1万個ですが、なんとAngular, React, Vue.js は 1fps をキープして動いています。すごいですね。
メモリ使用量は、Angular が 60MB と、ダントツですが。
Vue.jsReactを抜いて2位の 42MB になりました。

3-1. メモリ使用量のまとめ

まとめると
image.png

軽いのは、最初はVue.jsで、途中はknockoutjs、最後にはReact になる波乱の展開でした。メモリ対決でknockoutjsが善戦してくるのは意外でしたが、1万個が計測できないのはわたしの書いたコードが悪いのだと思います。すみません。動いたらもしかするともしかするかも、、。

4. 速度(fps)

適度に負荷のかかっている、object 1000 個のときの、fps で比較しています。

image.png

ズバリ、もっとも早いのは、Angular!!これは意外(失礼)

なんでみんな使わないの??

React も早いけど、Vue.jsのほうが早いと思っていた。速度に関しては、予想と逆の結果が出ました。

knockoutjsは、ごめんなさい。たぶん、わたしの書き方がいけないんです。

5. まとめ

調べてみるといろいろ予想外なことがありました(エビデンス重要?)。

コードの書きやすさ、ファイルの軽さで選ぶなら、Vue.jsknockoutjs あたり。

処理速度で選ぶなら(どれも頑張っているが) Angular が早い。が、メモリは食う。

メモリ使用量で選ぶなら Vue.jsknockoutjs

で、「結局のところ、なにを使うのがいいの?」の答えは「どれもいいんじゃない?」でした。

意外にも数値にあまり差がみられなかった(みなさんがんばっているのですね、もっと差があると思っていました)ので、書きやすさ、読みやすさ、メンテのしやすさで選ぶとよいかと。

個人的には「Vue.jsすげー! 流石だね?」と言って締めたかったのだけど、逆に、「knockoutjsAngular もすごいじゃん、なんでみんな使わないの?」って思った。ってか、React をすすめるひとが多いように感じていたけど、どのあたりにメリットを感じてすすめているのかわからなくなった。盲目的に React 一択にするのはちょっと考えたほうがよさそう。

まとめると。UIライブラリは「いまのところ好みで選んで良し!数値から結論は出ない!」以上。?

最後に

最後まで読んでいただきましてありがとうございます。結論が出なくて申し訳ありません。?‍♂️

UIライブラリ界隈の進化は凄まじく、まさに日進月歩。来月にはあたらしいライブラリが台頭してくるかもしれません。
そんな状況のなか、2020年7月の瞬間風速を記録しておく価値を感じたので、このエントリを書かせていただきました。

おそらく、見落としや勘違いもあると思います。すべてはわたしの力不足が原因でございます。あたたかい眼差しでコメントをいただけますと幸いです。

みなさまの快適なプログラミングライフを願いつつ、失礼します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】【CRUD】Vue.js(Nuxt.js)とRailsでユーザー新規登録・ログイン・退会・ログアウト・編集を実装してみる

はじめに

Vue.js(Nuxt.js)とRailsで新規開発を行っており、JWTを用いてユーザー新規登録・ログイン・ログアウト等の認証周りを担当したのでここに記しておきます。備忘録。
今回はauthmoduleとdevise_token_authを使用して実装しました。
別々で解説している記事はあったのですが、全体の流れがわかる記事が少なかったので、執筆しました:frowning2::frowning2:
この記事が見知らぬ誰かの糧となれば良いなあと思っております〜〜:muscle_tone2:
authmodule公式
devise_token_auth公式

環境

  • MacOS
  • yarn 1.21.1
  • node 12.0.0
  • vue 2.6.1
  • rails 5.2.4
  • ruby 2.6.3

新規登録の流れ

Nuxt側: APIにEメールパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。

プラグインの導入

Axiosを導入

Nuxt側にaxiosを追加します。
axios公式

ターミナル
$ yarn add @nuxtjs/axios

Auth Moduleを導入

Nuxt側にAuth Moduleも追加します。

ターミナル
$ yarn add @nuxtjs/auth

Vuex用にindex.jsを作成

Auth moduleはVuexを使用して、ユーザの認証情報を管理します。
そこで、Vuex用のindex.jsファイルを用意しておく必要があります。
nuxtアプリのルートディレクトリにstoreというファイルが作成されるので、そこにindex.jsという名前のファイルを作成しておきます。index.jsの中はなにも記載しなくて大丈夫です。
スクリーンショット 2020-07-15 17.15.55.png

Nuxt

nuxt.config.jsにプラグインを記載

追加したaxiosとauthをnuxtアプリに読み込ませます。

nuxt.config.js
modules: [
  '@nuxtjs/axios',
  '@nuxtjs/auth'
]

nuxt.config.jsにauthオプションを記載

authで用意されているオプションを記載していきます。

nuxt.config.js
auth: {
  redirect: {
      login: '/users/login',
      logout: '/',
      callback: false,
      home: '/users/profile',
  },
  strategies: {
    local: {
      endpoints: {
        login: { url: '/api/v1/auth/login', method: 'post', propertyName: 'token' },
        logout: { url: '/api/v1/auth/logout', method: 'post' },
        user: false,
      },
    }
  }
}

redirect

ユーザーのアクションに応じたリダイレクト先のURL設定。

  • login:未ログイン時にリダイレクトされる先のURL
  • logout:ログアウトした後にリダイレクトされる先のURL
  • callback:コールバック用のURL。Oauth認証(SNS認証)等に使われる。
  • home:ログイン後にリダイレクトされる先のURL

strategies

Auth Moduleの認証ロジックの設定です。JWTとCookieを使うlocalと、OAuthを使うsocialの2種類の設定ができます。今回はOauth認証は使わないので、socialは記載していません。

  • endpoint:どのメソッドが呼ばれた際に、APIのどのエンドポイントに飛ばすかを設定します。例えば、loginメソッドが呼び出されたら、APIにpost: /api/auth/loginにHTTPリクエストを送信します。

ログイン

ログイン画面の実装です。
vuetifyというUIフレームワークを使用しています。template部分は適宜変えてください。

users/login
<template>
  <v-container>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">
          ログイン
        </h1>
      </v-card-title>
      <v-card-text>
        <v-form ref="form" lazy-validation>
          <v-text-field
            v-model="email"
            prepend-icon="mdi-email"
            label="メールアドレス"
          />
          <v-text-field
            v-model="password"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード"
          />
          <v-card-actions>
            <v-btn
              color="light-green darken-1"
              class="white--text"
              @click="loginWithAuthModule"
            >
              ログイン
            </v-btn>
          </v-card-actions>
        </v-form>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script>
export default {
  name: 'App',
  auth: false,
  data() {
    return {
      password: '',
      email: '',
    }
  },
  methods: {
    // loginメソッドの呼び出し
    async loginWithAuthModule() {
      await this.$auth
        .loginWith('local', {
         // emailとpasswordの情報を送信
          data: {
            email: this.email,
            password: this.password,
          },
        })
        .then(
          (response) => {
       // レスポンスで返ってきた、認証に必要な情報をlocalStorageに保存
            localStorage.setItem('access-token', response.headers['access-token'])
            localStorage.setItem('client', response.headers.client)
            localStorage.setItem('uid', response.headers.uid)
            localStorage.setItem('token-type', response.headers['token-type'])
            return response
          },
          (error) => {
            return error
          }
        )
    },
  },
}
</script>

スクリーンショット 2020-07-15 18.33.17(2).png
見た目はこんな感じですね。vuetifyすごい、、、

methodの部分をざっくり説明すると、

1.入力されたemailとpasswordの情報をloginメソッドを用いて、APIに送信する。
2.RailsAPI側からのレスポンスから認証機能に必要な情報(access-token・client・uid・token-type)をlocalStorageに保存する

ということをやっています。他にもやり方はありますし、仕様によっても異なるので、あくまで参考程度に、、、
ただ、APIに入力された情報を送信して、認証機能に必要な情報を保持するというのは基本的に同じです。
この仕組みをある程度理解しておくと、応用できると思います。

新規登録

新規登録画面の実装です。

users/signup
<template>
  <v-container>
    <v-card width="400px" class="mx-auto mt-5">
      <v-card-title>
        <h1 class="display-1">
          新規登録
        </h1>
      </v-card-title>
      <v-card-text>
        <v-form ref="form" lazy-validation>
          <v-text-field
            v-model="user.email"
            prepend-icon="mdi-email"
            label="メールアドレス"
          />
          <v-text-field
            v-model="user.password"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード"
          />
          <v-text-field
            v-model="user.password_confirmation"
            prepend-icon="mdi-lock"
            append-icon="mdi-eye-off"
            label="パスワード確認"
          />
          <v-card-actions>
            <v-btn
              color="light-green darken-1"
              class="white--text"
              @click="registerUser"
            >
              新規登録
            </v-btn>
          </v-card-actions>
        </v-form>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script>
export default {
  name: 'App',
  auth: false,
  data() {
    return {
      user: {
        password: '',
        email: '',
        password_confirmation: '',
      },
    }
  },
  methods: {
    registerUser() {
      this.$axios.post('/api/v1/auth', this.user).then((response) => {
        window.location.href = '/users/comfirmation'
      })
    },
  },
}
</script>

auth moduleには新規登録のヘルパーメソッドはないので、axiosを使ってメソッドを自作します。
といっても、registerUserが発火されたら新規登録のエンドポイントにメールアドレスやパスワードの必要情報を送信するだけです。
スクリーンショット 2020-07-20 18.11.30.png

見た目は各自適当に弄ってください!!

ログアウト・退会

users/account
<template>
  <v-app>
    <v-container>
      <v-row>
        <v-spacer></v-spacer>
        <v-col cols="12" lg="4">
          <v-row>
            <v-col
              cols="12"
              lg="7"
              class="grey--text text--darken-3 font-weight-bold pa-2 text-h6"
            >
              <p>
                アカウント設定
              </p>
            </v-col>
          </v-row>
          <v-row class="my-5">
            <v-col cols="12" lg="7" class="pa-2">
              <a
                href="/"
                class="grey--text text--darken-3 mb-1"
                @click="$auth.logout()"
              >
                ログアウト
              </a>
            </v-col>
            <v-col cols="12" lg="5" class="pa-2 text-right">
              <font-awesome-icon icon="angle-right" />
            </v-col>
          </v-row>
          <v-divider></v-divider>
          <v-row class="my-5">
            <v-col cols="12" lg="7" class="pa-2">
              <a
                href="#"
                class="red--text text--darken-3 mb-1"
                @click="deleteUser"
              >
                退会
              </a>
            </v-col>
            <v-col cols="12" lg="5" class="pa-2 text-right">
              <font-awesome-icon icon="angle-right" />
            </v-col>
          </v-row>
        </v-col>
        <v-spacer></v-spacer>
      </v-row>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data: () => ({}),
  methods: {
    deleteUser() {
      this.$axios
        .delete('api/v1/auth', {
          headers: {
            'access-token': localStorage.getItem('access-token'),
            uid: localStorage.getItem('uid'),
            client: localStorage.getItem('client'),
          },
        })
        .then((response) => {
          this.$auth.logout()
          window.location.href = '/'
        })
    },
  },
}
</script>

ログアウトはauth moduleにヘルパーメソッドがあるので利用しましょう。
クリックと同時に$auth.logout()を発火すれば、完了です。
header内に保存されていた認証情報がすべて削除され、未ログイン状態になります。

退会は新規登録と同様にメソッドを作ります。
axiosでエンドポイントに認証情報を付与して送信します。httpメソッドはdeleteですね。
その後、ログアウトメソッドを実行しとけばエラーにならず、安心です。
簡単ですね。

編集

users/edit
<template>
  <v-app>
    <v-container>
      <v-card width="400px" class="mx-auto mt-5">
        <v-card-title>
          <h1 class="display-1">
            メールアドレス変更
          </h1>
        </v-card-title>
        <v-card-text>
          <v-form ref="form" lazy-validation>
            <v-text-field
              v-model="user.email"
              prepend-icon="mdi-email"
              label="新しいメールアドレス"
            />
            <v-text-field
              v-model="user.password"
              prepend-icon="mdi-lock"
              append-icon="mdi-eye-off"
              label="パスワード"
            />
            <v-card-actions>
              <v-btn
                color="light-green darken-1"
                class="white--text"
                @click="editEmail"
              >
                保存する
              </v-btn>
            </v-card-actions>
          </v-form>
        </v-card-text>
      </v-card>
    </v-container>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      user: {
        password: '',
        email: '',
      },
    }
  },
  methods: {
    editEmail() {
      this.$axios
        .put('api/v1/auth', this.user, {
          headers: {
            'access-token': localStorage.getItem('access-token'),
            uid: localStorage.getItem('uid'),
            client: localStorage.getItem('client'),
          },
        })
        .then((response) => {
          localStorage.setItem('access-token', response.headers['access-token'])
          localStorage.setItem('client', response.headers.client)
          localStorage.setItem('uid', response.headers.uid)
          localStorage.setItem('token-type', response.headers['token-type'])
          window.location.href = '/'
        })
    },
  },
}
</script>

メールアドレスの編集ページです。見た目はほとんど新規登録・ログイン画面と同じです。
編集は認証情報を渡して、受け取らなければなりません。
リクエストで認証情報を付与して送信する。成功したら受け取った認証情報をlocalStrageに保存する。
理由はアカウント編集すると認証情報が変わるからです。
これで編集もできました。

auth module補足

一連の流れを見ていただいた方ならおわかりかと思いますが、auth moduleはそこまで機能がもりもりではありません。
すべてのメソッドを作ってくれているわけではなく、ある程度は自力で頑張る必要があります。
認証機能の補助輪くらいに考えておいてください!!
ただ、リダイレクト先の指定やloginメソッドなど優秀な機能もありますので、Nuxtで認証機能を実装する方は是非使ってみてください。
他に認証機能で良いプラグインがあれば教えて下さい:joy::joy:

devise_token_auth公式

Rails

フロント側(Nuxt側)ばかりやってきましたが、APIが飛ばないとお話になりませんので、バックエンド側(Rails側)もやっていきましょう!!

devise_token_authを導入

Gemfileにdevise_token_authとrack-corsを記載し、bundle installをします。

Gemfile
# ログイン機能 
gem 'devise' 
gem 'devise_token_auth'

# CORS設定
gem 'rack-cors'
ターミナル
$ rails g devise:install
$ rails g devise_token_auth:install User auth

これで色々ファイルが生成されると思います。
※すでにUserモデルがある方は設定方法が少し変わるので、対処法は公式を読んでください。

DB作成

db/migrate/~_devise_token_auth_create_users.rb
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.2]
  def change

    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, :default => false

      ## Rememberable
      t.datetime :remember_created_at

      # ここを追記 --------------------------------------------
      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip
      # -----------------------------------------------------

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      ## User Info
      t.string :name
      t.string :nickname
      t.string :image
      t.string :email

      ## Tokens
      t.text :tokens

      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,       unique: true
  end
end

migrateしてDBを作成します。

devise_token_authの設定

config/initializers/devise_token_auth.rb
DeviseTokenAuth.setup do |config|
  # リクエストごとにトークンを更新するか
  config.change_headers_on_each_request = false

  # トークンの有効期間
  config.token_lifespan = 2.weeks

  # headersの名前対応
  config.headers_names = {:'access-token' => 'access-token',
                          :'client' => 'client',
                          :'uid' => 'uid',
                          :'token-type' => 'token-type' }
end

ここで諸々の設定をします。
トークンの有効期限やheadersの送信名もここで設定します。

controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  skip_before_action :verify_authenticity_token, if: :devise_controller?
end

APIではCSRFチェックをしないようにapplication_controller.rbをすこしいじったら、
準備完了です。

ルーティング設定

routes.rb
Rails.application.routes.draw do
  devise_for :users

  namespace :api do
    scope :v1 do
      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end
end

公式通りだとこんな感じですね。

リクエストテスト

では実際にリクエストを投げてみましょう。
今回はログイン機能で試してみます。
※email:example@example.com,password:passwordというユーザーが登録されている想定です。
スクリーンショット 2020-07-21 10.04.08(2).png

ログイン機能のエンドポイントである/api/v1/auth/sign_inにpostでリクエストを送信します。
Content-Typeはapplication/jsonを選択しておきましょう。
BODYにemailとpasswordの情報をjson形式で付与します。

これでリクエストを投げます。

うまく行けばステータスコード200でレスポンスが返ってきます。
スクリーンショット 2020-07-21 10.31.12(2).png

こんな感じですね。

新規登録やログアウトも同様です。
エンドポイント、HTTPメッソド、付与情報を変更して、リクエストを送れば、それに応じたレスポンスが返ってくるはずです。

devise_token_auth補足

devise_token_authではdeviseと同じようにbefore_action :authenticate_user!current_userも使えます。
フロント側で保持しておくべき情報も基本的にはレスポンスで返してくれるので、それを取り出してlocalStrage等で保存しておきましょう。

詳しくは公式を読んでみてください。
devise_token_auth公式

完成!!!

これで完成です!!!長かったですね、、、
フロント側(Nuxt側)もバックエンド側(Rails側)もバッチリだと思います。
もう一度、新規登録を例にとって流れを振り返ります。

Nuxt側: APIにEメールパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。

これができているはずです。
ログインや退会等のその他の機能も流れは基本的に変わりません!!

最後に

auth moduleとdevise_token_auth別々の解説記事はあったのですが、まとまった記事(流れがわかるような記事)がなかったため、今回執筆しました。
長かった、、、かなり、、、
もしわからない点があれば気軽にコメントしてくださいね〜:ok_hand_tone2::ok_hand_tone2:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【vue】 あまり使わないと思うが、postscribeを使ってtemplateタグ内部にscriptタグを埋め込む

はじめに

あまり使うことはないかもしれないが、vueのtemplateタグ内部にscriptを埋め込んで、src内部を実行させて結果をもらうということをしたいときに、困ったときのメモ。

通常、vueのtemplateタグにscriptを埋め込むとエラーとなり実行をしてくれない。
そこで、何とかしてscriptを埋め込む方法&ライブラリはないかと探っていたら、「postscribe」なるものがあることがわかった。

結論

まずは、インストール

npm i --save postscribe

次に読み込みと実行

<template>
    <div id="main">
        <div id="data-script"></div>
    </div>
</template>
<script>
import postscribe from 'postscribe'

export default {
    methods: {
        dataInfo: function(){
            postscribe('#data-script', `<script src="http://hogehoge.com"><\/script>`, {
                done: function() {
                    console.info('Dblclick script has been delivered.');
                }
            })
        }
    }
}
</script>

参考

https://www.npmjs.com/package/postscribe

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】Nuxt文法編:$attrs

? この記事はWP専用です
https://wp.me/pc9NHC-li

前置き

属性の受け継ぎに関する
$attrsをまとめました!

inheritAttrsと
v-bind="$attrs"の2種類があります?

inheritAttrs とは

inherit
英語で「受け継ぐ」

親で与えられた属性を受け継ぐか
真偽値で決めることができます。

inheritAttrs: true

picture_pc_48118aa444ea32334dbe1db1befa80bc.gif

リンク先が '/' で
新しいタブで開かれていますね!

これだけ見ると普通のリンクなのですが
解説・コードを見ると
奇妙に感じるかもしれません?笑

【解説】

挙動を確認するために親子どちらでも
nuxt-linkタグのto属性でリンク先を指定しています?

to属性は子が優先されているのに
target属性は親のを引き継いでます。

もちろんtarget属性を子で指定すれば
子が優先されます!

が!

そもそも通常は親から子に何も使わず
何かを渡すことはできません。
属性に限らず、
テキストであれば
propsなりslotなり、何かを使います。
とっても変な感じがしますね?

ちなみに属性を渡す場合は、
この後に解説するv-bind="$attrs"を使います?

うっかり何も使わず親で属性を渡しちゃってた!
ということは防ぎたいです。。。

Link.vue
<template>
 <nuxt-link to="/">
   リンク!
 </nuxt-link>
</template>

<script lang="ts">
export default {
 inheritAttrs: true,
}
</script>
index.vue
<template>
<div class="page">
  <Link
    to="/sample"
    target="_blank"
  />
</div>
</template>

<script>
import Link from '~/components/Link.vue'

export default {
components: {
  Link,
},
}
</script>

inheritAttrs: false

attrs.gif

inheritAttrs: true
この奇妙な状態を解決するために
falseにしておくのが無難です?

今度は親の属性を引き継がず、
子の属性のみが反映されていますね!

【コード】

inheritAttrs: trueのコードをfalseに変えるだけ

v-bind="$attrs" とは

? 続きはWPでご覧ください?
https://wp.me/pc9NHC-li

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelとVue.jsの環境構築

プロジェクト作成

$ laravel new プロジェクト

Vueのインストール

laravel/uiパッケージをインストール

$ composer require laravel/ui

Vueのインストール

$ php artisan ui vue

JavaScriptのライブラリのインストールとコンパイル

$ npm install && npm run dev

Vue Routerのインストール

$ npm install --save-dev vue-router

Vue Routerプラグイン設定

resource/js/app.js
/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');
import Vue from 'vue';
import VueRouter from 'vue-router';
import router from './router'

window.Vue = require('vue');

/**
 * The following block of code may be used to automatically register your
 * Vue components. It will recursively scan this directory for the Vue
 * components and automatically register them with their "basename".
 *
 * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
 */

// const files = require.context('./', true, /\.vue$/i)
// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default))

Vue.component('example-component', require('./components/ExampleComponent.vue').default);

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

Vue.use(VueRouter);

const app = new Vue({
    el: '#app',
    router,
});

router.jsファイルの作成

router.jsを新規作成する

resource/js/router.js
import Router from 'vue-router'
import Home from './views/Home.vue'

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
  ]
});

vueファイルの作成

viewsフォルダ、Home.vueファイルの新規作成

resource/js/views/Home.vue
<template>
    <h1>Home Page</h1>
</template>

bladeファイルへのVue Routerの設定

routes/web.php
Route::get('/', function () {
    return view('welcome');
});
resources¥views¥welcome.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Example</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body>

    <div id="app">
        <div id="nav">
          <router-link to="/">Home</router-link>
        </div>
        <router-view/>
        </div>
    </div>

<script src="{{ mix('js/app.js') }}"></script> 
</body>
</html>

ブラウザで確認

 $ php artisan serve
 $ npm run watch
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js twitter 風 いいねボタンを押したときのアニメーション

Twitter風のいいねボタンを押したときのアニメーションを再現したい。

参考
https://codepen.io/robeen/pen/PbvJjy

上記の通りでコピペでできるんだが、デフォルトがオンという動作の方法がわからない。
コンポーネントにしたい。
使いまわしたい。

ということで自己流にした。

コピペで動く。

app.SCSS
.heart-svg {
  cursor:pointer; overflow:visible; width:60px;//ここのサイズを変更すればサイズが変わる

  #heart{
    fill:#AAB8C2;//ねずみいろ
    transform-origin:center;
    animation:animateHeartOut 0s linear forwards;
  }

  #main-circ{transform-origin:29.5px 29.5px;}
}


.heart-on {
  fill:#E2264D !important;
}


.checkheart{display:none;}

.checkheart:checked + label svg{
  #heart{transform:scale(.2); fill:#E2264D; animation:animateHeart .3s linear forwards .25s;}
  #heart2{transform:scale(.2); fill:#E2264D; animation:animateHeart .3s linear forwards .25s;}

  #main-circ{transition:all 2s; animation:animateCircle .3s linear forwards; opacity:1;}
  #grp1{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(0, -30px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(10px, -50px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp2{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(30px, -15px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(60px, -15px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp3{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(30px, 0px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(60px, 10px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp4{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(30px, 15px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(40px, 50px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp5{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(-10px, 20px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(-60px, 30px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp6{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(-30px, 0px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(-60px, -5px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }
  #grp7{
    opacity:1; transition:.1s all .3s;
    #oval1{
      transform:scale(0) translate(-30px, -15px);
      transform-origin:0 0 0;
      transition:.5s transform .3s;}
    #oval2{
      transform:scale(0) translate(-55px, -30px);
      transform-origin:0 0 0;
      transition:1.5s transform .3s;}
  }

  #grp2{opacity:1; transition:.1s opacity .3s;}
  #grp3{opacity:1; transition:.1s opacity .3s;}
  #grp4{opacity:1; transition:.1s opacity .3s;}
  #grp5{opacity:1; transition:.1s opacity .3s;}
  #grp6{opacity:1; transition:.1s opacity .3s;}
  #grp7{opacity:1; transition:.1s opacity .3s;}
}

@keyframes animateCircle{
  40%{transform:scale(10); opacity:1; fill:#DD4688;}
  55%{transform:scale(11); opacity:1; fill:#D46ABF;}
  65%{transform:scale(12); opacity:1; fill:#CC8EF5;}
  75%{transform:scale(13); opacity:1; fill:transparent; stroke:#CC8EF5; stroke-width:.5;}
  85%{transform:scale(17); opacity:1; fill:transparent; stroke:#CC8EF5; stroke-width:.2;}
  95%{transform:scale(18); opacity:1; fill:transparent; stroke:#CC8EF5; stroke-width:.1;}
  100%{transform:scale(19); opacity:1; fill:transparent; stroke:#CC8EF5; stroke-width:0;}
}

@keyframes animateHeart{
  0%{transform:scale(.2);}
  40%{transform:scale(1.2);}
  100%{transform:scale(1);}
}


Heartのコンポーネント

Heart.vue

<template>

    <div>
        <div>


            このハートのIDは {{id}} です。
            現在の選択状況は {{res}} です。

            <div v-if="selected">


                <svg @click="check" class="heart-svg" viewBox="467 392 58 57" xmlns="http://www.w3.org/2000/svg">
                    <g id="Group" fill="none" fill-rule="evenodd" transform="translate(467 392)">
                        <path class="heart-on" d="M29.144 20.773c-.063-.13-4.227-8.67-11.44-2.59C7.63 28.795 28.94 43.256 29.143 43.394c.204-.138 21.513-14.6 11.44-25.213-7.214-6.08-11.377 2.46-11.44 2.59z" id="heart" fill="#E2264D" />
                        <circle id="main-circ" fill="#E2264D" opacity="0" cx="29.5" cy="29.5" r="1.5"/>
                        <g id="grp7" opacity="0" transform="translate(7 6)">
                            <circle id="oval1" fill="#9CD8C3" cx="2" cy="6" r="2"/>
                            <circle id="oval2" fill="#8CE8C3" cx="5" cy="2" r="2"/>
                        </g>

                        <g id="grp6" opacity="0" transform="translate(0 28)">
                            <circle id="oval1" fill="#CC8EF5" cx="2" cy="7" r="2"/>
                            <circle id="oval2" fill="#91D2FA" cx="3" cy="2" r="2"/>
                        </g>
                        <g id="grp3" opacity="0" transform="translate(52 28)">
                            <circle id="oval2" fill="#9CD8C3" cx="2" cy="7" r="2"/>
                            <circle id="oval1" fill="#8CE8C3" cx="4" cy="2" r="2"/>
                        </g>

                        <g id="grp2" opacity="0" transform="translate(44 6)">
                            <circle id="oval2" fill="#CC8EF5" cx="5" cy="6" r="2"/>
                            <circle id="oval1" fill="#CC8EF5" cx="2" cy="2" r="2"/>
                        </g>

                        <g id="grp5" opacity="0" transform="translate(14 50)">
                            <circle id="oval1" fill="#91D2FA" cx="6" cy="5" r="2"/>
                            <circle id="oval2" fill="#91D2FA" cx="2" cy="2" r="2"/>
                        </g>

                        <g id="grp4" opacity="0" transform="translate(35 50)">
                            <circle id="oval1" fill="#F48EA7" cx="6" cy="5" r="2"/>
                            <circle id="oval2" fill="#F48EA7" cx="2" cy="2" r="2"/>
                        </g>

                        <g id="grp1" opacity="0" transform="translate(24)">
                            <circle id="oval1" fill="#9FC7FA" cx="2.5" cy="3" r="2"/>
                            <circle id="oval2" fill="#9FC7FA" cx="7.5" cy="2" r="2"/>
                        </g>
                    </g>
                </svg>

                                <div style="height: 8px;"></div>

            </div>

            <div v-else>

                <input type="checkbox" class="checkheart" :id="field" @click="check" v-model="msg"/>
                <label :for="field">
                    <svg class="heart-svg" viewBox="467 392 58 57" xmlns="http://www.w3.org/2000/svg">
                        <g id="Group" fill="none" fill-rule="evenodd" transform="translate(467 392)">

                            <path d="M29.144 20.773c-.063-.13-4.227-8.67-11.44-2.59C7.63 28.795 28.94 43.256 29.143 43.394c.204-.138 21.513-14.6 11.44-25.213-7.214-6.08-11.377 2.46-11.44 2.59z" id="heart" fill="#AAB8C2" />

                            <circle id="main-circ" fill="#E2264D" opacity="0" cx="29.5" cy="29.5" r="1.5"/>

                            <g id="grp7" opacity="0" transform="translate(7 6)">
                                <circle id="oval1" fill="#9CD8C3" cx="2" cy="6" r="2"/>
                                <circle id="oval2" fill="#8CE8C3" cx="5" cy="2" r="2"/>
                            </g>

                            <g id="grp6" opacity="0" transform="translate(0 28)">
                                <circle id="oval1" fill="#CC8EF5" cx="2" cy="7" r="2"/>
                                <circle id="oval2" fill="#91D2FA" cx="3" cy="2" r="2"/>
                            </g>

                            <g id="grp3" opacity="0" transform="translate(52 28)">
                                <circle id="oval2" fill="#9CD8C3" cx="2" cy="7" r="2"/>
                                <circle id="oval1" fill="#8CE8C3" cx="4" cy="2" r="2"/>
                            </g>

                            <g id="grp2" opacity="0" transform="translate(44 6)">
                                <circle id="oval2" fill="#CC8EF5" cx="5" cy="6" r="2"/>
                                <circle id="oval1" fill="#CC8EF5" cx="2" cy="2" r="2"/>
                            </g>

                            <g id="grp5" opacity="0" transform="translate(14 50)">
                                <circle id="oval1" fill="#91D2FA" cx="6" cy="5" r="2"/>
                                <circle id="oval2" fill="#91D2FA" cx="2" cy="2" r="2"/>
                            </g>

                            <g id="grp4" opacity="0" transform="translate(35 50)">
                                <circle id="oval1" fill="#F48EA7" cx="6" cy="5" r="2"/>
                                <circle id="oval2" fill="#F48EA7" cx="2" cy="2" r="2"/>
                            </g>

                            <g id="grp1" opacity="0" transform="translate(24)">
                                <circle id="oval1" fill="#9FC7FA" cx="2.5" cy="3" r="2"/>
                                <circle id="oval2" fill="#9FC7FA" cx="7.5" cy="2" r="2"/>
                            </g>
                        </g>
                    </svg>
                </label>
            </div>

        </div>
    </div>

</template>

<script>
    export default {
        props: ['id','firstSelect'],
        data() {
            return {
                res:'',
                field:'',
                msg:false,
            }
        },

        created(){

            this.selected = this.firstSelect;
            this.res = this.firstSelect;

            this.field = 'heart' + this.id;

        },

        methods: {
            check(){


                if(this.selected){
                    this.res = false;
                } else {
                    if(this.msg){
                        this.res = false;
                    } else {
                        this.res = true;
                    }
                }
                this.selected = false;
                this.$emit('heartSelect',this.id,this.res);

            }
        }
    }
</script>



あとは、使うだけ。
非同期読み込みだと、後で値が渡らないので、同期で読み出す。

Top.vue
    import heart from './Heart'
    export default {
        components: {
            heart
        },

        data () {
            return {
//子要素にわたす
                id:'777',


        <heart :id="id" :firstSelect="true" @heartSelect="heartSelect"></heart>
        <heart :id="id" :firstSelect="false" @heartSelect="heartSelect"></heart>



        methods: {


            heartSelect(id,bool){


                console.log(id);//どのIDが
                console.log(bool);//true or false?


            },

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js ディレクティブの省略記号

Vue.jsのディレクティブについて

省略記号は2つのみ(公式より)
https://jp.vuejs.org/v2/guide/syntax.html

v-bind と v-onの2つ

結論

v-bind:は「:」

v-on:〇〇は「@〇〇」

に省略可能

以下は公式より

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

 v-bind 省略記法
<!-- 完全な構文 -->
<a v-bind:href="url"> ... </a>

<!-- 省略記法 -->
<a :href="url"> ... </a>

<!-- 動的引数の省略記法 (2.6.0 以降) -->
<a :[key]="url"> ... </a>

v-on 省略記法
<!-- 完全な構文 -->
<a v-on:click="doSomething"> ... </a>

<!-- 省略記法 -->
<a @click="doSomething"> ... </a>

<!-- 動的引数の省略記法 (2.6.0 以降) -->
<a @[event]="doSomething"> ... </a>

</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue(Nuxt.js) で親子コンポーネント間でdataを受け渡す方法

Vue(Nuxt.js) で親子コンポーネント間でdataを受け渡す方法です。

親コンポーネントから子コンポーネントにdataを渡す方法

↓親コンポーネント↓

Parent.vue
<template>
  <div class="parent">
    <Childe :user="user" />
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Childe from '~/components/Childe.vue'

export default Vue.extend({
  components: {
    Childe
  },
  data() {
    return {
      user: {
        name: 'Taro',
        age: 20,
        comment: 'Hello'
      }
    }
  }
})
</script>

↓子コンポーネント↓

Childe.vue
<template>
  <div class="child">
    {{ user.name }} <!--Taro-->
    {{ user.age }}  <!--20-->
    {{ user.comment }}  <!--Hello-->
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  props: ['user']
})
</script>

親コンポーネントは<Childe :user="user" />で子コンポーネントにdataを受け渡しています。
子コンポーネントは props: ['user'] で受け取ります。
{{ user.name }}{{ user.age }}{{ user.comment }}で受け取ったdataを表示可能です。
複数のdataを受け取る場合はprops: ['user', 'user2', 'user3'] のように配列に追加することできます。

propsは型を定義して受け取ることも可能

Childe.vue
<script lang="ts">
export default Vue.extend({
  props: {
    user: {
      type: Object,
      required: true,
      default: {}
    }
  }
})
</script>

上記の方が明示的です。

子コンポーネントから親コンポーネントにdataを渡す方法

Childe.vue
<template>
  <div class="child"></div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      user: {
        name: 'Taro',
        age: 20,
        comment: 'Hello'
      }
    }
  },
  mounted() {
    this.$emit('childe-data', this.user)
  }
})
</script>

Parent.vue
<template>
  <div class="parent">
    <Childe @childe-data="childeData($event, user)" />
    {{ user.name }}
    {{ user.age }}
    {{ user.comment }}
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Childe from '~/components/Childe.vue'

export default Vue.extend({
  components: {
    Childe
  },
  data() {
    return {
      user: {}
    }
  },
  methods: {
    childeData(user: {}) {
      this.user = user
    }
  }
})
</script>

子コンポーネントはthis.$emit('childe-data', this.user) を用いて、親コンポーネントにイベントとdataの値を引数として渡しています。

親コンポーネント<Childe @childe-data="childeData($event, user)" />で受け取り、this.user = userで自らのdataに代入しています。

$emitやイベント(メソッド)の受け渡しについては下記の記事でまとめています。
Vue(Nuxt.js) で親子コンポーネント間でイベント(メソッド)を呼び出す方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

.gitignoreに記載したのにnode_modulesがいつまでもpushされてしまう。

はじめまして、Yuiです。

私は現在Vue.jsを使って受託開発を行っているのですが、そこで詰まったことを紹介します。

何が起こったか

Vueで書いたコードをBitbucketにpushした際、node_modulesが.gitignoreに書かれてあるにも関わらず、まとめてpushされていた。

.gitignoreを見直してもきちんとnode_modulesの記載があり、しばらく原因がわからなかった。

原因

どうやらキャッシュが原因だった。

7/22追記 kmdkukさんよりコメントを頂き、原因がキャッシュではないことがわかりました。すでにgitの追跡対象になっているファイルはどんだけignoreしようとしても追跡対象から外さないとコミット履歴が残ってしまうようです。私はその部分をまとめてキャッシュと呼んでいました・・・・。すみません!

試したこと

とりあえず、いかなる条件でもignoreするように .gitignoreに

*node_modules/

と記載した。

そしてその後

$ git add .gitignore
$ git commit -m "comment"
$ git push origin hoge_branch

これで解決!
と思いきやまだ残ったままだった・・・。

解決法

*node_modules/

こう書いても消えないということは、キャッシュの可能性が高いと思い、キャッシュを全削除してみた。

7/22追記 kmdkukさんのコメントより引用

すでにgitの追跡対象になっているファイルはどんだけignoreしようとしても追跡対象から外さないと変更履歴を永遠と追跡してコミット履歴に残ってしまいます.
その追跡対象から外すコマンドがgit rmコマンドです.
これにrオプションを付けることで再帰的にフォルダ以下のファイルを対象から外してくれます.
cachedオプションは,追跡対象から外したときに,ファイルをそのまま残してくれるようにしてくれるオプションです.

つまり、下記のように消すと今まで追跡するべくして追跡してたファイル郡までaddし直す羽目になるそうなので、きちんとファイル名を明示する必要があるらしいです。
(この辺もざっくりとは認識していたのですが、もう一回addすればよいかと思い、あまり重要視していませんでした。)

ですので、

$ git rm -r --cached .

ではなく、

$ git rm -r --node_modules

とファイル名を明示的に指定する方が良いです。

感想

なにか挙動がおかしいときはキャッシュをチェックするということを忘れないようにしたい。
kmdkukさん、本当にありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js Rails ページ遷移

前提

Rails 5.2
Ruby 2.5.1

package.json
{
  "name": "app",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "5.1.1",
    "vue": "^2.6.11",
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.11"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}

ファイル構成

app
|--javascript
   |--packs
      |--hello_vue.js
   |--app.vue

本題

かなり初歩的ですが、とても躓いたため記述しました。

・VueRouterを導入

$ npm install vue-router

・app.vueにを追記

app.vue
<template>
  <div id="app">
    <!-- 追記 -->
    <RouterView />
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      message: "Hello Vue!",
    };
  }
};
</script>

・app/javascript/packsの中にpagesフォルダー作成
・作成したpagesフォルダーのなかに、ファイルを2つ作成。

pages/Hoge.vue
<template>
  <div>
    <router-link to="/">Test</router-link>
  </div>
</template>
pages/test.vue
<template>
  <div>
    <router-link to="/hoge">Hoge</router-link>
    <p>login</p>
  </div>
</template>

・packsフォルダーにrouter.jsを作成

router.js
import Vue from "vue";
import VueRouter from "vue-router";

import Test from "./pages/Test.vue";
import Hoge from "./pages/Hoge.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    component: Test,
  },
  {
    path: "/Hoge",
    component: Hoge,
  },
];

const router = new VueRouter({
  mode: "history",
  routes,
});

export default router;

・hello_vue.jsにvue-routerを追記

hello_vue.js
import Vue from "vue";
import router from "./router";
import App from "../app.vue";

document.addEventListener("DOMContentLoaded", () => {
  const app = new Vue({
    router,
    components: { App },
    template: "<App />",
    render: (h) => h(App),
  }).$mount();
  document.body.appendChild(app.$el);
});

ビルド後にページリロードして動作確認。

# bin/webpack-dev-server
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む