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

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

Laravelのブラウザtest duskとDBtestを混在させる場合に use RefreshDatabase;を使ってハマった話

Laravelでブラウザテストをしていますが、testの際のシナリオとして、以下の様な検証をするケースがありました。

  • DBに値を入れない状態でtest開始
  • いくつかのtestを行う。
  • あるtableにレコードを入れた状態で同様のtestを行い表示の確認

tableが0件の状態とレコードがある状態で表示が変わるので、その検証。という事です。

ところが、テストの各項目の都度、 setUp() メソッドに書かれた refresh:migrate, db:seed とかいちいちやっていると時間がかかるので、以前の記事にある様に、初回だけ やって、後は現状のDBを使ったまま破綻しないDB操作の手順を考えつつtestを書く。という事をしてました。

<?php
    protected static $db_inited = false;
    use RefreshDatabase;

    protected static function initDB()
    {
        Artisan::call('migrate:refresh');
       // 個別でシーディング
        Artisan::call('db:seed', ['--class' => 'CommunitiesTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'CommunitiesUsersStatusesTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'CommunityUserTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'MacAddressesTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'RolesTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'RoutersTableSeeder']);
        Artisan::call('db:seed', ['--class' => 'UsersTableSeeder']);
        // Tumolink Tableは後で検証するので今は使わない
        // Artisan::call('db:seed', ['--class' => 'TumolinkTableSeeder']);
    } 

    public function setUp()
    {
        parent::setUp();
        // 以前の記事にもある通り、testの初回だけシーディングを実施
        if (!static::$db_inited) {
            static::$db_inited = true;
            static::initDB();
        }
    }
    // 以下省略
}

さて、上記の様な検証をしようと思っていざ以下の様なtestを書いた所、DBに値が入らないままブラウザtestが実施されて散々悩みました。

<?php
    /**
     * @test
     */
    public function 未ログインで一覧画面表示のテスト()
    {
        // 検証用のデータを入れる
        factory(Tumolink::class)->create([
            'community_user_id' => 4,
        ]);
        factory(Tumolink::class)->create([
            'community_user_id' => 5,
        ]);
        factory(Tumolink::class)->create([
            'community_user_id' => 30,
        ]);
        // 入った検証データが表示される筈なので検証、しかしエラーとなる 
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
            ->assertSee('Tumolinkレコードが入った事で表示される文言');
        });
        // クエリも書かれずなぜかこのassertは通る
        $this->assertDatabaseHas('tumolink', ['community_user_id' => 30]);
    }

で、logを追うと、

2019-02-14T05:37:21.443929Z        90 Query     START TRANSACTION
2019-02-14T05:37:21.574578Z        90 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:37:21.575225Z        90 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (4, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21')
2019-02-14T05:37:21.575591Z        90 Close stmt
2019-02-14T05:37:21.576674Z        90 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:37:21.577048Z        90 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (5, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21')
2019-02-14T05:37:21.579742Z        90 Close stmt
2019-02-14T05:37:21.581257Z        90 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:37:21.582885Z        90 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (30, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21')
2019-02-14T05:37:21.583499Z        90 Close stmt
2019-02-14T05:37:23.829384Z        91 Connect   homestead@localhost on whois_test using TCP/IP
2019-02-14T05:37:23.832003Z        91 Query     use `whois_test`
2019-02-14T05:37:23.833594Z        91 Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:37:23.833821Z        91 Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:37:23.834181Z        91 Close stmt
2019-02-14T05:37:23.834538Z        91 Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:37:23.834770Z        91 Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:37:23.834991Z        91 Close stmt
2019-02-14T05:37:23.835343Z        91 Prepare   select `url_path` from `communities` where `url_path` = ? limit 1
2019-02-14T05:37:23.836422Z        91 Execute   select `url_path` from `communities` where `url_path` = 'hoge' limit 1
2019-02-14T05:37:23.836692Z        91 Close stmt
2019-02-14T05:37:24.005278Z        91 Prepare   select * from `communities` where `url_path` = ? limit 1
2019-02-14T05:37:24.005837Z        91 Execute   select * from `communities` where `url_path` = 'hoge' limit 1
2019-02-14T05:37:24.006340Z        91 Close stmt
2019-02-14T05:37:24.046781Z        91 Prepare   select `user_id` from `communities` where `id` = ?
2019-02-14T05:37:24.047358Z        91 Execute   select `user_id` from `communities` where `id` = 1
2019-02-14T05:37:24.047703Z        91 Close stmt
2019-02-14T05:37:24.081444Z        91 Prepare   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?)
2019-02-14T05:37:24.081937Z        91 Execute   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 1)
2019-02-14T05:37:24.082748Z        91 Close stmt
2019-02-14T05:37:24.116417Z        91 Prepare   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?)
2019-02-14T05:37:24.116823Z        91 Execute   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 0)
2019-02-14T05:37:24.117543Z        91 Close stmt
2019-02-14T05:37:24.153092Z        91 Prepare   select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> ? and `community_id` = ? and `hide` = ?)
2019-02-14T05:37:24.153243Z        91 Execute   select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> 1 and `community_id` = 1 and `hide` = 0)
2019-02-14T05:37:24.153519Z        91 Close stmt
2019-02-14T05:37:24.178974Z        91 Prepare   select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = ?) and `community_user`.`user_id` in (?, ?, ?, ?, ?) order by `last_access` desc
2019-02-14T05:37:24.179504Z        91 Execute   select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = 1) and `community_user`.`user_id` in (4, 9, 10, 11, 12) order by `last_access` desc
2019-02-14T05:37:24.180164Z        91 Close stmt
2019-02-14T05:37:24.204641Z        91 Prepare   select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = ?
2019-02-14T05:37:24.205081Z        91 Execute   select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = 1
2019-02-14T05:37:24.205533Z        91 Close stmt
2019-02-14T05:37:24.925167Z        91 Quit
2019-02-14T05:37:25.928286Z        90 Query     ROLLBACK
2019-02-14T05:37:25.933217Z        90 Quit

DBに検証用の値を入れるのは確認できます。その後、該当ページを表示して検証する際に呼ばれるクエリを読んだ直後にROLLBACKが走っています。
そしてそのあとに本来であれば、DBの値を検証する

$this->assertDatabaseHas('tumolink', ['community_user_id' => 30]);

に相当するクエリが走るべきなのですが、これをlogで確認できないまま、次のtestのクエリが走っていました。

で、色々なやんだ結果testクラスの上に書くこいつを消したところ上手くいきました。

// use RefreshDatabase;

SQL LOG

