作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

ゆるふわLaravel勉強会 (認証/JWT) 認証に関する資料

Laravel 認証についての色々まとめ

以下の記事は 2019/4/1 コワーキングスペース秋葉原Weeybleで行われる輪読会 [秋葉原] ゆるふわLaravel勉強会 (認証/JWT)のための認証に関する資料となります。

内容は以下の有志によるリファレンスサイトの記事の要約となります。 Laravel 5.8 認証

また、バージョンはLaravel 5.8.8 を前提にしています。

認証クイックスタート

Laravelインストール直後は認証系がフロント側で動く状態にはなっていないが、Controller等は既に準備されている
Controllers/Auth配下

コントローラー 用途
RegisterController 新ユーザーの登録
LoginController 認証処理
ForgotPasswordController パスワードリセットのためのメールリンク処理
ResetPasswordController パスワードリセット処理

ひとまず認証付きのアプリを作るには、まずは以下のコマンドを打って、フロント側やルーティングに認証系の処理を自動生成させる

php artisan make:auth

コマンドを叩くとファイルに記述が追加されたり、新規ファイルが作られたりする
もし認証付きのアプリケーションを作るのであればfirst commit直後位に実施してしまうのが良い

変化のあるファイルの紹介

ルーティング
routes/web.php

// 以下2行が追加される
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');

Auth::routes();で登録画面、ログイン画面、パスワードリセットのすべてのルーティングを設定してくれている、個別に編集が必要な場合は、この行を廃止して画面毎にルーティングを定義する。


HomeControllerの追加
app\Http\Controllers\HomeController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('home');
    }
}

ルーティングの2行目に追加された処理を行うControllerだが、ログイン直後の画面のサンプル例となる。 ログインすると/homeに移動するので、アプリケーションの仕様に従い、表示を作り込めば良いし、Homeという名前が気に食わないなら随意に変更する


その他以下の各viewファイルが自動生成されます。

resources\views\home.blade.php
resources\views\auth\login.blade.php
resources\views\auth\register.blade.php
resources\views\auth\verify.blade.php
resources\views\auth\passwords\email.blade.php
resources\views\auth\passwords\reset.blade.php
resources\views\layouts\app.blade.php

ログイン・登録・パスワードリマインダ等のページと機能も自動で生成してほぼ機能するようになります。

ブラウザでルートのpathにアクセスすると、画面左上に[LOGIN]と[REGISTER]のリンクが表示されるようになります。


認証の動作確認

まずはDBが無いので作ります。(vagrant環境でMySQLがある前提)

$ mysql -u root -p secret
mysql> create database your_database_name;
mysql>exit

migrateしてDBにtableを作ります。

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table

user table以外にpassword resetのtableもcreateされました。
これでREGISTER出来る様になりました。
登録画面に移動して、登録すると、DBにuserが生成され、ログインして/homeにリダイレクトされます。
当然一度ログアウトしてログインができることも確認できます。
また、ログイン画面にパスワードを忘れた際のMailリマインダ―がありますが、mailの設定をしないと飛びませんので、今回は割愛

以上が、クイックスタートで作られた認証の初期概要です。


認証のカスタマイズ (初級編)

初期状態は以下の様な認証の仕様となっていますが、これは簡単に変更が可能です。
以下はリファレンスサイトの内容をほぼ転載しています。

ログイン後のリダイレクト先 /homeの変更

LoginControllerRegisterControllerResetPasswordControllerVerificationControllerredirectToプロパティで、認証後のリダイレクト先の場所を定義してください。

protected $redirectTo = '/';


ログイン時のEmailをユニークな username, user_id,等に変更する

ログイン時はemailとpasswordの組み合わせが認証のデフォルトだが、emailをuser_id等に変更したい場合。

これをカスタマイズしたい場合は、LoginControllerusernameメソッドを定義してください。

public function username()
{
    return 'user_id';
}

当然user Tableをmigrateして変更したいユニークとなるカラムを追加してください。


登録済みユーザーのロール別制限 guard メソッド

webアプリを普通に作ってる場合は個人的にあまり使いませんが、APIやSPAの際にはよく使う事になるそうです。

LoginControllerRegisterControllerResetPasswordControllerguardメソッドを定義してください。メソッドからガードインスタンスを返してください。

use Illuminate\Support\Facades\Auth;

protected function guard()
{
    return Auth::guard('guard-name');
}


認証済みユーザーの取得 -Authファサード超便利-

Auth::user()->email とかでuser関連のデータをControllerやviewですぐ取得できる。
基本は認証時に行った user Tableのカラムのデータが取得できるので、自分のデータを取得したい際にとても便利に使えます。

use Illuminate\Support\Facades\Auth;

Auth::user()->email; // taro@gmail.com


認証中のユーザーか調べる -必須並みの便利機能-

Auth::check() これも 認証してる/してない を簡単に切り替え判断できる。controllerでもviewでも使える。認証の有無で処理や表示を変える際に便利!よく使う。

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
    // ログイン中の場合の処理
} else {
    // 非ログイン中の処理
}


認証済みのみ通すページをルーティングで指定

この辺はルーティングの説明時にも紹介した内容で、ルーティングrouter/でチェーンメソッド->middleware('auth')と書くと、認証時のみ有効となるルーティングとして定義できます。

Route::get('profile', function() {
    // 認証済みのユーザーのみが入れる
})->middleware('auth');

それ以外でも例えばコントローラのコンストラクタでもmiddlewareメソッドを呼べる

public function __construct()
{
    $this->middleware('auth');
}


認証回数制限

brute-force対策が最初から出来ている感じ?

Laravelの組み込みLoginControllerクラスを使用している場合、Illuminate\Foundation\Auth\ThrottlesLoginsトレイトが最初からコントローラで取り込まれています。デフォルトでは何度も正しくログインできなかった後、一分間ログインできなくなります。制限はユーザーの名前/メールアドレスとIPアドレスで限定されます。

自前のユーザー認証 (中級編)

リファレンスにある内容を紹介する
app\Http\Controllers\Auth\LoginController.php
authenticateメソッドを新たに定義する。

認証系のカスタマイズはvendor\laravel\framework\src\Illuminate\Foundation\Auth\AuthenticatesUsers.phpにあるメソッドをオーバーライドするのが応用編の入り口の様です。

ちなみに下記のIlluminate\Foundation\Auth\AuthenticatesUsers.php内のauthenticatedメソッドの実体は空メソッドで、カスタマイズ専用のメソッドである事がわかります。

    /**
     * The user has been authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  mixed  $user
     * @return mixed
     */
    protected function authenticated(Request $request, $user)
    {
        //
    }

これを以下の様にLoginController.phpに追加で記述をします。
以下、リファレンスサイトの例を転載します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * 認証を処理する
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return Response
     */
    public function authenticate(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            // 認証に成功した
            return redirect()->intended('dashboard');
        }
    }
}

ここで重要なのはAuth::attempt($credentials)です。
これが認証するか否かを判定できる仕組みで、引数に渡すのはモデルのカラム名となります。

オレオレの実装例

    public function authenticate(Request $request)
    {
        // login_id カラムは email + '@' + community_id(int) で構成されたユニークの文字列として登録時に保存された値、これでログイン認証を行う
        $login_id = $request->email . '@' . $request->community_id;
        $credentials  = array(
            'login_id' => $login_id,
            'password' => $request->password,
        );
        $request->validate([
            'email' => 'required|string|email|max:170',
            'password' => 'required|string|min:6',
        ]);
        if (Auth::attempt($credentials)) {
            return redirect('/')->with('message', 'ログインしました');
        } else {
            return redirect()->back()->withErrors(array('email' => 'E-mailかPasswordが正しくありません'))->withInput();
        }
    }

Auth::attempt($credentials)に渡す認証の値は追加が可能です。以下の様に認証時の条件を3つ以上に設定することができます。

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // ユーザーは存在しており、かつアクティブで、資格停止されていない
}

以降の認証に関する記述はリファレンスサイトを参考にしてください。
これ以降はケースバイケースで使用するかも。といったものが多い印象です。


よくありそうなカスタマイズについての実例など

RegisterController のカスタマイズ

ユーザー登録を行う際のvalidatorcreateメソッドの変更が必要であれば変える。
この辺は大変解りやすいコードだし、見たまんまで弄ってしまって基本OKです。
バリデートの変更や、ユーザー登録時に発行すべきカラムのデータ等を生成します。ありがちなのはユーザーの権限を初期状態で追加する。等の処理を行う事になるかと思います。

app\Http\Controllers\Auth\RegisterController.php抜粋

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }


認証カスタマイズ(オレオレ編)

実はAuth::user()で呼べるのは通常,user Tableのカラムだけとなります。ところがアプリの仕様上 user Tableがユニークにならない様な場合は、Auth::user() で欲しい一意のユーザーのデータが取得できず、偉い苦労しました。

どんなことをやったかというと、この記事にあるような事をしました。

Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法

Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得できるようにしました。

という訳で後半はこれについて説明します。

Laravel 認証カスタマイズ 複数tableを結合しての認証で Auth::user() に必要な値を入れる方法

Laravelの認証機能をカスタマイズして、認証時に3つのカラム条件で認証をし、さらに認証後にAuth::user() ファサードに複数tableからの値を取得することをしました。

環境Laravel5.6
windows10Pro 64bit
vagrant環境にて実施

