Homestead環境で複数環境がある場合、外部から接続するアプリを選択する際の小技
先に結論
homestead(Laravel Homestead', '7.12.0)で複数のアプリを設定して外部端末から公開ipアドレスでアクセスする場合。
sites: の -map に指定するドメインのアルファベットの最昇順のサイトが表示される。
sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public # アルファベット順に最初のこれが外部アクセスからのデフォルト表示となる。 - map: aaa.test to: /home/vagrant/hoge/Laravel/public # 省略 networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
こんな方法で設定するのは不本意なのですが、有志の日本語ドキュメントの以下の方法では上手くいきませんでした。
そもそもの話
windows10でvagrantのHomestead環境を使ってLaravelの開発をしていますが、複数のアプリケーションのプロジェクトを動かしているため、Homestead.yamlとhostに複数のアプリの環境を設定しています。
詳しくは以前の記事を参照の事 windows vagrant Homestead環境でLaravelアプリを追加する際の覚書 - 作りたいものがありすぎる
以下の例の様に複数のアプリの環境を設定できる。
Homested.yaml
folders: - map: C:/Vagrant/Whois to: /home/vagrant/code - map: C:/Vagrant/zero to: /home/vagrant/zero sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public
また、詳細は省きますがwindowsのhosts
ファイルにもローカルドメインとipを設定します。
この特定のアプリをローカルネットワーク(wi-fi環境)で他のマシンからも開発環境が見れるようにする必要がありましたので、これまではHomested.yaml
ファイルに以下の様な記述をすることで、他のPCから閲覧できるようにしていました。
Homested.yaml
networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
上記の設定の192.168.11.99
に別のPCからアクセスすれば、これまで問題なく閲覧出来ていたのですが、先日の記事を書いて以降、意図しない別の開発環境が表示されてしまい、困った事になりました。
しかしこれまで、意図したアプリの画面が出ていたことがむしろ偶然の幸運で、複数アプリを設定してる場合、外部からアクセス可能なipでどのアプリが出るかを設定項目はyamlには書いて無い訳です。従来はたまたま偶然に望みのアプリが出ていた。という訳です。
そこで、どうすれば意図したアプリを外部から接続するか調べましたが、上手く行かず結論として上記の様なかなりその場しのぎの方法となりました。
環境の共有
共同作業者やクライアントと、現在作業中の内容を共有したい場合もあるでしょう。Vagrantには、vagrant shareにより、これをサポートする方法が組み込み済みです。しかし、この方法はHomestead.yamlファイルに複数サイトを設定している場合には動作しません。この問題を解決するため、Homesteadは独自のshareコマンドを持っています。使用を開始するには、vagrant sshによりHomesteadマシンとSSH接続し、share homestead.testを実行してください。これにより、Homestead.yaml設定ファイルのhomestead.testサイトが共有されます。もちろん、homestead.testの代わりに他の設定済みサイトを指定できます。
share homestead.test
とあるので、上記の通りshare whois.test
とか打った所以下の様な画面が表示されました。
vagrant@homestead:~$ share whois.test ngrok by @inconshreveable (Ctrl+C to quit) Session Status connecting Session Status online Sesion Expires 7 hours, 58 minutes Versionerface 2.2.8 10:4040 R gio United States (us) Web Interface http://192.168.10.10:4040t5 p50 p90 Forwarding http://a3e2a379.ngrok.io -> localhost:80 Forwarding https://a3e2a379.ngrok.io -> localhost:80 Connections ttl opn rt1 rt5 p50 p90 0 1 0.00 0.00 0.00 0.00 HTTP Requests ------------- GET /favicon.ico 200 OK
しかし、望みのサイトはip 192.168.11.99 や 192.168.10.10:4040t5 のいずれにアクセスしても他のアプリがでてきたり、アクセスできなかったりでした。
また、コンソールに表示されたhttps://a3e2a379.ngrok.io
にもアクセスしましたが、cssが適用されない画面であるうえ、レスポンスが大変遅く意図したもにはなりえません。
という事で困ったなー。でも外部から見た際に複数アプリがある場合、そもそもどういう法則で、今のアプリが表示されるんだろう?と悩ませている際にたまたま閃いて試したらドンピシャだったという訳です。
でもいずれ複数のアプリで外部PCからアクセスしたくなった場合は…。
これ以上良い方法がない場合はいちいちyamlフィアル書き換えるって事で対処します。
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:
翻訳:おめでとうございます、すべての更新は成功しました。以下の証明書が更新されました。
という訳で無事完了。普段サーバーの設定関連は散々ググって悩んで解決するのですが、今回は皆が同じ対応を迫られている時期だったこともあり、とても簡単に問題が解決できました。良かった。