2019-02-14T05:45:25.585096Z       124 Connect   homestead@localhost on whois_test using TCP/IP
2019-02-14T05:45:25.585600Z       124 Query     use `whois_test`
2019-02-14T05:45:25.585867Z       124 Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:45:25.586105Z       124 Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:45:25.586397Z       124 Close stmt
2019-02-14T05:45:25.586579Z       124 Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:45:25.586777Z       124 Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:45:25.586958Z       124 Close stmt
2019-02-14T05:45:25.587218Z       124 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:45:25.587477Z       124 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (4, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25')
2019-02-14T05:45:25.591928Z       124 Close stmt
2019-02-14T05:45:25.592896Z       124 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:45:25.593254Z       124 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (5, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25')
2019-02-14T05:45:25.594983Z       124 Close stmt
2019-02-14T05:45:25.595811Z       124 Prepare   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?)
2019-02-14T05:45:25.596246Z       124 Execute   insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (30, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25')
2019-02-14T05:45:25.597019Z       124 Close stmt
2019-02-14T05:45:27.846052Z       125 Connect   homestead@localhost on whois_test using TCP/IP
2019-02-14T05:45:27.848972Z       125 Query     use `whois_test`
2019-02-14T05:45:27.850521Z       125 Prepare   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:45:27.850730Z       125 Execute   set names 'utf8mb4' collate 'utf8mb4_unicode_ci'
2019-02-14T05:45:27.850912Z       125 Close stmt
2019-02-14T05:45:27.851136Z       125 Prepare   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:45:27.851316Z       125 Execute   set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
2019-02-14T05:45:27.851492Z       125 Close stmt
2019-02-14T05:45:27.851773Z       125 Prepare   select `url_path` from `communities` where `url_path` = ? limit 1
2019-02-14T05:45:27.852839Z       125 Execute   select `url_path` from `communities` where `url_path` = 'hoge' limit 1
2019-02-14T05:45:27.853121Z       125 Close stmt
2019-02-14T05:45:28.046130Z       125 Prepare   select * from `communities` where `url_path` = ? limit 1
2019-02-14T05:45:28.046618Z       125 Execute   select * from `communities` where `url_path` = 'hoge' limit 1
2019-02-14T05:45:28.047275Z       125 Close stmt
2019-02-14T05:45:28.091173Z       125 Prepare   select `user_id` from `communities` where `id` = ?
2019-02-14T05:45:28.091653Z       125 Execute   select `user_id` from `communities` where `id` = 1
2019-02-14T05:45:28.092026Z       125 Close stmt
2019-02-14T05:45:28.127509Z       125 Prepare   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?)
2019-02-14T05:45:28.128013Z       125 Execute   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 1)
2019-02-14T05:45:28.128830Z       125 Close stmt
2019-02-14T05:45:28.161408Z       125 Prepare   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?)
2019-02-14T05:45:28.162015Z       125 Execute   select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 0)
2019-02-14T05:45:28.162965Z       125 Close stmt
2019-02-14T05:45:28.188154Z       125 Prepare   select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> ? and `community_id` = ? and `hide` = ?)
2019-02-14T05:45:28.188561Z       125 Execute   select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> 1 and `community_id` = 1 and `hide` = 0)
2019-02-14T05:45:28.189100Z       125 Close stmt
2019-02-14T05:45:28.231102Z       125 Prepare   select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = ?) and `community_user`.`user_id` in (?, ?, ?, ?, ?) order by `last_access` desc
2019-02-14T05:45:28.231659Z       125 Execute   select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = 1) and `community_user`.`user_id` in (4, 9, 10, 11, 12) order by `last_access` desc
2019-02-14T05:45:28.232347Z       125 Close stmt
2019-02-14T05:45:28.258731Z       125 Prepare   select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = ?
2019-02-14T05:45:28.259224Z       125 Execute   select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = 1
2019-02-14T05:45:28.260029Z       125 Close stmt
2019-02-14T05:45:29.048109Z       125 Quit
2019-02-14T05:45:29.981275Z       124 Prepare   select count(*) as aggregate from `tumolink` where (`community_user_id` = ?)
2019-02-14T05:45:29.981864Z       124 Execute   select count(*) as aggregate from `tumolink` where (`community_user_id` = 30)
2019-02-14T05:45:29.982288Z       124 Close stmt

‘use RefreshDatabase‘が無いため当然ROLLBACKはかかりません。また、DBの値を検討する以下のクエリもlogの最後の方に見られます。

2019-02-14T05:45:29.981864Z       124 Execute   select count(*) as aggregate from `tumolink` where (`community_user_id` = 30)

RefreshDatabase の動きですが、有志作成の日本語リファレンスでは以下の様にあります。

https://readouble.com/laravel/5.5/ja/database-testing.html

各テスト後のデータベースリセット 前のテストがその後のテストデータに影響しないように、各テストの後にデータベースをリセットできると便利です。インメモリデータベースを使っていても、トラディショナルなデータベースを使用していても、RefreshDatabaseトレイトにより、マイグレーションに最適なアプローチが取れます。テストクラスてこのトレイトを使えば、全てが処理されます。

ところが実際に動かしてSQLのLOGを見ると、どうもtestのfunction単位でrollbackが発生するのでは無いのは明らかです。

このような動きの為、ブラウザの表示をして確認が取れる前に rollback が走ってせっかく挿入したレコードが消え、その後ブラウザの表示が完了して検証をする。といった動作の為、testが失敗する様です。

<?php
    public function 未ログインで恵比寿_滞在者一覧画面閲覧_ツモリスト有り()
    {
        factory(Tumolink::class)->create([
            'community_user_id' => 30,
        ]);
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
            ->assertSee('Tumolinkレコードが入った事で表示される文言');
        });
        // RefreshDatabase を使うとここでrollbackが発生した上
        // select count(*) as aggregate from `tumolink` where (`community_user_id` = 30) のクエリも走らず次のtestに行く
        $this->assertDatabaseHas('tumolink', ['community_user_id' => 30]);
    }

ではなぜ?という細かい所までは追ってませんが、ひとまずこんなハマり所があるので気を付けましょう。という話でした。

【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ

以下の記事は2019/2/14 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch3 イベントとフォーム入力 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER3 イベントとフォーム入力の受け取り のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

CHAPTER3 イベントとフォーム入力の受け取り

ちなみに Weeybleで去年同じ本の輪読会で使ったドキュメント、およびソースコードは以下にありました。

GitHub yasugahira0810/vuejs_chapter3

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

これまでの輪読会資料

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

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

SECTION 13 イベントハンドリング

イベントハンドラ
これまでのサンプルのボタンに出てた v-onの事をここでは解説する

イベントに紐づける処理の内容をこの本では「イベントハンドラと呼び
イベントハンドラとイベントを紐づけることを「ハンドル」と呼ぶ
イベントはmousewheelIE9では動かないものもあるので注意

<button v-on:click="doRemove(index)">モンスターを削除</button>

@で記述も可能

<button @click="doRemove(index)">モンスターを削除</button>
new Vue({
    el: '#app',
    data: {
    },
    methods: {
        doRemove: function (index) { // ボタンクリックでこの処理が走る
            this.list.splice(index, 1)
        }
    }
})

click同様ブラウザが対応していれば以下のイベントも使える

  • scroll
  • mousewheel

フォーム入力の取得

v-onディレクティブで入力内容を確認してからデータに代入することができる

<input v-bind:value="message" v-on:change="handleInput">
new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue.js',
    },
    methods: {
        handleInput: function (event) {
            // 代入前に何か処理を行う…
            // バリデード処理とかできるのかな?
            this.message = event.target.value
        }
    }
})

イベント修飾子

click などのDOMのふるまいを変更する

  • .stop event.stopPropagation(); イベント伝播,バブリングを止める
  • .prevment event.preventDefault(); 禁止操作の指定 リンク操作、submitの処理をキャンセル
  • .capture キャプチャーモードDOMイベントをハンドルする
  • .self
  • .native
  • .once
  • .passive { passive: true } でDOMイベントはハンドルする

クリックイベント マウスボタンを指定できる

  • .left
  • .right
  • .middle

作例

<!-- いずれもhandlerメソッドでマウス右クリックでconsole.logにmouse event のlogを出力する -->

<div v-on:click.right="handler">example</div>
<!-- こっちは`.prevment`修飾子で右クリックメニューの表示を禁止している -->
<div v-on:click.right.prevent="handler">example</div>
new Vue({
    el: '#app',
    methods: {
        handler: function (comment) {
            console.log(comment)
        }
    }
})

Extra DOMイベント伝播,バブリングについて

そもそもJavaScriptのバブリングの概念を知っておく必要あり
DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017

その上でイベント修飾子を付与することでバブリングを制御できる。

入れ子のDOMイベントの発生順序は JSの addEventListenerには第三引数に省略可能でデフォルトはfalseの値がある。
これをuseCaptureという

<div id="outer">
    <div id="inner" align="center"></div>
</div>
function out(s) {return function() {console.log(s);}}
document.getElementById('outer').addEventListener('click', out('outer'), false); // ←コレ
document.getElementById('inner').addEventListener('click', out('inner'));

#outer,.addEventListenerの第三引数を false または省略した場合、innerのイベントが先に発火する。
逆にtureにすればouterが先に発火する