そもそもの経緯として、DB構造が特殊になり、 通常の users tableのみでの認証では必要な値を取得できなくなっていました。
全体のDB設計をしてからやれば済む問題だったのですが、単数のtableでの認証を前提として、システムを作り込んでしまい。後からどうしても複数tableからの値を取得する必要に迫られて、Laravelの認証系のドキュメントやサイトをかなり読込むことになりました。

結果を先に書いておくと 複数tableをjoinしたMySQLのviewを migration で書いて、そのviewを認証用table,auth_usersとして登録し、viewから一意の値を導きます。
通常はusers tableから、 email, password と2種類のカラムから一意、かつパスワードの一致で認証をしますが、今回は unique_name, password, community_id の3つのカラム条件から認証条件を導き出しています。

table構造図

パワポで書いたのでちょっと変ですが意図は伝わるかと思います。)
f:id:sakamata:20181116045527p:plain

user は複数のコミュニティに任意に所属・登録できるというシステムで、これを中間tableである community_user でどのユーザーがどのコミュニティに所属しているかを管理しています。

さらに community_user tableにはと1対1(hasOne)の関係で追加table communities_users_statuses があり、特定のユーザーの特定のコミュニティ内での情報(権限レベルのidや表示設定、日時等)が保存されています。さらにその先には roles(権限)tableがあり、idと権限名が記載されているという、かなりがっつりなtable設計をした構造になってます。

これら

users
community_user
communities_users_statuses
roles

上記4つのtabelのからのカラムデータを、Laravelのファサード Auth::user()->name といった具合に、 Auth::user()->role も Auth::user()->user_idAuth::user()->community_id も "->"を一つ書くだけで一気に取得できる様にして、その値をアプリケーションの様々な処理のトリガーとして利用したいのです。(というかそういう風に作ってしまって後から変えるの凄いしんどい。)

まず、通常のLaravelの標準の認証系の追加をさくっとします。この辺は他のサイトにお任せします。

Laravel 5.6 認証 イントロダクション

で、色々ファイルが作られますので、それをどう変えるか、というお話です。

まず、MySQLのviewを auth_users という名前でマイグレーションファイルで作成します。通常のマイグレーションファイルの記述方法とは少し異なります。

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

// 認証時に使用される view  Auth::user() に収納される
// $credentials array ('id', 'unique_name', 'password' )
class CreateViewAuthUsersTable20181114 extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        DB::statement( 'DROP VIEW IF EXISTS auth_users' );
        DB::statement( "
            CREATE VIEW auth_users AS
            SELECT
                community_user.id,
                community_user.user_id,
                community_user.community_id,
                users.name,
                users.unique_name,
                users.email,
                users.facebook_id,
                users.password,
                users.remember_token,
                communities.user_id AS reader_id,
                communities.name AS community_unique_name,
                communities.service_name,
                communities_users_statuses.role_id,
                communities_users_statuses.hide,
                communities_users_statuses.last_access,
                communities_users_statuses.created_at,
                communities_users_statuses.updated_at,
                roles.role
            FROM community_user
            JOIN users ON (community_user.user_id = users.id)
            JOIN communities ON (community_user.community_id = communities.id)
            JOIN communities_users_statuses ON (community_user.id = communities_users_statuses.id)
            JOIN roles ON (roles.id = communities_users_statuses.role_id)
        " );
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        DB::statement( 'DROP VIEW IF EXISTS auth_users' );
    }
}

がっつりとview作成のSQL文を書いています。
DB::statement は通常のSQL文を書くという事ですね。 宣言の部分に use DB; は書かなくても大丈夫でした。書いたら逆にエラーになった。
で、以下のコマンドを実施

php artisan migrate

SQL文を走らせて view,auth_usersを作ります。
この方法はこちらのサイトを参考にさせてもらいました。

LaravelのマイグレーションでView Tableを作成する

これにより先ほど説明した4つのtableが結合されたviewが作成されます。
f:id:sakamata:20181116045531p:plain

viewは一つの大きなtableの様に扱う事ができます。出来上がったviewには所により同じ値が2度以上でますが、 unique_name password community_id の3つの値が全て重複するレコードは存在せず、これにより一意の認証が可能となります。

次にこのviewを認証用のtableとして使うための認証用モデルを作ります。
これは、標準で認証に使われているファイル app\User.php をコピーして書き換えました。

