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 イベントとフォーム入力の受け取り のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
CHAPTER3 イベントとフォーム入力の受け取り
ちなみに Weeybleで去年同じ本の輪読会で使ったドキュメント、およびソースコードは以下にありました。
GitHub yasugahira0810/vuejs_chapter3
書籍用のサイトのCHAPTER3記述ページ(サンプルコード有り)
これまでの輪読会資料
CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる
CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita
SECTION 13 イベントハンドリング
イベントハンドラ
これまでのサンプルのボタンに出てた v-on
の事をここでは解説する
イベントに紐づける処理の内容をこの本では「イベントハンドラ」と呼び
イベントハンドラとイベントを紐づけることを「ハンドル」と呼ぶ
イベントはmousewheel
等IE9では動かないものもあるので注意
<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
,color
等HTML5の入力タイプも使える
横スライドレンジの数値が出る奴
<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
を使ってイベント検知が出来る
以下はjQueryのval
メソッドと絡めた例
<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とデータの登録と更新 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
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">
要素にユニークなキー属性を追加するのが望ましい。ほぼ必須と考えて良い。
キーが無いと要素全部の更新が入る。なのでSQLのid
を入れる位に考えると良い。
繰り返し描画しながら様々な条件を適用する
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>
リストの更新
注意点として以下のケースで更新を検知できない
- インデックス数値を使った配列要素の更新
- 後から追加されたプロパティの更新
リストに要素を追加
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ライブラリのaxios
のCDNを読み込む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 の空配列[]
が一瞬だが、適用されている。ここが表示されるまでに、ローディングアニメーション処理とか入るとカッコよくなる。
[ { "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とフレームワークの基礎知識 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
目次
- 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
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 | 任意の子孫コンポーネントからエラーが補足されたとき |
実施されるタイミングのダイアグラムは以下のリンク先を参照すると良いでしょう。
(書籍P48には日本語での同様の図がある)
Vue.jsのライフサイクルダイアグラム図
コラム要約
ちなみに、1行目にある new Vue()
を実行するのは基本的に、操作したい全ての部品を包含している要素に対して1つだけ new Vue() を行う。
そこにコンポ―ネントとしてのUI部品を追加してゆくらしい
まとめ
- Vue.jsではDOM構造の本体はJavaScriptのデータ
- ディレクティブの値はJavaScriptの式になっている
- HTMLコーディングの為にコンポーネントを使っても良い
- 必要なデータやメソッドはオプションに定義してゆく
- new Vue()は1つ作りコンポーネントでUIを構築する