結果として、これを理解していないでアッチコッチにイベント仕込むと、親要素にイベントが伝播しまくって困った事になるらしい。
そこでVue.jsでは前述のイベント修飾子を使って伝播の制御を行う、という事らしい

では以下のJSを元に書くイベント修飾子の動きを確認してゆく

new Vue({
    el: '#app',
    methods: {
        handler: function (comment) {
            console.log(comment)
        }
    }
})

.stop

event.stopPropagation(); イベント伝播,バブリングを止める
div2クリックでdiv2のみが出力

<div v-on:click="handler('div1')">
  div1
  <a href="#top" v-on:click.stop="handler('div2')">div2</a>
</div>

.prevent

event.preventDefault(); 禁止操作の指定 リンク操作、submitの処理をキャンセル
div2クリックでdiv2,div1と出力(操作の禁止をするのみなので伝播は通常通り起こるという事?)

<div v-on:click="handler('div1')">
  div1
  <a href="#top" v-on:click.prevent="handler('div2')">div2</a>
</div>

.capture

キャプチャーモードでイベントを発生させる バブリングモードのイベントよりも先に発生する
div3クリックでdiv1,div3,div2の順で出力

<div v-on:click.capture="handler('div1')">
  div1
  <div v-on:click="handler('div2')">
    div2
    <div v-on:click="handler('div3')">div3</div>
  </div>
</div>

.self

evant.targetが自分自身の時だけハンドラが呼び出される

<div class="overlay" v-on:click.self="close">div</div>

.native

直接イベントを発火させたい場合に使う 詳細はCAPTER5に

<!-- コンポーネントをクリックするとハンドラが呼び出される -->
<my-component v-on:click.native="handler"></my-component>
<!-- コンポーネントをクリックしてもハンドラは呼び出されない -->
<my-component v-on:click="handler"></my-component>

.passive

event.prevmentDefault()を呼び出さない事を明示的にする
.preventとの併用はNG
モバイル環境でのスクロールカク追記を防ぐ等に使用

キー修飾子

キーボード入力時に呼び出される様になる修飾子,キーコードか、キー指定でもOK

<!-- どちらもEnterキーを表す -->
<input v-on:keydown.13="handler">
<input v-on:keydown.enter="handler">

システム修飾子

キーが押されている場合のみハンドラが呼び出される
以下はshiftキーの例

<button v-on:click.shift="doDelete"></button>

その他詳細はVue.js公式ガイド「イベントハンドリング」「システム修飾子キー」を参照のこと

SECTION14 フォーム入力バインディング

フォームの入力や選択値を、データを同期する 「双方向データバインディング にはv-modelディテクティブを使う

v-modelの使い方

テキストフォームをmessageプロパティとバインディングした例

<div id="app">
    <input v-model="message">
    <p>{{ message }}</p>
</div>
new Vue({
  el: '#app',
  data: {
    message: 'Hello!'
  }
})

Vue.jsの双方向データディバイディング

入力した文字をデータに反映したい場合は、入力イベントをハンドルして取得したデータをリアクティブデータに代入する必要がある。

this.message = event.taget.value // ここでデータが書き換わる

一連の例に出て来るmessageを使用した処理は良く行われる

v-modeディレクティブはDOMのデータバインディングと要素から取得したデータをリアクティブにするための鉄板構文らしい。

v-modelで受け取りデータの型

基本、入力フォームは文字列型、複数選択フォームは配列型となるが、値にデータバインディングを使用した場合、値の型はバインドされているデータによって変わる。

複数行テキスト

文字列となる。

<textarea v-model="message"></textarea>
<pre>{{ message }}</pre>
new Vue({
  el: '#app',
  data: {
    message: 'Hello!'
  }
})

チェックボックス

単数の場合、は単純に bool

<label>
  <input type="checkbox" v-model="val"> {{ val }}
</label>
new Vue({
  el: '#app',
  data: {
    val: true
  }
})

複数要素は配列、各要素にvalue属性を設定

<label><input type="checkbox" v-model="val" value="A"> A</label>
<label><input type="checkbox" v-model="val" value="B"> B</label>
<label><input type="checkbox" v-model="val" value="C"> C</label>
<p>{{ val }}</p>
new Vue({
  el: '#app',
  data: {
    val: []
  }
})

AとCの選択では ["A", "C"]となる

ラジオボタン

デフォルトは文字列

<label><input type="radio" value="a" v-model="val"> A</label>
<label><input type="radio" value="b" v-model="val"> B</label>
<label><input type="radio" value="c" v-model="val"> C</label>
<p>{{ val }}</p>
new Vue({
  el: '#app',
  data: {
    val: ''
  }
})

セレクトボックス

単一選択プルダウン形式
デフォルト文字列

<select v-model="val">
  <option disabled="disabled">選択してください</option>
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>
<p>{{ val }}</p>
new Vue({
  el: '#app',
  data: {
    val: ''
  }
})

複数選択リスト形式

<select v-model="val" multiple>
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>
<p>{{ val }}</p>
new Vue({
  el: '#app',
  data: {
    val: []
  }
})

AとCの選択では ["A", "C"]となる

画像ファイル

v-modelは使用できない。リアクティブにするならchangeイベントをハンドルする

<input type="file" v-on:change="handleChange">
<div v-if="preview"><img v-bind:src="preview"></div>
new Vue({
  el: '#app',
  data: {
    preview: ''
  },
  methods: {
    handleChange: function (event) {
      var file = event.target.files[0]
      if (file && file.type.match(/^image\/(png|jpeg)$/)) {
        this.preview = window.URL.createObjectURL(file)
      }
    }
  }
})

画像選択するとプレビューが出る!カッコイイ!

その他入力タイプ

range,colorHTML5の入力タイプも使える

横スライドレンジの数値が出る奴

<input type="range" v-model.number="val">{{ val }}
new Vue({
  el: '#app',
  data: {
    val: 50
  }
})

修飾子

v-modelにくっつく奴

修飾子 作用
.lazy inputの代わりにchangeイベントはハンドルする
.number 値を数値に変換する
.trim 値の余分なスペースを削除する

.numberの使用例
テキストフォームに入った値はtype="number"としても文字列となる。
だが、これで数値として取得することが出来る。

<input type="text" v-model.number="price"> {{ price }}
new Vue({
el: '#app',
    data: {
    price: 50
    }
})

SECTION 15 マウント要素外のイベントと操作

v-onはDOMのwindow,bodyでは使用できない為、それらを扱いたい場合はJS純正のaddEventLisnerメソッドを使う事になる。注意点として、不要になっても自動的に解除されないので、不要になった際はフック(ライフサイクルフックCAPTER1の最後の奴)を使って解除する必要がある。

スクロールイベントの取得

発生頻度の高いイベント等は、タイマーを使用して処理の実行頻度を抑えると良い。
以下はwindowのスクロールイベントを200ms間隔でwindow.scrollYプロパティを更新する例
これを応用して、サイドバーを画面に常に固定したり、スクロールすると表示を変化させるメニュー等に使用可能。

<header v-bind:class="{ compact: scrollY > 200 }">
  200pxより下にスクロールしたら .compact を付与する
</header>
new Vue({
  el: '#app',
  data: {
    scrollY: 0,
    timer: null
  },
  created: function () {
    // ハンドラを登録
    window.addEventListener('scroll', this.handleScroll)
  },
  // CAPTER1最後のライフサイクルダイアグラムの beforeDestroy
  beforeDestroy: function () {
    // ハンドラを解除(コンポーネントやSPAの場合忘れずに!)
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    // 違和感のない程度に200ms間隔でscrollデータを更新する例
    handleScroll: function () {
      if (this.timer === null) {
        this.timer = setTimeout(function () {
          this.scrollY = window.scrollY
          clearTimeout(this.timer)
          this.timer = null
        }.bind(this), 200)
      }
    }
  }
})

bodyに適当な要素を縦長に書いてから開発コンソールのElementsを開いて確認してみる
スクロール前