app\AuthUser.php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class AuthUser extends Authenticatable
{
    use Notifiable;

    /**
     * モデルと関連しているテーブル
     *
     * @var string
     */
    protected $table = 'auth_users';

    /**
     * The attributes that should be hidden for arrays.
     * JSON出力の際誤って含めいない属性という事らしい
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    // 日時表記変更の ->format('Y-m-d') を使いたいカラム名を指定する
    protected $dates = [
        'last_access',
        'created_at',
        'updated_at',
    ];
}

これが結構忘れがちですが大事です!

次に上記の app\AuthUser.php を認証時に使うように config ファイルの model を書き換えます。

config/auth.php (抜粋)

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\AuthUser::class,
        ],

また、login画面のviewファイルにはコミュニティ毎にログイン画面のURLが異なる仕様となっています。
ここにformの hidden で表示されているログイン画面の community_id の値をPOSTできる様にしています。また、標準のemailの入力欄であるフォームを ユーザーID(unique_name)の入力欄に変更しています。

resources\views\auth\login.blade.php

省略
<form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}">
@csrf
<input type="hidden" name="community_id" value="{{$community->id}}">
省略
<input id="unique_name" type="text" class="form-control{{ $errors->has('unique_name') ? ' is-invalid' : '' }}" name="unique_name" value="{{ old('unique_name') }}" required autofocus>

最後?に LoginController を書き換えます。

app\Http\Controllers\Auth\LoginController.php (抜粋)

// 前後省略
use DB;
use Illuminate\Support\Facades\Auth;

//Class宣言部分 省略

    // ログイン時に使用するユニークであるカラムを指定
    public function username()
    {
        return 'id';
    }

    public function authenticate(Request $request)
    {
        $request->validate([
            'unique_name' => ['required', 'string', 'min:6', 'max:40',  'regex:/^[a-zA-Z0-9@_\-.]{6,40}$/u'],
            'password' => 'required|string|min:6|max:100',
        ]);

        //  該当の community_user の id を取得
        $community_user_id = DB::table('community_user')
            ->leftJoin('users', 'users.id', '=', 'community_user.user_id')
            ->where([
                ['unique_name', $request->unique_name],
                ['community_id', $request->community_id],
        ])->pluck('community_user.id')->first();

        if (!$community_user_id) {
            // 他のコミュニティで認証が取れるか?
            $result_bool = $this->CheckOtherCommunityExists($request->unique_name, $request->password);
            if ($result_bool) {
                // 他のコミュニティにいる場合中間table等にレコード追加
                $community_user_id = $this->InsertNewStatuses($request->community_id, $request->unique_name);
            } else {
                // 他のコミュニティにいない場合
                return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput();
            }
        }

        // community_user_id を含めた通常の承認フロー
        $credentials  = array(
            'unique_name' => $request->unique_name,
            'password' => $request->password,
            'id' => $community_user_id,
        );
        // 認証許可
        if (Auth::attempt($credentials)) {
            // session にcommunity値保存
            $request->session()->put('community_id', $request->community_id);
            $request->session()->put('community_user_id', $community_user_id);
            return redirect('/')->with('message', 'ログインしました');
        } else {
            return redirect()->back()->withErrors(array('unique_name' => 'ユーザーIDかPasswordが正しくありません'))->withInput();
        }
    }

通常は$credentials の配列に email と password のみを指定して Auth::attempt($credentials) と渡してやれば認証を結果を返してくれますが、auth_usersから一意の値として取得できる条件として id を配列に追加してやります。

        $credentials  = array(
            'unique_name' => $request->unique_name,
            'password' => $request->password,
            'id' => $community_user_id,
        );

こうすることにより、 MySQLのviewで作られた auth_users table を探しに行き、一意の値を取得して認証が行われるという訳です。
ちなみにコードの中ほどの独自メソッド群は、$community_user_id を特定する処理として、認証前に値を探しに行ったり、ユーザーがまだログインしたことのないコミュニティに初ログインした際に既存ユーザーであるか?の確認をしたり、community_user にtableに値を追加したり、と、このWebアプリケーション独自の処理を色々してます。

こっからは蛇足ですが、その処理の際に、認証を全て自分でカスタマイズできそうなヒントになりそうな処理を書いたので紹介。

    // return bool 他のコミュニティに存在するかを判定する
    public function CheckOtherCommunityExists($unique_name, $password)
    {
        $hash_password = DB::table('users')->where([
            ['unique_name', $unique_name],
        ])->pluck('password')->first();

        if ($hash_password) {
            if (Hash::check($password, $hash_password)) {
                return true;
            }
        }
        return false;
    }

上記の自作メソッドは 認証時の id と password を使って、クイックスタートで作られる認証系の処理を通さないで ユーザー認証の判定ができています。(unique_name とpassword)この処理を書いていたら図らずも結構普通な感じの認証処理になってました。 なので、単純に自分で認証処理を作りたい際は上記を参考に返り値となるboolの判定を基に Auth::login($user);

とやってしまえば、ログイン認証っぽい事はできるようです。 $user はAuth::user() に入る一意のユーザーのオブジェクト(DBのrecord)ですね。ただ、tokenに値を入れたり、リダイレクト先を指定したりの処理がどこまで必要になるかは未検証です。

Laravel 5.6 認証 イントロダクション
https://readouble.com/laravel/5.6/ja/authentication.html

【輪読会資料】基礎から学ぶVue.js CHAPTER7 より大規模なアプリ開発 読書メモ

以下の記事は2019/3/28 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch7 より大規模なアプリ開発(初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER7 より大規模なアプリ開発 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

CHAPTER 7 より大規模なアプリケーション開発

SECTION 33 アプリケーションを拡張しよう

Vue.js本体は画面の描画に関わる機能しか備えていないが、以下の様なエコシステムを導入すると機能を拡張できる。

Vuex

複数のコンポーネントでデータを共有したり、アプリケーション全体の状態を一元管理する状態管理用のライブラリ

Vue Router

コンポーネントで構造化された複数の画面をURLと紐づけるSPA(Single Page Application)構築の為のルーティング用のライブラリ

Vuex と Vue Routerは機能として独立しており、状況に応じて片方だけでも構築が可能。 それぞれの特徴はCHAP8,CHAP9で解説している。

ここでは単一ファイルコンポーネントを使ったコンポーネント構築とVueCLIを使ったビルド環境の導入を前提としての説明をする。

SECTION 34 Vue CLIとは

Vue CLIとはVue.jsを使ったコマンドラインインターフェース、Vue.jsの開発環境構築が簡単にできる

ソースコードを分けて管理

分割して作ったファイルをコンパイル時等に結合して一つのファイル化させる方法

モジュール化でさらに快適に!

モジュール化で作られたファイルは多くの機能から必要なものだけピックアップして組み込める。
Vue.jsのモジュール化は「単一ファイルコンポーネント」を採用している。
単純に結合しても動くわけではなく、補完する機能として代表的なものに「webpack」といったバンドルツールがある。
webpack以外にも多くのツールの複雑な設定を自動管理してくれるのが「Vue CLI」となる

webpackとは

モジュール化した複数のファイルをまとめるバンドルツール、他にも[Rollup]や[Parcel]といった別種が存在する。詳細は公式サイトを参照の事 webpack 公式サイト

モジュール化の際は、必要なリソースだけをバンドルするようになっているので、自動的に余計なものが付かないで最小単位で出力されるようになっている。

SECTION 35 単一ファイルコンポーネントとは

「単一ファイルコンポーネント」はコンポーネントの定義方法の一つでSFC(Single File Components) とも言われる
一つのファイルで管理しきれないHTML,JavaScript,CSSを[.vue]という拡張子のファイルにまとめて管理する。

単一ファイルコンポーネントの例
Example.vue

<template>
  <div class="example">
    <span class="title">{{ text }}</span>
  </div>
</template>

<script>
  export default {
    name: 'Example',
    data() {
      return {
        text: 'example'
      }
    }
  }
</script>

<!-- scoped CSS -->
<style scoped>
  .title {
    color: #ffbb00;
  }
</style>

こいつをビルド環境でプレーンなJavaScriptにプリコンパイルする

スコープ付きCSS(Scoped CSS)

<style>タグでscoped オプションを付けるとスコープ付きCSSを利用できる。(<style scoped>)このテンプレート内でしかスタイルが適用されないようにできる。

出力される際には以下の様なユニーク属性で管理される

<div class="example" data-v-xxxxx>
  <span class="title" data-v-xxxxx> hoge </span>
</div>
span.title[data-v-xxxxx] { color: #ff0000; }

これでクラス名の衝突を気にしなくて良くなる。なお、全くクラスの指定がないとパフォーマンスが落ちるので classでスタイル定義をするのが推奨されている。

コンポーネントの扱い

テンプレート内に子コンポーネントが存在する場合、子のルート要素とスロットの要素は親と子の両方のスコープを持つ

<div class="example">
  <child-comp/>
</div>

親と子両方に<style scoped>をを指定した際は以下の様に展開される

<div class="example" data-v-aaaaa>
  <div data-v-aaaaa data-v-bbbbb><!-- このルート要素 -->
    <span data-v-bbbbb>child-comp</span>
  </div>
</div>

この様にルート要素のスタイルはどちらのファイルにも書くことができるので、柔軟にレイアウト変更が可能となる。

スコープをまたぐ設定

お互いにスコープの付いたコンポーネントから子のセレクタ .bを指定したい例
cssの場合

<style scoped>
.a >>> .b {color: #ff0000; }
</style>

scssの場合

<style lang="scss" scoped>
.a /deep/ .b {color: #ff0000; }
</style>

外部ファイルの読み込み

<template src="./template.html"></template>
<style src="./script.js"></style>
<style src="./style1.css"></style>
<style src="./style2.css" lang="scss" scoped></style>

他のマークアップ言語やスタイルシート言語の仕様

cssやscssの他に、[Pug],[Sass]を使用等をすることができる。Vue CLIで作成したプロジェクトなら、パッケージを以下の様にインストールしてから…

npm install pug pug-loader --save-dev

以下の様に書くだけで使用可能

<template lang="pug>
  div#example
    span {{ text }}
</template>

SECTION 36 ES2015モジュールの書き方

ES2015モジュールは .js でファイルを作成するが、モジュールファイルは独自のスコープを持ち、他のファイルとは切り離される、変数の衝突や他の機能への干渉を気にしなくて良くなる反面、モジュール内で定義したデータや関数は他のファイルから直接アクセスできなくなる。

モジュールを定義する

Example.js

// stateオブジェクトはこのモジュール内でしか使用できない
var state = {
  count :1
}

モジュール内のデータや関数を別のファイルに渡すにはエクスポート&インポートが必要
次の例はstateオブジェクトをエクスポートして、外部でインポートできる様にした例

Example.js

var state = {
  count :1
}
// デフォルトのインポート文で呼ばれた時に返すデータ
export default state

モジュールを使用する

前述のExample.jsstateオブジェクトをExampleという変数名でインポートした例
main.js

import Example from './Example.js'
console.log(Example.count)  // => 1と出力

このような書き方は Vuex, Vue Router ファイルを書く際に必要なので覚えておくこと!

SECTION 37 Node.jsの導入

node.jsJavaScriptの実行環境、サーバーサイドでJSを動かす際に必須
npmはJSのパッケージ管理ツール、node.jsに一体化して付いてくる。 PHPでいう所のcomposer的な奴

以下からインストール
node.js 日本語公式サイト windows10の場合はそのまま画面の指示に従いインストール
コマンドプロンプトで以下のコマンドでバージョンを出してインストールされたか確認する。
今回は推奨版10.15.3 LTS をインストールした

node -v
v10.15.3
npm -v
6.4.1

Babelとは

ECMAScript準拠およびJSXのトランスパイラ、Vue CLIはデフォルトでBabelをインストールしてトランスパイルを行う。

トランスパイル(transpile)
一言で言えば、ある言語で書かれたコードを元に別の言語のコードを生成すること。

BabelはES2015のコードを古いES5のコードに変換してくれる

ES2015のコード トランスパイル ES5のコード
const count = 1 => var count = 1

Babelの目的やメリットは最新の記法を使って書ける様にすることにある

SECTION 38 Vue CLIの導入

Vue CLIをインストール

npm install -g vue-cli

バージョンを確認

vue --version
2.9.6

プロジェクト作成コマンドのひな形

vue init [テンプレート名] [プロジェクト名]
cd [プロジェクト名]
npm install

テンプレートは[webpack], [webpack-simple]が良く使われる,他にも[PWA],[Electron]等の様々なアプリケーション用のテンプレートが公開されている。
ここでは[webpack]を使って説明をして行く

新しいプロジェクトの作成

アプリを入れたい適当なフォルダに移動(cd)してから次のコマンドを実行
(Git bashで操作)
作ったフォルダ自体が localhost:8080 の実行環境になってそのまま開発環境のsever化してくれるので、本当にフォルダの場所とか適当で良い様です。

# htdocsに移動
$ cd /c/xampp/htdocs

# プロジェクトの作成
vue init webpack vue-my-app
$ vue init webpack vue-my-app
# 質問が出るので色々選択しながら進む、今回は以下の設定で進んだ。
? Project name (vue-my-app)
? Project name vue-my-app
? Project description (A Vue.js project)
? Project description A Vue.js project
? Author (Taro Yamada <yamada@gmail.com>)
? Author Taro Yamada <yamada@gmail.com>
? Vue build runtime
? Install vue-router? (Y/n) n
? Install vue-router? No
? Use ESLint to lint your code? (Y/n) n
? Use ESLint to lint your code? No
? Set up unit tests (Y/n) n
? Set up unit tests No
? Setup e2e tests with Nightwatch? (Y/n)
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recom
? Should we run `npm install` for you after the project has been created? (recom
mended) npm

すると htdocsフォルダ内に vue-my-appフォルダが作られ、中に色々ファイルが作られる

$ ls -a vue-my-app/
./   .babelrc       .gitignore     build/   index.html     package.json       README.md  static/
../  .editorconfig  .postcssrc.js  config/  node_modules/  package-lock.json  src/       test/

フォルダとファイルの構成

[webpack]を使用した際、いろんなファイルやフォルダが作られるが、基本的に[src]フォルダ以下の物しか使わない
単一ファイルコンポーネントの[.vue]ファイルは慣例的に src/componentsにまとめる

src/main.js でコンストラクタの呼び出しが行われている

/* eslint-disable no-new */
new Vue({
  el: '#app',
  render: h => h(App)
})

開発サーバーを起動する

vue-my-appフォルダ内に cd してから以下のコマンドを入れてサーバーを起動する

npm run dev

ブラウザでhttp://localhost:8080/にアクセスしてVue.jsのロゴとWelcome to Your Vue.js App のメッセージが出力されればOK!
サーバーを起動させたコンソールは開きっぱなしにしておくこと

ホットリロード

ホットリロードはブラウザリロード不要でファイル変更の際に更新してくれる機能
vue-my-app/src/components/HelloWorld.vueファイル内のmsg: 'Welcome to Your Vue.js App'を編集して上書きすると即座ぶブラウザが更新されるのが確認できる。超便利!

プロジェクトをビルドする

実際に運用モードとしてビルドするには以下のコマンドを入れる

npm run build

数分かかる。ビルドが終わると[dist]フォルダが作成される。
この[dist]フォルダだけを本番サーバーにアップロードすれば、ローカルからのビルドとデプロイが完了する。
ちなみに以下のサポートページで、静的サイト向けの高機能なホスティングサービスNetlifyの自動デプロイの方法についての記述がある。
Vue.js+Netlifyで自動デプロイ

開発時のAPIのパス統一やクロスドメイン対策

本番環境と開発環境でAPIのパスが異なる場合や、別ドメインAPIからデータを取得する際はプロキシ設定が必要となる。config/index.jsに以下の様に記述することで切り替えができる

before config/index.js

    proxyTable: {},

after config/index.js

    proxyTable: {
      '/api' : {
        taget: 'http://localhost:8081',
        changeOrigin: true,
        pathRewrite: {
          '^api'; '/vue-my-app/api'
        }
      }
    },

例えばAPI/api/userを叩くと 異なるドメインから user のAPIを取得できるように実装がされているとする。

プロキシを使うと以下へのアクセスは

http://localhost:8080/api/user

以下の様に変更できる

http://localhost:8081/vue-my-app/api/user

より詳細はwebpackのドキュメントを参照のこと

SECTION 39 Vue.jsプラグイン

CHAPTER1で紹介されたElement,Awesome Vue,VueCurated等のUIコンポーネントや、Vuex,Vue RouterはVue.jsプラグインとして作成されている。
ここではVue.jsプラグインの使い方と自作プラグインの作成方法の紹介となる。

Vue.jsプラグインの使い方

コンストラクタを使用する前にグローバルメソッドのVue.useを使用してVue.jsに登録する

src/main.js の上の行に書く?

// VueとVuexのモジュールを読み込む
import Vue from 'vue'
import Vuex from 'vuex'
// VueにVuexを登録
Vue.use(Vuex)

スタンドアロン版でプラグインを使用する場合

Vuexの様な公式提供の奴はVue.useを使わずとも良い。
Vue.js本体を読み込んだ後にプラグインのファイルを読み込む

公開されているリソースはプラグインだけじゃない

コンポーネントやカスタムディレクティブとして登録するのものある

プラグインを自作してみる

入門向けではないが一応説明、大規模アプリの開発リソースの再利用の際にプラグイン作成できると便利だそうな。
具体的にどのファイルのどこに書けばいいのか分らずなのだが、それはここまで読んだ内容を理解してないせいか、後の章で書かれるかだと思います。

スクロール数値を共有するプラグイン

var windowPlugin = {
  install: function(Vue) {
    // プラグインデータ用にVueインスタンスを利用する
    var store = new Vue({
      data: {
        scrollY: 0
      }
    })
    // ウィンドウのスクロールイベントをハンドル
    var timer = null
    window.addEventListener('scroll', function() {
      if (timer === null) {
        timer = setTimeout(function() {
          // 200ms間隔でscrollYプロパティに代入
          store.scrollY = window.scrollY
          clearTimeout(timer)
          timer = null
        }, 200)
      }
    })
    // インスタンスプロパティに登録
    Vue.prototype.$window = store.$data
  }
}

プラグインを登録

Vue.use(windowPlugin)

すべての Vue インスタンスで使用可能

Vue.component('my-component', {
  template: '<div>{{ scrollY }}</div>',
  computed: {
    scrollY: function() {
      return this.$window.scrollY
    }
  }
})

SECTION 40 ES2015で書いてみよう

新しいJavaScriptの書き方、ES2015(ES6)は対応してないブラウザがあるが、既に説明した通り、Babelが古いJS形式にトランスパイルしてくれるので、Vue CLIではES2015形式で遠慮せずに書ける。ここではES2015の書き方の説明となる。

正直ちゃんと追って無かったので、この知識はありがたい。

変数宣言

ES5

var x = 0

ES2015

// 再代入可能
let x = 0

// 定数 再代入不可
const x = 0

ES2015はスコープで仕切られる
ES5

{
  var x = 1
}
console.lod(x) // 1

ES2015

{
  let x = 1
}
console.lod(x) // not defined

constで宣言した配列は空配列を再代入できない。か、 lengthを0にすると配列を空にできる

const array = [1, 2]
array.push(3)
console.log(array) // -> (3) [1, 2, 3]
array.length = 0
console.log(array) // -> []

関数とメソッドの書き方

functionの省略

ES5

new Vue({
  methods: {
    handleClick: function() { ... }
  }
})

function は書かなくても良い
ES2015

new Vue({
  methods: {
    handleClick() { ... }
  }
})

アロー関数

functionを省略して => で書ける関数thisの扱いが従来の関数と異なり、アロー関数式で宣言された関数は、宣言された時点で、thisを確定(=束縛)させてしまう。
その為メソッド内のthisが必要な場面では使用できないが無名関数の定義で便利とのこと
【JavaScript】アロー関数式を学ぶついでにthisも復習する話

let normalFunc = function(x){
    console.log(x);
}
normalFunc('今までの関数');

let arrowFunc = (y) => {
    console.log(y);
}
arrowFunc('アロー関数式');

STEP1

const newArray = array.map(el => {
  return el * 2
})

STEP2 returnを省略

const newArray = array.map(el => el * 2)

慣れの問題だが、ここまで省略されると認識し辛いなぁ

STEP3 複数の引数

const newArray = array.map((el, index) => el * 2)

STEP4 オブジェクトの return

const newArray = array.map(el => ({ value: el * 2 }))

簡単な奴から使って慣れると良いよ。と作者もおっしゃってます。

テンプレートリテラル

クォーテーションで囲んでバックスラッシュで改行
ES5

var name = 'hoge'
var template = '\
  <div class="template">\
    <strong>' + this.name + '</strong>\
  </div>'
console.log(template)

バッククォートで囲むだけで改行含めて受け付けてくれる
ES2015

const name = 'hoge'
const template = `
  <div class="template">
    <strong>${ name }</strong>
  </div>`
console.log(template)

オブジェクトプロパティのショートハンド

ES2015では変数名とプロパティが同じなら省略して書ける
ES5

const newObject = {a: a, b: b}

ES2015

const newObject = {a, b}

分割代入

べんりー
ES2015

// 配列要素1,2をそれぞれ変数a,bに代入
const [a, b] = [1, 2]
console.log(a) // -> 1
// nameプロパティだけ代入
const { name } = { id: 1, name: 'りんご' }
console.log(name) // -> りんご

引数のオブジェクトを分割して受け取ることも可能
ES2015

function myFunction({ id, name }) {
  console.log(name) // -> りんご
}
myFunction({ id: 1, name: 'りんご' })

v-forディレクティブでも使える

<ul>
  <li v-for="{ id, name } in list" :key="id">...</li>
</ul>

スプレッド演算子

配列、オブジェクトのリテラルを展開する  ...hoge
ES2015

const array = [1, 2, 3]
// バラバラの3つの引数として渡す
myFunction(...array)
// arrayを展開して4を加えた新しい一次配列を作成
const newArray = [...array, 4] // -> (4) [1, 2, 3, 4]

配列メソッド

findメソッド

条件に一致した最初の要素の値を1つ返す
(そういえばこういう処理、スゲー大変だった気がする)

ES2015

const array = [
  { id: 1, name: 'りんご' },
  { id: 2, name: 'ばなな' }
]
const result = array.find(el => el.id === 2)
console.log(result) // -> { id: 2, name: 'ばなな' }

見つからない場合は undefind が返る

findindex

条件に一致した最初の要素のインデックスを1つ返す

const array = [
  { id: 1, name: 'りんご' },
  { id: 2, name: 'ばなな' }
]
const result = array.findIndex(el => el.id === 2)
console.log(result) // -> 1  (indexは0始まりなので2番目は1)

Promise

非同期処理を抽象化したオブジェクト。(もうajaxとかじゃない!)
Promiseで非同期処理の完了を知ることができる。
引数としてresolve(解決した)と、reject(拒否した)のコールバック関数を受け取る。
( $.ajaxの .done .fail みたいな感じか )

成功を知る例

function myFunction() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 成功したことを通知
      resolve('success!')
    }, 1000)
  })
}
// 1秒後にmyFunctionが終わった知らせを受けてthenの処理が行われる
myFunction().then(value => {
  console.log(value) // -> success!
})

拒否(失敗)した事を知る例、かつ成功、失敗の両方の処理を書いた例

function myFunction(num) {
  return new Promise((resolve, reject) => {
    if (num < 10) {
      resolve('success!')
    } else {
      reject('error!')
    }
  })
}
myFunction(100).catch(value => {
  console.log(value) // -> error!
})

成功、拒否、どちらでも処理する際はfinallyを使う

myFunction().then().catch().finally(() => {
  // 成功でも失敗でも行われる
})

Gitリポジトリからcloneして動かす際

プロジェクトフォルダをそのままリモートリポジトリに上げる。 別の環境でnode.jsをインストールして任意のフォルダで以下の操作をするだけでOK

git clone **url**
npm install
npm run dev

これだけで動く

□まとめ

  • Vue CLI はVue.jsプロジェクトのベースを構築してくれる
  • Vue CLI で単一ファイルコンポーネントを手軽に使用できる
  • ES2015以降のECMAScriptを使ったコーディングが可能、基本を把握しておく

【勉強会資料】Laravelのルーティングについてのいろいろ

以下は[秋葉原] 第二回 初心者向けLaravel 勉強会 (ルーティング/テスト)にて行われた発表用の資料となります。 Laravel5.6の有志制作のリファレンス・ルーティングや、いくつかの記事を参考にかかせていただいたものです。