<header class="">
  200pxより下にスクロールしたら .compact を付与する
</header>

スクロール後

<header class="compact">
  200pxより下にスクロールしたら .compact を付与する
</header>

当然、JS内の最後の値200の数値を大きくすると反応はニブくなる。

スムーススクロールの実装

よくある『ページTOP』で滑らかに移動する奴はwindowオブジェクトを操作している、ライブラリを使えば簡単に実装できる、ここでは「Smooth Scroll」を使った例を示す。
GitHub Smooth Scroll

<script src="https://cdn.jsdelivr.net/npm/smooth-scroll@12.1.5"></script>
<div id="app">
  <div class="content">...</div>
  <div v-on:click="scrollTop">
    ページ上部へ移動
  </div>
</div>
// ここでSmoothScrollを変数に入れている
var scroll = new SmoothScroll()
new Vue({
    el: '#app',
    methods: {
        scrollTop: function () {
            // scrollTopのバインドでSmoothScrollのメソッド animateScroll() を呼んでいる
            // 引数に画面最上部からの位置を指定できる
            scroll.animateScroll(0)
        }
    }
})

COLUMN Vue.js以外からのイベントの読み取り

プラグインの実装等で、Vue.js以外のDOM操作ライブラリを使わざるを得ない場合、JSのdispatchEventを使ってイベント検知が出来る

以下はjQueryvalメソッドと絡めた例

<div id="app">
  <input id="message" v-on:input="handleInput">
  <button data-update="jQuery!">jQueryからの更新</button>
</div>
<!-- html内でjQueryのCDNを別途読み込むこと -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>  
$(document).on('click', '[data-update]', function () {
    $('#message').val($(this).attr('data-update'))
    // 入力値を更新したらイベントを発生させる
    $('#message')[0].dispatchEvent(new Event('input'))
})
new Vue({
    el: '#app',
    methods: {
        handleInput: function (event) {
            console.log(event.target.value)
        }
    }
})

まとめ

Laravelのブラウザテストでtest用DBを使う際はコマンドに注意

短めですが、ブラウザテストの際の注意点。
以下のサイトにもあるような設定をしてから、テスト用のDBに切り替えて自動ブラウザテストが行われる様に諸々設定をしていたんですが...

Laravel5.6 テスト用データベースを作成してテストを実行するための設定方法

mysqlのlogを調べた所。なぜかArtisanコマンドはtest用のDBでシーディングをしているにも関わらず、いざブラウザテストとなると、ローカルの通常のDBを見てtestをしているようなのです。

原因はtest実施の際のコマンドでした、以下じゃだめです。

 ./vendor/bin/phpunit tests/Browser/IndexTest.php

ちゃんと duskのコマンドでやりましょう。

 php artisan dusk tests/Browser/IndexTest.php

ユニットテストとブラウザテストは別物、と意識した方が良いですね。

基礎から学ぶVue.js CHAPTER2 データの登録と更新 読書メモ

今回は輪読担当ではありませんが、ひとまずメモをまとめたのでアップします。

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

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js


CAPTER2

SECTION 07 基本データのバインディング

Mustache(マスタッシュ記法) hogeプロパティをhtmlにバインドする

<p>{{ hoge }}</p>

バインドは属性に使えない
下はエラー

<input type="text" value="{{ message }}">

これが正しい
属性へのバインドはv-bindディレクティブを使う

<input type="text" v-bind:value="message">
<!-- 省略して書くとこう -->
<input type="text" :value="message">

cssスタイルはキャメルケースで指定

<button v-on:click="isActive=!isActive">isActiveを切り替える</button>
<p v-bind:class="{ child: isChild, 'is-active': isActive }" class="item">
  動的なクラス
</p>
<p v-bind:style="{ color: textColor, backgroundColor: bgColor }" class="item">
  動的なスタイル
</p>
new Vue({
    el: '#app',
    data: {
        isChild: true,
        isActive: true,
        textColor: 'red',
        bgColor: 'lightgray'
    }
})
.item {
  padding: 4px 8px;
  transition: background-color 0.4s;
}
.is-active {
  background: #ffeaea;
}

オブジェクトデータで渡す方法
Vue.js側でひとまとめで定義

<p v-bind:class="classObject">Text</p>
<p v-bind:class="classObject_2">Text</p>
new Vue({
    el: '#app',
    data: {
        classObject: {
            isChild: true,
            isActive: true,
            textColor: 'red',
            bgColor: 'lightgray'
        },
        classObject_2: {
            isChild: true,
            isActive: true,
            textColor: 'red',
            bgColor: 'lightgray'
        }
    }
})

複数の属性のデータバインディング

沢山のプロパティがあっても

new Vue({
    el: '#app',
    data: {
        item: {
            id: 1,
            src: 'item1.jpg,
            alt: 'サムネ画像です',
            width: 200,
            height: 100,
        }
    }
})

まとめて定義できる

<img v-bind="item">

特定の要素のみに変更を加えることも可能

<img v-bind="item" v-bind:id="'thunb-' + item.id">
<!-- thunb-1 の id が付与される -->

SVGのデータバインディングベクター画像)

<div id="app">
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
    <circle cx="100" cy="75" v-bind:r="radius" fill="lightpink" />
  </svg>
  <input type="range" min="0" max="100" v-model="radius">
</div>
new Vue({
    el: '#app',
    data: {
        radius: 50
    }
})

SECTION 09 テンプレートにおける条件分岐

v-if``v-showディレクディブは付与した要素の描画・表示に条件を適用する。

okプロパティがtrueの時のみdiv要素を表示する

<div v-if="ok">hoge</div>
<div v-show="ok">hoge</div>
new Vue({
    el: '#app',
    data: {
        ok: false
    }
})

条件を満たさない場合に生成されるhtml display:none となる

<div style="display: none;">hoge</div>

v-if と v-show の違いと使い分け

v-if の場合

DOMレベルでない事になる

v-show の場合

display:none が付与される
切り替え頻度が高いならこっちが処理早い

タグによるv-if グループ化

複数の要素を if で切り替えたい場合グループ化できる

<tamplate>
    <header>title</header>
    <div><contents/div>
</tamplate>

v-else-if 及び v-else によるグループ化

<div v-if="type === 'A'">AAA</div>
<div v-else-if="type === 'B'">BBB</div>
<div v-else>どちらでも無い場合</div>

v-else-if v-else key

keyを設定して属性の重複による発動しない状態を回避する

<!-- 2つのdivが違う要素である事を明示的にする -->
<div v-if="loaded" key="content-visible">
  content
</div>
<div v-else key="content-loading">
  loading now...
</div>

SECTIOM 10 リストデータの表示と更新

要素を繰り返して描画する

みたまんま、こんな感じで繰り返し描画できる。
v-for="item in list"php や JS のfor 文や foreach と同じ様に使える

  <ul>
    <li v-for="item in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    }
})

出力結果

<ul>
    <li>ID.1 スライム HP.100</li>
    <li>ID.2 ゴブリン HP.200</li>
    <li>ID.3 ドラゴン HP.500</li>
</ul>

インデックスとオブジェクトキーの使用

変数部分をカッコで囲んで配列インデックスを任意に受け取れる

<li v-for="(item, index ) in list"> ...</li>

オブジェクトなら「値」「キー」「インデックス」の順で任意に受け取れる

<li v-for="(item, key, index ) in list"> ...</li>

キーの役割

これ大事! v-bind:key="item.id"

<li v-for="item in list" v-bind:key="item.id">

要素にユニークなキー属性を追加するのが望ましい。ほぼ必須と考えて良い。
キーが無いと要素全部の更新が入る。なのでSQLidを入れる位に考えると良い。

繰り返し描画しながら様々な条件を適用する

v-if を絡めて、比較演算子で条件付けて、特定条件での表示などもできる。

  <ul>
    <li v-for="item in list" v-bind:key="item.id" v-bind:class="{ tuyoi: item.hp > 300 }">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <span v-if="item.hp > 300">つよい!</span>
    </li>
  </ul>