ルーティング知っているようで、知らない事が多くありました。自分の書き方が古いバージョンのものだけど、動いているとか。普段は自分に必要な事柄だけしか読まないのですが、改めてリファレンスの隅々まで読んだ事で、より効率的に、汎用的な事が出来そうです。

Gitの履歴変更で盛大に失敗した話をメモベースで書く

GitHubに公開リポジトリを使ってアプリを作っているが、上げちゃいけないモノをアップしてしまったのをきっかけに色々やったメモです。要はバットノウハウ、失敗談、こんな風になっちゃだめですよという自分への戒めです。

そもそもの経緯

branchを二つ作って、個別に新機能を開発をしていた。

branchB(ゆっくり目の作業)で .gitignoreを編集した後 branchA(急ぎ目の作業)をcheckoutして別作業を続ける。

branchBでしていた .gitignore の設定がなくなった為、 branchAでgit add . すると 隠したいはずの追加ファイルがステージングに上がり、そのまま気付かずコミットしてしまう。(ぎゃー)

しばらくしてその事実に気付き、リモートリポジトリの履歴から該当ファイルを削除しようと以下の記事を見つつ試みる

Git リポジトリに上がっているファイルを履歴ごと消すには? - Qiita

Git ファイルの履歴を完全に削除する · GitHub

ところが、コマンドの意味を理解しないままガチャガチャと操作(ぎゃー)

禁断の git push -fを良くわからないまま実行する(ぎゃー)

何が起こったか?

まずはこいつを見てくれ、どう思う?

f:id:sakamata:20190302161920p:plain

すごく…複雑です。

盛大にやらかして色々やってこりゃ駄目だ終わった状態のリポジトリである。 画面にはローカルとリポートのbranch両方が表示されて同じコミットのコメントが複数にコピーされてしまっている。

何が起こったかを自分なりに理解した範囲で解説。

本当はもっと根本を理解してからまとめて述べた方が、為になると思うのだけど、自分がやりたいのはアプリの制作であってツールに振り回された顛末を詳しく分析することじゃない。従ってこんな風にややこしい自体にハマるGitという複雑なツールを理解できない俺が悪いのか、ツールが悪いのかという議論はしないし、受け付けない。(けど、こういう事じゃない?という見解は良かったらくれると助かります。)

git filter-branch --tree-filter "rm -f [消したいファイルパス]" HEAD

このコマンドですべての履歴から特定のファイルの履歴を消せるとあったので消したが、 上記サイトを参考に実行した、このコマンドかこの後行った操作が、どうも過去の履歴に渡るコミット時のhash値を変えるという操作だったらしい。そもそも変更されたファイルの状態からhashを生成しているっぽいので、もしかして履歴が変わればhash値もブロックチェーン的に変わる感じかな?

で、Gitのそもそも論の記事がある。

P117~P146あたりにrebase絡みでコミットのリビジョンについての解説がある。で、おそらく状況としては、履歴の書き換えをすることで、rebaseに近いhash値の書き換えが発生、それに気づかないまま、強制pushであるgit push -fを怖さも知らずに実行し、ローカル、リモート、それぞれで古い履歴を消さないまま、良くわからず複数のbranchで履歴の削除を繰り返した結果、複数の履歴が存在し、収集が付かなくなった。という事の様だ。

こえーよ、Gitまじこえーよ。理解してない人間が使っちゃいけないオーバーテクノロジーだよ。

そもそもの問題の発生は .gitignore ファイルを編集した状態で別branchをcheckoutした事が原因でこんなことになるとは思いもよらなんだ。カスケード破壊ってやつですか。

という事で、理解するのは諦めてこの後、何をしたかという話ですが…。

実は、履歴の変更をしようと決めた時点で、リモートリポジトリ非公開にしてから潰すつもりだったので、ローカルリポジトリにバックアップを取って、上みたいになるまで安心して弄り倒して壊しまくってました。

現在はバックアップを取ったリポジトリのbranchを整理してから master branch で該当ファイルの履歴からの削除を行いました。ヤバいファイルを履歴から完全に削除したのですが、どうもこれで コミット時のhash値は、初期のオリジナルの物とは変わってしまいました。どうもそれは仕方のない事らしいというのが昨日今日で調べた範囲で判明している事です。(そんなこと無いって見解と知識が欲しい)チーム開発でこれをやったら終わりですが、ほとんど個人でやっていたので、リセットさえすりゃ簡単な事態であった事は確かです。 しかし、これチームで履歴の編集をやらかす事態が発生すると、チーム全員に一旦 Cloneでやり直しをさせるって事になるのか…。これは申し訳ない事態だ。

Laravelの場合はせいぜい .envファイルのコピーと storage/log の chmod 777 位の手間ですが、他のプロジェクトでは色々面倒な初期設定も他のメンバーにしてもらわんといけないかもなので、履歴を書き換えるような事態は発生させてはいけませんね。

閑話休題。現在は奇麗になったリポジトリを再度リネームしてGitHubに上げたのと、開発環境から本番環境にpushするだけでデプロイできるようにサーバー側も再設定しました。(本当はSeleniumを挟んでデプロイ出来るような環境をそろそろ作りたいのですが)

今回のリポジトリの再設定の際は、これまで sourceTreeに頼りっぱなしでコマンドラインをあまり使ってませんでしたが、VPSサーバーにpushする際の.ssh/config やGit configをそこそこ設定したり、testでcommitした際は全てコマンドラインで作業を行い。以前よりはGUIに頼らず操作ができる様になってきた感じです。ですが、やはり分岐しまくったbranchの状況を追うにはGUIでの可視化が便利ですね。

という訳で履歴管理ツールに悩まされるなんていうアホな事態はとっとと終わらせてモノを作ります。

【輪読会資料】基礎から学ぶVue.js CHAPTER5 コンポーネントでUI部品を作る 読書メモ

今回は輪読担当ではなかったので、完全な自分のメモですが、アップしておきます。

var で定義できるローカルのコンポーネントもある、

// コンポーネントを定義
var myComponent = {
  template: '<p>MyComponets</p>'
}
new Vue({
  el: '#app',
  componets: {
    // 処理
  'myComponet' : myComponet
  }
})

コンポーネント間の通信
P155

親から子

// コンポーネントの定義 comp-child
Vue.component('comp-child', {
  // テンプレートで受け取ったvalを使用
  template: '<p>{{ val }}</p>',
  // 受け取る属性名を指定
  props: ['val']
})

// 親コンポーネント
new Vue({
  el: '#app',
  data: {
    valueA: 'これは子A',
    valueB: 'これは子B'
  }
})
<div id="app">
    <comp-child val="これは子A"></comp-child>
    <comp-child val="これは子B"></comp-child>
</div>

出力結果 子コンポーネント

で囲む状態が出力されている

<p>これは子A</p>
<p>これは子B</p>

propsで受け取ってないものは子供側では上書きが基本、class等の属性は親と子でマージされ両方反映

コンポーネントのルートタグ

Vue.component('comp-child', {
    template: '<p id="child" class="child">ChildComponent</p>',
})

コンポーネントのカスタムタグ

<comp-child id="parent" class="parent"></comp-child>

出力される実態

<comp-child id="parent" class="child parent">ChildComponent</comp-child>


// 子
Vue.component('comp-child', {
    template: '<li>{{ name }} HP.{{ hp }}</li>',
    props: ['name', 'hp']
})
// 親
new Vue({
    el: '#app',
    data: {
      list: [
        { id: 1, name: 'スライム', hp: 100 },
        { id: 2, name: 'ゴブリン', hp: 200 },
        { id: 3, name: 'ドラゴン', hp: 500 }
      ]
    }
})

list は親を使っている name, hp は 子のprops定義で使える様になっている

<!-- これも親となる  comp-child で子を呼び出し-->
<ul>
    <comp-child v-for="item in list"
        v-bind:key="item.id"
        v-bind:name="item.name"
        v-bind:hp="item.hp"></comp-child>
</ul>

コンポーネントで値を書き換えてもconsoleに[Vue warn]が出力される。
当然親の値は書き換えられない

Vue.component('comp-child', {
  template: '<li>{{ name }} HP.{{ hp }}\
  <button v-on:click="doAttack">攻撃する</button></li>',
  props: ['name', 'hp'],
  methods: {
    doAttack: function () {
      // 勝手に攻撃!
      this.hp -= 10 // -> [Vue warn] error!
    }
  }
})

もし、値の書き換えを行いたいなら算出プロパティを使って新しいデータを作成する。
もし親のデータ自体を変更したい場合は、$emitを使って親にアクションを起こさせる。等する

propsで値を受け取る際は、データ型を指定しておくのが推奨されている。

Vue.component('comp-child', {
    props: {
        val: String  // 文字列型のみ許可
    }
])