出力結果

<ul>
    <li>ID.1 スライム HP.100</li>
    <li>ID.2 ゴブリン HP.200</li>
    <li>ID.3 ドラゴン HP.500<span>つよい!</span></li>
</ul>

リストの更新

注意点として以下のケースで更新を検知できない

  1. インデックス数値を使った配列要素の更新
  2. 後から追加されたプロパティの更新

リストに要素を追加

push,unshiftを使う

this.list.push(要素)

以下、サンプル、ボタン押下でフォーム内の名前のモンスターがリストに追加される。IDは自動生成。HPは500固定

  <!-- このフォームの入力値を新しいモンスターの名前に使う -->
  名前
  <input v-model="name">
  <button v-on:click="doAdd">モンスターを追加</button>
  <ul>
    <li v-for="item in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        name: 'キマイラ',
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 追加ボタンをクリックしたときのハンドラ
        doAdd: function () {
            // リスト内で1番大きいIDを取得
            var max = this.list.reduce(function (a, b) {
                return a > b.id ? a : b.id
            }, 0)
            // 新しいモンスターをリストに追加
            this.list.push({
                id: max + 1, // 現在の最大のIDに+1してユニークなIDを作成
                name: this.name, // 現在のフォームの入力値
                hp: 500
            })
        }
    }
})

リストから削除する

リストからの削除は配列メソッドのspliceを使う

li毎に削除ボタンが表示され、クリックで対象を消せる

  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <!-- 削除ボタンをv-for内に作成 -->
      <button v-on:click="doRemove(index)">モンスターを削除</button>
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 要素を削除ボタンをクリックしたときのハンドラ
        doRemove: function (index) {
            // 受け取ったインデックスの位置から1個要素を削除
            this.list.splice(index, 1)
        }
    }
})

リスト要素( <li>hoge</li> )に関しては以下の様な配列メソッドを使用して操作が可能

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

これは駄目

this.list[0] = { id: 1, name: 'hoge', hp: 500 }

これなら大丈夫

Vue.setメソッドを使用して明示的に更新できる。エイリアスthis.$setとなる

this.$set(更新するデータ , インデックスorキー , { 新しい値 })

上記からの具体例だとこんな感じ

this.$set(this.list, 0, { id: 1, name: 'hoge', hp: 500 })

プロパティを追加する

this.$setメソッドは持ってないプロパティをリアクティブデータとして追加するために使用できる。

new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    created: function() {
        // すべての要素にactiveプロパティを追加したい
        this.list.forEach(function(item) {
            this.$set(item, 'active', false)
            // 「item.active = false」ではリアクティブにならない
        }, this)
    }
})

リスト要素プロパティを更新する

プロパティ hp を更新する 作例

  <ul>
      <li v-for="(item, index) in list" v-bind:key="item.id" v-if="item.hp">
          ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <span v-if="item.hp < 50">瀕死!</span>
      <!-- ボタンはv-for内に作成 -->
      <button v-on:click="doAttack(index)">攻撃する</button>
    </li>
  </ul>

liの最後にあるv-if="item.hp"は hpが0になると消える処理になる。
<span v-if="item.hp < 50">瀕死!</span>は hp 50未満で表示される。

new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 攻撃ボタンをクリックしたときのハンドラ
        doAttack: function (index) {
            this.list[index].hp -= 10 // HPを減らす
        }
    }
})

ユニークキーを持たない配列

出来ない訳ではない、簡易にする際はこれでもOK

<option v-for="item in list">{{ item }}</option>
data: {
    list: ['aaa', 'bbb', 'ccc']
}

オプションにデータを持たないv-for

v-forに数値をセットすると以下の例の様にspanで囲まれた1~15の値を出力できる

<span v-for="item in 15">{{ item }}</span>

同様に 1,5,10,15 の4つを出力する

<span v-for="item in [1, 5, 10, 15]">{{ item }}</span>

文字列に対するv-for

文字列にv-for を使うと1文字ずつ別々の要素で描画される

<span v-for="item in text">{{ item }}</span>
new Vue({
    el: '#app',
    data: {
        text: 'hoge'
    }
})

出力結果

<span>h</span>
<span>o</span>
<span>g</span>
<span>e</span>

これを利用するとテキストアニメーションが作れるらしい

外部からデータを取得する

外部データはJSONやWebAPIで取得する必要がある。
JSONを外部データから取り込んでみる。
htmlの下の方にある<script>内に javascriptライブラリのaxiosCDNを読み込む1行を追加する。

  <script src="https://cdn.jsdelivr.net/npm/axios@0.17.1/dist/axios.min.js"></script>

これでAJAXが使える様になる

  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        // あらかじめ空リストを用意しておく
        list: []
    },
    created: function () {
        axios.get('list.json').then(function (response) {
            // 取得完了したらlistリストに代入
            this.list = response.data
        }.bind(this)).catch(function (e) {
            console.error(e)
        })
    }
})

ライフサイクルフックの created を使って new 直後にjsonを非同期で取り込む、取り込む前は data.list の空配列[]が一瞬だが、適用されている。ここが表示されるまでに、ローディングアニメーション処理とか入るとカッコよくなる。

JSONファイル list.json

[
  { "id": 1, "name": "スライム", "hp": 100 },
  { "id": 2, "name": "ゴブリン", "hp": 200 },
  { "id": 3, "name": "ドラゴン", "hp": 500 }
]

SECTION 11 DOMを直接参照する $el と $refs

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

$el の使い方

テンプレートを囲んでいるルート要素は$el を使って参照できる。
例えば<canvas>要素などにアクセスしたい時などに使用する。

var app = new Vue({
  el: '#app'
      mounted: function() {
      console.log(this.$el)  // <div id="app"></div>
  }
})

$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ではないので描画処理の最適化をしない
操作の都度描画するので注意

<div id="app">
  <button v-on:click="handleClick">カウントアップ</button>
  <button v-on:click="show=!show">表示/非表示</button>
  <span ref="count" v-if="show">0</span>
</div>
new Vue({
  el: '#app',
  data: {
    show: true
  },
  methods: {
    handleClick() {
      var count = this.$refs.count
      if (count) {
        count.innerText = parseInt(count.innerText, 10) + 1
      }
    }
  }
})

カウントアップに対して、表示・非表示ボタンがあるが、count up した状態で、非表示・再表示すると、カウントが0にもどってしまう。これは、DOMに対して加算をしたのみなので、DOMが消えると、値も消えてしまうため。 vue.js での指定なら仮想DOMなのでこうはならない。

SECTION12 テンプレート制御ディレクティブ

ディレクティブ 作用
v-pre テンプレートのコンパイルをスキップする XSS対策に有効
v-once 一度だけバインディングを行う
v-text Mustash {{ }} の代わりにテキストコンテンツを描画
v-html HTMLタグをそのまま描画する
v-cloak インスタンスの準備が終わると取り除かれる

v-pre

XSS対策などで使う

<a v-bind:href="#" v-pre>
    hello {{ message }}
</a>

<!-- これは以下のよう生だし描画される -->
<a v-bind:href="#" v-pre> hello {{ message }}</a>

v-once

描画されたあとに 指定したプロパティの値が変わってもDOMは更新されない

v-text

Mustash {{ kore }} を使わないで書くパターンがある場合に使える

var app = new Vue({
  el: '#app'
      data: {
          message: 'Hello!'
  }
})

こんな風にmessageでバインドできる。

<span v-text="message"></span>

v-html

v-pre (XSS対策)とは逆に、htmlのタグ等、をそのまま出してしまう奴
自分がコントロールできない外部やユーザー要因のデータ部分に使うと脆弱性ありありなので使い所には注意が必要。

var app = new Vue({
  el: '#app'
      data: {
          message: 'hello<strong>Vue.js!</strong>'
  }
})

こんな風にhtmlがそのまま出力する事ができる。

<span v-html="message"></span>
<!-- これは以下のよう描画される -->
<span>hello<strong>Vue.js!</strong></span>

v-cloak

インスタンスの準備ができると自動的に取り除かれる。コンパイル前のテンプレートが表示されるのを防げる

CSSに以下の様なスタイル定義をする

[v-cloak] { display: none}

以下のやつで画面読み込み時に#app要素を隠せる。
インスタンス生成でv-cloak属性が外れてフェードイン表示される。

@keyframes cloak-in {
  0% {
    opacity: 0;
  }
}
#app {
  animation: cloak-in 1s;
}
#app[v-cloak] {
  opacity: 0;
}

仮想DOMとは?

超要約するとDOMツリーの上にもう一枚仮想DOMのツリーを作ってVue.jsでは基本的に仮想側の操作をする。 DOM自体が変わったり、変えたりした際は非同期で本来のDOMを変更しているので、タイムラグや、DOMの入れ替え時に反映されない事がある。
例えば v-if,v-elseで分岐表示させた際等にこれが起こりうる。そのため、分岐の各要素に異なる key を付けて別物ということを認識させる事でちゃんと描画されるようになる。

jQuery などのDOMライブラリとの併用

も、一応可能だが、Vue.jsがDOMを直接いじる $el,$refsが同様の事ができるので、併用はできるけど、まあ、無意味化しつつあるよね。ということらしい。

まとめ

  • 使用したいデータはdataオプションに登録しよう
  • 操作するリストには不変でユニークなkey属性を設定しよう
  • 配列インデックスを使った更新はVue.setを使う
  • 関数の呼び出し方ではthisは変化することがある
  • $elと$refsはmounted以降で使う

Laravelのブラウザテストでテストメソッド毎にシーディングを毎回しない方法

前置き

アプリをある程度作り込んでから、自動テストやTDD(テスト駆動開発)を覚え、いざ自分のアプリで実践しようとした所、かなり手を入れないとろくなユニットテストができない状態という事が分りました。
なにしろユーザーのロール権限が5つもあり、権限毎に表示や動作が異なる箇所が多々ある。(どうしてこうなった)
メソッドの中で他のメソッドを呼びまくり、値を拾っては他に投げ、みたいな入出力の制御が複雑で大変危うい処理もある。
また、例えばあちらのバグをつぶすと、こちらでバグになる。とか、この権限での仕様を変えたけど、他の権限での動作や表示には対応してる?といった確認作業も多くなりました。

で、やむなく現在のアプリを自動ブラウザテストでそれぞれの権限での表示や動作を検証する、という事をしてます。要は自分で画面見て、操作して、というのを機械にやらせる処理を淡々と書く作業です。(正直しんどい)

本題

で、その際によくあるのが setUp() メソッド(テストのメソッド毎に実行される処理)に、シーディング処理を書く奴、こんなのです。

<?php

namespace Tests\Browser;
// use 省略
class Profile_superAdmin_user_Test extends DuskTestCase
{

    use RefreshDatabase;

    protected function setUp()
    {
        parent::setUp();
        Artisan::call('migrate:refresh');
        Artisan::call('db:seed');
    }
    // 以下略
}

この処理なんですが、setUp()はこの下に書かれるブラウザテストの複数のメソッド実行前に毎回実施されます。なので、DBの再構成をテストメソッド毎に毎回やると、テスト実施に結構時間がかかってしまいます。

なので、この中の処理をテストクラスの最初の1回のみ実施される、 setUpBeforeClass()を書いたのですが、理由は忘れましたがうまく行きませんでした。(BrowserテストにsetUpBeforeClass()が無かったか? Artisanコマンドが使えなかった?だったと思います。)

では上手い方法はないか?と調べた所、以下のページの記述に当たりました。

How Do I Seed My Database in the setupBeforeClass Method in a Laravel 4 Unit Test?

日本語直訳サイト
Laravel 4ユニットテストでsetupBeforeClassメソッドにデータベースをシードするにはどうすればよいですか?

<?php

namespace Tests\Browser;
// use 略

class Admin_Community_superAdmin_user_Test extends DuskTestCase
{
    protected static $db_inited = false;
    use RefreshDatabase;

    protected static function initDB()
    {
        Artisan::call('migrate:refresh');
        Artisan::call('db:seed');
    } 

    protected function setUp()
    {
        parent::setUp();
        if (!static::$db_inited) {
            static::$db_inited = true;
            static::initDB();
        }
    }
    // 略
}

setUp() メソッドをあたかもsetUpBeforeClass()の様にクラスインスタンス後に1度だけやってくれる書き方です。
$db_initedという静的プロパティを持たせてsetUp()で一度 true にしてしまったら、 setUp()内の ifの処理で以降は Artisanファサードを呼ばない。という実装。ピタゴラスイッチ的な動きですよね。こういうのって見ればそれなりに動き追えますが、自分で書ける気がしません。が、なにはともあれ解決。

しかしこれだと、テストクラス内でDBの値を変更してしまう処理を書くと、以降のテストに影響がでてしまうのですが、そこは、テストクラスを上手く分けて書いて行けば解決できそうです。

追記

その後以下の様にテストメソッド内に Artisanファサードを使ってDBのシーディングを行う処理を書くことで、さらに必要な時にだけ、シーディングが行えることがわかりました。このまま行くとsetUp()メソッドが無くても何とかなるる感じですね。

<?php

    /**
     * @test
     */
    public function DBを編集するテスト()
    {
        // 省略
    }

    /**
     * @test
     */
    public function 後処理()
    {
        $this->browse(function ($browser) {
            $browser->visit('/user/edit?id=1')
               // assert が書かれて無いとテスト完了時に警告が出る為、便宜上入れた assert
                ->assertSeeIn('.comp-title', 'プロフィール編集');
                echo 'now seeding!';
                Artisan::call('migrate:refresh');
                Artisan::call('db:seed');
        });
    } 

Let's Encrypt のTLS-SNI-01から http-01 方式への変更をした備忘録

無料で使えるSSL認証Let's Encrypt から以下の様なメールが来ました。

Action required: Let's Encrypt certificate renewals

Hello,

Action may be required to prevent your Let's Encrypt certificate renewals
from breaking.

If you already received a similar e-mail, this one contains updated
information.

Your Let's Encrypt client used ACME TLS-SNI-01 domain validation to issue
a certificate in the past 60 days. Below is a list of names and IP
addresses validated (max of one per account):

 www.livelynk.jp (160.16.207.76) on 2018-12-25

TLS-SNI-01 validation is reaching end-of-life. It will stop working
temporarily on February 13th, 2019, and permanently on March 13th, 2019.
Any certificates issued before then will continue to work for 90 days
after their issuance date.

You need to update your ACME client to use an alternative validation
method (HTTP-01, DNS-01 or TLS-ALPN-01) before this date or your
certificate renewals will break and existing certificates will start to
expire.

Our staging environment already has TLS-SNI-01 disabled, so if you'd like
to test whether your system will work after February 13, you can run
against staging: https://letsencrypt.org/docs/staging-environment/

If you're a Certbot user, you can find more information here:
https://community.letsencrypt.org/t/how-to-stop-using-tls-sni-01-with-certbot/83210

Our forum has many threads on this topic. Please search to see if your
question has been answered, then open a new thread if it has not:
https://community.letsencrypt.org/

For more information about the TLS-SNI-01 end-of-life please see our API
announcement:
https://community.letsencrypt.org/t/february-13-2019-end-of-life-for-all-tls-sni-01-validation-support/74209

Thank you,
Let's Encrypt Staff

Google翻訳にかけてみる

必要なアクション:証明書の更新を暗号化しましょう

こんにちは、

あなたのLet's Encrypt証明書の更新
が壊れないようにするための行動が必要かもしれません。

すでに同じようなEメールを受け取っている場合は、これには更新された
情報が含まれています。