型の指定方法一覧

データ型 説明
String 文字列 '1'
Number 数値 1
Boolean 真偽値 true, false
Function 関数 function() {}
Object オブジェクト { name: 'foo' }
Array 配列 [1, 2, 3], [{ id: 1 }, { id: 2 }]
カスタム インスタンス new Cat()
null すべての型 1, '1', [1]

型チェック無しの場合の書き方

Vue.component('example', {
  props: ['value'] // どんな型も受け入れる
})

型チェックする場合の書き方

Vue.component('example', {
  props: {
    value: // ここにデータ型を指定
  }
})

型チェック以外にオプションで様々なバリデーションに対応できる
参照元
ほとんどControllerみたいになってきた。

Vue.component('my-component', {
  props: {
    // 基本的な型の検査 (`null` と `undefined` は全てのバリデーションにパスします)
    propA: Number,
    // 複数の型の許容
    propB: [String, Number],
    // 文字列型を必須で要求する
    propC: {
      type: String,
      required: true
    },
    // デフォルト値つきの数値型
    propD: {
      type: Number,
      default: 100
    },
    // デフォルト値つきのオブジェクト型
    propE: {
      type: Object,
      // オブジェクトもしくは配列のデフォルト値は
      // 必ずそれを生み出すための関数を返す必要があります。
      default: function () {
        return { message: 'hello' }
      }
    },
    // カスタマイズしたバリデーション関数
    propF: {
      validator: function (value) {
        // プロパティの値は、必ずいずれかの文字列でなければならない
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

子から親

P161
字のデータを親に渡すには$emitを使う

親のコード
on で受け取る

<child-comp v-on:childs-event="parentMethod">

子のコード
$emitで渡す

this.$emit('childs-event')

parentsMethodchilds-eventを通じて親に渡る

<comp-child v-on:childs-event="parentsMethod"></comp-child>
Vue.component('comp-child', {
    template: '<button v-on:click="handleClick">イベント発火</button>',
    methods: {
      // ボタンのクリックイベントのハンドラでchilds-eventを発火する
      handleClick: function () {
        //  `parentsMethod`が`childs-event`を通じて親に渡る
        this.$emit('childs-event')
      }
    }
})
new Vue({
    el: '#app',
    methods: {
      // childs-eventが発生した!
      parentsMethod: function () {
        alert('イベントをキャッチ! ')
      }
    }
})

親が持つデータを操作する

前述のモンスターのHPを子のメソッドで処理していた例を動くものにしたのが以下の例
良く読み込んで動きはなんとか理解はしたけど、今は書ける気がしない...。

<ul>
    <comp-child v-for="item in list"
        v-bind:key="item.id"
        v-bind:name="item"
        v-on:attack="handleAttack"></comp-child>
</ul>
Vue.component('comp-child', {
    template: '<li>{{ name }} HP.{{ hp }}\
    <button v-on:click="doAttack">攻撃する</button></li>',
    props: {
      id: Number,
      name: String,
      hp: Number
    },
    methods: {
      // ボタンのクリックイベントのハンドラから$emitでattackを発火する
      doAttack: function () {
        // 引数として自分のIDを渡す v-on:attackの中身`handleAttack`を親に渡す
        this.$emit('attack', this.id)
      }
    }
  })

new Vue({
    el: '#app',
    data: {
      list: [
        { id: 1, name: 'スライム', hp: 100 },
        { id: 2, name: 'ゴブリン', hp: 200 },
        { id: 3, name: 'ドラゴン', hp: 500 }
      ]
    },
    methods: {
      // attackが発生した!
      handleAttack: function (id) {
        // 引数のIDから要素を検索
        var item = this.list.find(function (el) {
          return el.id === id
        })
        // HPが0より多ければ10減らす
        if (item !== undefined && item.hp > 0) item.hp -= 10
      }
    }
})

カスタムタグのイベントハンドリンク

この書き方だと、コンポーネント側からclick$emitで呼び出さないと発火しないらしい。

<my-icon v-on:click="handleClick"></my-icon>

元々のイベントは直接発火したい場合は.native修飾子を付ける。

<my-icon v-on:click.native="handleClick"></my-icon>

非親子コンポーネント

親子関係以外だと、this,propsの通信はできない。
Vueインスタンスを「イベントバス」として使用するが、多用しない方が良い。コードがカオス化するので、詳細はP166参照

コンポーネントを参照する「$refs」

親コンポ―ネント

<comp-child ref="child">

親のメソッド内

new Vue({
  el: '#app',
  methods: {
    handleClick: function () {
      // 子コンポーネントのイベントを発火
      this.$refs.child.$emit('open')
    }
  }
})

コンポーネント

Vue.component('comp-child', {
  template: '<div>...</div>',
  created: function () {
    // 自分自身のイベント
    this.$on('open', function () {
      console.log('なにか処理')
    })
  }
})

親側からはv-onを使うようにする

コンポーネントの属性のスコープ

コンポーネントの属性の値部分は親のスコープになる

<comp-child v-on:child-event="親のメソッド">
<comp-child v-on:child-event="parentMethod(親のデータ)">

コンポーネントが引数をもって$emitを実行している場合、子コンポーネントの引数は$event変数で使用できる

<comp-child v-on:child-event="parentMethod($event, parantsData)">
new Vue({
    el: '#app',
    data: {
        parentsData: '親のデータ'
    },
    methods: {
        methods: {
            parentsMethod: function(childArg, parentsArg) {
                //
            }
        } 
    }
})

$event変数は$emitの第一引数しか持たないので、複数の引数を渡す際は1つのオブジェクトにまとめる。

this.$emit('childs-event', {id: 1, name: '新しい名前'})

【輪読会資料】基礎から学ぶVue.js CHAPTER4 データの監視と加工 読書メモ

以下の記事は2019/2/21 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch4 データの監視と加工 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER4 データの監視と加工 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

さすがに輪読担当4回中3回ともなると疲れて来た…。

書籍用のサイトのCHAPTER4記述ページ(サンプルコード有り)

これまでの輪読会資料

CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる

CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita

CHAPTER 3
【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ - 作りたいものがありすぎる

さきにまとめを書いて全体を把握

  • 算出プロパティは結果のキャッシュをしてくれる
  • データの状態に対して処理をしたいならウォッチャ
  • カスタムディレクティブは監視をしながらDOMの操作ができる
  • 更新後のDOMにアクセスしたいならnextTick

リアルタイムにDOMの値を調べて、処理(関数)をして、その結果を画面にリアルタイムに反映できる!という事っぽい。

CHAPTER4 データの監視と加工

SECTION 16 算出プロパティで処理を含むデータを作成する

要は普通に自分で作った関数での処理ができてデータもバインドできるので、リアルタイムな処理が可能という事らしい。

算出プロパティの使い方

functionとあるのでメソッド(関数)っぽいけど、プロパティ(変数・値)
従来散々使ったVueのdata:と並行した値でcomputed:を持たせ、そこに各プロパティを定義した関数を入れる構成
computedはのプロパティ以降は関数で定義する。
マスタッシュ({{ hoge }})等でプロパティとして使える

<p>{{ width }} の半分は {{ halfWidth }}</p>
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    // 算出プロパティhalfWidthを定義
    halfWidth: function() {
      return this.width / 2
    }
  }
})

算出プロパティを組み合わせて使用しよう

算出プロパティを組み合わせたり、配列を使う事もできる

this.プロパティ名で他の算出プロパティで定義したり計算した結果を引っ張って利用するとこができる。かなり普通のプログラムっぽい感じになる。
たぶん、画面の高さや幅を算出して、必要な位置に表示物を出す際なんかに強力に使える感じ?

<p>X: {{ halfPoint.x }}</p><!-- 400 -->
<p>Y: {{ halfPoint.y }}</p><!-- 300 -->
new Vue({
  el: '#app',
  data: {
    width: 800,
    height: 600
  },
  computed: {
    halfWidth: function() {
      return this.width / 2      // 400
    },
    halfHeight: function() {
      return this.height / 2     // 300
    },
    // 「width × height」の中心座標をオブジェクトで返す
    halfPoint: function() {
      return {
        x: this.halfWidth,       // 400
        y: this.halfHeight       // 300
      }
    }
  }
})

ゲッターとセッター

上の例では算出プロパティの値を代入(ユーザーからの入力等)しても実はエラーになってしまう
しかし、セッターもあるので、入力への対応も可能方法はこんな感じ
先週やった相互に値を影響し合える v-modelを使う!

セッターの例 halfWidthをユーザーフォームにバインディングする

width:<input v-model.number="width"> {{ width }}
<br>
halfWidth:<input v-model.number="halfWidth"> {{ halfWidth }}

セッターを利用するには以下の様にget,setの2つの名前決め打ちのプロパティを定義すること

new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    halfWidth: {
      // width の半分の値を取得する
      get: function() {
        return this.width / 2
      },
      // halfWidth の2倍の数値を width に代入する
      set: function(val) {
        this.width = val * 2
      }
    }
  }
})

これをデモすると、2つの入力フォームに数値が入っており、片方の値を入れると、もう片方の値に影響を与える事ができる!

halfWidth:プロパティ(内の各function)がしていること
getwidthの値を取得して
sethalfWidthの値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる

算出プロパティのキャッシュ機能

算出プロパティとメソッド(関数)の違い
算出プロパティ  リアクティブなデータをキャッシュ化してしまう。