Let's Encryptクライアント
が過去60日間に証明書を発行するためにACME TLS-SNI-01ドメイン検証を使用しました。以下は
検証された名前とIP アドレスのリストです(アカウントごとに最大1つ):2018-12-25の

 www.livelynk.jp(160.16.207.76)

TLS-SNI-01検証は廃止予定です。それは動作を停止します
2月13日、2019年に一時的に、かつ永久月13日に、2019年
それ以前に発行された証明書は90日間働き続ける
彼らの発行日後。


この日までに ACMEクライアントを更新して別の検証方法(HTTP-01、DNS-01、またはTLS-ALPN-01)を使用する必要があります。そうしないと
証明書の更新が中断され、既存の証明書の
有効期限が切れます。

私たちのステージング環境は、すでにあなたが好きなので、もし、TLS-SNI-01無効になってい
2月13日後にシステムが動作するかどうかをテストするために、あなたが実行することができます
ステージングに対して:https://letsencrypt.org/docs/s taging環境/

あなたがCertbotユーザーであるならば、あなたはここでより多くの情報を見つけることができます:
https://community.letsencrypt。org / t /どうやってtls-snを使うのかi-01-with-certbot / 83210

このフォーラムにはたくさんのスレッドがあります。あなたのかどうかを確認するために検索してください
質問が答えられたら、新しいスレッドを開いてください(
https://community.letsencrypt)。org /

TLS-SNI-01のサポート終了についての詳細は、当社のAPI 
発表
https://community.letsencryptをご覧ください。org / t / 2月13日 - 1919年 - すべてのtls-sni-01- validation-support / 74209

ありがとう、
スタッフを暗号化しましょう

とのことで、従来の検証方法TLS-SNI-01から HTTP-01、DNS-01、またはTLS-ALPN-01 へのいずれかへの変更が必要とのことで、ググって対応しました。

結論から言うと、先人方が既に変更方法を確立されており、同様の方法で事が済みましたが、参考にさせていただいたサイトのリンクと、その方法を張り付けておきます。
サーバー環境はさくらVPSのcentos7となります。

Let’s encryptのドメイン認証の方法をHTTP-01に変更するための準備で試行錯誤した件。
Let’s Encryptの更新エラーを直す(certbot renew失敗)
Let’s EncryptのTLS-SNI-01認証のバリデーションに伴う対応策まとめ

まず サーバーにログインして rootになって Let's Encryptを更新します。

# yum update certbot*

次に以下のサイトを参考に設定変更のコマンドを叩きました

Let’s encryptのドメイン認証の方法をHTTP-01に変更するための準備で試行錯誤した件。

# certbot renew –dry-run –preferred-challenges http-01,dns-01

ところが何やらエラーが

Traceback (most recent call last):
  File "/bin/certbot", line 5, in <module>
    from pkg_resources import load_entry_point
  File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 3011, in <module>
    parse_requirements(__requires__), Environment()
  File "/usr/lib/python2.7/site-packages/pkg_resources.py", line 626, in resolve
    raise DistributionNotFound(req)
pkg_resources.DistributionNotFound: acme>=0.29.0

なんかよくわかりませんが、大事な事は最初か最後に書いてあるので、今回は最後の1行でググると以下のサイトの情報に行き当たりました。 Let’s Encryptの更新エラーを直す(certbot renew失敗)

同じエラーが載ってるのでドンピシャです。 手順も全く同じ経緯で原因の特定と解決ができました。

# yum search acme
読み込んだプラグイン:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
 * base: ftp.iij.ad.jp
 * epel: mirrors.aliyun.com
 * extras: ftp.iij.ad.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.iij.ad.jp
====================================================== N/S matched: acme ======================================================
acme-tiny-core.noarch : core python module of acme-tiny
python2-acme.noarch : Python library for the ACME protocol
acme-tiny.noarch : Tiny auditable script to issue, renew Let's Encrypt certificates
dehydrated.noarch : A client for signing certificates with an ACME server

  Name and summary matches only, use "search all" for everything.

python2-acme.noarch の更新が必要とのことで、epelからupdate

# yum --enablerepo=epel update python2-acme

依存性解決をしてアップデートができました。

再度以下のサイトを参考に本来の目的である、Let's Encrypt のTLS-SNI-01から http-01 方式への変更を行います。
Let’s EncryptのTLS-SNI-01認証のバリデーションに伴う対応策まとめ

本当に最新か確認

# certbot --version
certbot 0.29.1

現時点でリンク先のバージョンと同じなので、まず大丈夫でしょう。 そして

新しいバージョンの場合は自動的にHTTP-01の認証が実行されるようです。 とありますが、一応リンク先同様、 -dry-run オプションで更新テストをしてみます。

# certbot renew --dry-run --preferred-challenges http

出力内容

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/www.livelynk.jp.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator apache, Installer apache
Starting new HTTPS connection (1): acme-staging-v02.api.letsencrypt.org
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for www.livelynk.jp
Waiting for verification...
Cleaning up challenges
Resetting dropped connection: acme-staging-v02.api.letsencrypt.org

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of apache server; fullchain is
/etc/letsencrypt/live/www.livelynk.jp/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/www.livelynk.jp/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IMPORTANT NOTES:
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.

Performing the following challenges:

http-01 challenge for www.livelynk.jp

//中略

Congratulations, all renewals succeeded. The following certs have been renewed:

翻訳:おめでとうございます、すべての更新は成功しました。以下の証明書が更新されました。

という訳で無事完了。普段サーバーの設定関連は散々ググって悩んで解決するのですが、今回は皆が同じ対応を迫られている時期だったこともあり、とても簡単に問題が解決できました。良かった。

【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ

以下の記事は2019/1/31 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 #初回 ch1 フレームワークの基礎知識(初心者歓迎!) のための読書メモとなります。
以下の書籍の CHAPTER1 Vue.jsとフレームワークの基礎知識 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

目次

  • CHAPTER 1 Vue.jsとフレームワークの基礎知識
    • 01 Vue.jsについて
    • 02 Vue.jsのキーコンセプト
    • 03 豊富なリソースを活用しよう
    • 04 Vue.jsのインストール
    • 05 Vue.jsの基本機能
    • 06 オプションの構成を見てみよう
    • まとめ

はじめに

書籍の為のがっつりしたサイトがあります!必見!!なんて手厚いサポート体制なんでしょう!
基礎から学ぶ Vue.js

書籍内のコードはまずここを参考にすると良いでしょう
基礎から学ぶ Vue.js コード&動作メモ

CHAPTER 1 Vue.jsとフレームワークの基礎知識

01 Vue.jsについて

Laravelのフロントエンドで採用されて今人気!色々アツい!
jQuery的な手軽さがあり、学習コストが低い
日本語ドキュメントが充実
フレームワークとの相性が良い

02 Vue.jsのキーコンセプト

データ駆動のしくみ
× DOMが存在してそれを読み込んで操作
○ 最初にデータが存在、そのデータに適したDOMを構築する

テンプレートを使う

<div v-if="show">Hello Vue.js</div>

v-ifは仮想DOMをつくるテンプレートの記法

<body>
    <div id="app"></div><!-- ここに配置できる -->
</body>

#app配置する要素とアプリケーションを紐づけることをmountと呼ぶ

データバインディング

データと描画を同期させる仕組み
生JSだと色々面倒だし管理も大変だけどVue.jsなら簡単

DOMの更新はフレームワークに任せよう

それを解決するのがデータでバインディング型のライブライリ、やフレームワーク
Vue.jsもデータでバインディングの多くの機能を持つHTMLを扱う感覚で機能を使用できる

v- からはじまるディレクティブ

htmlの属性にv-if,v-bind,key,等で始まるやつをディレクティブといい、データディバイングを行うために使われる。

<div key="id"></div><!-- 1 -->
<div v-bind:key="id"></div><!-- 2 -->

1,は単純に[id]という文字列を表す
2,はv-で始まるディレクティブJSの変数idで、正確にはアプリに登録されたidというプロパティになる
例外はあるがひとまず、v-で始まってなければ単なる文字列と考えて良い

コンポーネント指向の画面構成

1ファイルの中に例えば部品であるheader,main,footer毎のファイルを作る。その個別のファイル毎にHTML,CSS,JS を書いて管理する。
1ファイルの中にHTML,CSS,JS を書いて管理することもある。
さらに従来のcssの使い方としてポピュラーな方法である、共有コードを別ファイルまとめて書くこともできる。

コンポーネントが増えても大丈夫

コンポーネントはVue.jsのもっとも強力な機能
コンポーネントのネスト化、構造化が容易
複雑な構造化が必要な場合は以下の拡張機能等を導入するとよい

静的サイトジェネレート機能のあるVue.jsの拡張フレームワーク

  • Nuxt.js (Weeybleさんで輪読会やるらしいです)
  • VuePress

03 豊富なリソースを活用しよう

jQueryやBootstlapを使わずとも、Vue.jsに最適化されたコンポ―ネントUIがWebにたくさんあるらしい

Vue.jsと相性のいいライブラリ

代表的なコンポーネント

04 Vue.jsのインストール

  • この本の6章まではスタンドアローン版の「Vue.js」ファイルを使う
  • 開発モードで使う(非minファイルという)
  • 本番環境では「vue.min.js」に置き換えた最適化ファイルを使う

ダウンロード版もありますが、手っ取り早くCDN(コンテンツデリバリーネットワーク  htmlのheadタグ内にscriptとして読み込む1行を書く)の方が良いと思います。

ダウンロードの場合
Vue.js GET STANDARD > インストール(左メニュー) > 開発バージョン(ボタン)

CDNの場合 下記いずれかを使用htmlのheadに記述する。
学習では当然開発バージョン

<!-- 開発バージョン、便利なコンソールの警告が含まれています -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<!-- 本番バージョン、サイズと速度のために最適化されています -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

学習用のひな形ファイルを作る index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Vue.js App</title>
  <link href="main.css" rel="stylesheet">
</head>
<body>
  <div id="app">
    <!-- この#appの内側に色々なサンプルを書き込んでいく -->
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  <script src="main.js"></script>
</body>
</html>

main.js

var app = new Vue({
  el: '#app'
})

これで適当なフォルダに保存したhtmlファイルをブラウザで開き、開発用コンソールを立ち上げる
chromeなら ctrl + shift + I

本にはないがcssも作った方が良い
main.css

body {
   /* ひとまず適当に書いて反映を確認してみる */
    color: blue;
}

さらにブラウザがchromeならこのような開発用の拡張機能をインストールすると便利そう
(但しlocalサーバー環境で無いと起動しない様です)
Vue.js devtools

05 Vue.jsの基本機能

htmlがわかる人なら簡単に扱えるそうです。

index.html 抜粋

  <div id="app">
    <p>{{ message }}</p><!-- ここに一行追加 -->
  </div>

main.js

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})

これでブラウザの画面にはHello Vue.js!が出力される

開発タブの Consoleに以下を入力すると…

console.log(app.message);

Hello Vue.js!と出力される

繰り返しの描画

一覧要素はdataオプションに登録され、そこに入れる配列やオブジェクトからv-forディレクティブで描画できる。

index.html

<ol>
  <li v-for="item in list">{{ item }}</li>
</ol>

main.js

var app = new Vue({
  el: '#app',
  data: {
    list: ['りんご', 'ばなな', 'いちご']
  }
})

開発用コンソールにapp.list.push('おれんじ')と入力すると、要素が追加できる。

1.りんご   
2.ばなな   
3.いちご   
4.おれんじ   

イベントの利用

クリックや選択要素が変わった際などのDOMイベントはv-onディレクティブを使う。
詳細はCHAPTER3
クリックするとhandleClickメソッドが呼ばれる例

index.html

<button v-on:click="handleClick">Click</button>

main.js

var app = new Vue({
  el: '#app',
  methods: {
    handleClick: function (event) {
      alert(event.target) // [object HTMLButtonElement]
    }
  }
})

ボタンが表示されクリックでalertがポップアップされるようになる

フォーム入力との同期

v-modelディレクティブを使用する
入力や選択で即時DOMに変更が反映される
詳細はCHAPTER3

index.html

<p>{{ message }}</p>
<input v-model="message">

main.js

var app = new Vue({
  el: '#app',
  data: {
    message: '初期メッセージ'
  }
})

フォームに入力した内容を編集するとmessage 部分に即反映されるのが確認できる

条件分岐

v-ifディレクティブを使用
詳細はCHAPTER2

index.html

<button v-on:click="show=!show">切り替え</button>
<p v-if="show">Hello Vue.js!</p>

main.js

var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})

showプロパティがtrueの際だけ<p>要素を出力する
コンソールにapp.show=false,app.show=trueと入力することでも表示の切り替えが出来る

トランジション&アニメーション

組み込みコンポーネント<transition>タグを使うと、CSSトランジションやアニメーションが使える
詳細はCHAPTER6

index.html

<button v-on:click="show=!show">切り替え</button>
<transition>
  <p v-if="show">Hello Vue.js!</p>
</transition>

main.js

var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})

main.css

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}
/* opacity:0から1へのフェードイン&フェードアウト */
.v-enter, .v-leave-to {
  opacity: 0;
}

ボタンを押す際にフェードアウト、フェードインでの表示切替アニメーション効果が適用される。

06 オプションの構成を見てみよう

基本的なオプションの構成

だいたいこんな感じらしいです。

var app = new Vue({

    // mountする要素
    el: '#app',
    // アプリケーションを紐づける要素のセレクタ

    // アプリケーションで使用するデータ
    data: {
        show: true
    }
    // `data`はアプリで使用するデータ、配列、オブジェクトが登録可能

    // 算出プロパティ
    computed: {
      computedMessage: function () {
        return this.message + '!'
      }
    }
    // `computed`関数によって算出されたデータ ここで処理した結果を返せる

    // ライフサイクルフック
    created: function () {  // created はすぐ実施される処理、他にも予約語がある。
      // 行いたい処理
    },
    // あらかじめ登録した処理を自動呼出しする`フック`と呼ばれる割り込み処理

    // アプリケーションで使用するメソッド
    methods: {
      myMethod: function () {
        // 行いたい処理
      }
    }
    // 処理の分割、細かな実装等で使う

})

created - ライフサイクルフック

あらかじめ登録した処理を自動呼出しするフックと呼ばれる割り込み処理が予約語でいくつかある。

ライフサイクルフック各種

メソッド タイミング
beforeCreate インスタンスが初期化されリアクティブの初期化がされる前
created インスタンスが初期化されリアクティブの初期化がされる後
beforeMount インスタンスがマウントされる前
mounted インスタンスがマウントされる後
beforeUpdate データが更新され、DOMに適用される前
updated データが更新され、DOMに適用される後
beforeDestroy Vueインスタンスが吐きされる前
destroyed Vueインスタンスが吐きされる後
errorCaptured 任意の子孫コンポーネントからエラーが補足されたとき

参考
Vue.jsのライフサイクルメモ

実施されるタイミングのダイアグラムは以下のリンク先を参照すると良いでしょう。
(書籍P48には日本語での同様の図がある)
Vue.jsのライフサイクルダイアグラム図

コラム要約

ちなみに、1行目にある new Vue() を実行するのは基本的に、操作したい全ての部品を包含している要素に対して1つだけ new Vue() を行う。
そこにコンポ―ネントとしてのUI部品を追加してゆくらしい

まとめ

  • Vue.jsではDOM構造の本体はJavaScriptのデータ
  • ディレクティブの値はJavaScriptの式になっている
  • HTMLコーディングの為にコンポーネントを使っても良い
  • 必要なデータやメソッドはオプションに定義してゆく
  • new Vue()は1つ作りコンポーネントでUIを構築する