<p>算出プロパティ キャッシュ化されるので一度しか処理が走らず同じ値が出力される</p>
<ol>
  <li>{{ computedData }}</li>
  <li>{{ computedData }}</li>
</ol>
<p>メソッド 毎回処理が走る為、値が変わる</p>
<ol>
  <li>{{ methodsData() }}</li>
  <li>{{ methodsData() }}</li>
</ol>
new Vue({
    el: '#app',
    computed: {
        computedData: function () { return Math.random() }
    },
    methods: {
        methodsData: function () { return Math.random() }
    }
})

ここがわかった上での使い分けが大事

リストの絞り込みに利用しよう

算出プロパティは内のfunctionは一度だけ処理されキャッシュ化されるので、複雑な処理に向いている
例えば絞り込みリスト等を作る際に便利

<div id="app">
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p>
  <ul>
    <!-- v-forでは最終結果、算出プロパティのlimitedを使用する -->
    <li v-for="item in limited" v-bind:key="item.id">
      {{ item.name }} {{ item.price }}円
    </li>
  </ul>
</div>
new Vue({
    el: '#app',
    data: {
        // フォームの入力と紐付けるデータ
        budget: 300,
        // 表示件数
        limit: 2,
        // もとになるリスト
        list: [
            { id: 1, name: 'りんご', price: 100 },
            { id: 2, name: 'ばなな', price: 200 },
            { id: 3, name: 'いちご', price: 400 },
            { id: 4, name: 'おれんじ', price: 300 },
            { id: 5, name: 'めろん', price: 500 }
        ]
    },
    computed: {
        // budget以下のリストを返す算出プロパティ
        matched: function () {
            return this.list.filter(function (el) {
                return el.price <= this.budget
            }, this)
        },
        // matchedで返ったデータをlimit件返す算出プロパティ
        limited: function () {
            return this.matched.slice(0, this.limit)
        }
    }
})

一回読み込んだ、mached処理のキャッシュの上にlimitedの処理が乗ってlimitedのみで算出の修正が行われている事になるらしい。

ソート機能を追加しよう

さらに書籍には 上記にソート機能を追加したコードがのっているが、最初動かなかった。 _.orderByってのが怪しんで、最初はタイポで本来どう書けばいいのか悩む。JSにはない関数でVue.jsの1.X系にはある?でも _.って何をしている事なのか不明だったが、よく見るとLodash入れろとあった…。だめじゃん、俺

ということでLodashを入れる。この1行追加

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <!-- 追加されたボタン -->
  <button v-on:click="order=!order">切り替え</button>
  <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p>
  <ul>
    <!-- v-forでは最終結果、算出プロパティのlimitedを使用する -->
    <li v-for="item in limited" v-bind:key="item.id">
      {{ item.name }} {{ item.price }}円
    </li>
  </ul>
new Vue({
  el: '#app',
  data: {
    // 追加
    // フォームの入力と紐付けるデータ
    budget: 300,
    limit: 2,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 400 },
      { id: 4, name: 'おれんじ', price: 300 },
      { id: 5, name: 'めろん', price: 500 }
    ]
  },
  computed: {
    // budget以下のリストを返す算出プロパティ
    matched: function() {
      return this.list.filter(function(el) {
        return el.price <= this.budget
      }, this)
    },
    // sortedを新しく追加   この _.orderBy ってのが Lodash のメソッドらしい
    // https://lodash.com/docs/#orderBy
    sorted: function() {
      return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc')
    },
    // limitedで使用するリストをsortedに変更
    limited: function() {
      return this.sorted.slice(0, this.limit)
    }
  }
})

Lodashの_.orderBy関数。

_.orderBy(collection, [iteratees=[_.identity]], [orders])

今回の例の第三引数はthis.order ? 'desc' : 'asc'三項演算子としている。

SECTION 17 ウォッチャでデータを監視して処理を自動化する

ウォッチャとは特定のデータ、算出プロパティの状態を監視して、データに変化があった際に処理を実行してくれるフック機能のこと
ウォッチャ自体は値を返さない

ウォッチャの使い方

コンポーネントwatchを用意して、監視対象の名前と、変化した際の処理を書く 第二引数は無くてもOK オプションを使用しない場合

new Vue({
  // ...
  watch: {
    監視するデータ: function (新しい値, 古い値) {
      // valueが変化したときに行いたい処理
    },
    'item.value': function (newVal, oldVal) {
      // オブジェクトのプロパティも監視できる
    }
  }
})

オプションを使用する場合

プロパティ 説明
deep bool ネストされたオブジェクトも監視
immdiate bool 読み込み時に即呼び出し
new Vue({
  // ...
  watch: {
    list: {
      handler: function (newVal, oldVal) {
        // listが変化したときに行いたい処理
      },
      // ここからがオプション
      deep: true,
      immediate: true
    }
  }
})

インスタンスメソッドでの登録

this.$watchと書くと各メソッド内でウォッチャが使える

this.$watch('value', function(newVal, oldVal) {
  // ...
})

こんな風に書く,オプションの位置が独特

created: function () {
    this.$watch('value', function () {
        // ...処理をかく
    }, {
        immediate: true,
        deep: true
    })
}

ウォッチャの解除

インスタンスメソッドで登録した場合、返り値を使って監視を解除できる

var unwatch = this.$watch('value', handler)
unwatch()  // value の監視を解除

一度だけ動作するウォッチャ

unwatchを利用する

new Vue({
  el: '#app',
  data: {
    edited: false,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
    ]
  },
  created: function() {
    var unwatch = this.$watch('list', function () {
      // listが編集されたことを記録する
      this.edited = true
      // 監視を解除
      unwatch()
    }, {
      deep: true
    })
  }
})

実行頻度の制御

フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeoutLodashなどを利用してウォッチャの実行頻度を制御すると良い。

watch: {
    //  "_." なのでLodashですね 指定ミリ秒後にコールバック呼び出しをする
    value: _.debounce(function (newVal) {
        // ここへコストの高い処理を書く
        console.log(newVal)
      },
      // valueの変化が終わるのを待つ時間をミリ秒で指定
      500)
  }

デモ、 最後のミリ秒経過後に、フォーム入力後の値がconsoleに出力される

<input type="text" v-model="value">
new Vue({
    el: '#app',
    data: {
        value: '編集してみてね'
    },
    watch: {
        value: _.debounce(function (newVal) {
            // ここへコストの高い処理を書く
            console.log(newVal)
        },
            // valueの変化が終わるのを待つ時間をミリ秒で指定
            1500)
    }
})

複数の値を監視する

インスタンスメソッドを使い監視対象を関数にして登録する。

this.$watch(function () {
    return [this.width, this.height]
}, function(){  // 上で言ってる関数ってここの funtionの事?
    // width , height が変化した際の処理
})

解らん場合はリファレンス
Vue.js インスタンスメソッド / データ

vm.$watch( expOrFn, callback, [options] )

今回は時間がないけど、余裕があるならリファレンスを書籍を行ったり来たりすると理解は進みそう。

オプションに登録する場合は、算出プロパティを監視することができる

computed: {
    watchTarget: function () {
        return [this.width, this.height]
    }
},
// ここからオプション
watch: {
    // これで算出プロパティを監視できる
    watchTarget: function() { ... }
}

POINT

大事そうだけど省略

フォームを監視してAPIからデータを取得しよう

GitHub APIからリポジトリ一覧を取得するかんたんなアプリのデモ
axiosAJAX(非同期通信)のgetでAPIからの値を取得して画面に表示している。

<div id="app">
  <select v-model="current">
    <option v-for="topic in topics" v-bind:value="topic.value">
      {{ topic.name }}
    </option>
  </select>
  <div v-for="item in list">{{ item.full_name }}</div>
</div>
new Vue({
  el: '#app',
  data: {
    list: [],
    current: '',
    topics: [
      { value: 'vue', name: 'Vue.js' },
      { value: 'jQuery', name: 'jQuery' }
    ]
  },
  watch: {
    current: function (val) {
      // GitHubのAPIからトピックのリポジトリを検索
      axios.get('https://api.github.com/search/repositories', {
        params: {
          q: 'topic:' + val
        }
      }).then(function (response) {
        this.list = response.data.items
      }.bind(this))
    }
  },
})

htmlのv-model="current"になんでもかんでも入れてる感じ?
開発コンソールを開きNetworkタブにあるgetパラメータをクリックすれば 取得した内容が見れる

要は以下の様なURLを生成している
https://api.github.com/search/repositories?q=topic:vue
https://api.github.com/search/repositories?q=topic:jQuery

Vue内のcurrent:では以下のGETのパラメータを生成している
q=topic:vue
q=topic:jQuery

で、帰ったJSONlistに入れて、html側でv-for="item in listのfor文で回して items内の配列の中に存在する項目full_nameをhtml側で表示している。

SECTION 18 フィルタでテキストの変換処理を行う

ここでいう「フィルタ」とは、文字数を丸める。数字にカンマを入れる。等のテキストベースの変換処理を指す。
注意: ローカルに登録した場合でもthisへのアクセスはできない

フィルタの使い方

{{ Mustache }} か、v-bindディレクティブに | で繋げて呼び出せる

<!-- Mustashのケース -->
{{ 対象のデータ | フィルタの名前 }}
<!-- v-bindのケース -->
<div v-bind:id="対象のデータ| フィルタの名前"></div>

ローカルへの登録

コンポーネントfiltersオプションに登録することで特定のコンポーネント内のみで仕様できる

new Vue({
  el: '#app',
  data: {
    price: 19800
  },
  filters: {
    localeNum: function (val) {
      return val.toLocaleString()
    }
  }
})
{{ price | localeNum }}円

こんな感じにつかえる
出力例 カンマ付き
19,800円

グローバルへの登録

グローバルメソッドVue.filterに登録すると全てのコンポーネントから利用できる

Vue.filter('localeNum' ,function(val){
    return val.toLocaleString()
})

フィルタに引数を持たせる

引数を持たせることもできる

{{ message | filter(foo, 100) }}

この例は
第一引数messageプロパティの値
第二引数fooプロパティの値
第三引数100が受け取れる

複数のフィルタをつなげて使用する

{{ value | filter1 | filter2 }}
new Vue({
  el: '#app',
  filters: {
    // 小数点以下を第2位に丸めるフィルタ
    round: function (val) {
      return Math.round(val * 100) / 100
    },
    // 度からラジアンに変換するフィルタ
    radian: function (val) {
      return val * Math.PI / 180
    }
  }
})
180 度は {{ 180 | radian | round }} ラジアンだよ

こんな風に180度のラジアンを出して、それを四捨五入表記にできる

SECTION 19 力スタムディレクティブでデータを監視しながらDOMを操作する

v-bindの様なディレクティブを自作できる機能、DOMのAPIを使いたいケース等で、DOMの状態を検知しながらDOM操作ができる様になる。仮想DOMではないので描画の最適化はされない。

カスタムディレクティブの使い方

こんな風に任意の名前を付けられる?

<div v-hoge>  </div>

トリガとなるデータを値としてバインドすることもできる

<!-- valueが変化すれば呼び出される -->
<div v-hoge="value">  </div>

ローカルへの登録

カスタムディレクティブはdirectives:オプションに登録すると特定のコンポーネントで使える

new Vue({
  el: '#app',
  directives: {
    focus: {
      // 紐付いている要素がDOMに挿入されるとき
      inserted: function (el) {
        el.focus() // 要素にフォーカスを当てる(文字入力待ち状態)
      }
    }
  }
})
<input type="text"><!-- フォーカスされない -->
<input type="text" v-focus><!-- フォーカスされる -->

上のデモをすると入力フォームの二番目に |の点滅が入り、文字入力受付状態が確認できる。

グローバルへの登録

グローバルメソッドに登録する方法はVue.directiveメソッドを使う
コンポーネント以外でどこでも使う汎用的なものとかを登録すると良いかも?)

Vue.directive('forcus', {
    inserted: function (el) {
        el.focus() // 要素にフォーカスを当てる(文字入力待ち状態)
    }
})

使用可能なフック

メソッド名 タイミング
bind ディレクティブが初めて要素と紐づいた時に
inserted 紐づいた要素が親Nodeに挿入された時に
update (仮想DOM更新時)紐づいた要素を包含しているコンポーネントのVNodeが更新された時に
componentUpdated (仮想DOM更新時)包含しているコンポーネントと子コンポーネントのVNodeが更新された時に
unbind 紐づいていた要素からディレクティブが削除される時に
Vue.directive('example', {
  // ディレクティブが初めて要素と紐づいた時に
  bind: function (el, binding) {
    console.log('v-example bind')
  },
  // 紐づいた要素が親Nodeに挿入された時に
  inserted: function (el, binding) {
    console.log('v-example inserted')
  },
  // 紐づいた要素を包含しているコンポーネントのVNodeが更新された時に
  update: function (el, binding) {
      console.log('v-example update')
  },
  // 包含しているコンポーネントと子コンポーネントのVNodeが更新された時に
  componentUpdated: function (el, binding) {
    console.log('v-example componentUpdated')
  },
  // 紐づいていた要素からディレクティブが削除される時に
  unbind: function (el, binding) {
    console.log('v-example unbind')
  }
})

フックの引数

vnodeから呼び出したコンポーネントを参照できるが難しいらしいので割愛。
『公式ガイド、仮想DOMを読め』とある

引数 内容
el 以下省略、書籍参照のこと
binding
vnode
oldVnode

フックの関数による省略方法

第二引数に関数を入れると bindupdate にフックして同じ処理を呼び出せる。

Vue.directive('example', function(el, vnode, oldVnode) {
    // bind と updateで呼び出される
}

動画の再生を操作する例

    <!-- 動画1 -->
    <video src="demo1.mp4" v-video="video1" width="400"></video>
    <button v-on:click="video1=true">再生</button>
    <button v-on:click="video1=false">停止</button>
    <br>
    <!-- 動画2 -->
    <video src="demo2.mp4" v-video="video2" width="400"></video>
    <button v-on:click="video2=true">再生</button>
    <button v-on:click="video2=false">停止</button>
new Vue({
    el: '#app',
    data: {
        video1: false,
        video2: false
    },
    directives: {
        video(el, binding) {
            binding.value ? el.play() : el.pause()
        }
    }
})

上記のコードをデモすると2つの動画をボタンで再生・停止できるようになる

基本的な読み方がいまだあいまいなので解説
html中のv-videoでバインドしたvideo1の値はまず,スクリプト内でfalseに設定されている video1: false,
再生ボタンを押すとvideo1の値がhtml側でtrueにされる

その値はスクリプト側のでdirectives:のhtmlと紐づけたvideo( ... )で監視されていて elで渡ったの引数に収納されたboolに対して、play(),またはpause()メソッドを実行している
という感じか?

ただし、この実装だと、video1のプロパティを変更すると、video2プロパティの要素のディレクティブも一緒に呼び出されるので、複雑な処理を書く際には不向きらしい。
次の方法はこれを改善できるという

前の状態と比較して処理を行う

力スタムディレクティブはメソッドの引数に古いVnodeと古いバインドデータを受け取れるので、バインドしたデータに変化があった際のみに処理を行わせることが可能。

arg 引数
modifiers 修飾子のオブジェクト
value 新しい値
odlvalue 古い値(updateとcomponetUpdateのみ)

では先ほどの動画デモを修正してoldValueプロパティの比較で無駄な処理を走らせないように改修したのが下の例

new Vue({
    el: '#app',
    data: {
        video1: false,
        video2: false
    },
    directives: {
        video(el, binding) {
            // ここに oldValue での判定を加えた
            if (binding.value !== binding.oldValue) {
                binding.value ? el.play() : el.pause()
            }
        }
    }
})

SECTION 20 nextTickで更新後のDOMにアクセスする

非同期処理が走ると、JSのコードの中では処理が遅れて、更新状態を判定できない事がままある。 (なので、非同期じゃないPHPなんかでPOSTとかすると、値が返るまでそこで処理が止まる。)
特にJSで非同期でDOM絡みの操作を書くと、非同期でPOST、GETをして値を取ったので、JS内で監視してても、それは先にプログラムが走り終わってるので、取得した値を元に処理ができない!
なんてことが発生してしまう。それを何とかしてくれるのがnextTickらしい
DOM更新を待ち受けて処理をしてくれるそうだ

nextTickの使い方

方法1, グローバルメソッドVue.nextTickのコールバック関数として定義する。
方法2, インスタンスメソッドとしてthis.$nextTickとして使う。

this.$nextTick(function(){
    // DOM更新後に行いたい処理を書く
})

これで、関数内の処理がDOM更新後に行うように予約できる

更新後のDOMの高さを取得しよう

例えば、プルダウンのリストの高さを通常のVue.js的な書き方で取得しようとした場合。 DOMへのアクセスをするなら$refsを使う

      console.log("通常:", this.$refs.list.offsetHeight);

CH2のおさらい ここから------
DOMにアクセスするには、インスタンスプロパティ$el,$refsを使用する。
但し、ライフサイクルフックのmounted以降でないと使えない

$refsの使い方

<div id="app">
    <p ref="hello">hello</p>
    <!--  p要素にhelloと名を付けた   -->
</div>

以下の様にアクセス

new Vue({
    el: '#app',
      mounted: function() {
      console.log(this.$refs.hello)  // p要素のDOMとなる
    }
})

$el や$refsは一時的な変更です!

これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意

CH2のおさらい ここまで------

でも、これだと処理の後にDOMが追加されてもその値を拾う事ができない。
こんなデモがある

new Vue({
  el: "#app",
  data: {
    list: []
  },
  watch: {
    list: function() {
      // 更新後のul要素の高さを取得できない…
      console.log("通常:", this.$refs.list.offsetHeight);
      // nextTickを使えばできる!
      this.$nextTick(function() {
        console.log("nextTick:", this.$refs.list.offsetHeight);
      });
    }
  }
});
<!-- list. の高さは最初は0なので数値1からを出力させる処理が,実はここに書いてある -->
<button v-on:click="list.push(list.length+1)">追加</button>
<ul ref="list">
    <li v-for="item in list">{{ item }}</li>
</ul>

開発タブでconsole見てボタンを押して<li>を追加しても 通常:の方の値は0となって取得できてない。が、$nextTickを使った方は上書きしたDOMからの値が取得できている

nextTickの注意点:
ウェブフォント、画像が含まれていた場合、それらのロード(詳細情報?)は持たないので、画像の高さは決め打ちにして計算して使う。といったテクニックが必要になる。ということらしいです。

まとめ

  • 算出プロパティは結果のキャッシュをしてくれる
  • データの状態に対して処理をしたいならウォッチャ
  • カスタムディレクティブは監視をしながらDOMの操作ができる
  • 更新後のDOMにアクセスしたいならnextTick