Wiz テックブログ
Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。
2022-03-01T14:53:18+09:00
wiz012
Hatena::Blog
hatenablog://blog/26006613647999692
サポート終了迫る!CentOS7 を AlmaLinux8 へ移行してみた
hatenablog://entry/13574176438064755997
2022-03-01T14:53:18+09:00
2022-03-01T15:03:25+09:00 CentOS7からAlmaLinux8への移行を試してみます。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20220218/20220218165105.png" alt="f:id:wiz-akakura:20220218165105p:plain" width="800" height="386" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
こんにちは、インフラエンジニアの赤倉です。</p>
<p>昨年末にCentOS8のサポートが終了したばかりですが、CentOS7のサポート期限が<span style="color: #dd830c">2024年6月末</span>に近づいてきています。</p>
<p>そこで今回は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/CentOS">CentOS</a>の次にくる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%B9%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%B7%A5%E7%A5%F3">ディストリビューション</a>候補の中から<span style="color: #dd830c">AlmaLinux8</span>への移行を試してみたいとおもいます。</p>
<h1>移行環境</h1>
<p>移行元<a class="keyword" href="http://d.hatena.ne.jp/keyword/CentOS">CentOS</a>の環境は弊社でよく使っているLaravel構成としました。</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/CentOS">CentOS</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> release 7.9.2009 (Core)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Apache">Apache</a>/2.4.6</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a> 7.4.28</li>
<li>Laravel Framework 8.83.1</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a> Ver 14.14 Distrib 5.7.37</li>
</ul>
<p>Laravelは初期インストール+認証設定まで完了した環境を用意しました。</p>
<h1>移行ツール</h1>
<p>今回の移行ではAlamaLinuxから提供されている<a href="https://almalinux.org/elevate">「Elevate」</a>を使います。<br>手順は公式の<a href="https://wiki.almalinux.org/elevate/ELevate-quickstart-guide.html">クイックスタートガイド</a>を参考にしています。</p>
<p>Elevateは他にも下記<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%B9%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%B7%A5%E7%A5%F3">ディストリビューション</a>への移行をサポートしています。</p>
<ul>
<li>AlmaLinux 8</li>
<li>Rocky <a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> 8</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Oracle">Oracle</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> 8</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/CentOS">CentOS</a> Stream 8</li>
</ul>
<h1>移行の流れ</h1>
<ol>
<li><p>最新の<a class="keyword" href="http://d.hatena.ne.jp/keyword/CentOS">CentOS</a>アップデートをインストールして、再起動します。</p>
<pre><code class="`"> $ yum update -y
$ reboot
</code></pre></li>
<li><p>elevate-releaseプロジェク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%EA%A5%DD%A5%B8">トリポジ</a>トリとGPGキーを使用してパッケージをインストールします。</p>
<pre><code class="`"> $ yum install -y http://repo.almalinux.org/elevate/elevate-release-latest-el7.noarch.rpm
</code></pre></li>
<li><p>アップグレードするOSのleappパッケージと移行データをインストールします。</p>
<pre><code class="`"> $ yum install -y leapp-upgrade leapp-data-almalinux
</code></pre>
<ul>
<li><p>下記のオプションに変更することにより、移行先の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%B9%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%B7%A5%E7%A5%F3">ディストリビューション</a>を選択できます。</p>
<ul>
<li>leapp-data-almalinux</li>
<li>leapp-data-<a class="keyword" href="http://d.hatena.ne.jp/keyword/centos">centos</a></li>
<li>leapp-data-oraclelinux</li>
<li>leapp-data-rocky</li>
</ul>
</li>
</ul>
</li>
<li><p>アップグレード前のチェックを開始します。</p>
<pre><code class="`"> $ leapp preupgrade
</code></pre>
<p> ここで移行要件を満たしていない場合は下記のエラーメッセージが出力されます。</p>
<pre><code class="`"> ============================================================
UPGRADE INHIBITED
============================================================
Upgrade has been inhibited due to the following problems:
1. Inhibitor: Possible problems with remote login using root account
2. Inhibitor: Detected loaded kernel drivers which have been removed in RHEL 8. Upgrade cannot proceed.
3. Inhibitor: Missing required answers in the answer file
Consult the pre-upgrade report for details and possible remediation.
</code></pre>
<p> レポートファイル(/var/log/leapp/leapp-report.txt)を確認して修正対応します。</p>
<p> 弊社環境の場合、下記の通り対応しています。</p>
<p> <strong>1. Inhibitor: Possible problems with remote login using root account</strong></p>
<p> <a class="keyword" href="http://d.hatena.ne.jp/keyword/ssh">ssh</a>のrootログインを許可する。</p>
<pre><code class="`"> $ echo PermitRootLogin yes | sudo tee -a /etc/ssh/sshd_config
</code></pre>
<p> <strong>2. Inhibitor: Detected loaded kernel drivers which have been removed in <a class="keyword" href="http://d.hatena.ne.jp/keyword/RHEL">RHEL</a> 8. Upgrade cannot proceed.</strong></p>
<p> CentOS8で廃止された<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AB%A1%BC%A5%CD%A5%EB">カーネル</a>モジュールを読み込まないようにする。</p>
<pre><code class="`"> $ rmmod pata_acpi
$ rmmod floppy
</code></pre>
<p> <strong>3. Inhibitor: Missing required answers in the answer file Consult the pre-upgrade report for details and possible remediation.</strong></p>
<p> 公式ガイドに記載のあったコマンドを実行する。</p>
<pre><code class="`"> $ leapp answer --section remove_pam_pkcs11_module_check.confirm=True
</code></pre>
<p> もう一度アップグレードのチェックコマンドを実行してエラーがないことを確認します。</p>
<pre><code class="`"> $ leapp preupgrade
</code></pre></li>
<li><p>アップグレードを開始します。</p>
<pre><code class="`"> $ leapp upgrade
</code></pre>
<ul>
<li><p>途中、パッケージ重複エラーでプロセスが終了することがあります。</p>
<pre><code class="``"> Error: Transaction test error:
file /usr/lib64/.libcrypto.so.1.1.1k.hmac from install of openssl-libs-1:1.1.1k-5.el8_5.x86_64 conflicts with file from package openssl11-libs-1:1.1.1k-2.el7.x86_64
</code></pre>
<p> その場合は、対象のパッケージを削除して再度アップグレードを開始します。</p>
<pre><code class="``"> $ yum remove openssl-libs-*
</code></pre></li>
<li><p>弊社環境では<span style="color: #dd830c">約5分</span>でアップグレードが完了しました。また、その間も<a class="keyword" href="http://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>は継続していました。</p></li>
</ul>
</li>
<li><p>アップグレード完了後、サーバを再起動します。</p>
<pre><code class="`"> $ reboot
</code></pre>
<p> 弊社環境では<span style="color: #dd830c">約15分</span>で再起動が完了しました。</p></li>
<li><p>AlmaLinuxにアップグレードされていることを確認します。</p>
<pre><code class="`"> $cat /etc/redhat-release
AlmaLinux release 8.5 (Arctic Sphynx)
</code></pre></li>
</ol>
<h2>移行後の問題</h2>
<ul>
<li><p>なぜか<code>php-xml</code>のみ消えていたので、再度インストールしました。</p>
<pre><code class="``"> $ dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
$ dnf module install php:remi-7.4
$ dnf module enable php:remi-7.4
$ dnf install php74-php-xml
</code></pre></li>
</ul>
<h2>Webサイト(Laravel)確認</h2>
<p>上記の問題を対応すれば、移行後の環境でもLaravelは正常に稼働していました。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20220224/20220224101806.png" alt="f:id:wiz-akakura:20220224101806p:plain" width="1200" height="543" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1>最後に</h1>
<p>「Elevate」
アップグレード前の問題を修正するのに時間がかかりましたが、思っていたより簡単に移行することができました。サーバの再起動に時間がかかるので実施の際は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Web%A5%B5%A1%BC%A5%D3%A5%B9">Webサービス</a>の停止を予定しておく必要はありそうです。</p>
<p>インフラエンジニアの皆様も、CentOS7移行の際にはElevateお試しください!</p>
<p>株式会社Wizではエンジニアを募集しています!</p>
<p>↓↓↓興味ある方はぜひご覧ください!↓↓↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
wiz-akakura
継続は力なり!心理的安全性を高めるための取り組み
hatenablog://entry/13574176438059236808
2022-02-04T10:36:56+09:00
2022-02-04T10:36:56+09:00 こんにちは、フロントエンドエンジニアの柳田です。 今までは、技術的なインプットが大半を占めていたのですが、半年前くらいから組織づくりについてのインプットを意識して行うようになりました。(技術的インプットが減ってきているので、がんばらないと…!)。 最近読んでいる本で、心理的安全性の大切さを改めて感じたので、今回はその本の紹介と、弊社で行っている心理的安全性を高める取り組みをご紹介したいと思います。 最近読んでいる本 最近、私が読んでいる本は、産業・組織心理学者の山浦一保さん著の『武器としての組織心理学 人を動かすビジネスパーソン必須の心理学』です。 組織心理学とは 組織のトラブルの原因を突き止…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20220202/20220202165247.png" alt="f:id:rainymoment0616:20220202165247p:plain" width="1200" height="850" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、フロントエンドエンジニアの柳田です。</p>
<p>今までは、技術的なインプットが大半を占めていたのですが、半年前くらいから組織づくりについてのインプットを意識して行うようになりました。(技術的インプットが減ってきているので、がんばらないと…!)。</p>
<p>最近読んでいる本で、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性の大切さを改めて感じたので、今回はその本の紹介と、弊社で行っている<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性を高める取り組みをご紹介したいと思います。</p>
<h2>最近読んでいる本</h2>
<p>最近、私が読んでいる本は、産業・組織心理学者の山浦一保さん著の<a href="https://www.diamond.co.jp/book/9784478111376.html">『武器としての組織心理学 人を動かすビジネスパーソン必須の心理学』</a>です。</p>
<p>組織心理学とは</p>
<blockquote><p>組織のトラブルの原因を突き止め、うまくいっている集団に共通する「リーダーシップ」や「人間関係」を明らかにする学問</p></blockquote>
<p>だそうです。</p>
<p>組織とは色んな能力のメンバーが集まっていて、個々のメンバーの能力を最大限に引き出すことはなかなか難しいことです。</p>
<p>リモートワークになり、今まで当たり前だった働き方から大きく変わった今、それはより難しくなっています。</p>
<p>この本には、組織をまとめるマネジャーの立場の人たちに、組織心理学の観点から組織のパフォーマンスを高めるためヒントが多く書かれてあります。</p>
<h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性が高いことで組織はどう変わる?</h3>
<p>この本の中では、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性とは下記の意味で展開されています。</p>
<blockquote><p>個人がリスクテイクしても大丈夫な職場だと信じているということ</p></blockquote>
<p>よく、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性の意味を「組織のメンバーが仲良しであること」という内容と捉えがちですが、そうではありません。</p>
<p>組織のメンバーが「失敗しても大丈夫!」「成功するか不安だけど、何かあったとしても助けてくれる!」と感じられるような環境であることが、本当の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性なのですね。</p>
<p>ざっくりとした要約になりますが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性の高い・低いというのは、</p>
<ul>
<li>上司やメンバーとの良好な関係性</li>
<li>職場が支援的であること</li>
<li>学ぶ姿勢を持った個人や組織であること</li>
</ul>
<p>の3点が影響・関与しており、このような状況が充実した環境になることで、メンバーは「意見を言ってもいいんだ!」「失敗しても良いんだ!」と安心感が生まれ、組織のパフォーマンスの向上につながっていく、といった内容でした。</p>
<p>この本を読んで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性の大切さを再認識することはできましたが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性を良くするための施策を実行しすぐに改善するということは、なかなか難しいものです。</p>
<p>ですが、弊社には<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性を高くするべく、様々な施策を企画・実行するチームがいるのです。</p>
<h2>We are Creative Team</h2>
<p>エンジニアが所属しているクリエイティブチームの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性向上を目的として【We are Creative Team】、略してWCTというチームが存在しています。</p>
<p>このチームの前衛が、<a href="https://tech.012grp.co.jp/entry/2021/03/16/182241">以前このテックブログでも紹介した【雑談会】</a>を行っていた集まりになります。</p>
<p>現在は、私を含めた5名で定期的に様々な施策を展開しています。</p>
<p>今回は、WCTが定期的行っている施策を一部ご紹介します。</p>
<h3>Creative Night</h3>
<p>4月・8月・12月の年3回開催している、クリエイティブチームの交流会になります。</p>
<p>全員リモートワークなので、バーチャルオフィスを利用しての開催になります(クリエイティブチームでは<a href="https://tech.012grp.co.jp/entry/2021/02/16/185606">普段からバーチャルオフィスを利用して業務を行っています</a>)。</p>
<p>多くのメンバーと話をできるように、時間を決めてメンバーをシャッフルしたり、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%CB%A5%B2%A1%BC%A5%E0">ミニゲーム</a>などを盛り込んで行っています。</p>
<p>特に4月は、新年度で新入社員が入ってくる時期でもありますので、大々的に行います。</p>
<p>ちなみに去年は、参加者みんなで同じ食事を食べながらの会でした。</p>
<p><figure class="figure-image figure-image-fotolife" title="2021年4月のCreative Nightの様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20220202/20220202111641.png" alt="f:id:rainymoment0616:20220202111641p:plain" width="1200" height="807" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>2021年4月のCreative Nightの様子</figcaption></figure></p>
<h3>Bar Creative</h3>
<p>毎日業務終了後にオープンする、コミュニケーションの場になります。</p>
<p>普段のバーチャルオフィスとは異なる環境で、読書会や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A4%E2%A4%AF%A4%E2%A4%AF%B2%F1">もくもく会</a>、期間限定で他事業部も巻き込んで交流会など、雑談してもよし、勉強会してもよし、な自由なスペースとなっています。</p>
<h3>ウィンセッション</h3>
<p>ウィンセッションは、エンジニアチームで現在試験開催中のものになります。</p>
<p>ウィンセッションとは、メンバーのがんばったことを発表しあい、お互いに褒め合う会のことです。</p>
<p>月に2回、金曜日の終業時間前に集まり、1週間で取り組んだことやがんばったことを振り返りながら、他メンバーに共有します。</p>
<p>今後、クリエイティブチーム全体で開催することを目標としています。</p>
<p><figure class="figure-image figure-image-fotolife" title="ウィンセッションの様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20220202/20220202121428.png" alt="f:id:rainymoment0616:20220202121428p:plain" width="1200" height="818" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ウィンセッションの様子</figcaption></figure></p>
<h2>最後に</h2>
<p>皆さん、いかがでしたでしょうか?</p>
<p>もちろん、WCTだけでなく各職種チームでも、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性を高めるための施策を行っています。</p>
<p>このような施策は、一度行って終わりではなく、定期的に実行してこそ効果が出てくるものです。</p>
<p>少しずつではありますが、組織のパフォーマンスを最大化すべく、組織全体で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性の向上に取り組んでいきたいと思っています。</p>
<p>こんなチームのいる会社で一緒に働いてみませんか?</p>
<p>株式会社Wizではエンジニアを募集しています!</p>
<p> ↓↓↓興味ある方はぜひご覧ください!↓↓↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
rainymoment0616
【Laravel】L5-Swaggerを導入しつつBasic認証も設定する
hatenablog://entry/13574176438055031650
2022-01-25T16:23:00+09:00
2022-01-25T16:23:00+09:00 LaravelプロジェクトへのL5-Swagger導入とBasic認証の設定について簡単に紹介します。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20220124/20220124170246.png" alt="f:id:hnak0210:20220124170246p:plain:w730" width="900" height="500" loading="lazy" title="" class="hatena-fotolife" style="width:730px" itemprop="image"></span></p>
<p>こんにちは、バックエンドエンジニアの中嶋です。</p>
<p>昨今のプロダクト開発ではOpenAPIを用いた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>駆動で行うことが多くなってきました。</p>
<p>もちろん規模や目的によって何を導入すべきか、あるいは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%EC%A5%C3%A5%C9%A5%B7%A1%BC%A5%C8">スプレッドシート</a>やチャットでの共有で済ませてしまうかは検討が必要ですが、今回はその中の選択肢のひとつとして、LaravelプロジェクトへのL5-Swagger導入と<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>の設定について簡単に紹介したいと思います。</p>
<h2>1. 前提</h2>
<h3>確認環境</h3>
<ul>
<li>Laravel 8.67</li>
<li>L5-Swagger 8.0.9</li>
</ul>
<h3>OpenAPIとSwaggerとL5-Swagger</h3>
<p>OpenAPIは「RESTful <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>の仕様を記述するフォーマット」で、<br>
SwaggerはSmartBear社が提供する「OpenAPIを便利に扱うためのツール」の一つです。</p>
<p><a href="https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/">What is OpenAPI? Swagger vs. OpenAPI | Swagger Blog</a></p>
<p>ざっくり、要は「どんな<a class="keyword" href="http://d.hatena.ne.jp/keyword/URI">URI</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>があってどんなリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トとレスポンスなのかを定義したり共有できるもの」と言えましょう。
<br>
<br>
そして今回紹介するL5-Swaggerは、Swaggerの機能をLaravelプロジェクトで使えるようにしたライブラリです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FDarkaOnLine%2FL5-Swagger" title="GitHub - DarkaOnLine/L5-Swagger: OpenApi or Swagger integration to Laravel" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/DarkaOnLine/L5-Swagger">github.com</a></cite></p>
<p>Laravelのプロジェクトの中で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>を定義すれば「/<a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>/documentation」にアクセスすることで以下のようなページが生成&表示でき、<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>の実行例を示したりPostmanのように実際にリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送信することもできます。</p>
<p><figure class="figure-image figure-image-fotolife" title="swagger-ui"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20220124/20220124161344.png" alt="f:id:hnak0210:20220124161344p:plain" width="1200" height="862" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p>
<h2>2. L5-Swaggerの導入</h2>
<h3>Composerでインストールする</h3>
<p>LaravelプロジェクトにおいてComposerでインストールします。<br>
今回はLaravel8系を使用しています。</p>
<pre class="code bash" data-lang="bash" data-unlink>composer require "darkaonline/l5-swagger"</pre>
<h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>を作成する</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>はPHPDocにア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>で記述する方法と<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを直接記述する方法があります。</p>
<p>●ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>の例</p>
<p>提供されているサンプルの記法に倣ってController等のPHPDocにア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>を記述していきます。</p>
<pre class="code lang-php" data-lang="php" data-unlink>/**
* @OA\Get(
* path="/projects/{id}",
* operationId="getProjectById",
* tags={"Projects"},
* summary="Get project information",
* description="Returns project data",
* @OA\Parameter(
* name="id",
* description="Project id",
* required=true,
* in="path",
* @OA\Schema(
* type="integer"
* )
* ),
* @OA\Response(
* response=200,
* description="successful operation"
* ),
* @OA\Response(response=400, description="Bad request"),
* @OA\Response(response=404, description="Resource Not Found"),
* security={
* {
* "oauth2_security_example": {"write:projects", "read:projects"}
* }
* },
* )
*/
</pre>
<p>記述し終わったら</p>
<pre class="code" data-lang="" data-unlink>php artisan l5-swagger:generate</pre>
<p>を実行することでSwaggerドキュメントが生成されます。</p>
<p>なおドキュメント生成については.envファイルに、</p>
<pre class="code" data-lang="" data-unlink>L5_SWAGGER_GENERATE_ALWAYS=true</pre>
<p>を記載すれば、ロードするたびに自動で実行してくれるように設定することもできます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FDarkaOnLine%2FL5-Swagger%2Fblob%2Fmaster%2Ftests%2Fstorage%2Fannotations%2FOpenApi%2FAnotations.php" title="L5-Swagger/Anotations.php at master · DarkaOnLine/L5-Swagger" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/DarkaOnLine/L5-Swagger/blob/master/tests/storage/annotations/OpenApi/Anotations.php">github.com</a></cite></p>
<p>●<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>の例</p>
<p>プロジェクトの/storage配下に<code>/api-docs/api-docs.json</code>を作成し、OpenAPI Specificationの記法にて記述していきます。</p>
<pre class="code" data-lang="" data-unlink>{
"/pets": {
"get": {
"description": "Returns all pets from the system that the user has access to",
"responses": {
"200": {
"description": "A list of pets.",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/pet"
}
}
}
}
}
}
}
}
}</pre>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>で直接定義する場合は特にコマンド実行は不要です。</p>
<p>なお<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>はブラウザ版のSwaggerEdditerで編集・記述したり、エラーがないかの検証をすることができます。</p>
<p><a href="https://editor.swagger.io">https://editor.swagger.io</a></p>
<h3>画面で表示してみる</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>が作成できたら<code>/api/documentation</code>にアクセスします。</p>
<p>プロジェクトにファイルが含まれているので、ローカル環境でも検証サーバーの環境でもアクセスできるのが確認できると思います。</p>
<p>開発の初期段階で仮のレスポンスを返すControllerを定義してサーバーに置いておけば、フロントエンドと共有できるモックサーバーとしても使えそうですね。</p>
<h2>3. <a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>を設定する</h2>
<p>作成した<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>ドキュメントは便利に使えるのですが、このままでは世界中にSwaggerドキュメントを公開した状態になってしまうので<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>を設定していきます。</p>
<p>Laravelにおいて<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>を設定する方法としては、</p>
<ul>
<li>Laravel標準の「auth<a class="keyword" href="http://d.hatena.ne.jp/keyword/.basic">.basic</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>」を用いる</li>
<li>外部ライブラリを用いる</li>
<li>独自<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を作成する</li>
</ul>
<p>の3つになるかと思います。</p>
<p>標準の「auth<a class="keyword" href="http://d.hatena.ne.jp/keyword/.basic">.basic</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>」はDBを使用する形式であることと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>のAuthライブラリに依存してしまうこと、また<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>のために他の外部ライブラリに依存したくないという考えから、今回は独自<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を作成して設定していきます。</p>
<h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を作成</h3>
<p>適当な分かりやすい名称で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を作成します。</p>
<pre class="code bash" data-lang="bash" data-unlink>php artisan make:middleware OriginalBasicAuthMiddleware</pre>
<p>App/Http/Meddleware配下にファイルが生成されるので、中身を実装していきます。
今回の例では実行環境によってenvファイルに定義したユーザー名とパスワードを適用するように設定しています。</p>
<pre class="code" data-lang="" data-unlink><?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class OriginalBasicAuthMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (config('app.env') === 'local' || config('app.env') === 'testing') {
return $next($request);
}
$username = $request->getUser();
$password = $request->getPassword();
if ($username == config('app.basic_auth_username')
&& $password == config('app.basic_auth_password')) {
return $next($request);
}
header('WWW-Authenticate: Basic realm="plase user and passwoard!"');
header("HTTP/1.0 401 Unauthorized");
abort(401);
return $next($request);
}
}</pre>
<h3>kernel.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>に追記</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>が作成できたら、kernel.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>にも忘れずに追記して有効にしておきます。今回はルート<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>として設定します。</p>
<pre class="code kernel.php" data-lang="kernel.php" data-unlink> /**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'org.basic.auth' => \App\Http\Middleware\OriginalBasicAuthMiddleware::class, // 追加
];</pre>
<h3>ルーティング設定を変更</h3>
<p>最後に、適用させたいルートに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を適用させます。
これにはvendorのconfigファイルをpublishする必要があります。</p>
<pre class="code bash" data-lang="bash" data-unlink>php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"</pre>
<p>/config/.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>が作成されるので、「Route Group options」の項目に作成した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を設定します。</p>
<pre class="code lang-php" data-lang="php" data-unlink> 〜略〜
/*
* Route Group options
*/
'group_options' =<span class="synError">></span> [
'middleware' =<span class="synError">></span> ['org.basic.auth'] // 追加
],
],
〜略〜
</pre>
<h3>アクセスしてみる</h3>
<p>認証を設定した環境においてブラウザアクセスしてみると、きちんと<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>を求められるように設定できました。
<figure class="figure-image figure-image-fotolife" title="basic-auth"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20220124/20220124174042.png" alt="f:id:hnak0210:20220124174042p:plain:w350" width="644" height="460" loading="lazy" title="" class="hatena-fotolife" style="width:350px" itemprop="image"></span></figure></p>
<p>あとはenvファイルに設定したユーザー名とパスワードを入力すればSwaggerドキュメントを表示することができます。</p>
<h2>まとめ</h2>
<p>今回はLaravelにおけるL5-Swaggerの導入と<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>の設定方法について紹介しました。<br>
なお<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>の定義方法は、ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>か<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>かymlファイルか、Stoplightを使うか等、日々議論していたりします。</p>
<p>これからもフロントエンドはSPAでバックエンドは<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>を提供するという構成は多いと思いますので、
チームメンバーとも話し合いながらより良い開発手法を模索していきたいと思います。</p>
<p><br></p>
<h2>さいごに..</h2>
<p>Wizではみんなで良いものを楽しく作るべく、積極的にエンジニアを募集しております。</p>
<p>↓↓興味がある方はぜひご覧ください!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
hnak0210
【Laravel/TDD】100日後でも死なないテスト駆動開発のやり方〜Laravel8編〜
hatenablog://entry/13574176438038359217
2021-12-10T13:22:04+09:00
2021-12-10T13:22:04+09:00 半年間、TDDに触れてみて自分なりにテストしやすい構造を練ってみました!それらの手法をご紹介したいと思います。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/t-yone3/20211206/20211206131321.jpg" alt="f:id:t-yone3:20211206131321j:plain" width="760" height="700" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
こんにちは、Wizプロダクト開発チームの米山です。</p>
<p>なんだか名前負けしそうなタイトルですが(笑)
今回はTDD(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>)の、主にテスト手法について書いてみようと思います。</p>
<p>TDDは、弊社の開発手法にも取り入れており、通常の<a class="keyword" href="http://d.hatena.ne.jp/keyword/MVC">MVC</a>モデルの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>の他、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>で動かすためのテストコードも作成しています。</p>
<p>テストコードの書き方は人それぞれなのですが、私が約半年TDDを実践してみて「だいたいこんな感じで良いかな」というようなスタンダードモデルができてきたので紹介したいと思います。</p>
<h2>setUpBeforeClassとsetupは必ず作成する</h2>
<p>setUpBeforeClassは全テストケースで最初に1度だけ、setUpはテストケース毎に動作するメソッドです。</p>
<p>私の場合は、public/index.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>で行っている操作(例えば、定数「LARAVEL_START」の定義など)をsetUpBeforeClassで行い、setUpでテストケース実行前にDBクリアを行うなどをしています。</p>
<pre class="code" data-lang="" data-unlink>public static function setUpBeforeClass(): void
{
if (!defined('LARAVEL_START')) {
define('LARAVEL_START', microtime(true));
}
}
public function setup(): void
{
parent::setUp();
$this->clearDB();
}</pre>
<h2>メソッド名にケース番号とケース内容を入れてわかりやすくする</h2>
<p>この辺は個人のやり方で異なる部分ですが、私の場合は「テストケースがいくつあって、何番目のケースなのか」がわかるよう、テストケースメソッドにはケース番号と確認したいことを入れています。</p>
<p>例えば</p>
<pre class="code" data-lang="" data-unlink>/**
* @test
*/
public function Case1_Getするとステータス200が返る(): void
{
// 実際のテストコード
}</pre>
<p>こんな感じです。
こちらについては、別途「<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>-code-coverage」というライブラリも併用している関係で、あるコードはどのテストケースで通ったのかを視覚的に確認する時にも有効な手段です。</p>
<h2>テストデータはFactoryを有効活用する</h2>
<p>この記事の執筆時点のLaravelバージョンは8.67なのですが、Laravel8系になってFactoryはクラスとして定義できるようになりました。</p>
<p>各Modelクラスに「HasFactory」トレイトをuseすることで使用可能になります。</p>
<p>単に値を自動生成するだけでなく、テストケースによって固定値を入れておきたいケースもあると思います。</p>
<p>どちらでも対応できるよう、デフォルトの「definition」とは別にメソッドを作っておきます。</p>
<pre class="code" data-lang="" data-unlink>class ItemFactory extends Factory
{
protected $model = Item::class;
public function featureTestSetup(array $attributes = []): ItemFactory
{
$replaceData = collect($attributes);
return $this->state(fn (array $attributes) => [
'id' => $replaceData->get('id') ?? $this->faker->randomNumber(),
'name' => $replaceData->get('name') ?? $this->faker->name(),
}
}
}</pre>
<p>こうしておけば、引数$attributesに入っているものは固定値を使うことができますし、直接テストには関係ないが、DB上必要な項目はランダムにfakerを使って値を生成できます。</p>
<h2>TestCaseとTestDataの責務を分ける</h2>
<p>Laravelのテスト、すなわち<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>には「DataProvider」という機能があります。</p>
<p>これを利用すると、テストケースメソッドの開始時にテストデータが入った状態(メソッドの引数にある状態)でテストが開始できます。</p>
<p>が、私は敢えてこれを使わずにコードを書きます。</p>
<p>理由はテストケースとテストデータ作成のコードを両方1つのクラスに書き込むことで、コードが肥大化しメンテナンスしづらくなるからです。</p>
<p>テストケースにはテストケースだけを書いておく。データ作成は別のところでやる。</p>
<p>私はこれをトレイトを使って実現しました。</p>
<p>例えば</p>
<pre class="code" data-lang="" data-unlink>class GetItemsTest extends TestCase
{
/**
* @test
*/
public function Case1_Getするとステータス200が返る(): void
{
// 実際のテストコード
}
}</pre>
<p>というテストコードがあったとして、DataProvidorを使うと</p>
<pre class="code" data-lang="" data-unlink>class GetItemsTest extends TestCase
{
/**
* @test
* @dataProvider case1_data
*/
public function Case1_Getするとステータス200が返る($data): void
{
// 実際のテストコード
}
public function case1_data(): array
{
return [
// ここにデータを書く
];
}
}</pre>
<p>となるのですが、これをトレイトに置き換えると</p>
<pre class="code" data-lang="" data-unlink>class GetItemsTest extends TestCase
{
use ItemTestData;
/**
* @test
*/
public function Case1_Getするとステータス200が返る(): void
{
$this->createTestData();
// 実際のテストコード
}
}</pre>
<p>というコードにすることができます。</p>
<p>「なんで便利な機能(DataProvider)があるのに使わないんだ?」とギモンを持つ方もいるかもしれません。</p>
<p>コードを分けることは前述した通り、メンテナンスしやすくすることが目的ですが、トレイトにすることでいくつかのテストで共有して利用することが可能です。</p>
<p>例えば、上の例では「ItemTestData」というトレイトを使い、テストデータを作成しますが、他のケースでもこの「ItemTestData」を利用したければuseすることで使用可能になります。</p>
<p>テストケースごとにDataProviderを書く必要はありません。</p>
<h2>ここまでのまとめ</h2>
<p>いかがだったでしょうか。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>といっても人によって、あるいは言語によって様々なスタイルがあると思いますが、今回はLaravel(<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>)でのやり方を紹介しました。</p>
<ul>
<li>setUp系のメソッド(初期化処理)は必ず行う</li>
<li>テストケースメソッド名はケース番号とやりたいことを書き、わかりやすくする</li>
<li>データ作成はFactory機能を使って柔軟に作成</li>
<li>DataProviderではなくtraitを使ってテストデータを作成する</li>
</ul>
<p>1つできれば使いまわして量産することも可能です。参考になれば幸いです。</p>
<hr />
<p>Wizではエンジニアを募集しております。</p>
<p>興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
t-yone3
突撃!隣のリモート環境 ~推しキーボード編~
hatenablog://entry/13574176438033857628
2021-11-24T14:15:58+09:00
2021-11-24T14:15:58+09:00 突撃!隣のリモート環境 第1弾。Wizエンジニアの皆様に推しキーボードを教えてもらいました。
<p>こんにちは!フロントエンドの松本です。</p>
<p>現在、Wizで働くエンジニアチームは殆どが在宅勤務をしております。
社内Slackの雑談チャンネルでは、ガジェットやリモート環境をシェアするチャンネルがあったりと、リモート環境のアップデートに力を入れているメンバーも少なくないです。</p>
<p>今回は<strong>突撃!隣のリモート環境 第1弾</strong>として、エンジニアの皆様に推しキーボードを教えてもらいました。</p>
<p>それではさっそくご紹介します。</p>
<p><strong>【Vortexgear Tab75 茶軸】 KDMさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211118/20211118154737.jpg" alt="f:id:yukiji_03:20211118154737j:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>無線</li>
<li>84キーというちょうどいいサイズ感</li>
<li>複数台とペアリングできる</li>
</ul>
<p><strong>【Niz Plum82】 SSKさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211118/20211118155035.jpg" alt="f:id:yukiji_03:20211118155035j:plain" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>Win、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a>両対応</li>
<li>スイッチ : 静電容量無接点</li>
<li>キーキャップがCHERRY MX互換</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/APC">APC</a>機能で反応ポイントを調節できるので地味に良いです</li>
<li>サイズ感と打鍵音がスコスコという音で気に入ってます</li>
</ul>
<p><strong>【Keychron K8】 MTIさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211118/20211118155232.jpg" alt="f:id:yukiji_03:20211118155232j:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>Win、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a>両対応</li>
<li>有線 無線切り替え可能</li>
<li>軸も変更可能(赤、青、茶)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DB%A5%C3%A5%C8%A5%B9%A5%EF%A5%C3%A5%D7">ホットスワップ</a>対応モデルなので簡単にスイッチを交換できる</li>
<li>別売りの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D1%A1%BC%A5%E0%A5%EC%A5%B9%A5%C8">パームレスト</a>が木製でかっこいい!</li>
</ul>
<p><strong>【Magic Keyboard(JIS)】 YKさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211118/20211118155521.jpg" alt="f:id:yukiji_03:20211118155521j:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>薄いとにかく薄い</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Apple">Apple</a>純正<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%C3%A5%AF%A5%D1%A5%C3%A5%C9">トラックパッド</a>と高さが揃っていてGood</li>
<li>デザインが美しい</li>
</ul>
<p><strong>【HHKB HYBRID Type-S】 <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BF%A5%B3%A5%E9%A5%A4%A5%B9">タコライス</a>Nさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211119/20211119165438.png" alt="f:id:yukiji_03:20211119165438p:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>言わずもがな</li>
<li>指が喜んでます</li>
</ul>
<p><strong>【7V】sevenium777さん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211119/20211119165622.jpg" alt="f:id:yukiji_03:20211119165622j:plain" width="1200" height="778" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>やわらかい打鍵感</li>
<li>低音で心地よい打鍵音</li>
<li>スムーズなスイッチ</li>
<li>重い(重さは正義!)</li>
</ul>
<p><strong>【<a class="keyword" href="http://d.hatena.ne.jp/keyword/logicool">logicool</a> G512】DSKさん</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20211124/20211124101330.jpg" alt="f:id:yukiji_03:20211124101330j:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>推しポイント</code></p>
<ul>
<li>光ってるw</li>
<li>うるさ過ぎない</li>
</ul>
<p><code>推しじゃないポイント</code></p>
<ul>
<li>有線</li>
<li>テンキーあり</li>
</ul>
<h3>最後に</h3>
<p>こうやってズラッと並べると購買欲が上がってきてしまいます。<br>
打鍵感がいいとコードを書くのが楽しくなりそうですね。<br></p>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!<br></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
yukiji_03
Apacheのデータをrubyで整える
hatenablog://entry/13574176438035356716
2021-11-24T13:24:45+09:00
2021-11-24T13:24:45+09:00 タイムトライアル的なタスクを、手になじんだ言語でさくっとこなす。今回はrubyの例です。
<p>こんにちは。バックエンドエンジニアの河内です。</p>
<p>先日、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Apache">Apache</a>のログデータの解析結果をレポートするタスクが入ってきました。われわれの業務の大半はプロダクト開発なのですが、その中には、実際に要件を策定しコードを書いていく純粋な開発タスクもあれば、その開発のための技術調査(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>であれば「スパイク」でしょうか)もあります。</p>
<p>前者はなんとなく<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>を把握したうえであとは実装していく…といった感じですが(それでもスケジュール管理は欠かせません)、後者は暗中模索でおこなわなければならないタフなタスクです。</p>
<p>また、それとは別に、開発中の案件とは関係なく不具合などの調査が入ってくることがあるかと思います。なんとなくやることは見えていて、あとはどれだけ速くこなすか…といったタイムトライアル的なタスクです。今回のログデータ解析タスクもそんな分類になるのかな、と思っています。</p>
<h3>作業概観</h3>
<p>さて、ログを調べた結果、今回解析が必要なデータは以下の5行と導けました。<a href="#f-5b1991a9" name="fn-5b1991a9" title="この行を特定するのにも要件からDBから特定条件でクエリを投げ、得られた日時とログデータのアクセス日時を突合しなければならない…というタスクがありました。">*1</a></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/IP%A5%A2%A5%C9%A5%EC%A5%B9">IPアドレス</a>など、一部の情報は適当にマスクしてます。下記の内容をlogsというファイル名で保存します。</p>
<pre class="code" data-lang="" data-unlink>000.000.000.000 - - [02/Nov/2021:15:41:14 +0900] "POST /awesome HTTP/1.1" 200 5009 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [04/Oct/2021:20:04:09 +0900] "POST /awesome HTTP/1.1" 200 4868 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 11; SO-03L Build/55.2.D.0.447; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/94.0.4606.61 Mobile Safari/537.36 Line/11.17.1/IAB"
000.000.000.000 - - [04/Oct/2021:19:49:50 +0900] "POST /awesome HTTP/1.1" 200 4870 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari Line/11.17.0"
000.000.000.000 - - [04/Oct/2021:18:11:32 +0900] "POST /awesome HTTP/1.1" 200 4865 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [03/Oct/2021:01:33:15 +0900] "POST /awesome HTTP/1.1" 200 4866 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 9; SO-04J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36"</pre>
<p>前回、別のメンバーが同様の対応をおこなったときのフォーマットは以下のとおりだったそうなので、それに従います。</p>
<pre class="code" data-lang="" data-unlink>・アクセス日時: 2021-01-01 00:00:00
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: iOS
OSのバージョン: 14.8
ブラウザ: Google
ブラウザのバージョン: 181.0.401558652
デバイス: iPhone</pre>
<p>あとは文字列を拾って特定フォーマットに落とし込めばよいのですが…対象は複数行あります。目で追って手で拾う…というのは不正確ですし疲れるので避けたいです。</p>
<p>また、こういうタスクは一回やりおおせても、おかわりとして同じような作業依頼が再びやってくるのが常です。</p>
<p>せっかくテキストは規則的に並んでいることですし、ちゃちゃっとコードを書くことにします。こういうときに自分は<a class="keyword" href="http://d.hatena.ne.jp/keyword/ruby">ruby</a>を使うことが多いです<a href="#f-20c4e1aa" name="fn-20c4e1aa" title="みなさんも、手になじんだ言語があるのではないでしょうか。">*2</a>。</p>
<p>今回のようなタスクを何度かこなしているので、rbenvで特定バージョンの<a class="keyword" href="http://d.hatena.ne.jp/keyword/ruby">ruby</a>が動作する環境が用意されています(今回の例は<code>ruby 2.6.3p62</code>)。</p>
<p>作業<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リの中は、以下のような感じに配備しました。logsは先ほどの5行のログが書かれたファイルで、処理をおこなうreport.rbがあり、処理後にレポート内容がREPORT.txtに吐き出されるようにします。</p>
<pre class="code" data-lang="" data-unlink>./
logs
report.rb
REPORT.txt(report.rbにより生成される)</pre>
<h4>コードを書いていく</h4>
<h5>ログをパースする</h5>
<p>それでは、report.rbにコードを書いていきます。<a href="https://github.com/weppos/apachelogregex">Apacheログのパーサーライブラリ</a>を使い、以下のように書きました。ログフォーマットは、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Apache">Apache</a>のものをそのまま流用できるようです。</p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">apachelogregex</span><span class="synSpecial">'</span>
format = <span class="synSpecial">'</span><span class="synConstant">%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"</span><span class="synSpecial">'</span>
logParser = <span class="synType">ApacheLogRegex</span>.new(format)
texts = <span class="synType">File</span>.foreach(<span class="synSpecial">'</span><span class="synConstant">./logs</span><span class="synSpecial">'</span>).map <span class="synStatement">do</span> |<span class="synIdentifier">line</span>|
result = logParser.parse(line)
puts result; <span class="synStatement">exit</span> <span class="synComment"># 出力確認</span>
<span class="synStatement">end</span>
</pre>
<p>コードで一行目パースしたところで止めてますが、こんな感じで取得できます。</p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink>{<span class="synSpecial">"</span><span class="synConstant">%h</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">000.000.000.000</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%l</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">-</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%u</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">-</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%t</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">[02/Nov/2021:15:41:14 +0900]</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%r</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">POST /awesome HTTP/1.1</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%>s</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">200</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%b</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">5009</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%{Referer}i</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">https://xxx.dev/awesome</span><span class="synSpecial">"</span>, <span class="synSpecial">"</span><span class="synConstant">%{User-Agent}i</span><span class="synSpecial">"</span>=><span class="synSpecial">"</span><span class="synConstant">Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1</span><span class="synSpecial">"</span>}
</pre>
<h4>日時を読みやすくする</h4>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Apache">Apache</a>の日時を<a class="keyword" href="http://d.hatena.ne.jp/keyword/ruby">ruby</a>でフォーマットする…誰かやってそうですよね? <a href="https://blog.bungu-do.jp/archives/4433">されてる方がいらっしゃった</a>ので拝借します。</p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">apachelogregex</span><span class="synSpecial">'</span>
+ <span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">time</span><span class="synSpecial">'</span>
format = <span class="synSpecial">'</span><span class="synConstant">%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"</span><span class="synSpecial">'</span>
logParser = <span class="synType">ApacheLogRegex</span>.new(format)
texts = <span class="synType">File</span>.foreach(<span class="synSpecial">'</span><span class="synConstant">./logs</span><span class="synSpecial">'</span>).map <span class="synStatement">do</span> |<span class="synIdentifier">line</span>|
result = logParser.parse(line)
+ datetime = <span class="synType">Time</span>.strptime(result[<span class="synSpecial">"</span><span class="synConstant">%t</span><span class="synSpecial">"</span>], <span class="synSpecial">'</span><span class="synConstant">[%d/%b/%Y:%H:%M:%S %z]</span><span class="synSpecial">'</span>).strftime(<span class="synSpecial">"</span><span class="synConstant">%Y-%m-%d %H:%M:%S</span><span class="synSpecial">"</span>)
puts datetime; <span class="synStatement">exit</span> <span class="synComment"># 出力確認</span>
<span class="synStatement">end</span>
</pre>
<p>日時も読みやすくなりました。</p>
<pre class="code" data-lang="" data-unlink>2021-11-02 15:41:14</pre>
<h4>ホスト名を取得する</h4>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">apachelogregex</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">time</span><span class="synSpecial">'</span>
+ <span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">resolv</span><span class="synSpecial">'</span>
format = <span class="synSpecial">'</span><span class="synConstant">%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"</span><span class="synSpecial">'</span>
logParser = <span class="synType">ApacheLogRegex</span>.new(format)
texts = <span class="synType">File</span>.foreach(<span class="synSpecial">'</span><span class="synConstant">./logs</span><span class="synSpecial">'</span>).map <span class="synStatement">do</span> |<span class="synIdentifier">line</span>|
result = logParser.parse(line)
datetime = <span class="synType">Time</span>.strptime(result[<span class="synSpecial">"</span><span class="synConstant">%t</span><span class="synSpecial">"</span>], <span class="synSpecial">'</span><span class="synConstant">[%d/%b/%Y:%H:%M:%S %z]</span><span class="synSpecial">'</span>).strftime(<span class="synSpecial">"</span><span class="synConstant">%Y-%m-%d %H:%M:%S</span><span class="synSpecial">"</span>)
+ host_name = <span class="synType">Resolv</span>.getname(result[<span class="synSpecial">"</span><span class="synConstant">%h</span><span class="synSpecial">"</span>])
puts host_name; <span class="synStatement">exit</span> <span class="synComment"># 出力確認</span>
<span class="synStatement">end</span>
</pre>
<p>サンプルでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/IP%A5%A2%A5%C9%A5%EC%A5%B9">IPアドレス</a>が適当ですが、実際のIPでアドレスではホスト名も取得できました。</p>
<h4>ユーザーエージェントから各種情報を取得する</h4>
<p>ユーザーエージェントはとくに目で見ての作業はめんどうなので、<a href="https://github.com/ua-parser/uap-ruby">ライブラリ</a>に任せましょう。</p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">apachelogregex</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">time</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">resolv</span><span class="synSpecial">'</span>
+ <span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">user_agent_parser</span><span class="synSpecial">'</span>
format = <span class="synSpecial">'</span><span class="synConstant">%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"</span><span class="synSpecial">'</span>
logParser = <span class="synType">ApacheLogRegex</span>.new(format)
texts = <span class="synType">File</span>.foreach(<span class="synSpecial">'</span><span class="synConstant">./logs</span><span class="synSpecial">'</span>).map <span class="synStatement">do</span> |<span class="synIdentifier">line</span>|
result = logParser.parse(line)
datetime = <span class="synType">Time</span>.strptime(result[<span class="synSpecial">"</span><span class="synConstant">%t</span><span class="synSpecial">"</span>], <span class="synSpecial">'</span><span class="synConstant">[%d/%b/%Y:%H:%M:%S %z]</span><span class="synSpecial">'</span>).strftime(<span class="synSpecial">"</span><span class="synConstant">%Y-%m-%d %H:%M:%S</span><span class="synSpecial">"</span>)
host_name = <span class="synType">Resolv</span>.getname(result[<span class="synSpecial">"</span><span class="synConstant">%h</span><span class="synSpecial">"</span>])
+ user_agent_part = result[<span class="synSpecial">"</span><span class="synConstant">%{User-Agent}i</span><span class="synSpecial">"</span>]
+ user_agent = <span class="synType">UserAgentParser</span>.parse(user_agent_part)
+ os_name, os_version = user_agent.os.to_s.split(<span class="synSpecial">"</span><span class="synConstant"> </span><span class="synSpecial">"</span>)
+ browser_version = user_agent.version.to_s
+ browser_name = user_agent.family.to_s
+ device_name_family = user_agent.device.family.to_s
+ device_name = device_name_family == <span class="synSpecial">"</span><span class="synConstant">iPhone</span><span class="synSpecial">"</span> ? device_name_family : <span class="synSpecial">"#{</span>user_agent.device.brand<span class="synSpecial">}</span><span class="synConstant"> </span><span class="synSpecial">#{</span>user_agent.device.model<span class="synSpecial">}"</span>
<span class="synStatement">end</span>
</pre>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/iPhone">iPhone</a>と<a class="keyword" href="http://d.hatena.ne.jp/keyword/Android">Android</a>のときの表示で違和感があったので、条件分岐してます<a href="#f-be9c5254" name="fn-be9c5254" title="たとえば、iPadなどはなかったので今回は考慮してません。アプリ開発でないので、とりあえず条件を満たして早さ優先。">*3</a>。</p>
<h4>整形する</h4>
<p>仕上げに入っていきます。フォーマットに従い出力します(ログ一行ごとのブロックで適当に仕切り線を入れて表示)。</p>
<pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">apachelogregex</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">time</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">resolv</span><span class="synSpecial">'</span>
<span class="synPreProc">require</span> <span class="synSpecial">'</span><span class="synConstant">user_agent_parser</span><span class="synSpecial">'</span>
format = <span class="synSpecial">'</span><span class="synConstant">%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"</span><span class="synSpecial">'</span>
logParser = <span class="synType">ApacheLogRegex</span>.new(format)
texts = <span class="synType">File</span>.foreach(<span class="synSpecial">'</span><span class="synConstant">./logs</span><span class="synSpecial">'</span>).map <span class="synStatement">do</span> |<span class="synIdentifier">line</span>|
result = logParser.parse(line)
datetime = <span class="synType">Time</span>.strptime(result[<span class="synSpecial">"</span><span class="synConstant">%t</span><span class="synSpecial">"</span>], <span class="synSpecial">'</span><span class="synConstant">[%d/%b/%Y:%H:%M:%S %z]</span><span class="synSpecial">'</span>).strftime(<span class="synSpecial">"</span><span class="synConstant">%Y-%m-%d %H:%M:%S</span><span class="synSpecial">"</span>)
host_name = <span class="synType">Resolv</span>.getname(result[<span class="synSpecial">"</span><span class="synConstant">%h</span><span class="synSpecial">"</span>])
user_agent_part = result[<span class="synSpecial">"</span><span class="synConstant">%{User-Agent}i</span><span class="synSpecial">"</span>]
user_agent = <span class="synType">UserAgentParser</span>.parse(user_agent_part)
os_name, os_version = user_agent.os.to_s.split(<span class="synSpecial">"</span><span class="synConstant"> </span><span class="synSpecial">"</span>)
browser_version = user_agent.version.to_s
browser_name = user_agent.family.to_s
device_name_family = user_agent.device.family.to_s
device_name = device_name_family == <span class="synSpecial">"</span><span class="synConstant">iPhone</span><span class="synSpecial">"</span> ? device_name_family : <span class="synSpecial">"#{</span>user_agent.device.brand<span class="synSpecial">}</span><span class="synConstant"> </span><span class="synSpecial">#{</span>user_agent.device.model<span class="synSpecial">}"</span>
+ text = <<~<span class="synSpecial">"TXT"</span>
<span class="synConstant">+ ・アクセス日時: </span><span class="synSpecial">#{</span>datetime<span class="synSpecial">}</span>
<span class="synConstant">+ ・IPアドレス: </span><span class="synSpecial">#{</span>result[<span class="synSpecial">'</span><span class="synConstant">%h</span><span class="synSpecial">'</span>]<span class="synSpecial">}</span>
<span class="synConstant">+ ・IPアドレスのホスト名: </span><span class="synSpecial">#{</span>host_name<span class="synSpecial">}</span>
<span class="synConstant">+ ・UserAgentからの情報</span>
<span class="synConstant">+ OS: </span><span class="synSpecial">#{</span>os_name<span class="synSpecial">}</span>
<span class="synConstant">+ OSのバージョン: </span><span class="synSpecial">#{</span>os_version<span class="synSpecial">}</span>
<span class="synConstant">+ ブラウザ: </span><span class="synSpecial">#{</span>browser_name<span class="synSpecial">}</span>
<span class="synConstant">+ ブラウザのバージョン: </span><span class="synSpecial">#{</span>browser_version<span class="synSpecial">}</span>
<span class="synConstant">+ デバイス: </span><span class="synSpecial">#{</span>device_name<span class="synSpecial">}</span>
<span class="synConstant">+ TXT</span>
<span class="synConstant">end</span>
<span class="synConstant">+ File.write('REPORT.txt', texts.join("-" * (50) + "</span><span class="synSpecial">\n</span><span class="synConstant">"))</span>
</pre>
<p>出力結果は以下のような感じです。</p>
<pre class="code" data-lang="" data-unlink>・アクセス日時: 2021-11-02 15:41:14
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: iOS
OSのバージョン: 14.8
ブラウザ: Google
ブラウザのバージョン: 181.0.401558652
デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 20:04:09
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: Android
OSのバージョン: 11
ブラウザ: LINE
ブラウザのバージョン: 11.17.1
デバイス: SonyEricsson SO-03L
--------------------------------------------------
・アクセス日時: 2021-10-04 19:49:50
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: iOS
OSのバージョン: 14.2
ブラウザ: LINE
ブラウザのバージョン: 11.17.0
デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 18:11:32
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: iOS
OSのバージョン: 14.7.1
ブラウザ: Mobile Safari
ブラウザのバージョン: 14.1.2
デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-03 01:33:15
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: Android
OSのバージョン: 9
ブラウザ: Chrome Mobile
ブラウザのバージョン: 94.0.4606.61
デバイス: SonyEricsson SO-04J
--------------------------------------------------
・アクセス日時: 2021-10-02 13:52:21
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
OS: iOS
OSのバージョン: 14.7.1
ブラウザ: Mobile Safari UI/WKWebView
ブラウザのバージョン:
デバイス: iPhone</pre>
<h3>まとめ</h3>
<p>タイムトライアル的なタスクは、ありもののライブラリや先人のナレッジ、自分の過去のメモを生かしてさくっと解決していく必要があります。一方で、一回だけでなく何度かおかわりが来ることを想定し、再現できるようにしておくとよいのかな、と思います。</p>
<h1>最後に</h1>
<p>Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
<div class="footnote">
<p class="footnote"><a href="#fn-5b1991a9" name="f-5b1991a9" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">この行を特定するのにも要件からDBから特定条件でクエリを投げ、得られた日時とログデータのアクセス日時を突合しなければならない…というタスクがありました。</span></p>
<p class="footnote"><a href="#fn-20c4e1aa" name="f-20c4e1aa" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">みなさんも、手になじんだ言語があるのではないでしょうか。</span></p>
<p class="footnote"><a href="#fn-be9c5254" name="f-be9c5254" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">たとえば、<a class="keyword" href="http://d.hatena.ne.jp/keyword/iPad">iPad</a>などはなかったので今回は考慮してません。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%D7%A5%EA%B3%AB%C8%AF">アプリ開発</a>でないので、とりあえず条件を満たして早さ優先。</span></p>
</div>
wz-tkch
express(nodejs)+node.jsからmysqlに接続してデータベースを作成する
hatenablog://entry/13574176438033154821
2021-11-18T11:03:30+09:00
2021-11-18T11:04:16+09:00 バックエンドはexpress(nodejs)を使ってMySqlに接続しクライアント側はNext(react)を使って簡データベースを作成してみたいと思います。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20211115/20211115154050.png" alt="f:id:thunder_fury:20211115154050p:plain" width="715" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>はじめに</h2>
<p>皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪<br></p>
<p>バックエンドはexpress(Node.js)を使って<a class="keyword" href="http://d.hatena.ne.jp/keyword/MySql">MySql</a>に接続しクライアント側はNext(react)を使って簡単データベースを作成してみたいと思います。</p>
<p>自分の場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/sql">sql</a>文が分からないのではデータベース管理する<a href="https://www.mysql.com/jp/products/workbench/">MySQLWorkbench</a>を使いました。</p>
<h2>express</h2>
<p><a href="https://expressjs.com/ja/">express</a>は、Webおよびモバイルアプリケーションのための一連の強力な機能を提供する、簡潔で柔軟なNode.js Webアプリケーション<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>です。事実上Node.jsの標準的なWebサーバ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>に付けて起動だけ多くの場所で使用されているようです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fexpressjs.com%2Fja%2F" title="Express - Node.js Web アプリケーション・フレームワーク" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://expressjs.com/ja/">expressjs.com</a></cite></p>
<h2>MySQLWorkbenchとは</h2>
<p>公式からの説明</p>
<blockquote><p><a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a> Workbench は、データ・<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E2%A5%C7%A5%EA%A5%F3%A5%B0">モデリング</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a> 開発、およびサーバー設定、ユーザー管理、バックアップなどの包括的な管理ツールを提供します。<a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a> Workbench は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac%20OS%20X">Mac OS X</a> で利用可能です。</p></blockquote>
<p>データベース自分の好みで管理ツールを導入しても良いと思います。自分は「<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/MySQL">MySQL</a> Sequel Pro」</strong>と「<strong>MySQLWorkbench」</strong>どちらかで悩みましたが「<strong>MySQLWorkbench」</strong>にしました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsequelpro.com%2F" title="Sequel Pro" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://sequelpro.com/">sequelpro.com</a></cite>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.mysql.com%2Fjp%2Fproducts%2Fworkbench%2F" title="MySQL :: MySQL Workbench" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.mysql.com/jp/products/workbench/">www.mysql.com</a></cite></p>
<h2>データベーステーブル</h2>
<p><strong>MySQLWorkbenchを使って</strong>member<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>を作成し<code>id</code>は重複できないようにして <code>user_email</code> <code>user_name</code> <code>password</code>三つのテーブルを用意しました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20211115/20211115143710.png" alt="f:id:thunder_fury:20211115143710p:plain" width="1134" height="326" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>全体<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リー構成</h2>
<pre class="code" data-lang="" data-unlink>├── client ( Next.js基本ディレクトリー)
└── api
├── config
| └──database.js
└── server.js</pre>
<h2>パッケージinstall</h2>
<pre class="code bash" data-lang="bash" data-unlink> $ npm init -y
$ npm i mysql
$ npm i express</pre>
<p>npm初期化の後<a class="keyword" href="http://d.hatena.ne.jp/keyword/mysql">mysql</a>とexpressをインストールします。</p>
<h2>データベースconnection</h2>
<p>データベースのconnection処理が必要です。
<a class="keyword" href="http://d.hatena.ne.jp/keyword/mysql">mysql</a>をimportして自分はデータベースの情報を返してくれる共通関数して使い回しできるように書いてみました。</p>
<p>データベース情報はセキュリティのためenvに書いた方が良いです。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>/config/database.js</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> mysql = require(<span class="synConstant">'mysql'</span>);
<span class="synStatement">const</span> database = () => <span class="synIdentifier">{</span>
<span class="synStatement">const</span> connection = mysql.createConnection(<span class="synIdentifier">{</span>
host: `$<span class="synIdentifier">{</span>process.env.MYSQL_HOST<span class="synIdentifier">}</span>`,
user: `$<span class="synIdentifier">{</span>process.env.MYSQL_USER<span class="synIdentifier">}</span>`,
password: `$<span class="synIdentifier">{</span>process.env.MYSQL_PASSWORD<span class="synIdentifier">}</span>`,
database: `$<span class="synIdentifier">{</span>process.env.MYSQL_DATABASE<span class="synIdentifier">}</span>`,
<span class="synIdentifier">}</span>);
<span class="synStatement">return</span> connection;
<span class="synIdentifier">}</span>;
exports.database = database;
</pre>
<h3>POST <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>用意</h3>
<p><code>/api/sign_up</code> のルートにポストする場合「member」のデータベースに格納できるように書いています。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>/server.js</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink>
<span class="synStatement">const</span> express = require(<span class="synConstant">'express'</span>);
<span class="synStatement">const</span> app = express();
<span class="synStatement">const</span> port = process.env.PORT || 3090;
app.post(`/api/sign_up`, (req, res) => <span class="synIdentifier">{</span>
<span class="synStatement">const</span> <span class="synIdentifier">{</span> email, password, user_name <span class="synIdentifier">}</span> = req.body;
<span class="synStatement">const</span> params = <span class="synIdentifier">[</span>email, password, user_name<span class="synIdentifier">]</span>;
<span class="synStatement">const</span> sql = `INSERT INTO member VALUES (<span class="synStatement">null</span>, ?, ?, ?)`;
database().connect();
database().query(sql, params, (err, rows, fields) => <span class="synIdentifier">{</span>
res.header(`Content-Type`, `application/json; charset=utf-8`);
res.<span class="synStatement">status</span>(200).send(<span class="synIdentifier">{</span> reow: rows <span class="synIdentifier">}</span>);
<span class="synIdentifier">}</span>);
database().end();
<span class="synIdentifier">}</span>);
app.listen(port, () => <span class="synIdentifier">{</span>
console.log(`Listening on port $<span class="synIdentifier">{</span>port<span class="synIdentifier">}</span>`);
<span class="synIdentifier">}</span>);
</pre>
<p><code>INSERT INTO member VALUES (null, ?, ?, ?)</code> は上記MySQLWorkbenchから作成したテーブルと繋いでいます。<code>(null, ?, ?, ?)</code> は(id, user_mail, user_name, password )です。</p>
<p>ここまでバックエンドの処理は完了となります。</p>
<h2>server立ち上げ</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>の直下で実行</p>
<pre class="code" data-lang="" data-unlink> node ./server.js</pre>
<p>これで<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>の使用が可能になります。</p>
<h2>ポスト入り口用意</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnodejs.org%2Fja%2F" title="Node.js" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://nodejs.org/ja/">nodejs.org</a></cite></p>
<p>データベースにPOSTするため簡単なフォーム作成します。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> useState <span class="synIdentifier">}</span> from <span class="synConstant">'react'</span>
<span class="synStatement">import</span> Axios from <span class="synConstant">'axios'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> css <span class="synIdentifier">}</span> from <span class="synConstant">'@emotion/react'</span>
<span class="synStatement">export</span> <span class="synStatement">const</span> SignUp = () => <span class="synIdentifier">{</span>
<span class="synStatement">const</span> <span class="synIdentifier">[</span> userEmeil, setUserEmail<span class="synIdentifier">]</span> = useState(``)
<span class="synStatement">const</span> <span class="synIdentifier">[</span> userPassword, setUserPassword<span class="synIdentifier">]</span> = useState(``)
<span class="synStatement">const</span> <span class="synIdentifier">[</span> userName, setUserName<span class="synIdentifier">]</span> = useState(``)
<span class="synStatement">const</span> submit = async () => <span class="synIdentifier">{</span>
console.log(userPassword,userEmeil )
await Axios.post(`/api/sign_up`, <span class="synIdentifier">{</span>
email: userEmeil,
password: userPassword,
user_name: userName
<span class="synIdentifier">}</span>)
.then(res => <span class="synIdentifier">{</span>
console.log(res)
<span class="synIdentifier">}</span>).<span class="synStatement">catch</span>(err =><span class="synIdentifier">{</span>
console.log(err)
<span class="synIdentifier">}</span>)
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> (
<div css=<span class="synIdentifier">{</span>css`
max-width: 300px;
width: 100%;
margin: 0 auto;
`<span class="synIdentifier">}</span>>
<>
<h1>Sign Up</h1>
<div>
<label htmlFor=<span class="synIdentifier">{</span>`user_email`<span class="synIdentifier">}</span>>Mail Address : </label>
<input
id=<span class="synIdentifier">{</span>`user_email`<span class="synIdentifier">}</span>
type=<span class="synIdentifier">{</span>`email`<span class="synIdentifier">}</span>
name=<span class="synIdentifier">{</span>`user_email`<span class="synIdentifier">}</span>
onChange=<span class="synIdentifier">{</span>(e:React.ChangeEvent<HTMLInputElement>) => <span class="synIdentifier">{</span>
setUserEmail(e.target.value)
<span class="synIdentifier">}}</span>
/>
</div>
<br />
<div>
<label htmlFor=<span class="synIdentifier">{</span>`user_name`<span class="synIdentifier">}</span>>User Name : </label>
<input
id=<span class="synIdentifier">{</span>`user_name`<span class="synIdentifier">}</span>
type=<span class="synIdentifier">{</span>`text`<span class="synIdentifier">}</span>
name=<span class="synIdentifier">{</span>`user_name`<span class="synIdentifier">}</span>
placeholder=<span class="synIdentifier">{</span>``<span class="synIdentifier">}</span>
onChange=<span class="synIdentifier">{</span>(e:React.ChangeEvent<HTMLInputElement>) => <span class="synIdentifier">{</span>
setUserName(e.target.value)
<span class="synIdentifier">}}</span>
/>
</div>
<br />
<div>
<label htmlFor=<span class="synIdentifier">{</span>`password`<span class="synIdentifier">}</span>>password : </label>
<input
id=<span class="synIdentifier">{</span>`password`<span class="synIdentifier">}</span>
name=<span class="synIdentifier">{</span>`password`<span class="synIdentifier">}</span>
type=<span class="synIdentifier">{</span>`password`<span class="synIdentifier">}</span>
onChange=<span class="synIdentifier">{</span>(e:React.ChangeEvent<HTMLInputElement>) => <span class="synIdentifier">{</span>
setUserPassword(e.target.value)
<span class="synIdentifier">}}</span>
/>
</div>
<button
css=<span class="synIdentifier">{</span>css`
text-align: center;
background: black;
color: white;
padding: 5px;
margin-<span class="synStatement">top</span>: 10px;
`<span class="synIdentifier">}</span>
onClick=<span class="synIdentifier">{</span>submit<span class="synIdentifier">}</span>
>
Sign Up
</button>
</>
</div>
)
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> SignUp
</pre>
<p>このようなサブミットフィールドになります。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20211115/20211115144115.png" alt="f:id:thunder_fury:20211115144115p:plain" width="582" height="386" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3>データベース確認</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20211115/20211115145008.png" alt="f:id:thunder_fury:20211115145008p:plain" width="686" height="96" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
<code>MySQLWorkbench</code>を確認してみたらちゃんとデータは格納されてるのが確認できました。⚡️🌪
今回のPOST+Nodemailerと組み合わせをして自動返信メール機能を入れても🤔良さそうな気がしました。</p>
<hr />
<p>最後になりますが、Wizではエンジニアを募集中です!</p>
<p>興味のある方は是非覗いてみてください!↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
thunder_fury
Nuxt Bridgeを使ってみて使用法、所感まとめ
hatenablog://entry/13574176438033520521
2021-11-16T17:20:14+09:00
2021-11-16T17:20:14+09:00 今回は先日リリースされたNuxt Bridgeについて試してみたいと思います。
<p>こんにちは、フロントエンドエンジニア小玉です。<br/></p>
<p>先日<code>Nuxt3</code>ベータ版のリリースが発表されましたね。
<code>Nuxt3</code>では<code>Vue3</code>と<code>Vite</code>のサポートに加えて。新しいサーバエンジンが搭載されるそうです。
いくつか新しい機能やアップデートがなされた中で、今回は<code>Nuxt Bridge</code>について試してみたいと思います。</p>
<h2>Nuxt Bridge</h2>
<p>こちらは端的に言うと、<code>Nuxt2</code>を使用しているプロジェクトをよりスムーズにアップグレードするためのシステムです。</p>
<p>公式には以下のように書かれてます</p>
<blockquote><p>Bridge is a forward-compatibility layer that allows you to experience many of new Nuxt 3 features by simply installing and enabling a Nuxt module.</p>
<p>(Bridgeは、Nuxtモジュールをインストールして有効にするだけで、Nuxt3の新機能の多くを体験できる上位互換性レイヤーです。:<a class="keyword" href="http://d.hatena.ne.jp/keyword/google%CB%DD%CC%F5">google翻訳</a>)</p></blockquote>
<p>主な機能として</p>
<ul>
<li>Nitroサーバーが使用できる</li>
<li>CompositionAPIが使用できる(Nuxt3と同じ)</li>
<li>新しい<a class="keyword" href="http://d.hatena.ne.jp/keyword/CLI">CLI</a>とDevtoolsが使用できる</li>
</ul>
<p>などなど。
これまで<code>Nuxt2</code>を使用していたプロジェクトをアップグレードするためにぜひ活用したいサービスですね。</p>
<h2>使用手順</h2>
<p>では早速公式に則って<code>Nuxt Bridge</code>を使用して<code>Nuxt2</code>プロジェクトをアップグレードしてみたいと思います。</p>
<h3>Nuxt Bridgeのインストール</h3>
<pre class="code shell" data-lang="shell" data-unlink>$ yarn add --dev @nuxt/bridge@npm:@nuxt/bridge-edge
or
$ npm install -D @nuxt/bridge@npm:@nuxt/bridge-edge</pre>
<p>※余談ですが、僕はnodeのバージョンが古くてインストールに一度失敗しました。</p>
<blockquote><p>The engine "node" is incompatible with this module. Expected version "^14.16.0 || ^16.11.0 || ^17.0.0". Got "15.8.0"</p></blockquote>
<p><code>node</code>のバージョンは上げておきましょう…。</p>
<h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の更新</h3>
<p><code>Nuxt3</code>では新しく<code>nuxi</code>という<a class="keyword" href="http://d.hatena.ne.jp/keyword/CLI">CLI</a>が導入されました。
そちらを使用するために<code>package.json</code>を以下のように更新します。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
- "<span class="synStatement">dev</span>": "<span class="synConstant">nuxt-ts</span>",
+ "<span class="synStatement">dev</span>": "<span class="synConstant">nuxi dev</span>", <span class="synError">//nuxi のみではダメ</span>
- "<span class="synStatement">build</span>": "<span class="synConstant">nuxt-ts build</span>",
+ "<span class="synStatement">build</span>": "<span class="synConstant">nuxi build</span>",
+ "<span class="synStatement">start</span>": "<span class="synConstant">node .output/server/index.mjs</span>",
- "<span class="synStatement">generate</span>": "<span class="synConstant">nuxt-ts generate</span>",
+ "<span class="synStatement">generate</span>": "<span class="synConstant">nuxi generate</span>"<span class="synError">,</span>
<span class="synError"> }</span>,
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
- "<span class="synStatement">nuxt</span>": "^2.15.7"
+ "<span class="synStatement">nuxt-edge</span>": "<span class="synConstant">latest</span>"
<span class="synSpecial">}</span>,
</pre>
<h3>nuxt.config.js</h3>
<p><code>module.exports</code>、<code>require</code>など、<code>Common.js</code>がサポートされなくなるそうなので、
<code>nuxt.config.js</code>を以下のように書き換える必要があります。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synIdentifier">{</span>
ssr: <span class="synConstant">false</span>,
......
<span class="synIdentifier">}</span>
</pre>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> defineNuxtConfig <span class="synIdentifier">}</span> from <span class="synConstant">'@nuxt/bridge'</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> defineNuxtConfig(<span class="synIdentifier">{</span>
ssr: <span class="synConstant">false</span>,
......
<span class="synIdentifier">}</span>)
</pre>
<p>以上で設定は完了です。</p>
<h2>まとめ</h2>
<p>今回は先日リリースされた<code>Nuxt Bridge</code>を試してみると言うことで、
自分は<code>Nuxt2</code>を使用したいくつかのプロジェクトを<code>Nuxt Bridge</code>でアップグレードしてみました。
大方問題なくアップグレードができましたが、もちろんベータ版ということもあり、</p>
<ul>
<li><code>tailwindcss</code> が非対応</li>
<li><code>@nuxt/content</code>(1.x)がサポートされない、(2.x)に関しては書き換えが必要</li>
</ul>
<p>とのことでした。
自分は<code>@nuxt/content</code>と<code>tailwindcss</code>を使用したブログも作っていたのでそれはうまくアップグレードできませんでした。</p>
<p>未だ対応していないモジュールもありますが、<code>Nuxt3</code>では<code>Nitroエンジン</code>の搭載や<code>Typescript</code>のサポート、<code>Auto Import</code>など恩恵は数々あります。正式版のリリースが期待できますね。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fv3.nuxtjs.org%2Fgetting-started%2Fbridge%2F%23install-nuxt-bridge" title="Bridge" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://v3.nuxtjs.org/getting-started/bridge/#install-nuxt-bridge">v3.nuxtjs.org</a></cite></p>
<h3>最後に</h3>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!<br></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
kdm012
MySQLの実行計画について
hatenablog://entry/13574176438028632422
2021-11-08T11:09:24+09:00
2021-11-08T11:09:24+09:00 MySQLにおける実行計画の確認方法とサンプルチューニングについて簡単にまとめました。
<p>こんにちはバックエンドエンジニアの小室です。</p>
<p>業務では主にLaravelを使って実装しています。現在担当している案件の検索機能の実装が複雑でクエリビルダーでは事足りず生<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>で実装する必要があり、改めて<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>の重要性を実感しました。そこで最近学んだ<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>の実行計画について簡単にまとめてみました。</p>
<h4>実行計画</h4>
<p>実行計画とは、テーブルに対して検索をかけた際、どういった手順を踏んでアクセスしたかを示す実行手順書のようなものになります。</p>
<p>データ量や統計情報(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A5%D7%A5%C6%A5%A3%A5%DE">オプティマ</a>イザ統計)などの情報をもとに、最適な実行計画がされますが、
同じ<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>であればいつも同じ実行計画が作成されるとは限らず、データ量が大きく変更されたときや、統計情報が古いままだと適切な実行計画が作成されず、パフォーマンスが低下する場合もあります。</p>
<h4>実行計画の確認方法</h4>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/mysql">mysql</a>で実行計画を確認するにはselect文の先頭に「EXPLAIN」をつければ確認できます。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">EXPLAIN</span>
<span class="synStatement">SELECT</span> departments.id, departments.name d_name, companies.name c_name
<span class="synSpecial">FROM</span> departments
<span class="synSpecial">INNER</span> <span class="synSpecial">JOIN</span> companies
<span class="synSpecial">ON</span> departments.company_id = companies.id
</pre>
<p>以下の様な表が表示されます。</p>
<table>
<thead>
<tr>
<th>id</th>
<th>select_type</th>
<th>table </th>
<th>partitions</th>
<th>type </th>
<th>possible_keys </th>
<th>key </th>
<th>key_len</th>
<th>ref </th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td> 1</td>
<td>SIMPLE </td>
<td>departments</td>
<td> </td>
<td>ALL </td>
<td>departments_company_id_foreign</td>
<td> </td>
<td> </td>
<td> </td>
<td> 9</td>
<td> 100.0</td>
<td> </td>
</tr>
<tr>
<td>1</td>
<td>SIMPLE </td>
<td>companies </td>
<td> </td>
<td>eq_ref</td>
<td>PRIMARY </td>
<td>PRIMARY</td>
<td>8 </td>
<td>practice_db.departments.company_id</td>
<td> 1</td>
<td> 100.0</td>
<td> </td>
</tr>
</tbody>
</table>
<p>DBeaverというツールを使えば、実行計画をツリー構造で表示する事もできます。「<a href="https://dbeaver.io/">https://dbeaver.io/</a>」からインストールできます。
ほぼ全てのDBに対応しており、実行計画の表示から、ER図の自動作成など便利な機能が豊富です。
<figure class="figure-image figure-image-fotolife" title="DBeaver-実行計画"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shuto_komuro/20211106/20211106152701.png" alt="f:id:shuto_komuro:20211106152701p:plain" width="712" height="328" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>DBeaver-実行計画</figcaption></figure></p>
<h4>各カラムの説明</h4>
<ul>
<li>id</li>
</ul>
<p>SELECT 識別子を表し、クエリー内の SELECT の連番になります。</p>
<ul>
<li>select_type</li>
</ul>
<p>SELECTの種類を表し、SIMPLE,PRIMARY,UNIONなどがあります。</p>
<ul>
<li>table</li>
</ul>
<p>出力の行で参照しているテーブルの名前を示します。</p>
<ul>
<li>partitions</li>
</ul>
<p>クエリーでレコードが照合される<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D1%A1%BC%A5%C6%A5%A3%A5%B7%A5%E7%A5%F3">パーティション</a>を示します。</p>
<ul>
<li>type</li>
</ul>
<p>結合のタイプを表し、ALL,CONST,eq_refなどがあります。</p>
<ul>
<li>possible_keys</li>
</ul>
<p>テーブル内の行の検索に使用するために選択できるインデックスを示します。</p>
<ul>
<li>key</li>
</ul>
<p>実際に使用することを決定したキー (インデックス) を示します。</p>
<ul>
<li>key_len</li>
</ul>
<p>実際に使用することを決定したキーの長さを示します。</p>
<ul>
<li>ref</li>
</ul>
<p>テーブルから行を選択するために、key カラムに指定されたインデックスに対して比較されるカラムまたは定数を示します。</p>
<ul>
<li>rows</li>
</ul>
<p>クエリーを実行するために調査する行数を示します。(推定数)</p>
<ul>
<li>filtered</li>
</ul>
<p>テーブル条件によってフィルタ処理されるテーブル行の推定の割合を示します。</p>
<ul>
<li>Extra</li>
</ul>
<p>クエリーを解決する方法に関する追加情報が含まれます。(where句など)</p>
<p>より詳しい説明は以下の公式リファレンスを参照してください。
<a href="https://dev.mysql.com/doc/refman/5.6/ja/explain-output.html">MySQL :: MySQL 5.6 リファレンスマニュアル :: 8.8.2 EXPLAIN 出力フォーマット</a></p>
<h2>チューニングサンプル</h2>
<h3>unionとcase分</h3>
<p>以下のような都市別、男女別の人口を示すpopulationsテーブル(table1)があるとします。</p>
<p>このテーブルから、都市別に性別を1レコードにまとめた結果(table2)を</p>
<p>出力したいとします。</p>
<p><strong>table1</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>city_name</th>
<th>sex</th>
<th>population</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>都市1 </td>
<td> 1</td>
<td> 63</td>
</tr>
<tr>
<td>2</td>
<td>都市1 </td>
<td> 2</td>
<td> 99</td>
</tr>
<tr>
<td>3</td>
<td>都市2 </td>
<td> 1</td>
<td> 39</td>
</tr>
<tr>
<td>4</td>
<td>都市2 </td>
<td> 2</td>
<td> 93</td>
</tr>
<tr>
<td>5</td>
<td>都市3 </td>
<td> 1</td>
<td> 42</td>
</tr>
<tr>
<td>6</td>
<td>都市3 </td>
<td> 2</td>
<td> 32</td>
</tr>
<tr>
<td>7</td>
<td>都市4 </td>
<td> 1</td>
<td> 38</td>
</tr>
<tr>
<td>8</td>
<td>都市4 </td>
<td> 2</td>
<td> 67</td>
</tr>
<tr>
<td>9</td>
<td>都市5 </td>
<td> 1</td>
<td> 79</td>
</tr>
<tr>
<td>10</td>
<td>都市5 </td>
<td> 2</td>
<td> 59</td>
</tr>
</tbody>
</table>
<p><strong>table2</strong></p>
<table>
<thead>
<tr>
<th>city_name</th>
<th>p_men</th>
<th>p_wom</th>
</tr>
</thead>
<tbody>
<tr>
<td>都市1 </td>
<td> 63</td>
<td> 99</td>
</tr>
<tr>
<td>都市2 </td>
<td> 39</td>
<td> 93</td>
</tr>
<tr>
<td>都市3 </td>
<td> 42</td>
<td> 32</td>
</tr>
<tr>
<td>都市4 </td>
<td> 38</td>
<td> 67</td>
</tr>
<tr>
<td>都市5 </td>
<td> 79</td>
<td> 59</td>
</tr>
</tbody>
</table>
<h4>unionを使った解</h4>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%C5%D4%C6%BB">都道</a>府県別に男性の合計値を求めた後、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C5%D4%C6%BB">都道</a>府県別に女性の合計値を求め
それらの結果をマージするという手順になると思います。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/sql">sql</a>は以下の様になります。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span> city_name, <span class="synIdentifier">sum</span>(p_men) <span class="synSpecial">AS</span> p_men, <span class="synIdentifier">sum</span>(p_wom) <span class="synSpecial">AS</span> p_wom
<span class="synSpecial">FROM</span> (
<span class="synStatement">SELECT</span> city_name, population <span class="synSpecial">AS</span> p_men, <span class="synSpecial">NULL</span> <span class="synSpecial">AS</span> p_wom
<span class="synSpecial">FROM</span> populations <span class="synSpecial">WHERE</span> sex = <span class="synConstant">1</span>
<span class="synStatement">UNION</span>
<span class="synStatement">SELECT</span> city_name, <span class="synSpecial">null</span> <span class="synSpecial">AS</span> p_men, population <span class="synSpecial">AS</span> p_wom
<span class="synSpecial">FROM</span> populations <span class="synSpecial">WHERE</span> sex = <span class="synConstant">2</span>
) tmp
<span class="synSpecial">GROUP</span> <span class="synSpecial">BY</span> city_name
</pre>
<p>以下の実行計画が出力されます。</p>
<table>
<thead>
<tr>
<th>id</th>
<th>select_type </th>
<th>table </th>
<th>partitions</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra </th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>PRIMARY </td>
<td><derived2> </td>
<td> </td>
<td>ALL </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> 4</td>
<td> 100.0</td>
<td>Using temporary</td>
</tr>
<tr>
<td>2</td>
<td>DERIVED </td>
<td>populations</td>
<td> </td>
<td>ALL </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> 10</td>
<td> 10.0</td>
<td>Using where </td>
</tr>
<tr>
<td>3</td>
<td>UNION </td>
<td>populations</td>
<td> </td>
<td>ALL </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> 10</td>
<td> 10.0</td>
<td>Using where </td>
</tr>
<tr>
<td> </td>
<td>UNION RESULT</td>
<td><union2,3> </td>
<td> </td>
<td>ALL </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td>Using temporary</td>
</tr>
</tbody>
</table>
<p>populationsテーブルに対してフルスキャンが2回実行されていることがわかります。</p>
<p>UNIONを使えば、問題を小さなサブ問題に分割して考えることができますが、</p>
<p>内部的に複数のSELECT文を実行する実行計画として解釈されるためI/Oコストが膨らみませす。</p>
<h4>CASE式を使った解</h4>
<p>CASE式を使えばアクセスを1回に減らしコスト改善が可能です。</p>
<p>CASE式を集約関数内に収め、男性だけの人口と女性だけの人口の列を作る方法です。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/sql">sql</a>は以下の様になります。</p>
<pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">SELECT</span>
city_name,
<span class="synIdentifier">sum</span>(<span class="synSpecial">CASE</span> <span class="synSpecial">WHEN</span> sex = <span class="synConstant">1</span> <span class="synSpecial">THEN</span> population <span class="synSpecial">ELSE</span> <span class="synConstant">0</span> <span class="synSpecial">end</span>) <span class="synSpecial">AS</span> p_men,
<span class="synIdentifier">sum</span>(<span class="synSpecial">CASE</span> <span class="synSpecial">WHEN</span> sex = <span class="synConstant">2</span> <span class="synSpecial">THEN</span> population <span class="synSpecial">ELSE</span> <span class="synConstant">0</span> <span class="synSpecial">end</span>) <span class="synSpecial">AS</span> p_wom
<span class="synSpecial">FROM</span> populations
<span class="synSpecial">GROUP</span> <span class="synSpecial">BY</span> city_name
</pre>
<p>以下の実行計画が出力されます。</p>
<table>
<thead>
<tr>
<th>id</th>
<th>select_type</th>
<th>table </th>
<th>partitions</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra </th>
</tr>
</thead>
<tbody>
<tr>
<td> 1</td>
<td>SIMPLE </td>
<td>populations</td>
<td> </td>
<td>ALL </td>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
<td> 10</td>
<td> 100.0</td>
<td>Using temporary</td>
</tr>
</tbody>
</table>
<p>populationsテーブルに対してフルスキャンが1回のみとなり、UNIONを使った解に比べ1/2のI/Oコストで済みました。</p>
<p>このように、実行計画を通しアクセスパスを確認する事で冗長な<a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>文を改善することができました。</p>
<p>今まで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>のクエリビルダに頼りきりでしたが、</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/SQL">SQL</a>を遅延させないためにも、実行計画を意識する習慣をつけていけたら良いなと思います。</p>
<h3>最後に</h3>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!<br></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
shuto_komuro
Next.jsのmiddlewareを使ってbasic認証を実装する
hatenablog://entry/13574176438029827349
2021-11-08T08:38:09+09:00
2021-11-08T08:38:09+09:00 こんにちは、フロントエンドエンジニアの髙橋です。 先日Next.js ConfでNext.js 12は発表されましたね! 色々な新機能が追加されましたが、新機能のMiddlewareが気になって試してみたのでその使い方などを書いてみようと思います。 middlewareとは? 公式には以下のように書かれております! Middleware enables you to use code over configuration. This gives you full flexibility in Next.js because you can run code before a request i…
<p>こんにちは、フロントエンドエンジニアの髙橋です。</p>
<p>先日Next.js ConfでNext.js 12は発表されましたね!</p>
<p>色々な新機能が追加されましたが、新機能の<code>Middleware</code>が気になって試してみたのでその使い方などを書いてみようと思います。</p>
<h2>middlewareとは?</h2>
<p>公式には以下のように書かれております!</p>
<blockquote><p>Middleware enables you to use code over configuration. This gives you full flexibility in Next.js because you can run code before a request is completed.</p></blockquote>
<p>リク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト完了前に特定のコードを実行できる機能のようです。</p>
<p>*<a href="https://nextjs.org/docs/middleware">公式ドキュメント</a>はこちら</p>
<p>例えば、こういった場合↓に使用することができるそうです。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20211105/20211105150022.png" alt="f:id:sotq17:20211105150022p:plain" width="640" height="360" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>何を試したか</h2>
<p>今回は1番に例に上がっているAuthentication(認証)を試してみました。
具体的に言えば、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>をNext.jsにつける、という作業です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20211105/20211105152843.gif" alt="f:id:sotq17:20211105152843g:plain" width="1000" height="528" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>Next.jsで作ったサイトをVercelにあげる場合、コンテンツに制限をかけることは通常有料となってしまいます。</p>
<p>ライブラリを導入して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>をつけることも可能なようですが、公式の機能でできればそれが一番なのでは無いかと思っております!</p>
<p>そんなわけで、早速試してみたいと思います!</p>
<h2>実装</h2>
<h4>プロジェクト/ファイル作成</h4>
<p><code>npx create-next-app@latest --ts</code></p>
<p>create-next-appで雛形を作ったあと…</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20211105/20211105153303.png" alt="f:id:sotq17:20211105153303p:plain" width="326" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>pages/_middleware.ts</code>を作成します。
こちらがmiddlewareを扱うファイルとなります。</p>
<h4>認証処理を書いていく</h4>
<p>早速ですが、以下のコードを貼り付けるだけでOKです!
(解説はコード内に記載します)</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> NextRequest<span class="synStatement">,</span> NextResponse <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'next/server'</span>
<span class="synStatement">export</span> <span class="synType">const</span> middleware <span class="synStatement">=</span> <span class="synStatement">(</span>req: NextRequest<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> basicAuth <span class="synStatement">=</span> req.headers.<span class="synStatement">get(</span><span class="synConstant">'authorization'</span><span class="synStatement">)</span>
<span class="synComment">//HeaderにAuthorizationが定義されているかをチェック</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>basicAuth<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synType">const</span> auth <span class="synStatement">=</span> basicAuth.split<span class="synStatement">(</span><span class="synConstant">' '</span><span class="synStatement">)</span><span class="synIdentifier">[</span><span class="synConstant">1</span><span class="synIdentifier">]</span>
<span class="synType">const</span> <span class="synIdentifier">[</span>user<span class="synStatement">,</span> pwd<span class="synIdentifier">]</span> <span class="synStatement">=</span> <span class="synSpecial">Buffer</span>.<span class="synStatement">from(</span>auth<span class="synStatement">,</span> <span class="synConstant">'base64'</span><span class="synStatement">)</span>.toString<span class="synStatement">()</span>.split<span class="synStatement">(</span><span class="synConstant">':'</span><span class="synStatement">)</span>
<span class="synComment">// basic認証のUser/Passが、envファイルにある値と同じかをチェック</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>user <span class="synStatement">===</span> <span class="synSpecial">process</span>.env.NEXT_PUBLIC_USER <span class="synConstant">&&</span> pwd <span class="synStatement">===</span> <span class="synSpecial">process</span>.env.NEXT_PUBLIC_PASS<span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> NextResponse.next<span class="synStatement">()</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synComment">// 同じでなければエラーを返す</span>
<span class="synStatement">return</span> <span class="synStatement">new</span> Response<span class="synStatement">(</span><span class="synConstant">'Auth required'</span><span class="synStatement">,</span> <span class="synIdentifier">{</span>
<span class="synStatement">status</span>: <span class="synConstant">401</span><span class="synStatement">,</span>
headers: <span class="synIdentifier">{</span>
<span class="synConstant">'WWW-Authenticate'</span>: <span class="synConstant">'Basic realm="Secure Area"'</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
<span class="synIdentifier">}</span>
</pre>
<pre class="code env" data-lang="env" data-unlink>// .env.local
NEXT_PUBLIC_USER=XXXXX
NEXT_PUBLIC_PASS=XXXXX
</pre>
<p>これでローカルを立ち上げれば<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>がかかるはずです!</p>
<h2>公開する</h2>
<p>当たり前ではありますが、Vercel上にenvファイルは置けないので<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を設定します。</p>
<p>以下の通りに設定すればlocal同様に<a class="keyword" href="http://d.hatena.ne.jp/keyword/Basic%C7%A7%BE%DA">Basic認証</a>がかかります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20211105/20211105154918.png" alt="f:id:sotq17:20211105154918p:plain" width="1200" height="494" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>まとめ</h2>
<p>実際に作ってみて、想像の何倍も簡単に実装することができたと思っています。</p>
<p>相変わらずのVercel依存はありますが、ここまで便利になるなら使わない手はないのでは?と最近考えるようになりました。</p>
<p>ちなみに<a href="https://github.com/Sotq17/next_middleware">こちら</a>がサンプルの<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>です!</p>
<p>Next.js 12ではmiddlewareの他にすごい機能がたくさんあります。(SWCめっちゃ早いです…!)</p>
<p>気になる方は他の機能もチェックしてみてはいかがでしょうか。</p>
<h3>最後に</h3>
<hr />
<p>Wizではエンジニアとして一緒に働く仲間を絶賛募集しております。</p>
<p>ご興味のある方、是非ご覧下さい..!!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
sotq17
Mock Service Workerを使ってOpenAPIに寄り添ったテストを行う。
hatenablog://entry/13574176438028478169
2021-11-05T17:08:35+09:00
2021-11-05T17:08:35+09:00 今回は、現在開発しているプロダクトに、テストを導入するにあたって、Mock Service Workerを導入し、OpenAPIと組みわせてMockAPIを作成しテスト実装を行った話
<p>Tech事業部プロダクトチームの仲本です。</p>
<p>10月から、フロントエンドチームからプロダクトチームになりました。</p>
<p>今回は、現在開発しているプロダクトに、テストを導入するにあたって、Mock Service Workerを導入し、OpenAPIと組みわせてMockAPIを作成しテストを作成しました。</p>
<p>今回の記事は導入記録/所感的なものを書いています。</p>
<h2>テストを導入する経緯</h2>
<ul>
<li>フロントエンドのテストがまずできていなかったこと</li>
<li>社内業務案件で、リリースのたびにチームで手作業でテストを行っていたこと</li>
<li>今後規模が大きくなると、手作業でのテストが辛くなる</li>
</ul>
<p>こういった経験のもと、テストを導入し安全性を確保したいということになりました。</p>
<h2>jest.mockによるmock化がかなり多くなる話</h2>
<p>実際にjestを導入して、テストを書いていくとmock化しないと通らないテストがあること気づきました。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>を実行する際のfetchの処理などもmock化させると、実際の動きと遠くなるなるのではないかという懸念があり調べているとMock Service Workerに出会いました。</p>
<h2>Mock Service Workerとは</h2>
<p>Service WorkerAPIを使用して実際のリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%BB%A5%D7%A5%C8">インターセプト</a>する<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>モックライブラリです。</p>
<h2>ユーザーログイン画面のテスト実装</h2>
<p>今回はログイン画面のテスト実装していきます。</p>
<p>Login画面で表示する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が以下になります。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> useEffect <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> useDispatch<span class="synStatement">,</span> useSelector <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react-redux'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> jsx <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'@emotion/react'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> useForm<span class="synStatement">,</span> UseFormReturn <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">"react-hook-form"</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> BrowserRouter <span class="synStatement">as</span> Router<span class="synStatement">,</span> useHistory <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react-router-dom'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> hot <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react-hot-loader'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span>
fetchAsyncLogin<span class="synStatement">,</span>
selectError<span class="synStatement">,</span>
<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../../stores/slices/authSlice'</span>
<span class="synComment">// component</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Button <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../components/atoms/Button'</span>
<span class="synComment">// style</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> LoginBox<span class="synStatement">,</span> LoginTitle <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../../style/pages/Login'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Form<span class="synStatement">,</span> FormLabel<span class="synStatement">,</span> FormInput<span class="synStatement">,</span> FormButton<span class="synStatement">,</span> FormValidButton<span class="synStatement">,</span> FormValidTxt <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../../style/components/block/Form'</span>
<span class="synComment">//type</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> AppDispatch <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../../stores'</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> LoginFormInput <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'../../../../types/user'</span><span class="synStatement">;</span>
<span class="synType">const</span> Login <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> methods: UseFormReturn<span class="synStatement"><</span>LoginFormInput<span class="synStatement">></span> <span class="synStatement">=</span> useForm<span class="synStatement"><</span>LoginFormInput<span class="synStatement">>();</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> register<span class="synStatement">,</span> handleSubmit<span class="synStatement">,</span> formState: <span class="synIdentifier">{</span> errors <span class="synIdentifier">}</span><span class="synStatement">,</span> reset <span class="synIdentifier">}</span> <span class="synStatement">=</span> methods
<span class="synType">const</span> dispatch: AppDispatch <span class="synStatement">=</span> useDispatch<span class="synStatement">()</span>
<span class="synType">const</span> history <span class="synStatement">=</span> useHistory<span class="synStatement">()</span>
<span class="synType">const</span> <span class="synSpecial">onSubmit</span> <span class="synStatement">=</span> <span class="synStatement">async</span> <span class="synStatement">(</span>data: LoginFormInput<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> result <span class="synStatement">=</span> <span class="synStatement">await</span> dispatch<span class="synStatement">(</span>fetchAsyncLogin<span class="synStatement">(</span>data<span class="synStatement">))</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>fetchAsyncLogin.fulfilled.match<span class="synStatement">(</span>result<span class="synStatement">))</span> <span class="synIdentifier">{</span>
<span class="synStatement">if</span> <span class="synStatement">(</span>result.payload.<span class="synStatement">status</span> <span class="synStatement">==</span> <span class="synConstant">200</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
history.push<span class="synStatement">(</span><span class="synConstant">'/'</span><span class="synStatement">)</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
reset<span class="synStatement">()</span>
<span class="synIdentifier">}</span>
<span class="synStatement">return</span> <span class="synStatement">(</span>
<span class="synStatement"><</span>div<span class="synStatement">></span>
<span class="synStatement"><</span>div css<span class="synStatement">=</span><span class="synIdentifier">{</span>LoginBox<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>form css<span class="synStatement">=</span><span class="synIdentifier">{</span>Form<span class="synIdentifier">}</span> <span class="synSpecial">onSubmit</span><span class="synStatement">=</span><span class="synIdentifier">{</span>handleSubmit<span class="synStatement">(</span><span class="synSpecial">onSubmit</span><span class="synStatement">)</span><span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>label css<span class="synStatement">=</span><span class="synIdentifier">{</span>FormLabel<span class="synIdentifier">}</span><span class="synStatement">></span>
メールアドレス
<span class="synStatement"><</span>input
autoComplete<span class="synStatement">=</span><span class="synConstant">"email"</span>
<span class="synStatement">type=</span><span class="synConstant">"email"</span>
aria-invalid<span class="synStatement">=</span><span class="synIdentifier">{</span>errors.mailAddress ? <span class="synConstant">"true"</span> : <span class="synConstant">"false"</span><span class="synIdentifier">}</span>
<span class="synIdentifier">{</span>...register<span class="synStatement">(</span><span class="synConstant">"mailAddress"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> required: <span class="synConstant">true</span><span class="synStatement">,</span> pattern: <span class="synConstant">/^([a-zA-Z0-9])+([a-zA-Z0-9\._+-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/ </span><span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span>
placeholder<span class="synStatement">=</span><span class="synConstant">"メールアドレスを入力してください"</span>
css<span class="synStatement">=</span><span class="synIdentifier">{</span>FormInput<span class="synIdentifier">}</span>
/<span class="synStatement">></span>
<span class="synStatement"><</span>/label<span class="synStatement">></span>
<span class="synStatement"><</span>label css<span class="synStatement">=</span><span class="synIdentifier">{</span>FormLabel<span class="synIdentifier">}</span><span class="synStatement">></span>
パスワード
<span class="synStatement"><</span>input
autoComplete<span class="synStatement">=</span><span class="synConstant">"password"</span>
<span class="synStatement">type=</span><span class="synConstant">"password"</span>
<span class="synIdentifier">{</span>...register<span class="synStatement">(</span><span class="synConstant">"passWord"</span><span class="synStatement">,</span> <span class="synIdentifier">{</span> required: <span class="synConstant">true</span><span class="synStatement">,</span> pattern: <span class="synConstant">/^[a-z\d]{1,100}$/i </span><span class="synIdentifier">}</span><span class="synStatement">)</span><span class="synIdentifier">}</span> aria-invalid<span class="synStatement">=</span><span class="synIdentifier">{</span>errors.passWord ? <span class="synConstant">"true"</span> : <span class="synConstant">"false"</span><span class="synIdentifier">}</span> placeholder<span class="synStatement">=</span><span class="synConstant">"パスワードを入力してください"</span>
css<span class="synStatement">=</span><span class="synIdentifier">{</span>FormInput<span class="synIdentifier">}</span>
/<span class="synStatement">></span>
<span class="synStatement"><</span>/label<span class="synStatement">></span>
<span class="synStatement"><</span>Button name<span class="synStatement">=</span><span class="synConstant">"ログイン"</span> cssStyle<span class="synStatement">=</span><span class="synIdentifier">{</span>FormButton<span class="synIdentifier">}</span> <span class="synSpecial">onClick</span><span class="synStatement">=</span><span class="synIdentifier">{</span>handleSubmit<span class="synStatement">(</span><span class="synSpecial">onSubmit</span><span class="synStatement">)</span><span class="synIdentifier">}</span> dataTestId<span class="synStatement">=</span><span class="synConstant">"test-2-submit-btn"</span> /<span class="synStatement">></span>
<span class="synStatement"><</span>/form<span class="synStatement">></span>
<span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement"><</span>/div<span class="synStatement">></span>
<span class="synStatement">)</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> hot<span class="synStatement">(module)(</span>Login<span class="synStatement">)</span>
</pre>
<p>要件としては</p>
<ul>
<li><p>メールアドレスが入力できる</p></li>
<li><p>パスワードが入力できる</p></li>
<li><p>ログインボタンを押すとログイン用<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>にPOSTする</p></li>
</ul>
<p>流れになります。</p>
<h3>レスポンスデータをOpenAPI定義から取得する</h3>
<p>現在携わっているプロジェクトでは、OpenAPIを使用して<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>のやりとり/認識合わせを行なっています。
OpenAPI仕様が記載された<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>ファイルを使用し、Mock Service Workerの設定を行います。</p>
<p>そうすることで、より使用されている<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>を忠実に再現できることや、もし仕様の変更があった際もしっかり新しい情報の入ったOpenAPIを取り込めていたら気付けるのではないかと思い使用しました。</p>
<p>以下がOpenAPIで定義している内容になります。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synError">// openapi.json</span>
"<span class="synStatement">paths</span>": <span class="synSpecial">{</span>
"<span class="synStatement">/api/login</span>": <span class="synSpecial">{</span>
<span class="synError">// ~~省略~~</span>
"<span class="synStatement">responses</span>": <span class="synSpecial">{</span>
"<span class="synStatement">200</span>": <span class="synSpecial">{</span>
"<span class="synStatement">description</span>": "<span class="synConstant">HTTP OK</span>",
"<span class="synStatement">content</span>": <span class="synSpecial">{</span>
"<span class="synStatement">application/json</span>": <span class="synSpecial">{</span>
"<span class="synStatement">schema</span>": <span class="synSpecial">{</span>
"<span class="synStatement">type</span>": "<span class="synConstant">object</span>",
<span class="synError">// ~~省略~~</span>
"<span class="synStatement">examples</span>": <span class="synSpecial">{</span> <span class="synError">// <= 今回レスポンスで使用するデータ</span>
"<span class="synStatement">default</span>": <span class="synSpecial">{</span>
"<span class="synStatement">value</span>": <span class="synSpecial">{</span>
"<span class="synStatement">status</span>": <span class="synConstant">200</span>,
"<span class="synStatement">response_time</span>": <span class="synConstant">1.1535649299621582</span>,
"<span class="synStatement">message</span>": "<span class="synConstant">処理が正常終了しました。</span>",
"<span class="synStatement">data</span>": <span class="synSpecial">{</span>
"<span class="synStatement">login_id</span>": "<span class="synConstant">test@hoge.co.jp</span>",
"<span class="synStatement">token</span>": "<span class="synConstant">123|njGYLOG9EuZuIrSv83dUvnIWzFLbo6Ri5mUOLm4q</span>"<span class="synError">,</span>
<span class="synError"> }</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>上記の<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>ファイルをimportし、<code>examples</code>を以下のファイルでimportします。</p>
<p>この設定をすることにより、<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>ファイルに変更があった際に変更に対応してくれます。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> schema <span class="synStatement">from</span> <span class="synConstant">'../../openapi.json'</span>
<span class="synType">const</span> components <span class="synStatement">=</span> <span class="synIdentifier">{</span>
LoginUser: schema.paths<span class="synIdentifier">[</span><span class="synConstant">'/api/login'</span><span class="synIdentifier">]</span>.post.responses<span class="synIdentifier">[</span><span class="synConstant">200</span><span class="synIdentifier">]</span>.content<span class="synIdentifier">[</span><span class="synConstant">'application/json'</span><span class="synIdentifier">]</span>.examples.<span class="synStatement">default</span>.value<span class="synStatement">,</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> components
</pre>
<h3>Mock Service Workerでhandlerを設定</h3>
<p>次にmockを作成していきます。</p>
<p><code>msw</code>から<code>rest</code>をimportしgetやpostの設定を行います。</p>
<p>今回はpostの設定を行います。</p>
<p>参考:
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmswjs.io%2Fdocs%2Fgetting-started%2Fmocks%2Frest-api%23imports" title="Mocking REST API - Getting Started" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://mswjs.io/docs/getting-started/mocks/rest-api#imports">mswjs.io</a></cite></p>
<p>以下ファイルで定義している内容としては</p>
<ul>
<li><p>login_idがtest@<a class="keyword" href="http://d.hatena.ne.jp/keyword/hoge">hoge</a>.co.jpかつpasswordがtest1234@</p></li>
<li><p>上の条件を満たしていた時ステータス200と、先ほど定義した<code>examples</code>を返す設定をしています。</p></li>
</ul>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> rest <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'msw'</span>
<span class="synStatement">import</span> components <span class="synStatement">from</span> <span class="synConstant">'./components'</span>
<span class="synType">const</span> handlers <span class="synStatement">=</span> <span class="synIdentifier">[</span>
rest.post<span class="synStatement"><</span>Record<span class="synStatement"><</span><span class="synType">string</span><span class="synStatement">,</span> <span class="synType">any</span><span class="synStatement">>>(</span><span class="synConstant">'http://localhost:3000/api/login'</span><span class="synStatement">,</span> <span class="synStatement">(</span>req<span class="synStatement">,</span> res<span class="synStatement">,</span> ctx<span class="synStatement">)</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> login_id<span class="synStatement">,</span> password <span class="synIdentifier">}</span> <span class="synStatement">=</span> req.body
<span class="synStatement">if</span> <span class="synStatement">(</span>login_id <span class="synStatement">===</span> <span class="synConstant">'test@hoge.co.jp'</span> <span class="synConstant">&&</span> password <span class="synStatement">===</span> <span class="synConstant">'test1234@'</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> res<span class="synStatement">(</span>
ctx.<span class="synStatement">status(</span><span class="synConstant">200</span><span class="synStatement">),</span>
ctx.json<span class="synStatement">(</span>components.LoginUser<span class="synStatement">)</span>
<span class="synStatement">)</span>
<span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span>
<span class="synStatement">return</span> res<span class="synStatement">(</span>
ctx.<span class="synStatement">status(</span><span class="synConstant">403</span><span class="synStatement">),</span>
ctx.json<span class="synStatement">(</span><span class="synIdentifier">{</span>
error: <span class="synConstant">'error: invalid username or password'</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
<span class="synStatement">)</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span><span class="synStatement">),</span>
<span class="synIdentifier">]</span>
<span class="synStatement">export</span> <span class="synIdentifier">{</span> handlers <span class="synIdentifier">}</span>
</pre>
<h3>jest.setup.js</h3>
<p>jest.setup.jsというファイルで、先程設定をしたmockを動かすためのserverの設定と</p>
<p>node環境だと、window.fetchが使えないので <code>node-fetch</code>をinstallして設定を行います。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fnode-fetch%2Fnode-fetch" title="GitHub - node-fetch/node-fetch: A light-weight module that brings the Fetch API to Node.js" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/node-fetch/node-fetch">github.com</a></cite></p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> server <span class="synStatement">from</span> <span class="synConstant">'./src/test/lib/msw/server'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> cleanup <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'@testing-library/react'</span>
<span class="synStatement">import</span> <span class="synSpecial">fetch</span> <span class="synStatement">from</span> <span class="synConstant">"node-fetch"</span>
beforeAll<span class="synStatement">(()</span> <span class="synStatement">=></span> server.listen<span class="synStatement">())</span>
afterEach<span class="synStatement">(async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
server.resetHandlers<span class="synStatement">()</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
afterAll<span class="synStatement">(()</span> <span class="synStatement">=></span> server.close<span class="synStatement">())</span>
<span class="synStatement">if</span> <span class="synStatement">(</span><span class="synConstant">!</span>globalThis.<span class="synSpecial">fetch</span><span class="synStatement">)</span> <span class="synIdentifier">{</span>
globalThis.<span class="synSpecial">fetch</span> <span class="synStatement">=</span> <span class="synSpecial">fetch</span>
<span class="synIdentifier">}</span>
</pre>
<h3>実際のテストコード</h3>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> React <span class="synStatement">from</span> <span class="synConstant">'react'</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synConstant">'@testing-library/jest-dom/extend-expect'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> fireEvent<span class="synStatement">,</span> getAllByText<span class="synStatement">,</span> render<span class="synStatement">,</span> screen<span class="synStatement">,</span> waitFor <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'@testing-library/react'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> Provider <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react-redux'</span><span class="synStatement">;</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> act <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'react-dom/test-utils'</span><span class="synStatement">;</span>
<span class="synStatement">import</span> store <span class="synStatement">from</span> <span class="synConstant">'../tsx/stores'</span><span class="synStatement">;</span>
<span class="synStatement">import</span> Login <span class="synStatement">from</span> <span class="synConstant">'../tsx/views/pages/login/Login'</span>
<span class="synType">const</span> LoginComponent <span class="synStatement">=</span>
<span class="synStatement"><</span>Provider store<span class="synStatement">=</span><span class="synIdentifier">{</span>store<span class="synIdentifier">}</span><span class="synStatement">></span>
<span class="synStatement"><</span>Login /<span class="synStatement">></span>
<span class="synStatement"><</span>/Provider<span class="synStatement">></span>
<span class="synType">const</span> mockHistoryPush <span class="synStatement">=</span> jest.fn<span class="synStatement">();</span>
jest.mock<span class="synStatement">(</span><span class="synConstant">'react-router-dom'</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synStatement">(</span><span class="synIdentifier">{</span>
useHistory: <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synStatement">(</span><span class="synIdentifier">{</span>
push: mockHistoryPush<span class="synStatement">,</span> <span class="synComment">// pushメソッドをダミー関数で上書きする。</span>
<span class="synIdentifier">}</span><span class="synStatement">),</span>
<span class="synIdentifier">}</span><span class="synStatement">));</span>
〜〜省略〜〜
describe<span class="synStatement">(</span><span class="synConstant">'test 2 ログイン処理'</span><span class="synStatement">,</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
it<span class="synStatement">(</span><span class="synConstant">'ログイン確認'</span><span class="synStatement">,</span> <span class="synStatement">async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synType">const</span> <span class="synIdentifier">{</span> getByTestId <span class="synIdentifier">}</span> <span class="synStatement">=</span> render<span class="synStatement">(</span>LoginComponent<span class="synStatement">)</span>
<span class="synStatement">await</span> act<span class="synStatement">(async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
fireEvent.change<span class="synStatement">(</span>screen.getByLabelText<span class="synStatement">(</span><span class="synConstant">/メールアドレス/i</span><span class="synStatement">),</span> <span class="synIdentifier">{</span>
target: <span class="synIdentifier">{</span> value: <span class="synConstant">'test@hoge.co.jp'</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
fireEvent.change<span class="synStatement">(</span>screen.getByLabelText<span class="synStatement">(</span><span class="synConstant">/パスワード/i</span><span class="synStatement">),</span> <span class="synIdentifier">{</span>
target: <span class="synIdentifier">{</span> value: <span class="synConstant">'test1234@'</span> <span class="synIdentifier">}</span><span class="synStatement">,</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">await</span> act<span class="synStatement">(async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
fireEvent.submit<span class="synStatement">(</span>getByTestId<span class="synStatement">(</span><span class="synConstant">'test-2-submit-btn'</span><span class="synStatement">))</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">await</span> waitFor<span class="synStatement">(()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
expect<span class="synStatement">(</span>mockHistoryPush<span class="synStatement">)</span>.toBeCalledWith<span class="synStatement">(</span><span class="synConstant">'/'</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">)</span>
</pre>
<p>ログインボタンを押した時の<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>処理のみのテストを表示しています。</p>
<p>ログインボタンを押すと先程handlerで設定したMockを実行するようになっています。</p>
<p>以上がユーザーログイン画面のテスト実装でした。</p>
<h2>今後考えていきたいこと</h2>
<ul>
<li>どの範囲をテストしていくべきかを明確にする</li>
<li>別のプロジェクトに導入するために、どういった開発手法がいいかを考えていく</li>
<li>まだ導入したてなので色々実装していきながらベストを探していきたい</li>
</ul>
<p>上記をまず、明確にできるように頑張っていきたいと思います。</p>
<h2>参考記事</h2>
<p><a href="https://zenn.dev/ryo_kawamata/articles/introduce-mock-service-worker">OpenAPI定義をmswに活用してお手軽モック</a></p>
<p><a href="https://zenn.dev/leaner_tech/articles/20210908-openapi-msw-handlers">Mock Service Worker で jest.mock を使わず非同期リクエストのテストを書く</a></p>
<h3>最後に</h3>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!<br></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
nakamoto03
【Laravel】DDDでセッションを取り扱う際の妥協した実装
hatenablog://entry/13574176438028742414
2021-11-05T13:43:00+09:00
2021-11-05T13:44:08+09:00 LaravelでDDDを採用する際のセッション取り扱いの一例について解説しました。
<p>こんにちは、バックエンドエンジニアの青山です。<br />
先日LaravelでDDDするときのセッション取り扱いに困った末、ある程度いい感じに妥協した実装に辿り着きました。今回はそちらについて書こうと思います。</p>
<h1>はじめに</h1>
<p>「LaravelでDDD〜」と書きましたが、本記事の内容は正確には「Laravelでレイヤード<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を採用する際のセッションの取り扱いの一例について」です。
DDDを行う中で出会った問題に対する解決策の一例なのでタイトルは上記のようにしました。</p>
<h1>仕様</h1>
<p>セッションの取り扱いと一言に言っても、その用途は多岐にわたります。今回は「Webサイトの多<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>」という仕様を例に実装します↓</p>
<ul>
<li>サイト上にドロップダウンメニューがあり、そこから使用言語(日本語 / 英語 / 中国語 / <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D9%A5%C8%A5%CA%A5%E0%B8%EC">ベトナム語</a> / <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A5%EB%A5%C8%A5%AC%A5%EB%B8%EC">ポルトガル語</a>など)を選択できる</li>
<li>使用言語を選択したらサイト上の一部のテキストが選択した言語に翻訳されて表示される</li>
<li>サイトには管理者用ページとユーザー用ページがあり、翻訳はユーザー用ページ全体でのみ行う</li>
</ul>
<h1>実装</h1>
<p>タイトルにある通り今回の趣旨は妥協です。困ったらLaravelの機能を利用したりして、いい感じに手を抜きます。<br />
Laravelにもともと実装されている多<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>機能は<a href="https://readouble.com/laravel/8.x/ja/localization.html">こちら</a>を参照してください。</p>
<h2>前提</h2>
<h3>レイヤー</h3>
<p>既存の部分は
Domain層(Domain modelやFactoryクラス、Repositoryのinterfaceなど)<br />
Infrastructure層(RepositoryやQueryServiceの実装クラスなど)<br />
UseCase層(UseCaseクラス、QueryServiceのinterfaceなど)<br />
Presentation層(ControllerやFormRequest、Middlewareなど)<br />
の4層に分割して実装されています(その前段階で文脈ごとに大きく分割されています。詳しくは以降の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ図参照)。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/qingshanhuangye/20211103/20211103184024.png" alt="" width="157" height="241" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回の設計では依存の向きは所謂レイヤード<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>のようにInfrastructure層に向かうものではなく、図の矢印で示すようにDomain層へ向かうものとなっています。
多<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>のためのセッションの取り扱いも、既存の作りと同じように層を分けて実装します。</p>
<h3>全体の流れ</h3>
<p>1.サイトのトップページのメニューから使用言語を選択<br />
2./lang/:localeにGETリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト送信。localeには"ja"や"en"など文字列のパラメーターが入る想定<br />
3.セッションにlocale値設定<br />
4.もとのページにリダイレクト(このときmiddlewareでlocale設定)</p>
<p>もう少し詳細に書くと<br />
リク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト -> Controllerでパラメーター取得 -> UseCaseクラスに渡して色々する(この"色々"の内部でセッションに値を保存したりする)<br />
みたいな感じです。</p>
<h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成</h2>
<p>localeはユーザー側ページ全体で取り回される共通のものなので、どこに置くべきか迷いました。<br />
現在以下のように分割されています</p>
<pre class="code text" data-lang="text" data-unlink>
│ ├── Packages
│ │ ├── AdminPage //管理者用ページ
│ │ ├── FrontPage //ユーザー用ページ
│ │ │ ├── Appointment
│ │ │ ├── Contact
│ │ │ ├── Home
│ │ │ └── StaticPages
│ │ └── Shared
</pre>
<p>考えた結果、<a class="keyword" href="http://d.hatena.ne.jp/keyword/FrontPage">FrontPage</a>全体で共通のものということでSharedという<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを作成しました。<br />
もはや何も考えていないのと同じですが、いい感じに妥協していくのが今回の趣旨です。どんどん妥協しましょう。</p>
<pre class="code text" data-lang="text" data-unlink>
│ ├── Packages
│ │ ├── AdminPage
│ │ ├── FrontPage
│ │ │ ├── Appointment
│ │ │ ├── Contact
│ │ │ ├── Home
│ │ │ ├── Shared //<-追加
│ │ │ │ ├── Domain
│ │ │ │ ├── Infrastructure
│ │ │ │ ├── Presentation
│ │ │ │ └── UseCase
│ │ │ └── StaticPages
│ │ └── Shared
</pre>
<h2>Domain model</h2>
<p>今回の場合localeを司るモデルを作るのがいいかなと判断しました。ということでlocale用のDomain modelを作成します。以下のように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E2%A5%C7%A5%EA%A5%F3%A5%B0">モデリング</a>しました。</p>
<p><script src="https://gist.github.com/c00482616b954c51babb350dc151ad9d.js"> </script></p>
<p><a href="https://gist.github.com/c00482616b954c51babb350dc151ad9d">gistc00482616b954c51babb350dc151ad9d</a></p>
<pre class="code text" data-lang="text" data-unlink>
│── Shared
├── Domain
│ └── Models
│ └── Locale.php
├── Infrastructure
├── Presentation
└── UseCase
</pre>
<h2>セッションを取り扱うクラス</h2>
<p>このクラスをShared以下のどの層に配置するかを考えます。そもそもどういうクラスなのかというと</p>
<ul>
<li>localeの値をセッションに保存する</li>
<li>セッションに保存されているlocaleの値を確認する</li>
</ul>
<p>この2つを行うクラスです。localeの値とセッションを使ってxxをする、という役割なのでUseCase層が適切かと思います。interfaceを作成してUseCase層に置きます。</p>
<p><script src="https://gist.github.com/5eb0257e22b7ceca7a2af9df60f1d2dc.js"> </script></p>
<p><a href="https://gist.github.com/5eb0257e22b7ceca7a2af9df60f1d2dc">gist5eb0257e22b7ceca7a2af9df60f1d2dc</a></p>
<pre class="code text" data-lang="text" data-unlink>
│── Shared
├── Domain
│ └── Models
│ └── Locale.php
├── Infrastructure
├── Presentation
└── UseCase
└── LocaleSessionInterface.php
</pre>
<p>次にこのinterfaceの実装クラスを作成し、Presentation層に配置します。</p>
<p><script src="https://gist.github.com/1150419297fcfac1fdc7c5d5c1728fed.js"> </script></p>
<p><a href="https://gist.github.com/1150419297fcfac1fdc7c5d5c1728fed">gist1150419297fcfac1fdc7c5d5c1728fed</a></p>
<p>getLocaleSession()から返す<a class="keyword" href="http://d.hatena.ne.jp/keyword/DTO">DTO</a>は以下のようになっています。</p>
<p><script src="https://gist.github.com/fbe39c56e70a119b97fb9faad28038df.js"> </script></p>
<p><a href="https://gist.github.com/fbe39c56e70a119b97fb9faad28038df">gistfbe39c56e70a119b97fb9faad28038df</a></p>
<pre class="code text" data-lang="text" data-unlink>
├── Shared //<-追加
│ ├── Domain
│ │ └── Models
│ │ └── Locale.php
│ ├── Infrastructure
│ ├── Presentation
│ │ ├── Dto
│ │ │ └── GetLocaleSessionResponse.php
│ │ └── LocaleSession.php
│ └── UseCase
│ └── LocaleSessionInterface.php
</pre>
<p>interfaceと実装クラスはAppServiceProviderでbindするのを忘れないでください。</p>
<h3>実装クラスをPresentation層に置く理由</h3>
<p>最初はinterfaceと同じUseCase層に置こうと考えました。しかし実装クラスではLaravelのsession()ヘルパーを利用して「具体的にどうするか」を記述しています。UseCase層にそこまで具体的なものを置くのはよくなさそうです。<br />
残るはDomain、Infrastructure、Presentationの三つの層ですが、そもそも何のためにセッションにlocaleの値を保存したりするのかというと、サイト上に表示されるテキストをlocaleに応じて変更するためでした。それを考えるとPresentation層に実装クラスを置くことが適切だと思われます。</p>
<h2>locale値を変更するUseCaseクラス</h2>
<p>Controllerから呼び出されるクラスです。リク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トがきたらControllerからこのクラスにパラメーターを渡して後続の処理を行います。
パラメーターからLocaleの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作成し、パラメーターの値をセッションに保存します。セッション保存の具体的な処理は上記のLocaleSessionクラスで行われるので、ここには抽象的な手続きのみ記述します。</p>
<p><script src="https://gist.github.com/05956c45ab6f0dcd4531d471c5fc6d28.js"> </script></p>
<p><a href="https://gist.github.com/05956c45ab6f0dcd4531d471c5fc6d28">gist05956c45ab6f0dcd4531d471c5fc6d28</a></p>
<p>こちらのクラスはトップページから使用言語を選択した時のUseCaseなので、Shared以下ではなく該当するコンテキストの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ以下に配置します。</p>
<pre class="code text" data-lang="text" data-unlink>├── Home
│ ├── Domain
│ ├── Infrastructure
│ ├── Presentation
│ └── UseCase
│ └── SetLocaleUseCase.php
</pre>
<h2>Controller</h2>
<p>こちらは特筆すべき部分はありません。素直に実装します。</p>
<p><script src="https://gist.github.com/2a5bade46210839f30a9c987b32f1ff0.js"> </script></p>
<p><a href="https://gist.github.com/2a5bade46210839f30a9c987b32f1ff0">gist2a5bade46210839f30a9c987b32f1ff0</a></p>
<pre class="code text" data-lang="text" data-unlink>├── Home
│ ├── Domain
│ ├── Infrastructure
│ ├── Presentation
│ │ ├── Controllers
│ │ └── LocaleController.php
│ └── UseCase
│ └── SetLocaleUseCase.php
</pre>
<h2>現在のlocaleを設定するmiddleware</h2>
<p>Laravelのlocale設定は</p>
<pre class="code lang-php" data-lang="php" data-unlink>app()-<span class="synError">></span>setLocale('locale_value')
</pre>
<p>で実現できます(Facadeを使用する方法もあります)。この部分はLaravelのmiddlewareで行い、ユーザー側の全ページに対して適用します。</p>
<p><script src="https://gist.github.com/860ff2f0cdde0dc7d3d2433b83decf6e.js"> </script></p>
<p><a href="https://gist.github.com/860ff2f0cdde0dc7d3d2433b83decf6e">gist860ff2f0cdde0dc7d3d2433b83decf6e</a></p>
<p>app/Http/Kernel.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>の$routeMiddleware配列に忘れず追加しておきます。</p>
<pre class="code lang-php" data-lang="php" data-unlink>['set.locale' =<span class="synError">></span> \App\Packages\FrontPage\Home\Presentation\Middleware\SetLocaleMiddleware::class,]
</pre>
<pre class="code text" data-lang="text" data-unlink>
├── Home
│ ├── Domain
│ ├── Infrastructure
│ ├── Presentation
│ │ ├── Controllers
│ │ │ └── LocaleController.php
│ │ ├── Middleware
│ │ └── SetLocaleMiddleware.php
│ └── UseCase
│ └── SetLocaleUseCase.php
</pre>
<h2>ルーティング</h2>
<p><script src="https://gist.github.com/fd3a74cc91d52ece76a6a23a2fa23c83.js"> </script></p>
<p><a href="https://gist.github.com/fd3a74cc91d52ece76a6a23a2fa23c83">gistfd3a74cc91d52ece76a6a23a2fa23c83</a></p>
<p>これで一通りの実装が完了しました。後は公式ドキュメントに従って言語ファイルを作成すれば、@langディレクティブや__()ヘルパーを使用して翻訳文字列が取得可能です。</p>
<h1>メリット</h1>
<p>この実装のメリットは、「ある文脈におけるセッションの取り扱い方法が限定されているので、コード内に散乱しがちなヘルパーやFacadeの乱用を抑制できたり、セッションのキーにする文字列の管理がしやすくなる」ということだと考えています。また、文脈ごとに区切ってセッションの取り扱いを限定するという実装は、セッションを取り扱う他の様々な場面でも応用できるのではと思います。粒度も場合によりけりだと思いますので、もっとスコープを広くして汎用的なセッション取り扱いクラスを作ってもいいかもしれません。</p>
<h1>最後に</h1>
<p>Wizではエンジニアを募集しております。<br />
興味のある方、ぜひご覧下さい!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
<p>参考図書: <a href="https://booth.pm/ja/items/1835632">ドメイン駆動設計 モデリング/実装ガイド - little-hands - BOOTH</a></p>
qingshanhuangye
現代を支配する究極のエンジニア像
hatenablog://entry/13574176438021272385
2021-10-18T17:18:35+09:00
2021-10-18T17:18:35+09:00 需要激増中のグロースハッカーという職業について書いてみました。
自分が目指す職業の一つの選択肢として読んでいただけると幸いです。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/d/daigo2895/20211012/20211012163521.jpg" alt="f:id:daigo2895:20211012163521j:plain" width="1200" height="801" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
はじめまして!フロントエンドの久保です。<br></p>
<p>今回は前回の<a href="https://tech.012grp.co.jp/entry/2021/06/21/121513">【営業部 → エンジニア】へジョブチェンジした話</a>に続き、<br>
<strong>「理想のエンジニア像」</strong>についてお話します。<br>
目指すエンジニア像は人それぞれなので一つの参考になれば嬉しいです。</p>
<h3>まずは結論から</h3>
<p>プログラムが書けて、デザインが出来て、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C6%A5%A3%A5%F3%A5%B0">マーケティング</a>も出来るエンジニアです!<br>
・・・なんじゃそれ(笑)</p>
<h3>実現させるには</h3>
<p>そんな野球少年が<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%CC%EE%B5%E5">プロ野球</a>選手になりたい!と言ってるわけではないのです。<br>
実はきちんとそういうポジションがあります。<br>
それを説明させてください。</p>
<h3>その名もグロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a></h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Growth">Growth</a>(成長)Hackerといい、<br>
急激な成長を遂げた世界的な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A6%A5%A7%A5%D6%A5%B5%A1%BC%A5%D3%A5%B9">ウェブサービス</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Dropbox">Dropbox</a>などの成長を支えたと言われています。<br><br></p>
<p>業務内容としては、端的にいうと<strong>最低限のコスト</strong>でユーザーをよりエンゲージメント(顧客・ユーザーの愛着度)の高い状態に持っていく事です。<br>
言い換えると、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%A4%A5%A2%A5%F3%A5%C9%A5%A8%A5%E9%A1%BC">トライアンドエラー</a>を繰り返しながら新たな成果を作り出すことだと言えます。<br>
そして、<strong>優秀なグロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>ほど、広告費や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C6%A5%A3%A5%F3%A5%B0">マーケティング</a>費をかけずに任務を遂行する</strong>といわれています。<br><br></p>
<p>用語自体は2010年に出来た言葉ですが、近年多様なサービスや類似のサービスが増えてきてる中で
顧客に向き合い、商品・サービスに最も合った戦略を自ら模索していく姿が求められることから<strong>需要が急激に伸びてきています。</strong></p>
<h3>なぜグロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>を目指すのか</h3>
<p>私は営業部時代に様々なサービスを取り扱ってきました。<br>
そこで見たものは、クオリティに関係なくユーザーにいい見せ方をしているサービスが数字を伸ばすという悲しい現実です。<br>
逆に同業他社と比べて優位性のあるものも何故か競り負けている状態です。<br><br></p>
<p>それを改善するには<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C6%A5%A3%A5%F3%A5%B0">マーケティング</a>の見直し、そして実装をするエンジニアが必要なことがわかりました。<br>
しかし現状自社にはその2つが出来る人材はおらず、他業種とのコミュニケーションにおいてもなかなかもどかしい経験をしました。<br><br></p>
<p>その経験から私は、<br>
おぼろげながらもその課題を解決出来るような人材になりたいと思うようになり、<br>
そこから現在、グロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>という明確な目標が生まれました。<br><br></p>
<p>目指すのは一つ、<br>
<strong>「本当に良いサービスが正しくユーザーに届くようにしたい」</strong>です。</p>
<h3>必要なスキル</h3>
<p>グロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>とは具体的に何が出来る人なのかざっくり2つに分けて説明します。<br></p>
<h4>① データ解析能力</h4>
<p>サービスを成長させるためにアクセス分析は勿論、カスタマー分析、事業分析、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C6%A5%A3%A5%F3%A5%B0">マーケティング</a>分析など総合的に行います。<br>
また、グロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>が打ち出す施策は、勘やこれまでの経験、定性的な情報では行わず、<br>
<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>的(数値化出来るもの)なデータを分析した結果に基づいて行われます。</strong><br>
そのため、分析ツールを使いこなす知識や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C5%FD%B7%D7%B3%D8">統計学</a>に関する知識も求められます。</p>
<h4>② 問題点を把握し、仮設を立て、解決する能力</h4>
<p>①で提案したものを自らが実装出来るのがグロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>の強みだと考えます。<br>
これによって従来よりもより細やかな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%A4%A5%A2%A5%F3%A5%C9%A5%A8%A5%E9%A1%BC">トライアンドエラー</a>が実行出来ることとなり、サービスとしての力強さが実現するのです。<br><br>
これはシンプルな仕事に思えますが、辛抱強く膨大なデータを検証し、徹底したA/Bテストを実施することが非常に重要なのです。<br>
グロースハックのために打ち出す<strong>施策の75%〜90%は失敗に終わる</strong>と言われています。<br>
その中で地道に素早いサイクルで実行し続ける「不屈の精神」も必要となってきます。<br><br></p>
<h3>5段階の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>「AARRR」</h3>
<p>グロースハックには「AARRR(アー)」という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>があります。<br>
具体的には、下記となります。<br><br>
① Acquisition(ユーザー獲得)<br>
② Activation(活性化)<br>
③ Retention(継続)<br>
④ Referral(紹介)<br>
⑤ Revenue(収益化)<br><br></p>
<p>これが実行出来る人材になれば市場での価値は勿論のこと、<br>
自分が思い描くエンジニア像を実現出来ると考えます。<br><br></p>
<p>WEBでの集客、交流が主流をなっている今、<br>
1人でも実行出来る人材が増えたらもっともっと<strong>エンジニア市場の価値が上がり、報酬も上がっていく</strong>ことでしょう。</p>
<h3>求められる資質「ABCDE」</h3>
<p>グロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a>に求められる資質を紹介します。<br><br></p>
<p>① Analyticity(分析力に長けている)<br>
② Broad interest(好奇心に富んでいる)<br>
③ Creative(創造性が豊かである)<br>
④ Discipline(自らを律し、地道に取り組める)<br>
⑤ Empathy(ユーザーの気持ちに共感できる)<br><br></p>
<p>これを見ると、いかにユーザーを想い地道に分析出来るかが重要となってくることが
わかります。<br>
<strong>ちなみに現時点では「B,D,E」を持ち合わせていると思っています!</strong><br>
色々な経験をし、分析力や創造性を高めていけるように努力したいと思います。<br></p>
<h3>参考</h3>
<p><a href="https://exidea.co.jp/journal/growth-hack">グロースハッカーとは?シリコンバレーでも注目される最先端のWEBマーケティング職 | 株式会社EXIDEA</a></p>
<p><a href="https://circu.co.jp/pro-sharing/mag/article/1658/">グロースハックとは?〜サービスを急成長させる方法と実践のための6ステップ〜 | ProSharing Consulting(プロシェアリングコンサルティング)</a></p>
<h3>最後に</h3>
<p>いかがだったでしょうか。
この記事で<strong>グロース<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CF%A5%C3%A5%AB%A1%BC">ハッカー</a></strong>を知ったという人もいるのではないでしょうか。
まだまだ書ききれなかったことも多くありますので少しでも興味を持った方は一度ググってみるのも良いかもしれません。</p>
<p>営業からエンジニアに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%E7%A5%D6%A5%C1%A5%A7%A5%F3%A5%B8">ジョブチェンジ</a>し半年が経過しようとしています。
まだまだ出来ることは少ないですが今までの経験を生かし、やるからには希少なエンジニアになるべく努力をしていきます。<br></p>
<p>そして…<br></p>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!<br></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
daigo2895
EMトライアングルから見るWizのエンジニアリングマネージャーの現役割と、さらなる成長に向けて組織再編した話
hatenablog://entry/13574176438020367687
2021-10-11T10:47:04+09:00
2021-10-11T10:47:04+09:00 こんにちは、株式会社Wizでエンジニアリングマネージャー(以下EM)をやっている上野です。 バックエンドエンジニアとして活動したのちEMとなり、数年経過しました。 今回、WizのEMがどんな活動を行っているのかを振り返った上で、 さらなるエンジニア組織の前進のために、10月から実施した組織再編について紹介します。 (前提)そもそもエンジニア組織はなにやっているの? 前提として、Wizのエンジニア組織は大きく2つの役割を担っており、すべてが自社開発となります。 各種Webメディアを開発し集客する広告的役割 社内外を業務改善したり、営業価値を最大化するようなWebプロダクト開発 WizのEMの現役…
<p>こんにちは、株式会社Wizでエンジニア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャー(以下EM)をやっている上野です。</p>
<p>バックエンドエンジニアとして活動したのちEMとなり、数年経過しました。</p>
<p>今回、WizのEMがどんな活動を行っているのかを振り返った上で、
さらなるエンジニア組織の前進のために、10月から実施した組織再編について紹介します。</p>
<h1>(前提)そもそもエンジニア組織はなにやっているの?</h1>
<p>前提として、Wizのエンジニア組織は大きく2つの役割を担っており、すべてが自社開発となります。</p>
<ul>
<li>各種Webメディアを開発し集客する広告的役割</li>
<li>社内外を業務改善したり、営業価値を最大化するようなWebプロダクト開発</li>
</ul>
<h1>WizのEMの現役割</h1>
<p>現役割の整理には、「エンジニア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネジメントトライアングル」を利用させていただきます。
先人の方々の知見が詰まった非常にありがたいモデルです。</p>
<ul>
<li><a href="https://steam.place/entry/2019/07/03/083000">https://steam.place/entry/2019/07/03/083000</a></li>
<li><a href="https://github.com/engineering-manager-meetup/engineering-management-triangle">https://github.com/engineering-manager-meetup/engineering-management-triangle</a></li>
</ul>
<p>「テク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A5%ED">ノロ</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A1%BC">ジー</a>」「プロダクト」「チーム」の軸の間をそれぞれ埋める空白領域が、EMの役割だという考え方です。</p>
<p>弊社において、役割として担っている部分に色をつけたものが以下です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uehi1206/20211008/20211008162609.jpg" alt="f:id:uehi1206:20211008162609j:plain" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>弊社のEMとしては、赤部分を担うことが主な役割です。</p>
<p>一方で、青部分のような特定のEMのみ担っているものもあり、
これは、各EMのやりたい領域や得意領域に合わせて遂行している部分です。
弊社の「言ったもの勝ち」「やったもの勝ち」の文化が出ている部分ではないでしょうか。</p>
<p>また、全体として「チーム」に寄っていると思いますが、これも文化として「人の成長」「何をやるかより誰とやるか」を大切にしている結果かもしれません。</p>
<p>具体的には主に以下のような活動を行っています。</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンやメンバーのリソース/進捗マネジメント
<ul>
<li>メンバーのやりたいこと、できることを考慮した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンと、完遂までの支援、負荷調整など。</li>
</ul>
</li>
<li>メンバーのキャリア開発・成長支援
<ul>
<li>1on1、コミュニケーションをベースとした関係構築</li>
</ul>
</li>
<li>組織としてのビジョン・ミッションの共有と評価・フィードバック
<ul>
<li>組織のやりたいと個人のやりたいをマッチングさせ目標を定め、取り組みをフィードバック。</li>
</ul>
</li>
<li>採用活動、面接</li>
</ul>
<h1>さらなる成長に向けての組織再編</h1>
<p>まずは結論から。このような Before → After にしました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uehi1206/20211008/20211008162907.jpg" alt="f:id:uehi1206:20211008162907j:plain" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uehi1206/20211008/20211008162921.jpg" alt="f:id:uehi1206:20211008162921j:plain" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>なぜこのような再編をやったかについては、以下のような課題解決の考えからです。</p>
<h2>「ミッション」に、よりフォーカスする</h2>
<p>EMトライアングルの現状でも出ていますが、「プロダクト」寄りの役割がまだまだ弱いと感じていました。
これは、ミッションが弱いということにもつながり、ここをもっと担っていきたいという課題がありました。</p>
<p>そこで、Beforeのような「フロントエンド」「バックエンド」といった職能別組織をやめ、
「メディア」「プロダクト」といったミッションの大枠で組織を定義しました。
そこには、各人が「フロントエンドだけ」「バックエンドだけ」を学習・業務遂行すればよいわけではなく、
ミッション達成のために得意分野を重ね合わせ、協力していってほしいというメッセージです。</p>
<h2>曖昧な部分を、役割として明確化し、整備したい。</h2>
<p>エンジニアのメイン業務は開発することですが、それだけでは強いエンジニア組織になりません。
例えば本ブログのような社外向けの発信といったものや、採用にまつわるイベント・カジュアル面談といったもの、
教育体制を作ったり、コミュニケーション改善のための施策などなど、開発体制をつくるための周辺業務があります。</p>
<p>もちろん全社的には採用や教育の事業部は存在しますが、エンジニア組織に適用するにはマッチしない部分もあるため、
これまでエンジニア組織内の有志のメンバーやEMが推進してくれていました。
これが、上記EMトライアングルの青部分「特定のEMのみ担っているもの」であったり、
赤部分「EM全員が担うもの」でもEMによって得意領域の違いから取り組みに濃淡がありました。</p>
<p>そのため今回、「本部」を組織構造として明言した、ということになります。</p>
<p>以上が、組織再編の思いです。</p>
<h1>おわりに</h1>
<p>このように、エンジニア組織は、EMとメンバーがともに
様々な試行錯誤を繰り返しながら、日々成長していっています。</p>
<p>一緒に切磋琢磨したい方、エンジニアもEMも募集していますので、
興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
uehi1206
知ってると得するかもしれないCSS(Sass)のニッチなテクニック集
hatenablog://entry/13574176438016330469
2021-09-28T18:21:43+09:00
2021-09-28T18:21:43+09:00 今回は私が実際に実務で使用しているCSS(Sass)のちょっとしたテクニック集を紹介します。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz_sasaki/20210927/20210927144411.jpg" alt="f:id:wiz_sasaki:20210927144411j:plain" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>皆様こんにちは、フロントエンドエンジニアの佐々木です。</p>
<p>今回は私が実際に実務で使用している<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>(Sass)のちょっとしたテクニック集を紹介しようと思います。</p>
<h2>否定疑似クラス「:not()」を利用した余白のとり方</h2>
<p><code>:not()</code>に<code>:first-child</code>を指定することで、<strong>最初の要素以外</strong>という指定ができます。</p>
<pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier"><</span><span class="synStatement">ul</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"list"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">li</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"list-item"</span><span class="synIdentifier">></</span><span class="synStatement">li</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">li</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"list-item"</span><span class="synIdentifier">></</span><span class="synStatement">li</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">li</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"list-item"</span><span class="synIdentifier">></</span><span class="synStatement">li</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">li</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"list-item"</span><span class="synIdentifier">></</span><span class="synStatement">li</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">ul</span><span class="synIdentifier">></span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.list-item</span> <span class="synIdentifier">{</span>
&:not(:<span class="synConstant">first</span>-<span class="synConstant">child</span>) <span class="synIdentifier">{</span>
<span class="synType">margin-top</span>: <span class="synConstant">16px</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>これの何が良いのかというと、リストのようなレイアウトは<strong>最初もしくは最後の余白を0</strong>にしたいことがほとんどだと思います(他の要素への影響を最小限にするため)。
<code>.list-item</code>に直接<code>margin-top</code>をとってしまうと<code>.list-item:first-child</code>で<code>margin-top</code>を打ち消す記述をしないといけません。<code>:not()</code>を使用することで不要な記述を減らすことができます。</p>
<h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A1%D6%26amp%3B%A1%D7">「&」</a>を利用した連続する同じ要素間の余白</h2>
<p>ネスト内で<code>& + &</code> とすることで<strong>次の要素が同じだったら</strong>という指定ができます(ここでは<code>.l-section</code>)。</p>
<pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier"><</span><span class="synStatement">section</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"l-section"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">h2</span><span class="synIdentifier">></span>セクション1<span class="synIdentifier"></</span><span class="synStatement">h2</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">section</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">section</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"l-section"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">h2</span><span class="synIdentifier">></span>セクション2<span class="synIdentifier"></</span><span class="synStatement">h2</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">section</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">section</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"l-section"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">h2</span><span class="synIdentifier">></span>セクション3<span class="synIdentifier"></</span><span class="synStatement">h2</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">section</span><span class="synIdentifier">></span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.l-section</span> <span class="synIdentifier">{</span>
& + & <span class="synIdentifier">{</span>
<span class="synType">margin-top</span>: <span class="synConstant">50px</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>先程の<code>&:not(:first-child)</code>に似ていますが、要素の間に別の要素が入って来た時の挙動が微妙に違います。</p>
<p>注意点としてこの方法は<strong>ネストの親でのみ使用可能</strong>です。</p>
<p>ちなみに以下と同じ意味です。</p>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.l-section</span> <span class="synIdentifier">{</span>
+ .l-section <span class="synIdentifier">{</span>
<span class="synType">margin-top</span>: <span class="synConstant">50px</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<h2>詳細度を無理やり上げる</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>には詳細度というのもが存在します。上書きするにはその詳細度を上回る必要があります。
<code>!important</code>を使えば簡単に上書き可能ですが、宗教上の理由で<code>!important</code>を使えない方がほとんどだと思います。以下はその代替方法です。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>詳細度計算サイト : <a href="https://specificity.keegan.st/">https://specificity.keegan.st/</a></p>
<pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier"><</span><span class="synStatement">div</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"content"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">p</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"text"</span><span class="synIdentifier">></span>この文字の色は赤です。<span class="synIdentifier"></</span><span class="synStatement">p</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">div</span><span class="synIdentifier">></span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink>//詳細度 : 1<span class="synSpecial">,</span> 1<span class="synSpecial">,</span> 0
<span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
&:not(#_) <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">red</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
//詳細度 : 0<span class="synSpecial">,</span> 1<span class="synSpecial">,</span> 0
<span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">blue</span>;
<span class="synIdentifier">}</span>
//詳細度 : 0<span class="synSpecial">,</span> 2<span class="synSpecial">,</span> 0
<span class="synIdentifier">.content</span> <span class="synIdentifier">{</span>
.<span class="synType">text</span> <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">blue</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>上記のように<code>&:not(#_)</code>とすることで、<code>.text</code>は<code>#_</code>ではないという指定になります。こうすることで<strong>IDを指定した時と同等の詳細度を得る</strong>ことができます。</p>
<p>この方法のメリットは<code>!important</code>を使用せずに高い詳細度を得ることにあります。AMPのような<code>!important</code>が使用するとエラーが出る環境だと使う可能性があります。</p>
<p><strong>可読性はあまり良くないので、そもそもこれを使わなくて済むような<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>設計を心がけたほうが良いです。</strong></p>
<p>他に有名な詳細度を上げる方法として<code>:not(:root)</code>という方法も存在します。</p>
<h2>画像の比率を固定する</h2>
<p>様々なデ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>に対応しないといけない現在のWEB制作において画像の比率を維持するということは非常に重要なことです。以下はpaddingの仕様を利用した画像の比率維持方法です。</p>
<p>ここでの画像は1920px * 1080pxを想定しています。</p>
<pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier"><</span><span class="synStatement">div</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"image-wrapper"</span><span class="synIdentifier">></span>
<span class="synIdentifier"><</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">"/path/to/image.jpg"</span><span class="synIdentifier"> </span><span class="synType">width</span><span class="synIdentifier">=</span><span class="synConstant">"1920"</span><span class="synIdentifier"> </span><span class="synType">height</span><span class="synIdentifier">=</span><span class="synConstant">"1080"</span><span class="synIdentifier"> </span><span class="synType">alt</span><span class="synIdentifier">=</span><span class="synConstant">"比率固定したい画像"</span><span class="synIdentifier">></span>
<span class="synIdentifier"></</span><span class="synStatement">div</span><span class="synIdentifier">></span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.image-wrapper</span> <span class="synIdentifier">{</span>
<span class="synType">position</span>: <span class="synConstant">relative</span>;
&::before {
<span class="synConstant">content</span>: <span class="synConstant">''</span>;
<span class="synType">display</span>: <span class="synConstant">block</span>;
<span class="synType">padding-top</span>: calc((<span class="synConstant">1080</span> / <span class="synConstant">1920</span>) <span class="synComment">*</span> <span class="synConstant">100</span>%);
<span class="synIdentifier">}</span>
<span class="synStatement">img</span> <span class="synIdentifier">{</span>
<span class="synType">position</span>: <span class="synConstant">absolute</span>;
<span class="synType">top</span>: <span class="synConstant">0</span>;
<span class="synType">left</span>: <span class="synConstant">0</span>;
<span class="synType">width</span>: <span class="synConstant">100</span>%;
<span class="synType">height</span>: <span class="synConstant">100</span>%;
<span class="synIdentifier">}</span>
<span class="synError">}</span>
</pre>
<p>上記の<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>で1920px * 1080pxの比率は画面の幅が変わっても維持されます。</p>
<p>ここで重要なのは疑似要素<code>::before</code>の<code>padding-top</code>です。padding-topは%で指定するとその値は<strong>親要素の幅に対しての割合</strong>となります。</p>
<p>以下の計算式に当てはめると<strong>幅に対する高さの比率を求めることができます</strong>。</p>
<pre class="code" data-lang="" data-unlink>(高さ / 幅) * 100%</pre>
<p>これを利用して疑似要素で高さをとり、imgをabsoluteで浮かせて配置することで画像の比率の維持が可能です。</p>
<p>しかし、<strong>非常に可読性が悪くなるのでこういった面倒な計算をするものはmixin化すると使い勝手がよくなります。</strong></p>
<pre class="code lang-css" data-lang="css" data-unlink>@mixin aspect-ratio($width<span class="synSpecial">,</span> $height<span class="synSpecial">,</span> $first: true) <span class="synIdentifier">{</span>
<span class="synType">position</span>: <span class="synConstant">relative</span>;
&::before {
<span class="synConstant">content</span>: <span class="synConstant">''</span>;
<span class="synType">display</span>: <span class="synConstant">block</span>;
<span class="synType">padding-top</span>: ($height / $width) <span class="synComment">*</span> <span class="synConstant">100</span>%;
<span class="synIdentifier">}</span>
@if $first == true <span class="synIdentifier">{</span>
& > :<span class="synConstant">first</span>-<span class="synConstant">child</span> {
position: <span class="synConstant">absolute</span>;
<span class="synType">top</span>: <span class="synConstant">0</span>;
<span class="synType">left</span>: <span class="synConstant">0</span>;
<span class="synType">width</span>: <span class="synConstant">100</span>%;
<span class="synType">height</span>: <span class="synConstant">100</span>%;
<span class="synIdentifier">}</span>
<span class="synError">}</span>
<span class="synError">}</span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink>@include aspect-ratio(1920<span class="synSpecial">,</span> 1080);
</pre>
<h2>親要素を突き破って横幅100%</h2>
<p>背景のみ横幅100%にして色をつけたいときなどに役に立ちます。</p>
<pre class="code lang-css" data-lang="css" data-unlink>margin-right: calc(50% - 50vw);
margin-left: calc(50% - 50vw);
padding-right: calc(50vw - 50%);
padding-left: calc(50vw - 50%);
</pre>
<p>ポイントは<code>padding</code>で内要素の幅を保っていることです。ですので<code>padding</code>削除すると完全に横幅100%になります。</p>
<p>状況に応じて使い分けていくと良いでしょう。</p>
<h2>否定形のメディアクエリ</h2>
<p><code>not</code><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>を使うとメディアクエリの内容を反転することができます。</p>
<p>例えば、767px以下768以上のスタイルを書きたい場合があるとします。</p>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
@media screen and (<span class="synType">min-width</span>: <span class="synConstant">768px</span>) <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">red</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
@media screen and (<span class="synType">max-width</span>: <span class="synConstant">767px</span>) <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">blue</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>このように書きたくなりますが、これは特定の条件でスタイルが当たりません。</p>
<p>特定の条件とは<strong>768px〜767pxの間である画面幅が767.5pxのなどの場合</strong>はスタイルが当たりません。これはブラウザの拡大率や画面の解像度の組み合わせによって起こる可能があります。</p>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
@media screen and (<span class="synType">min-width</span>: <span class="synConstant">768px</span>) <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">red</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synIdentifier">.text</span> <span class="synIdentifier">{</span>
@media not screen and (<span class="synType">min-width</span>: <span class="synConstant">768px</span>) <span class="synIdentifier">{</span>
<span class="synType">color</span>: <span class="synConstant">blue</span>;
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>上記のようにメディアクエリに<code>not</code><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B1%E9%BB%BB%BB%D2">演算子</a>を組み合わせることによって<strong>画面幅が768pxより小さい場合</strong>という指定が可能です。</p>
<p>つまり画面幅が767.5pxの場合でもスタイルが当たります。</p>
<p><a href="https://developer.mozilla.org/ja/docs/Web/CSS/Media_Queries/Using_media_queries">メディアクエリについての詳しい解説はコチラ</a></p>
<h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B6%A5%D3%A5%EA%A5%C6%A5%A3">ユーザビリティ</a>を考慮したコンテンツの非表示方法</h2>
<p>以下のように実装すると<strong>テキストは見えないけどスクリーンリーダーは読み上げ可能</strong>を実現できます。</p>
<pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier"><</span><span class="synStatement">p</span><span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">"visually-hidden"</span><span class="synIdentifier">></span>このテキストは見えないけどスクリーンリーダーは読み上げます。<span class="synIdentifier"></</span><span class="synStatement">p</span><span class="synIdentifier">></span>
</pre>
<pre class="code lang-css" data-lang="css" data-unlink><span class="synIdentifier">.visually-hidden</span> <span class="synIdentifier">{</span>
<span class="synType">position</span>: <span class="synConstant">absolute</span>;
<span class="synType">white-space</span>: <span class="synConstant">nowrap</span>;
<span class="synType">width</span>: <span class="synConstant">1px</span>;
<span class="synType">height</span>: <span class="synConstant">1px</span>;
<span class="synType">overflow</span>: <span class="synConstant">hidden</span>;
<span class="synType">border</span>: <span class="synConstant">0</span>;
<span class="synType">padding</span>: <span class="synConstant">0</span>;
<span class="synType">clip</span>: <span class="synIdentifier">rect(</span><span class="synConstant">0 0 0 0</span><span class="synIdentifier">)</span>;
<span class="synType">clip</span>-path: <span class="synConstant">inset</span>(<span class="synConstant">50</span>%);
<span class="synType">margin</span>: <span class="synConstant">-1px</span>;
<span class="synIdentifier">}</span>
</pre>
<p>詳しくは下記サイトの<strong>Hiding content visually</strong>の箇所をご確認ください。</p>
<p><a href="https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939">https://medium.com/@matuzo/writing-css-with-accessibility-in-mind-8514a0007939</a></p>
<p>他のメリットとしては<code>display: none</code>ではないので<strong>フォーカスが可能</strong>な点が挙げられます。</p>
<h2>最後に</h2>
<p>今回は「<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>のニッチなテクニック集」という内容でパッと思いついたものを紹介してみました。知っている人は「なんだそんなことか」と思われる内容だったかもしれません。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>はかなり自由に書けてしまうのでオレオレコーディングになりがちです。特に<code>:not()</code>を利用したものは便利な反面、知らない人は「ん?」ってなることが多いです。</p>
<p>他の人も<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>を触ることを考慮して複雑な内容ほどコメントを残しておくと良いでしょう。</p>
<p>それでは今回は以上となります。この記事が少しでも誰かの役の立てればいいなと思います。</p>
<p>また、</p>
<p>Wizではエンジニアを募集しております。</p>
<p>興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
wiz_sasaki
リモートで社会人スタートした新卒エンジニアの本音
hatenablog://entry/13574176438012318078
2021-09-28T14:25:04+09:00
2021-09-28T14:25:04+09:00 研修・実務のほとんどをリモートで行ってきた新卒エンジニアが、リモートワークに対する本音を語りました。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/miyabi-s0522/20210927/20210927173155.jpg" alt="アイキャッチ画像" width="1200" height="799" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
こんにちは!フロントエンドエンジニアの島田です。<br>
<a href="https://tech.012grp.co.jp/entry/internship-training">前回</a>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>研修について記事を書きましたが、今回はリモートワークについてお話しします。<br></p>
<p>研修・実務のほとんどをリモートで行ってきた新卒エンジニアが、リモートワークに対する本音を語っていきたいと思います。</p>
<h1>1.リモートワークのメリット</h1>
<p>早速良かった点からお話ししていきます!^^</p>
<h4>その1 通勤ストレスがゼロ</h4>
<p>まずこちらが真っ先に思い浮かびました。笑<br>
通勤時間って、合計するとかなり時間がかかると思いませんか?<br>
会社まで片道30分だとしても一日で60分、一週間で5時間も移動に時間を使っていることになりますよね…😓<br></p>
<p>しかし!これがリモートになると出勤や退勤で使う時間が0になる上に、満員電車のストレスからも逃れることができちゃいます!<span style="font-size: 80%">(大学へ行くのに電車で40分、片道1時間10分かけていた私<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A4%AB%A4%E9%A4%B7">からし</a>ましたら、本当に天国でした…!)</span><br></p>
<p>そして最も有り難かったのが、有効活用できる時間が大幅に増えたことです!<br>
本来通勤に使う時間で机に向かって勉強したり、趣味に使ったりと…浮いた時間を自由に使っています。</p>
<h4>その2 リラックスした環境で仕事できる</h4>
<p>自分の家や部屋って落ち着きますよね。<br>
リモートワークは出社する必要がないため、自分の部屋で仕事することができます!<br></p>
<p>落ち着いた環境で黙々と作業できるのは、集中できて良いと感じています。<br>
時々の出社も、気分転換のような感じで新鮮で楽しいです!</p>
<h4>その3 コミュニケーション量が増えた</h4>
<p>私がWizに入社するまでは、<br></p>
<p>「リモートで仕事するって聞いたけど、会社の人とのコミュニケーションはどうなるのかな…?」<br>
「リモートじゃ話す機会も少ないだろうし、先輩方や同期と仲良くなれるか心配だ…」<br></p>
<p>という、リモートワークに対する不安がいくつかありました。<br>
が!実際に入社してから感じたことは、<br></p>
<p>「あれっ?全然問題なくコミュニケーション取れてる」<br></p>
<p>でした。笑<br></p>
<p>Wizでは<a href="https://ovice.in/ja/">oVice</a>というツールを使って、業務に取り組んでいます。<br>
有り難いことにここでは雑談会等、コミュニケーションの活性化を図るイベントが沢山行われています。そういうイベントに参加することで、色々な職種の方とお話しする機会があります。<br></p>
<p>それ以外でも、業務終わりにoViceに残って雑談することもあり、コミュニケーションが不足していると感じることはあまり無かったです!</p>
<h1>2.リモートワークのデメリット</h1>
<p>何かと便利なリモートワークですが、もちろん良い点ばかりではありません😰<br>
ここでは、リモートで作業している際に「こ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A4%B3%A4%CF%A4%C1">こはち</a>ょっと大変だな…」と感じたことについてお話ししていきます。</p>
<h4>その1 相手に何かを伝える手段が限られている</h4>
<p>リモートワークでは、相手の顔を見ながら説明する機会が少ないです。基本的に文章や音声のみ、必要に応じて画面共有しながらやりとりをしています。<br></p>
<p>最初の頃は自分の伝えたい内容が上手く相手に伝わらなかったり、逆に相手の説明をなかなか理解できないといったことがありました。<br>
慣れてしまえば問題ないのですが、慣れるまでが大変でした。</p>
<h4>その2 慣れるまでは話しかけづらい</h4>
<p>リモートで作業している時は相手の様子がわかりません。そのため、先輩に質問をしたい時に<br></p>
<p>「今話しかけても大丈夫だろうか?作業の邪魔にならないだろうか…」<br></p>
<p>と、必要以上にタイミングを気にしてしまい、対面の時よりも話しかけづらいと感じてしまうことが多々ありました。<br></p>
<p>こちらも慣れてしまえば問題ないのですが、最初はリモートで他の人に話しかけることのハードルが高いと感じました。<br></p>
<h4>その3 環境によっては集中できない</h4>
<p>先程「リラックスした環境で仕事できる」と記述しましたが、自宅付近の環境によってはそれが難しい場合もあります。<br></p>
<p>在宅で仕事をしている際に気づいたことですが、外の音って意外と聞こえてくるんですよね😓<br>
近所で工事していたり外で子供が騒いでいたりすると、集中力が切れやすくなって逆に仕事が捗らないことが多いです…(体験談)<br></p>
<p>こればっかりは仕方のないことですが、しっかり集中できる環境で仕事したいのなら、自室の環境に加え外の環境も良いところを選ばなければと強く感じました。<br></p>
<p>余談ですが、仕事机や椅子も自分の身長やスタイルに合ったものを使わないと、腰が痛くなったり疲れやすくなります。<br>
多少高くても、自分に合ったものの利用をオススメします…!</p>
<h1>3.まとめ</h1>
<p>リモートワークには便利な点が多い反面、快適なリモート生活を得るには、慣れることが必要だったり周囲の環境が大きく影響します。<br></p>
<p>この記事を通して、リモートワークについて少しでも参考になりましたら幸いです!^^</p>
<p>最後に…<br></p>
<p>Wizではエンジニアを募集しております。<br>
興味のある方、ぜひご覧下さい!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
miyabi-s0522
ServiceWorkerをサクッと導入してみた
hatenablog://entry/13574176438014037619
2021-09-24T12:18:55+09:00
2021-09-24T13:17:56+09:00 こんにちは、インフラエンジニアの赤倉です。 今回は「ServiceWorker」のオフラインキャッシュを使ってWebページの読み込み高速化を実現してみたいとおもいます。 ServiceWorkerとは ServiceWorker とは、ブラウザが Web ページとは別にバックグラウンドで実行するJavaScript環境のことです。ページ内のコンテンツをブラウザ側にキャッシュしてオフライン状態でも利用することができます。 対応ブラウザ Chrome Edge Firefox Opera Safari また、ServiceWorkerの稼働条件としてWebページのHTTPS化が必須となっております…
<p> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20210922/20210922142424.png" alt="f:id:wiz-akakura:20210922142424p:plain" width="363" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p>こんにちは、インフラエンジニアの赤倉です。</p>
<p>今回は「ServiceWorker」の<span style="color: #333333; font-family: メイリオ, 'MS Pゴシック', Meiryo, 'MS PGothic', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">オフラインキャッシュ</span>を使ってWebページの読み込み高速化を実現してみたいとおもいます。</p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">ServiceWorkerとは</h3>
<p>ServiceWorker とは、ブラウザが Web ページとは別にバックグラウンドで実行する<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>環境のことです。ページ内のコンテンツをブラウザ側にキャッシュしてオフライン状態でも利用することができます。</p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">対応ブラウザ</h3>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Chrome">Chrome</a></li>
<li>Edge</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Firefox">Firefox</a></li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Opera">Opera</a></li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Safari">Safari</a></li>
</ul>
<p>また、ServiceWorkerの稼働条件としてWebページの<a class="keyword" href="http://d.hatena.ne.jp/keyword/HTTPS%B2%BD">HTTPS化</a>が必須となっております。(または<a class="keyword" href="http://d.hatena.ne.jp/keyword/localhost">localhost</a>でも実行可)</p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">ServiceWorkerのライフサイクル</h3>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">register</h5>
<ul>
<li>ブラウザに<span style="color: #202124; font-family: Roboto, 'Noto Sans', 'Noto Sans JP', 'Noto Sans KR', 'Noto Naskh Arabic', 'Noto Sans Thai', 'Noto Sans Hebrew', 'Noto Sans Bengali', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">ServiceWorkerの<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>ファイル</span>のパスを知らせます。</li>
<li>ServiceWorkerが操作できる<strong>スコープ(後述)</strong>を指定します。</li>
</ul>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">install</h5>
<ul>
<li>ブラウザにキャッシュが生成されます。</li>
<li>キャッシュ名(=バージョン)の定義やキャッシュ対象を指定します。</li>
</ul>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">activate</h5>
<ul>
<li><span style="color: #202124; font-family: Roboto, 'Noto Sans', 'Noto Sans JP', 'Noto Sans KR', 'Noto Naskh Arabic', 'Noto Sans Thai', 'Noto Sans Hebrew', 'Noto Sans Bengali', sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">ServiceWorkerの<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>ファイルを更新します。 ServiceWorkerはサーバ上の<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>ファイルとバイト単位で差が生じた場合に新しいファイルと認識します。</span></li>
<li>ユーザがブラウザを閉じたり、ページを更新したことを契機にServiceWorkerが更新されます。</li>
</ul>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">スコープとは</h3>
<p><span style="color: #333333; font-family: -apple-system, 'Segoe UI', 'Helvetica Neue', 'Hiragino Kaku Gothic ProN', メイリオ, meiryo, sans-serif;">ServiceWorker が管理するURLの範囲を示します。</span><br /><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">スコープは</span><span style="color: #333333; font-family: -apple-system, 'Segoe UI', 'Helvetica Neue', 'Hiragino Kaku Gothic ProN', メイリオ, meiryo, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">ServiceWorkerの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>ファイルが設置されているパス以下の階層を指定する必要があります。なお、このパス制限はService-Worker-Allowed: ヘッダで解除可能です。</span></p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">ServiceWorker 導入</h3>
<p>前置きが長くなってしまいましたが、ここからが導入手順になります。</p>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">準備するファイル</h5>
<pre class="code" style="box-sizing: border-box; overflow: auto; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.85rem; margin-top: 0px; margin-bottom: 1rem; background: #293030; border: none; white-space: pre-wrap; padding: 15px; color: #cccccc; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">DocumentRoot
├── main.js
├── serviceworker.js</pre>
<p>今回はどちらのファイルもDocumentRootに配置するものとします。</p>
<p>ファイル名に特に制約はありません。</p>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">1. ファイル作成</h5>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">main.js</h5>
<p>registerを行うファイルです。serviceworker.jsのパスを示します。</p>
<pre class="code lang-javascript" style="box-sizing: border-box; overflow: auto; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.85rem; margin-top: 0px; margin-bottom: 1rem; background: #293030; border: none; white-space: pre-wrap; padding: 15px; color: #cccccc; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="javascript" data-unlink=""><span class="synStatement" style="box-sizing: border-box; color: #d88a17;">if</span> (<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"serviceWorker"</span> <span class="synStatement" style="box-sizing: border-box; color: #d88a17;">in</span> navigator) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">window</span>.addEventListener(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"load"</span>, <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> () <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">//今回はDocRoot以下をServiceWorkerのスコープとします</span>
navigator.serviceWorker.register(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"/serviceworker.js"</span>, <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span> scope: <span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"./"</span> <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>).then(
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (registration) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">// 登録成功</span>
console.log(
<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"ServiceWorker registration successful with scope: "</span>,
registration.scope
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>,
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (err) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">// 登録失敗</span>
console.log(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"ServiceWorker registration failed: "</span>, err);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span></pre>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">serviceworker.js</h5>
<p>ServiceWorkerのコアファイルです。※コードの細かい説明は省略します。</p>
<pre class="code lang-javascript" style="box-sizing: border-box; overflow: auto; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.85rem; margin-top: 0px; margin-bottom: 1rem; background: #293030; border: none; white-space: pre-wrap; padding: 15px; color: #cccccc; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="javascript" data-unlink=""><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;"><span class="synComment" style="box-sizing: border-box; color: #4f80e5;">//キャッシュ名(=バージョン)を指定する</span><br />var</span> CACHE_NAME = <span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"cache-v1"</span>; <br /><span class="synComment" style="box-sizing: border-box; color: #4f80e5;">//キャッシュするファイル or ディレクトリを指定する</span><br /><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">var</span> urlsToCache = <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">[</span>
<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"/"</span>,
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">]</span>;
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">// install</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">self</span>.addEventListener(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"install"</span>, <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.waitUntil(
caches.open(CACHE_NAME).then(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (cache) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
console.log(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"Opened cache"</span>);
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> cache.addAll(urlsToCache);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>)
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">// activate</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">self</span>.addEventListener(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"activate"</span>, <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">var</span> cacheWhitelist = <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">[</span>CACHE_NAME<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">]</span>;
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.waitUntil(
caches.keys().then(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (cacheNames) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> Promise.all(
cacheNames.map(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (cacheName) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">if</span> (cacheWhitelist.indexOf(cacheName) === -1) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> caches.<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">delete</span>(cacheName);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>)
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>)
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);
<span class="synComment" style="box-sizing: border-box; color: #4f80e5;">// fetch</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">self</span>.addEventListener(<span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"fetch"</span>, <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.respondWith(
caches.match(<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.request).then(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (response) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">if</span> (response) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> response;
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">var</span> fetchRequest = <span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.request.clone();
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> fetch(fetchRequest).then(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (response) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">if</span> (!response || response.<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">status</span> !== 200 || response.type !== <span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"basic"</span>) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> response;
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">var</span> responseToCache = response.clone();
caches.open(CACHE_NAME).then(<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">function</span> (cache) <span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">{</span>
cache.put(<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">event</span>.request, responseToCache);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);
<span class="synStatement" style="box-sizing: border-box; color: #d88a17;">return</span> response;
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>)
);
<span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">}</span>);</pre>
<h5 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">2. main.js読み込み</h5>
<p>ページの共通ヘッダなどに記載します。</p>
<pre class="code lang-html" style="box-sizing: border-box; overflow: auto; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.85rem; margin-top: 0px; margin-bottom: 1rem; background: #293030; border: none; white-space: pre-wrap; padding: 15px; color: #cccccc; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="html" data-unlink=""><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;"><</span><span class="synStatement" style="box-sizing: border-box; color: #d88a17;">script</span><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;"> </span><span class="synType" style="box-sizing: border-box; color: #3ec63e;">src</span><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">=</span><span class="synConstant" style="box-sizing: border-box; color: #ff6666;">"/main.js"</span><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">></</span><span class="synStatement" style="box-sizing: border-box; color: #d88a17;">script</span><span class="synIdentifier" style="box-sizing: border-box; color: #51cfcf;">></span></pre>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">導入後の動作チェック</h3>
<p>ServiceWorkerが動作していることはブラウザから確認可能です。</p>
<p> </p>
<p>下記は<a class="keyword" href="http://d.hatena.ne.jp/keyword/GoogleChrome">GoogleChrome</a>の検証モードの画面です。</p>
<p>[Status Code]に"(from service worker)"と記載されていればオフラインキャッシュ化成功です。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20210922/20210922133406.png" alt="f:id:wiz-akakura:20210922133406p:plain" width="1200" height="574" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p> </p>
<p>また、[Application]の[Service Woerkers]でも動作していることが確認できます。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20210922/20210922141334.png" alt="f:id:wiz-akakura:20210922141334p:plain" width="1200" height="537" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">効果</h3>
<p>今回の検証では100MBの画像を読み込んだページでオフラインキャッシュの効果を試してみました。</p>
<p> </p>
<p>ServiceWorkerなしの状態では画像の読み込みに<strong>852ms</strong>かかっていますが・・</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20210922/20210922122154.png" alt="f:id:wiz-akakura:20210922122154p:plain" width="1200" height="246" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p>これがServiceWorkerのオフラインキャッシュを利用すると<strong>249ms</strong>まで短縮されていることがわかります。</p>
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wiz-akakura/20210922/20210922122158.png" alt="f:id:wiz-akakura:20210922122158p:plain" width="1200" height="248" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p>この検証はローカルの<a class="keyword" href="http://d.hatena.ne.jp/keyword/MAMP">MAMP</a>環境で実施したので読み込み時間にそこまで大きな差はありませんが、本番サーバであればもっと顕著に効果が出るとおもいます。</p>
<h3 style="box-sizing: border-box; margin: 1.5em 0px 0.8em; clear: both; line-height: 1.3; padding-bottom: 10px; font-size: 23.8px; border-bottom: 1px solid #cfd8d8; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">さいごに</h3>
<p>ServiceWorkerを使ってオフラインキャッシュを作ることによって、画像や<a class="keyword" href="http://d.hatena.ne.jp/keyword/css">css</a>、 jsファイルなどの静的コンテンツの読み込みをコストゼロで高速化することができるので、非常に便利ですね。コストや技術的な理由でキャッシュサーバを用意できない場合などに導入を検討してみては如何でしょうか?</p>
<p> </p>
<p style="box-sizing: border-box; margin: 0px 0px 0.7em; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">株式会社Wizではエンジニアを募集しています!</p>
<p style="box-sizing: border-box; margin: 0px 0px 0.7em; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">↓↓↓興味ある方はぜひご覧ください!↓↓↓</p>
<p style="box-sizing: border-box; margin: 0px 0px 0.7em; color: #293030; font-family: helvetica, 'Segoe UI', 游ゴシック体, YuGothic, '游ゴシック Medium', 'Yu Gothic Medium', 游ゴシック, 'Yu Gothic', メイリオ, Meiryo, sans-serif; font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="box-sizing: border-box; max-width: 500px; display: block; width: 500px; height: 155px; margin: 10px 0px;"></iframe></p>
<p> </p>
wiz-akakura
新卒がタスクの恐怖で殺されないためには(仮)
hatenablog://entry/13574176438011952482
2021-09-17T11:51:24+09:00
2021-09-17T13:32:50+09:00 こんにちは、フロントエンドの益田です。 OJT期間を終えてタスク管理について相談する機会があったので、今回は入社して5ヶ月経った自分なりのタスクの進め方について書いていきたいと思います。 1. 期限を確認する まずは、緊急か緊急でないかの確認をして、緊急を要する要件でないのならTODOリストに入れて一旦忘れるようにしています。 人によって管理の仕方が違うと思いますが、私はノートに殴り描きしてからリスト化するようにしています。 2. 日数がかかるタスクは定期的にハートビートを送る 大きなタスクや並行しているタスクがある場合は進捗を報告をして、一人で悩む時間を一定以上掛けないようにしています。(生…
<center>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/d/daisuke_john/20210916/20210916160830.png" alt="f:id:daisuke_john:20210916160830p:plain" width="400" height="325" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
</center>
<p>こんにちは、フロントエンドの益田です。<br>
<a class="keyword" href="http://d.hatena.ne.jp/keyword/OJT">OJT</a>期間を終えてタスク管理について相談する機会があったので、今回は入社して5ヶ月経った自分なりのタスクの進め方について書いていきたいと思います。</p>
<h3>1. 期限を確認する</h3>
<p>まずは、緊急か緊急でないかの確認をして、緊急を要する要件でないのならTODOリストに入れて一旦忘れるようにしています。<br>
人によって管理の仕方が違うと思いますが、私はノートに殴り描きしてからリスト化するようにしています。</p>
<h3>2. 日数がかかるタスクは定期的にハートビートを送る</h3>
<p>大きなタスクや並行しているタスクがある場合は進捗を報告をして、一人で悩む時間を一定以上掛けないようにしています。(生存報告のようなもの)</p>
<ul>
<li>ハートビート:心臓の鼓動、拍動、心拍という意味</li>
</ul>
<h3>3. なるべく早く反応する</h3>
<p>ディレクターやデザイナーさんとのやり取りや、差し込みでタスクが降ってきた時にすぐに回答できない場合などは、特に以下の2つを意識するようにしています。</p>
<p><strong>1. すぐに対応できない場合は期限を切って対応する。<br>
2. ここではあくまで「反応」するだけ。<br>(あなたのメッセージはたしかに受け取りましたよ!と返すだけ)</strong><br></p>
<p>メリットとしては、友達とのLINEでも同じだと思いますが相手の安心感が得られることと自分の未着手のタスクが1個減ります。</p>
<h3>4. 初回レビューのタイミングは予め決めておく</h3>
<p>タスクに着手する前から「ざっくりこれぐらい作って、いったんレビューしてもらう」という計画を立てて進めています。
早めのタイミングにレビューをして貰うことで後半になっての修正が入った場合に対応しやすくなったと感じています。<br>
通常業務以外にも目標ややりたいことリストなどの振り返りにも活用でき、2週間や月1などスケジュールを決めて定期的な見直しをしてフィードバックを貰っています。</p>
<h3>5. タスクを大きい粒度と小さい粒度に分けて管理する</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>分かりづらいタスクや期間が長いタスクがある場合には、Notion, ノート, リマインダーを使い分けて行なっています。<br>
とりあえずノートに書き出してそこからNotionやリマインダーに設定をして管理しやすいようにしています。</p>
<ul>
<li>ノート : 殴り書き、情報・思考の整理に使う</li>
<li>Notion : タスクの進捗。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>見積もりなど</li>
<li>リマインダー : 細かい作業など</li>
</ul>
<h3>6. 時間で区切る</h3>
<p>フィードバックで貰う事が多かったのが「時間で区切る」でした。<br>
1つのタスクを終えて次に取り掛かりたいタイプなので苦手な部分ではあるのですが、時間(タイムボックス)で区切ることで並行しながらタスクを進められ、自分のタスクが計測しやすいので<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>が見積もりづらいという部分もカバーされていくと思います。</p>
<blockquote><p><strong>タイムボックス</strong><br>
タスクに合わせて時間を決めるのではなく、時間を固定する手法を、「決まったサイズ(時間)の箱にタスクを入れる」というイメージでタイムボックス化(timeboxing)と言います。<br><a href="https://gihyo.jp/book/2018/978-4-7741-9876-7">エンジニアの知的生産術 ――効率的に学び,整理し,アウトプットする:書籍案内|技術評論社</a></p></blockquote>
<h3>7. <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A5%A4%A5%F3%A5%C9%A5%DE%A5%C3%A5%D7">マインドマップ</a>を描いて状況を俯瞰する</h3>
<p>現状のタスクを洗い出したり、タスクの詳細を整理する時に使っています。
メリットとして、文章が苦手な人でも単語で書けるので楽に始められることと、朝の5分〜10分程度ノートに書き出して気持ちの整理と優先順位付けを行うことでスッキリした状態で始められるのでお勧めです。派生して書き出せるので、企画でのアイディア出しにも使っています。</p>
<h3>その他Wizでやっていること</h3>
<p><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/KPT">KPT</a></strong></p>
<p>Wizでは隔週で1on1を行なっておりその中で進捗と以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/KPT">KPT</a>(Keep, Problem, Try)を使ってフィードバックをしてもらっています。次週に向けての改善点も見えてくるので細かく洗い出して振り返る時間を作っています。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/d/daisuke_john/20210916/20210916152823.jpg" alt="f:id:daisuke_john:20210916152823j:plain" width="1200" height="457" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><strong>雑談会</strong></p>
<p>コミュニケーション1on1以外にも他職種も含めた雑談会を月に1回程度行なっています。
デザイナーやディレクター・ライターの方も参加して頂けるので、業務での相談なども気軽にできるようになったと感じています。<br>
雑談会の詳細については下記の記事に記載していますので気になった方は是非ご覧下さい。<br>
<a href="https://tech.012grp.co.jp/entry/2021/03/16/182241">コロナ禍での他職種とのコミュニケーションについて - Wiz テックブログ</a></p>
<h3>参考</h3>
<p><a href="https://tech.unifa-e.com/entry/2021/07/16/130205">スケジュールにバッファを設けるのは悪か? - ユニファ開発者ブログ</a></p>
<p><a href="https://blog.jnito.com/entry/2020/05/14/081528#%E5%9B%B0%E3%81%A3%E3%81%9F%E3%82%89%E6%97%A9%E3%82%81%E3%81%AB%E4%BA%BA%E3%81%AB%E9%A0%BC%E3%82%8B">【新人ITエンジニア向け】僕が仕事をする上で大事にしているポイントあれこれ - give IT a try</a></p>
<h3>おわりに</h3>
<p>今回は入社して5ヶ月経って個人的に役に立ったことを書いてみました。<br>現状、模索段階ということもあり自分に合ったやり方を今後見つけていきたいと思います。</p>
<p>また、</p>
<p>Wizではエンジニアを募集しております。</p>
<p>興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
daisuke_john
php-cs-fixerをつかったコードフォーマットの自動化(git hook)
hatenablog://entry/13574176438011666443
2021-09-16T11:48:35+09:00
2021-10-12T10:07:32+09:00 phpのパッケージphp-cs-fixerを使ったコードフォーマットのやり方と、git hookを使った自動化について簡単に紹介します。
<p>こんにちは、バックエンドエンジニアの三井です。</p>
<p>社内でコードフォーマットを統一する活動を行っており、<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>のパッケージを使ったフォーマッターの導入と、git hookを使ったフォーマットの自動化に成功したので概要をまとめていきたいと思います!</p>
<h2>導入した<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>パッケージ</h2>
<hr />
<p>今回導入したパッケージは<a href="https://github.com/FriendsOfPHP/PHP-CS-Fixer">php-cs-fixer</a>です。</p>
<h3>導入環境</h3>
<p>laravel: 8.60.0<br />
composer: 2.1.6<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>: 8.0.9</p>
<h3>概要</h3>
<p>composer で導入できる<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>のパッケージです。導入後コマンドを打つことで任意のファイルにフォーマッターをかけることができます。<br />
フォーマットルールについて、PSR12をはじめ多くのフォーマットに対応しており、設定ファイルを作成することでオリジナルのフォーマットルールを生成することができます。<br />
導入時はPSR12のみを導入して運用してましたが、今後社内でフォーマットを統一するためにルールの取り決めも行っていきたいと思っております!</p>
<h2>導入方法・設定</h2>
<hr />
<h3>インストール</h3>
<p>今回は私の動作環境であるlaravelプロジェクト内での使用を想定して行います。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># パッケージのインストール</span>
composer require friendsofphp/php-cs-fixer
<span class="synComment"># 設定ファイルをプロジェクトディレクトリに追加</span>
<span class="synStatement">touch</span> .php-cs-fixer.dist.php
<span class="synComment"># gitignoreにキャッシュファイルを追加</span>
.php-cs-fixer.cache
</pre>
<h3>設定ファイル</h3>
<p>設定ファイル<code>.php-cs-fixer.dist.php</code>内を以下のように記述します。
ルールは<a href="https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/rules/index.rst">ここ</a>で確認することができ、<code>PSR12</code>等複数のルールをまとめた<a href="https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/ruleSets/index.rst">ルールセット</a>を指定することも可能です。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synComment">/*</span>
<span class="synComment"> * This document has been generated with</span>
<span class="synComment"> * https://mlocati.github.io/php-cs-fixer-configurator/#version:3.1.0|configurator</span>
<span class="synComment"> * you can change this configuration by importing this file.</span>
<span class="synComment"> */</span>
<span class="synStatement">$</span><span class="synIdentifier">config</span> <span class="synStatement">=</span> <span class="synPreProc">new</span> PhpCsFixer\Config<span class="synSpecial">()</span>;
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">config</span>
<span class="synType">-></span>setRiskyAllowed<span class="synSpecial">(</span><span class="synConstant">true</span><span class="synSpecial">)</span>
<span class="synType">-></span>setRules<span class="synSpecial">([</span>
<span class="synConstant">'@PSR12'</span> <span class="synStatement">=></span> <span class="synConstant">true</span>, <span class="synComment">// ここにルール追記</span>
<span class="synSpecial">])</span>
<span class="synType">-></span>setFinder<span class="synSpecial">(</span>PhpCsFixer\Finder<span class="synStatement">::</span>create<span class="synSpecial">()</span>
<span class="synType">-></span>exclude<span class="synSpecial">([</span> <span class="synComment">// 除外ファイル</span>
<span class="synConstant">'vendor'</span>
<span class="synSpecial">])</span>
<span class="synType">-></span>in<span class="synSpecial">(</span><span class="synConstant">__DIR__</span><span class="synSpecial">)</span>
<span class="synSpecial">)</span>
;
</pre>
<p><code>setRiskyAllowed()</code> : riskyなルール(コードを書き換える破壊的な修正を含むルール)を有効にするかを設定します。 <br />
<code>setRules()</code> : 使用したいルールをここで列挙します。 <br />
<code>setFinder()...->exclude([''])</code> : 除外したい<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ名を列挙することでその<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内のファイルは無視されます。</p>
<h3>ルール設定方法</h3>
<p>ルール生成用のツールで生成ファイルを作ることができます。</p>
<p><a href="https://mlocati.github.io/php-cs-fixer-configurator/#version:3.1">https://mlocati.github.io/php-cs-fixer-configurator/#version:3.1</a></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mtimti/20210915/20210915192833.png" alt="f:id:mtimti:20210915192833p:plain" width="1200" height="344" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>このページでは使用可能なルールが列挙されており、ページ内の歯車マークをクリックすると使用するルールの選別ができるようになります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mtimti/20210915/20210915193249.png" alt="f:id:mtimti:20210915193249p:plain" width="609" height="60" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>またその状態で<code>+</code>ボタンを押すことでルールセットの指定も可能です。
最後に<code>Export</code>ボタンを押すことで設定ファイルの出力ができます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mtimti/20210915/20210915193037.png" alt="f:id:mtimti:20210915193037p:plain" width="561" height="253" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mtimti/20210915/20210915193201.png" alt="f:id:mtimti:20210915193201p:plain" width="1148" height="912" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>実行コマンド</h2>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># version バージョン確認</span>
$ ./vendor/bin/php-cs-fixer <span class="synSpecial">--version</span>
<span class="synComment"># dry-run (修正はされないが、--diffオプションで修正前後の確認ができる)</span>
$ ./vendor/bin/php-cs-fixer fix <span class="synSpecial">-v</span> <span class="synSpecial">--diff</span> <span class="synSpecial">--dry-run</span>
<span class="synComment"># fix 実際に修正実行</span>
$ ./vendor/bin/php-cs-fixer fix <span class="synSpecial">-v</span> <span class="synSpecial">--diff</span>
</pre>
<pre class="code lang-sh" data-lang="sh" data-unlink>--dry-run <span class="synComment"># 変更なし実行</span>
--diff <span class="synComment"># 差分出力</span>
--allow-risky <span class="synComment"># riskyルールの許可</span>
</pre>
<h2>git hookの概要</h2>
<hr />
<p>詳しい説明は割愛しますが簡単に説明すると、gitのコミット等のコマンドの直前・後に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を実行できるという仕組みです。
今回は"コミット直前にフォーマッターを実行し、修正後コミットする"という機構を作ります。
隠し<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ.gitの配下に<code>hooks</code><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを作り<code>pre-commit</code>ファイルを作成します。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment">#!/usr/bin/env bash</span>
<span class="synComment">#ルートの設定(laravel projectの配下を指定)</span>
<span class="synIdentifier">ROOT</span>=<span class="synPreProc">$(</span><span class="synSpecial">git rev-parse --show-toplevel</span><span class="synPreProc">)</span>/projdir
<span class="synIdentifier">PHP_CS_FIXER</span>=<span class="synStatement">"</span><span class="synConstant">./vendor/bin/php-cs-fixer</span><span class="synStatement">"</span>
<span class="synIdentifier">HAS_PHP_CS_FIXER</span>=<span class="synStatement">false</span>
<span class="synStatement">if [</span> <span class="synStatement">-x</span> <span class="synPreProc">$ROOT</span>/vendor/bin/php-cs-fixer <span class="synStatement">];</span> <span class="synStatement">then</span>
<span class="synComment"># パッケージの存在チェック</span>
<span class="synIdentifier">HAS_PHP_CS_FIXER</span>=<span class="synStatement">true</span>
<span class="synStatement">fi</span>
<span class="synIdentifier">LIST</span>=<span class="synPreProc">$(</span><span class="synSpecial">git status </span><span class="synStatement">|</span><span class="synSpecial"> </span><span class="synStatement">grep</span><span class="synSpecial"> -e </span><span class="synStatement">'</span><span class="synConstant">\(modified:\|new file:\)</span><span class="synStatement">'|</span><span class="synSpecial"> </span><span class="synStatement">grep</span><span class="synSpecial"> </span><span class="synStatement">'</span><span class="synConstant">\.php</span><span class="synStatement">'</span><span class="synSpecial"> </span><span class="synStatement">|</span><span class="synSpecial"> cut -d</span><span class="synStatement">'</span><span class="synConstant">:</span><span class="synStatement">'</span><span class="synSpecial"> -f2</span><span class="synPreProc">)</span>
<span class="synStatement">if </span><span class="synPreProc">$HAS_PHP_CS_FIXER</span><span class="synStatement">;</span> <span class="synStatement">then</span>
<span class="synIdentifier">ERRCNT</span>=<span class="synConstant">0</span>
<span class="synStatement">for</span> file <span class="synStatement">in</span> <span class="synPreProc">$LIST</span>
<span class="synStatement">do</span>
<span class="synComment"># --dry-runオプションで先にエラーチェック</span>
./projdir/vendor/bin/php-cs-fixer fix <span class="synPreProc">$file</span> <span class="synSpecial">--diff</span> <span class="synSpecial">--dry-run</span> <span class="synSpecial">--path-mode=intersection</span> <span class="synSpecial">--config=</span><span class="synPreProc">$ROOT</span>/.php-cs-fixer.dist.php <span class="synStatement">></span>/dev/null <span class="synConstant">2</span><span class="synStatement">>&</span><span class="synConstant">1</span><span class="synStatement">;</span>
<span class="synIdentifier">ERR</span>=<span class="synPreProc">$?</span>
<span class="synComment"># ステータスコードが0(修正なし)か8(修正あり)以外はエラーに該当するのでエラーメッセージを表示</span>
<span class="synStatement">if [</span> <span class="synPreProc">$ERR</span> <span class="synStatement">!=</span> <span class="synConstant">0</span> <span class="synStatement">-a</span> <span class="synPreProc">$ERR</span> <span class="synStatement">!=</span> <span class="synConstant">8</span> <span class="synStatement">];</span> <span class="synStatement">then</span>
<span class="synIdentifier">ERRCNT</span>=<span class="synPreProc">$((</span><span class="synSpecial">ERRCNT+</span><span class="synConstant">1</span><span class="synPreProc">))</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">php-cs-fixer-</span><span class="synPreProc">$ERR</span><span class="synConstant"> エラーが検出されました </span><span class="synPreProc">$file</span><span class="synStatement">"</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">error </span><span class="synPreProc">$ERRCNT</span><span class="synStatement">"</span>
<span class="synStatement">else</span>
<span class="synComment"># 修正実行</span>
./projdir/vendor/bin/php-cs-fixer fix <span class="synPreProc">$file</span> <span class="synSpecial">--path-mode=intersection</span> <span class="synSpecial">--config=</span><span class="synPreProc">$ROOT</span>/.php-cs-fixer.dist.php <span class="synStatement">></span>/dev/null <span class="synConstant">2</span><span class="synStatement">>&</span><span class="synConstant">1</span><span class="synStatement">;</span>
git add <span class="synStatement">"</span><span class="synPreProc">$file</span><span class="synStatement">";</span>
<span class="synStatement">fi</span>
<span class="synStatement">done</span>
<span class="synStatement">if [</span> <span class="synPreProc">$ERRCNT</span> <span class="synStatement">-gt</span> <span class="synConstant">0</span> <span class="synStatement">];</span> <span class="synStatement">then</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">php-cs-fixer </span><span class="synPreProc">$ERRCNT</span><span class="synConstant"> 件エラーが検出されました</span><span class="synStatement">"</span>
<span class="synStatement">exit</span> <span class="synConstant">1</span>
<span class="synStatement">fi</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">php-cs-fixer フォーマット修正が完了しました</span><span class="synStatement">"</span>
<span class="synStatement">else</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">""</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">Please install php-cs-fixer, e.g.:</span><span class="synStatement">"</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">""</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">"</span><span class="synConstant">composer require --dev fabpot/php-cs-fixer:dev-master</span><span class="synStatement">"</span>
<span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">""</span>
<span class="synStatement">exit</span> <span class="synConstant">1</span>
<span class="synStatement">fi</span>
</pre>
<p>これでcommit時にこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>が自動実行され、フォーマッターが動きます。</p>
<p><a href="https://gist.github.com/jwage/b1614c96ea22ccaf68b7">参考にさせていただいたソース</a></p>
<h2>実際に開発で運用してみた感想</h2>
<hr />
<p>今回フォーマッターの統一に至った経緯としては社内で使用エディタが統一されていなかったため、各々のフォーマットでコーディングされていたという状況を受けてのことでした。<br />
実際に使ってみて思ったこととしては、エディタ内のフォーマッターと合わせて運用すると良いかもしれない、ということです。コミット直前に修正を行うデメリットとして、コードを書いている途中で整ったコードを見れないという点です。commit内容としては整っているコードですが、"実装中にコードを読みやすくする"にすることが目的なら本末転倒だと思います。そのため、"自分がわかりやすいように"エディタのフォーマッターも兼ねて運用するのも有効だと思いました。</p>
mtimti
Laravelで初めてのテストとTDDをサクッとやってみる【初心者向け】
hatenablog://entry/26006613802772465
2021-08-31T10:17:53+09:00
2021-08-31T10:47:48+09:00 TDD(テスト駆動開発)が気にはなるけど実践するのはハードルが高いという方に向けてTDDの雰囲気や手順を簡単に紹介します。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20210830/20210830190553.png" alt="TDDのイメージ" width="1180" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、バックエンドエンジニアの中嶋です。</p>
<p>今年の春に入社して主にLaravelを扱って開発をしていますが、</p>
<p>プロダクト開発はTDD(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>)で行うことが多くなってきました。
<br>
<br>
もちろん規模や目的によってTDDを導入すべきかは検討が必要ですが、</p>
<p>「<strong>テストなんて面倒だしそもそも何からやって良いかわからない</strong>」という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>なハードルがあることで導入していないパターンも割と多いのではないでしょうか?
<br>
<br>
そこで今回は、</p>
<ul>
<li>Laravelを用いてまだテストコードを書いたことがない</li>
<li>TDD(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>)を実践したことがない</li>
<li>気にはなるけど実践するのはハードルが高くて..</li>
</ul>
<p>という方に向けてTDDの雰囲気だけでも知ってもらうべく、その手順を簡単に紹介したいと思います。</p>
<h3>今回やること</h3>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>を用いてLaravelで実装した簡易<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>のFeatureテストを手軽に行う</li>
<li><p>GETでアクセスすると保存されているデータを<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>形式で返すというシンプルなもの</p></li>
<li><p>手軽に試して雰囲気をお伝えするのが目的なので、登録処理やデータベースのテストはやらない</p></li>
</ul>
<p>↓もし「<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>って何?」という方はこちらに簡単に纏めているので良ければご覧ください<br>
<a href="https://tech.012grp.co.jp/entry/rest_api_basics">REST APIとは?ざっくりと理解してみる【初心者向け】 - Wiz テックブログ</a></p>
<h3>TDD(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>)とは..</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%B9%A5%C8%B6%EE%C6%B0%B3%AB%C8%AF">テスト駆動開発</a>はその名の通りテストを中心とした開発手法のことです。</p>
<p>「<strong>Test-Driven Development</strong>」の頭文字を取ってTDDと呼ばれることが多く、</p>
<p>XP(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%AF%A5%B9%A5%C8%A5%EA%A1%BC%A5%E0%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0">エクストリームプログラミング</a>)の考案者である<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B1%A5%F3%A5%C8%A1%A6%A5%D9%A5%C3%A5%AF">ケント・ベック</a>氏が編み出した手法とされています。</p>
<p>概要をざっくり一言でまとめると、</p>
<p><strong>ちゃんと動く状態を保ちながら機能を素早く実装し、きれいなコードを目指して<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>していく</strong></p>
<p>です。</p>
<p>そのプロセスとしては</p>
<ol>
<li>きちんと動作するか?のテストを作成する</li>
<li>テストを実行して失敗することを確認する</li>
<li>コードが汚くても良いので最低限仕様を満たす実装を行う</li>
<li>テストを実行して成功するようになったことを確認する</li>
<li>テストが成功することを確認しながらきれいなコードに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>していく
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20210830/20210830115153.png" alt="TDDのフロー" width="917" height="334" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li>
</ol>
<p>というような流れになります。
<br>
<br>
コツとしては「テスト作成」「機能実装」「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>」を、それぞれ最小の単位で素早く行うことです。</p>
<p>テストや機能を素早く実装し、テストが常にグリーンの状態になる..このテストに守られている安心感があると、開発のリズムも良くなってくるはずです。</p>
<p>逆にテストなしに大きな単位で実装や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>を行うと、</p>
<p>「あれ?なんで動かないんだ?」「どこで間違えたんだ?」ということに時間や思考を取られるので、</p>
<p><strong>テストに守られた状態になれるというだけでもかなり恩恵が得られる</strong>と思います。</p>
<h3>テストの手順</h3>
<p>さて、概要がわかったところで「<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>」を用いたテストの作成をしていきます。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>におけるテストはこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>が<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D5%A5%A1%A5%AF%A5%C8%A5%B9%A5%BF%A5%F3%A5%C0%A1%BC%A5%C9">デファクトスタンダード</a>(事実上の標準)と言えると思います。</p>
<p>そしてLaravelでもあらかじめ用意されており、そのまま使用することができます。(必要に応じて設定は必要です)</p>
<h6>(Tips) 設定ファイルについて</h6>
<p>Laravelのプロジェクト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ直下に<code>phpunit.xml</code>というファイルが用意されています。</p>
<p>これは<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>のテスト実行の設定ファイルで、</p>
<p>今回のような簡易なテストではデフォルトのまま実行することができますが、テストケースをグループ化したりテスト用のデータベースを用意する場合など必要に応じて編集します。</p>
<h5>【手順①】テストしたいことリストを作成</h5>
<p>まずは「どんな機能をテストしたいか」をリストアップします。</p>
<p>今回の例では「お知らせ<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>」という新着ニュースのデータを返す簡易な<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>をテストしたいので、</p>
<ul>
<li>'<a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>/notifications' というエンドポイントにGET メソッドでアクセスできる(ステータス200を返す)</li>
<li>「お知らせ」データが<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>形式で取得できる</li>
<li>取得した<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>データがデータベースに登録されている値と一致する
..</li>
</ul>
<p>といった項目などがリストアップできると思います。</p>
<p>これはテストコードの中に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%E1%A5%F3%A5%C8%A5%A2%A5%A6%A5%C8">コメントアウト</a>として書く程度でもOKです。(今回はGETでアクセスできるかまで検証します)</p>
<h5>【手順②】 テストクラスを作成</h5>
<p>実際のテストコードを作成するにはartisanのmakeコマンドを使います。</p>
<p>今回の例では<code>NotificationApiTest</code>というクラスを作成します。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>$ php artisan make:test NotificationApiTest
</pre>
<p>これでTests/Feature/NotificationApiTest.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a>が雛形として生成されました。</p>
<h6>(Tips) FeatureテストとUnitテストについて</h6>
<p>このように特にオプションを付けないで実行すると、Featureテストの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リにファイルが生成されます。</p>
<pre class="code" data-lang="" data-unlink>tests
├── CreatesApplication.php
├── Feature
├── ExampleTest.php
└── NotificationApiTest.php
├── TestCase.php
└── Unit
└── ExampleTest.php</pre>
<p>これはTests\TestCaseクラスを継承していて、Laravelが<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>を拡張したものになります。</p>
<p>Laravelの機能を利用したテストを行う場合はこちらを用いるのが良いと思います。</p>
<p>一方、上記コマンドで<code>--unit</code>というオプションを付けて実行するとtests/Unit<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リにファイルが生成されます。</p>
<p>こちらは<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHPUnit">PHPUnit</a>\Framework\TestCaseクラスを継承しており、Laravelの機能を利用しない範囲で粒度の細かいテストを行う場合はこちらを用いると良いと思います。</p>
<h5>【手順③】 失敗するテストを書く</h5>
<p>先に述べた手順のとおり、機能実装する前にテストを作成します。</p>
<p>本来はテスト用のデータベースやもっと細かいテスト項目を用意すべきですが、今回はお試しなので簡易な格好としています。</p>
<p>・Tests/Feature/NotificationApiTest.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a></p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">..</span>
<span class="synType">class</span> NotificationApiTest <span class="synType">extends</span> TestCase
<span class="synSpecial">{</span>
<span class="synComment">/**</span>
<span class="synComment"> * </span><span class="synType">@test</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> GETで正常にアクセスできるか<span class="synSpecial">()</span>
<span class="synSpecial">{</span>
<span class="synComment">// エンドポイントにGETでアクセス</span>
<span class="synStatement">$</span><span class="synIdentifier">response</span> <span class="synStatement">=</span> <span class="synStatement">$</span><span class="synIdentifier">this</span><span class="synType">-></span>get<span class="synSpecial">(</span><span class="synConstant">'/api/notifications'</span><span class="synSpecial">)</span>;
<span class="synComment">// ステータスコード200が返るか</span>
<span class="synStatement">$</span><span class="synIdentifier">response</span><span class="synType">-></span>assertStatus<span class="synSpecial">(</span><span class="synConstant">200</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
</pre>
<p>テストメソッドの内容としては、エンドポイントにGETでアクセスする擬似リク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを作成し、</p>
<p>その結果を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A1%BC%A5%B7%A5%E7%A5%F3">アサーション</a>でチェックしています。</p>
<h6>(Tips) テストメソッドの名前について</h6>
<p>今回のテストメソッド名は日本語になっていて違和感のある方も多いと思います。</p>
<p>これは<code>@test</code>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>(/**の部分)を設けていることで、メソッド名に日本語を用いても問題なく実行できるというものです。</p>
<p><code>@test</code>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>を使わない場合はメソッド名を<code>testFoo()</code>のようにする必要があるのですが、</p>
<p>どちらも動作は同じなので、メソッド名を毎回考えるのが面倒なのと日本語の方がテスト結果を見た時にも直感的でわかりやすいという理由で、
個人的には<code>@test</code>ア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%CE%A1%BC%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ノーテーション</a>を使用してメソッド名は日本語で定義することが多いです。</p>
<h6>(Tips) assertメソッドについて</h6>
<p><code>assert</code>メソッドは「テストが成功したか」「失敗したか」をチェックして結果を返すものです。</p>
<p>テストコードを書くにあたってはこの<code>assert</code>メソッドをいかに知っているかが大事だったりします。</p>
<p>これは本当にたくさんの種類があるので具体的な紹介は<a href="https://phpunit.readthedocs.io/ja/latest/assertions.html">公式ドキュメント</a>に譲りたいと思いますが、</p>
<p>今回のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>のテストにおける代表的なものを少しだけ紹介すると以下のようなものがあります。</p>
<table>
<thead>
<tr>
<th>メソッド</th>
<th>テスト内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>assertSuccessful()</td>
<td><a class="keyword" href="http://d.hatena.ne.jp/keyword/HTTP%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">HTTPステータスコード</a>が200番台なら成功</td>
</tr>
<tr>
<td>assertStatus($status)</td>
<td><a class="keyword" href="http://d.hatena.ne.jp/keyword/HTTP%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">HTTPステータスコード</a>が$statusと一致していれば成功</td>
</tr>
<tr>
<td>assertJson(array $data, $strict=false)</td>
<td>レスポンスされた<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>の配列に$dataが含まれていれば成功</td>
</tr>
<tr>
<td>assertExactJson(array $data)</td>
<td>レスポンスされた<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>の配列が$dataと一致していれば成功</td>
</tr>
</tbody>
</table>
<h5>【手順④】 テスト実行 →失敗する</h5>
<p>最低限のテストを作成したら、まずは実行して失敗することを確認します</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>vendor/bin/phpunit tests/Feature/NotificationApiTest.php
PHPUnit <span class="synConstant">8</span>.<span class="synConstant">2</span>.<span class="synConstant">5</span> by Sebastian Bergmann and contributors.
F <span class="synConstant">1</span> / <span class="synConstant">1</span> <span class="synPreProc">(</span><span class="synConstant">100</span><span class="synSpecial">%</span><span class="synPreProc">)</span>
Time: <span class="synConstant">1</span>.<span class="synConstant">51</span> seconds, Memory: <span class="synConstant">30</span>.<span class="synConstant">00</span> MB
There was <span class="synConstant">1</span> failure:
<span class="synConstant">1</span><span class="synError">)</span> Tests\Feature\NotificationApiTest::GETで正常にアクセスできるか
Response <span class="synStatement">status</span> code <span class="synStatement">[</span><span class="synConstant">404</span><span class="synStatement">]</span> does not match expected <span class="synConstant">200</span> <span class="synStatement">status</span> code.
Failed asserting that <span class="synStatement">false</span> is <span class="synStatement">true</span>.
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:89
/var/www/html/tests/Feature/NotificationApiTest.php:39
FAILURES!
Tests: <span class="synConstant">1</span>, Assertions: <span class="synConstant">1</span>, Failures: <span class="synConstant">1</span>.
</pre>
<p>当然ですが、エンドポイントを実装していないので<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">ステータスコード</a>404(NotFound)が返り失敗します。</p>
<h5>【手順⑤】 最低限の実装 →テストを成功させる</h5>
<h6>エンドポイントを定義</h6>
<p>404が返っているので、まずはエンドポイントを定義します。</p>
<p>・<a class="keyword" href="http://d.hatena.ne.jp/keyword/api">api</a>.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a></p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">..</span>
Route<span class="synStatement">::</span>get<span class="synSpecial">(</span><span class="synConstant">'notifications'</span>, <span class="synConstant">'Api\NotificationApiController@index'</span><span class="synSpecial">)</span><span class="synType">-></span><span class="synIdentifier">name</span><span class="synSpecial">(</span><span class="synConstant">'notifications'</span><span class="synSpecial">)</span>;
</pre>
<h6>もう一度テストを実行</h6>
<pre class="code lang-sh" data-lang="sh" data-unlink>PHPUnit <span class="synConstant">8</span>.<span class="synConstant">2</span>.<span class="synConstant">5</span> by Sebastian Bergmann and contributors.
F <span class="synConstant">1</span> / <span class="synConstant">1</span> <span class="synPreProc">(</span><span class="synConstant">100</span><span class="synSpecial">%</span><span class="synPreProc">)</span>
Time: <span class="synConstant">2</span>.<span class="synConstant">58</span> seconds, Memory: <span class="synConstant">30</span>.<span class="synConstant">00</span> MB
There was <span class="synConstant">1</span> failure:
<span class="synConstant">1</span><span class="synError">)</span> Tests\Feature\NotificationApiTest::GETで正常にアクセスできるか
Response <span class="synStatement">status</span> code <span class="synStatement">[</span><span class="synConstant">500</span><span class="synStatement">]</span> does not match expected <span class="synConstant">200</span> <span class="synStatement">status</span> code.
Failed asserting that <span class="synStatement">false</span> is <span class="synStatement">true</span>.
/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:89
/var/www/html/tests/Feature/NotificationApiTest.php:39
FAILURES!
Tests: <span class="synConstant">1</span>, Assertions: <span class="synConstant">1</span>, Failures: <span class="synConstant">1</span>.
</pre>
<p>こちらも当然ですが失敗します。</p>
<p>今度は404ではなく、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">ステータスコード</a>500(ServerError)が返りました。</p>
<p>エラー内容はこれではわかりませんが、エンドポイントにはアクセスできたものの処理を実装していないのでサーバーエラーが返る状態ですね。</p>
<h6><a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>の処理を実装</h6>
<p>次に<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>の処理本体を最低限のレベルで実装します。
内容自体は今回は重要ではないので無視していただいてOKです。</p>
<p>・NotificationApiController.<a class="keyword" href="http://d.hatena.ne.jp/keyword/php">php</a></p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">..</span>
<span class="synType">class</span> NotificationApiController <span class="synType">extends</span> Controller
<span class="synSpecial">{</span>
<span class="synType">public</span> <span class="synPreProc">function</span> index<span class="synSpecial">()</span>
<span class="synSpecial">{</span>
<span class="synStatement">$</span><span class="synIdentifier">data</span> <span class="synStatement">=</span> Notification<span class="synStatement">::</span>getNotificationsIndex<span class="synSpecial">()</span>;
<span class="synStatement">$</span><span class="synIdentifier">status</span> <span class="synStatement">=</span> <span class="synConstant">200</span>;
<span class="synStatement">$</span><span class="synIdentifier">message</span> <span class="synStatement">=</span> <span class="synConstant">'Success'</span>;
<span class="synStatement">return</span> ResponseUtil<span class="synStatement">::</span>jsonResponse<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">status</span>, <span class="synStatement">$</span><span class="synIdentifier">message</span>, <span class="synStatement">$</span><span class="synIdentifier">data</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h6>三度、テストを実行</h6>
<p>この状態で改めてテストを実行します。</p>
<pre class="code lang-sh" data-lang="sh" data-unlink>PHPUnit <span class="synConstant">8</span>.<span class="synConstant">2</span>.<span class="synConstant">5</span> by Sebastian Bergmann and contributors.
<span class="synStatement">. </span> <span class="synConstant">1</span> / <span class="synConstant">1</span> <span class="synPreProc">(</span><span class="synConstant">100</span><span class="synSpecial">%</span><span class="synPreProc">)</span>
Time: <span class="synConstant">1</span>.<span class="synConstant">97</span> seconds, Memory: <span class="synConstant">30</span>.<span class="synConstant">00</span> MB
OK <span class="synPreProc">(</span><span class="synConstant">1</span><span class="synSpecial"> </span><span class="synStatement">test</span><span class="synSpecial">, </span><span class="synConstant">1</span><span class="synSpecial"> assertion</span><span class="synPreProc">)</span>
</pre>
<p>これでようやく<strong>テストグリーン</strong>(OKの状態)になりました🙌</p>
<p>あとはテストコードを信頼できるレベルに強化(DBも含めてデータが正しいかチェックする等)しつつ、</p>
<p>この繰り返しでテストグリーンを保ちながら、例えば<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>処理をtry〜catchの形に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%D5%A5%A1%A5%AF%A5%BF%A5%EA%A5%F3%A5%B0">リファクタリング</a>したり、新たな機能を実装していくことができます。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hnak0210/20210830/20210830115153.png" alt="TDDのフロー" width="917" height="334" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回は簡易な内容での導入でしたが、</p>
<p>実際の開発でも是非<strong>テストに守られた状態で実装していく気持ち良さ</strong>を体感してみてはいかがでしょうか??
<br>
<br>
Wizでは良いものをワイワイと作るべく、エンジニアを絶賛募集しています!(ワイズと読みます)</p>
<p>↓↓↓興味ある方はぜひご覧ください!↓↓↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
hnak0210
ドキュメント作成に向いている静的サイトジェネレータはどれ?
hatenablog://entry/26006613801707466
2021-08-27T10:25:14+09:00
2021-08-27T10:25:14+09:00 こんにちは、フロントエンドエンジニアの柳田です。 現在、チーム内でドキュメントを作成する機会が多くなってきました。 社内では、様々な方法でドキュメントを作成しているのですが、そのひとつの方法として、静的サイトジェネレータ(Static Site Generator:SSG)があります。 今回は、ドキュメント作成にあたって自分が試してみたSSGをいくつかご紹介したいと思います。 静的サイトジェネレータ(SSG)とは? 静的サイトジェネレータとは、入力ファイルから簡単に静的ページを生成してくれるツールのことです。 有名なものだと、Next.jsやGatsby、Nuxtがありますね。 実際に表示に利…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20210827/20210827092425.png" alt="f:id:rainymoment0616:20210827092425p:plain" width="860" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、フロントエンドエンジニアの柳田です。</p>
<p>現在、チーム内でドキュメントを作成する機会が多くなってきました。</p>
<p>社内では、様々な方法でドキュメントを作成しているのですが、そのひとつの方法として、静的サイトジェネレータ(Static Site Generator:SSG)があります。</p>
<p>今回は、ドキュメント作成にあたって自分が試してみたSSGをいくつかご紹介したいと思います。</p>
<h2>静的サイトジェネレータ(SSG)とは?</h2>
<p>静的サイトジェネレータとは、入力ファイルから簡単に静的ページを生成してくれるツールのことです。</p>
<p>有名なものだと、Next.jsや<a class="keyword" href="http://d.hatena.ne.jp/keyword/Gatsby">Gatsby</a>、Nuxtがありますね。</p>
<p>実際に表示に利用されるものは静的ページのため、動的ページに比べてサイトパフォーマンスが高く、SPAに比べて<a class="keyword" href="http://d.hatena.ne.jp/keyword/SEO">SEO</a>面で優れています。</p>
<p>一方、大規模サイトのようなページ数の多いものは生成するのに時間がかかったり、更新頻度が多い場合に頻繁に生成を行わなくてはいけないというデメリットもあります。</p>
<h2>ドキュメント作成するにあたってのポイント</h2>
<p>今回、ドキュメント作成にあたって、下記をポイントに置いています。</p>
<ul>
<li>マークダウン形式で書くことができる(誰もが簡単にドキュメントを書くことができるし、他サービスなどへの移行も簡単)</li>
<li>ドキュメントとしての見た目が担保されたデザインである</li>
<li>バージョン管理ができる</li>
<li>環境構築が簡単</li>
</ul>
<h2>ドキュメント作成に向いていると感じるSSG</h2>
<h3>VuePress</h3>
<p><a href="https://vuepress.vuejs.org/">https://vuepress.vuejs.org/</a></p>
<p>Vue.jsベースのドキュメント作成に特化したSSGです。</p>
<p><figure class="figure-image figure-image-fotolife" title="VuePressを利用したドキュメントの画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20210827/20210827083538.png" alt="f:id:rainymoment0616:20210827083538p:plain" width="1200" height="756" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>VuePressを利用したドキュメントの画面</figcaption></figure></p>
<ul>
<li>環境構築が簡単</li>
<li>Vue.jsの知識がなくてもマークダウンを利用したドキュメント作成が可能</li>
<li>デフォルトのテーマがシンプルかつ、ドキュメント向き</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>等のバージョン管理ツールでの管理が可能</li>
</ul>
<p>マークダウンの中で、Vue<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を利用することができるのも魅力のひとつで、独自に作った<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>も利用することが可能です。</p>
<h3>Docusaurus</h3>
<p><a href="https://docusaurus.io/">https://docusaurus.io/</a></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>製の、Reactベースのドキュメント作成に特化したSSGです(アイコンの怪獣が可愛い)。</p>
<p><figure class="figure-image figure-image-fotolife" title="Docusaurusを利用したドキュメントの画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20210827/20210827083557.png" alt="f:id:rainymoment0616:20210827083557p:plain" width="1200" height="756" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Docusaurusを利用したドキュメントの画面</figcaption></figure></p>
<ul>
<li>Reactの知識がなくても、マークダウンを利用したドキュメント作成が可能(※)</li>
<li>CodeSandBoxやStackBlitzを利用してお試し開発が可能</li>
<li>デフォルトのデザインがドキュメント向け</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>等のバージョン管理ツールでの管理が可能</li>
</ul>
<p>開発環境を作成する前に、お試しで弄ることができるのは、魅力的なポイントのひとつだと思います。</p>
<p>※一部ページはReact<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>で構成されているため、カスタマイズを行う場合にはReactの知識が少し必要になってきます</p>
<h3>Hugo</h3>
<p><a href="https://gohugo.io/">https://gohugo.io/</a></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のスター数53000超えの、Go言語ベースのSSGです。</p>
<p><figure class="figure-image figure-image-fotolife" title="Hugoを利用したドキュメントの画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rainymoment0616/20210827/20210827085219.png" alt="f:id:rainymoment0616:20210827085219p:plain" width="1200" height="1007" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Hugoを利用したドキュメントの画面</figcaption></figure></p>
<ul>
<li>ビルド時間が早い</li>
<li>マークダウンを利用したドキュメント作成が可能</li>
<li>テンプレートデザインが豊富(※)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>等のバージョン管理ツールでの管理が可能</li>
</ul>
<p>テンプレートのデザインがとにかく豊富で、ドキュメント向きのものから、ブログや<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A1%BC%A5%C8%A5%D5%A5%A9%A5%EA%A5%AA">ポートフォリオ</a>、ギャラリーサイトなど、様々なタイプのデザインが揃っています。</p>
<p>(今回、ドキュメント作成に<a href="https://themes.gohugo.io/themes/hugo-geekdoc/">Geekdoc</a>というテーマを利用しています。)</p>
<p>※テーマによっては<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成が変わったり、独自の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>記述があるものもあるので、テーマを変更する際は注意が必要です</p>
<h2>まとめ</h2>
<p>今回はドキュメント作成に向いていると感じたSSGをいくつかご紹介しました。</p>
<p>他にもたくさんあるので、プロジェクトの規模や用途にあったものを試して選んでみてください(<a href="https://jamstack.org/generators/">『Jamstack.org』</a>で色んなSSGを確認することができますよ)。</p>
<p>株式会社Wizではエンジニアを募集しています!</p>
<p> ↓↓↓興味ある方はぜひご覧ください!↓↓↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
rainymoment0616
PlaywrightとTestCafe比較
hatenablog://entry/26006613799010521
2021-08-24T18:15:26+09:00
2021-08-30T18:20:12+09:00 E2Eフレームワーク,ライブラリである「TestCafe」と「Playwright」について簡単に比較してみました。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/watsonpomelo/20210823/20210823183820.png" alt="f:id:watsonpomelo:20210823183820p:plain" width="1200" height="639" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、フロントエンドエンジニアの菅野です。</p>
<p>E2E<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>,ライブラリといえば
<a class="keyword" href="http://d.hatena.ne.jp/keyword/Selenium">Selenium</a>,Cypress,Puppeteer,TestCafe,Playwright などたくさんありますが、
今回は私もプロジェクトで導入しているTestCafeと昨年にリリースされたPlaywrightについて簡単に比較してみたのでお話したいと思います。</p>
<h3>TestCafe</h3>
<ul>
<li>開発会社:Developer Express Inc.</li>
<li>リリース年:2016年</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Github">Github</a>:<a href="https://github.com/DevExpress/testcafe">https://github.com/DevExpress/testcafe</a></li>
</ul>
<h3>Playwright</h3>
<ul>
<li>開発会社:<a class="keyword" href="http://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a></li>
<li>リリース年:2020年</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Github">Github</a>:<a href="https://github.com/microsoft/playwright">https://github.com/microsoft/playwright</a></li>
</ul>
<p>どちらも最新リリースが10日以内(8/23時点)なので活発に開発されています。</p>
<p>Playwrightは<a class="keyword" href="http://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a>が開発しているので今後開発されなくなるリスクは少なそうに思います。</p>
<h2>サポートブラウザ</h2>
<table>
<thead>
<tr>
<th style="text-align:center;"> </th>
<th style="text-align:center;"> TestCafe </th>
<th style="text-align:center;"> Playwright </th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Chrome">Chrome</a> </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Safari">Safari</a> </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Firefox">Firefox</a> </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Opera">Opera</a> </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> × </td>
</tr>
<tr>
<td style="text-align:center;"> Edge </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ ※<a class="keyword" href="http://d.hatena.ne.jp/keyword/Chromium">Chromium</a>のみ </td>
</tr>
<tr>
<td style="text-align:center;"> IE11 </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> × </td>
</tr>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Chrome">Chrome</a> mobile </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:center;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/iOS">iOS</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Safari">Safari</a> </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
</tbody>
</table>
<h2>機能 / 特徴</h2>
<table>
<thead>
<tr>
<th style="text-align:left;"> </th>
<th style="text-align:center;"> TestCafe </th>
<th style="text-align:center;"> Playwright </th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"> ヘッドレスモード </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:left;"> TypeScriptサポート </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:left;"> 同時テスト実行 </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:left;"> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>モード </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> ○ </td>
</tr>
<tr>
<td style="text-align:left;"> モバイル実機テスト </td>
<td style="text-align:center;"> ○ </td>
<td style="text-align:center;"> × </td>
</tr>
<tr>
<td style="text-align:left;"> コード生成 </td>
<td style="text-align:center;"> × </td>
<td style="text-align:center;"> ○ </td>
</tr>
</tbody>
</table>
<p>他にもたくさんあるのですがいくつか抜粋して書いてみました。</p>
<p>Testcafeでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>でURLや<a href="https://testcafe.io/documentation/402639/reference/command-line-interface#--qr-code">QRコードを読み込めば実機上でテストが実行できる機能</a>があり、
Playwrightにはテストケースをちまちま書かなくても<a href="https://playwright.dev/docs/cli/#generate-code">ユーザーの操作からコードを生成してくれる機能</a>があったりと、それぞれに特徴や強みがあるのが分かります。</p>
<h2>コード / 実行時間</h2>
<p>簡単にですが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮るコードで違いを見てみたいと思います。</p>
<h3>TestCafe</h3>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Selector <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'testcafe'</span><span class="synStatement">;</span>
fixture<span class="synConstant">`screenShot`</span>
.page<span class="synStatement">(</span><span class="synConstant">'https://example.com'</span><span class="synStatement">);</span>
test<span class="synStatement">(</span><span class="synConstant">'top'</span><span class="synStatement">,</span> <span class="synStatement">async</span> t <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">await</span> t
.takeScreenshot<span class="synStatement">(</span><span class="synIdentifier">{</span>
path: <span class="synConstant">`${t.browser.name}/testcafe/top.png`</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
</pre>
<h3>Playwright</h3>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">const</span> <span class="synIdentifier">{</span> chromium <span class="synIdentifier">}</span> <span class="synStatement">=</span> require<span class="synStatement">(</span><span class="synConstant">'playwright'</span><span class="synStatement">)</span>
<span class="synStatement">(async</span> <span class="synStatement">()</span> <span class="synStatement">=></span> <span class="synIdentifier">{</span>
<span class="synStatement">const</span> browser <span class="synStatement">=</span> <span class="synStatement">await</span> chromium.launch<span class="synStatement">(</span><span class="synIdentifier">{</span>
headless: <span class="synConstant">false</span>
<span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">const</span> context <span class="synStatement">=</span> <span class="synStatement">await</span> browser.newContext<span class="synStatement">();</span>
<span class="synStatement">const</span> page <span class="synStatement">=</span> <span class="synStatement">await</span> context.newPage<span class="synStatement">();</span>
<span class="synStatement">await</span> page.<span class="synStatement">goto(</span><span class="synConstant">'https://example.com'</span><span class="synStatement">);</span>
<span class="synStatement">await</span> page.screenshot<span class="synStatement">(</span><span class="synIdentifier">{</span> path: <span class="synConstant">`./screenshots/Chrome/playwright/top.png`</span> <span class="synIdentifier">}</span><span class="synStatement">);</span>
<span class="synStatement">await</span> browser.<span class="synIdentifier">close</span><span class="synStatement">();</span>
<span class="synIdentifier">}</span><span class="synStatement">)();</span>
</pre>
<p>Playwrightではブラウ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B6%A5%B3%A5%F3">ザコン</a>テキストとコンテキストの中でpageを生成する記述があるため、TestCafeと比べるとコードが少し長くなってしまいます。</p>
<p>あとは、<code>browser.close();</code>を書くことでブラウザを閉じ処理を終了するのですが、TestCafeは記述を書かなくても自動でブラウザを閉じてくれるのでそういう細かいところで便利だなと思います。</p>
<p>続いて実行時間を比べてみます。</p>
<p>今回はブラウザUIを表示させて時間を計測したかったので、Playwrightではデフォルトでtrueとなっているheadlessモードをfalseにしています。</p>
<table>
<thead>
<tr>
<th style="text-align:center;"> </th>
<th style="text-align:center;"> TestCafe </th>
<th style="text-align:center;"> Playwright </th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center;"> Time </td>
<td style="text-align:center;"> 10183.922ms </td>
<td style="text-align:center;"> 2313.172ms </td>
</tr>
</tbody>
</table>
<p>実行時間はPlaywrightが7秒以上速い結果となりました。</p>
<p>TestCafeはブラウザが立ち上がるまでに特に時間がかかってしまうのでそこで大きく差が出てしまいました。</p>
<h2>まとめ</h2>
<p>Playwrightは学び始めたところでまだまだ分かっていない部分も多いのですが、
比較をしてみると互いの長所短所が分かりやすくなりますね。</p>
<p>ユーザー操作からコードを生成してくれるPlaywrightはメンテナンスのしやすさからフォームテストなどのテストケースが多い場合に向いてると思います。</p>
<p>TestCafeはサポートブラウザが充実しているので<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>実機、<a class="keyword" href="http://d.hatena.ne.jp/keyword/IE">IE</a>など様々な環境でテストを行いたい場合に選ぶと良さそうです。</p>
<p>プロジェクトによってどういったテストを行いたいのか、検証範囲はどこまでなのかなどがあると思いますので、ぜひそれぞれにあった<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>,ライブラリを選んでみてください。</p>
<p>株式会社Wizではエンジニアを募集しています!
↓↓↓興味ある方はぜひご覧ください!↓↓↓
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
watsonpomelo
PHPでコンストラクタを複数定義する【初級〜中級者向け】
hatenablog://entry/26006613796772195
2021-08-16T15:51:36+09:00
2021-08-16T15:51:36+09:00 PHP(Laravel)を使って、コンストラクタを複数定義する方法について書いてみました
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/t-yone3/20210813/20210813094006.jpg" alt="f:id:t-yone3:20210813094006j:plain" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、バックエンドエンジニアの米山です。</p>
<p>突然ですがコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ、使っていますか?</p>
<p>特に、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D3%A5%B8%A5%CD%A5%B9%A5%ED%A5%B8%A5%C3%A5%AF">ビジネスロジック</a>をまとめるクラスを作った時や、似たような処理を行うクラスの初期化に役立つのがコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タです。</p>
<p>今回は、そんなコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを複数作る手法と、私なりのオススメ度も併せてご紹介しようと思います。</p>
<p>それではいってみましょう〜!</p>
<h1>そもそも、「コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ」って何?</h1>
<p><a href="https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF">Wikipedia</a>にはこうあります。</p>
<blockquote><p>コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ(英: constructor)は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A5%D6%A5%B8%A5%A7%A5%AF%A5%C8%BB%D8%B8%FE">オブジェクト指向</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>で新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なう関数あるいはメソッドのことである。</p></blockquote>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Java">Java</a>や<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>、<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>に代表されるような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A5%D6%A5%B8%A5%A7%A5%AF%A5%C8%BB%D8%B8%FE%B8%C0%B8%EC">オブジェクト指向言語</a>では、クラスの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作成する時に初期化処理を行うことができます。</p>
<p>この初期化処理のことを「<span style="color: #ff0000">コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ</span>」と呼んでいます。</p>
<p>具体的に<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>を例に、コードで表すとこんな感じになります。</p>
<pre class="code" data-lang="" data-unlink>class SomeClass
{
// これがコンストラクタ
public function __construct()
{
logger()->debug('初期化処理だよん');
}
}
class Hoge
{
public function fuga()
{
// クラスのインスタンスを作ると、初期化処理が呼ばれる
// ログに「初期化処理だよん」が出力される
$instance = new SomeClass();
}
}</pre>
<h1>コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを複数実装する方法</h1>
<p>それでは本題に入りましょう。</p>
<p>コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを複数実装し、引数の数や内容で処理を分岐させます。<br>
いくつか方法があるので、私なりのオススメ度も併せて記載しておきます。</p>
<h3>【オススメ度:★★★☆☆】方法その1:コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タをオーバーライドさせる</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>では関数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>ができません。</p>
<p>似たようなことをやるためには、abstractクラスを用意し継承した先のクラスで「自分自身のコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを呼ぶ」もしくは「親クラスのコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを呼ぶ」という分岐になります。</p>
<p>コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タだけ、オーバーライドすることができます。</p>
<h5>【余談】<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>とオーバーライド</h5>
<p>さて、上記で説明した通りなのですが「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>」「オーバーライド」は混同しやすい用語なので、解説を挟みます。</p>
<p>私なりに理解した内容で説明すると</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>:同名の、類似メソッド(または関数)の作成</li>
<li>オーバーライド:メソッド(または関数)の上書き</li>
</ul>
<p>です。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>を実際にコードで表すとこうです。</p>
<p>(<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>できないので<a class="keyword" href="http://d.hatena.ne.jp/keyword/Java">Java</a>で表現します)</p>
<pre class="code" data-lang="" data-unlink>class SomeClass {
// 元のメソッド
public void SampleMethod() {
}
// オーバーロードしたメソッド(返却値が異なる)
public String SampleMethod() {
return "hoge";
}
// これもオーバーロードしたメソッド(引数が異なる)
public void SampleMethod(int value) {
}
}</pre>
<p>オーバーライドは下記のようになります(同じく<a class="keyword" href="http://d.hatena.ne.jp/keyword/Java">Java</a>で表現します)</p>
<pre class="code" data-lang="" data-unlink>class AbstractSampleBase {
// 親クラスのメソッド
public String SampleMethod(String value) {
return "hoge" + value;
}
}
class ChildSample extends AbstractSampleBase {
// 継承先で再定義する
public String SampleMethod(String value) {
return "fuga";
}
}</pre>
<h5>【<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%D7%CF%C3%B5%D9%C2%EA">閑話休題</a>】結局、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>でコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タをオーバーライドすると・・・?</h5>
<p>話を戻して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>でコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タをオーバーライドさせると、こうなります。</p>
<pre class="code" data-lang="" data-unlink>abstract class SampleBase
{
public $output;
// 親クラスのコンストラクタは引数なし
public function __construct()
{
$this->output = 'これは親クラスのコンストラクタです';
}
}
class ConstructorSampleA extends SampleBase
{
// 子クラスのコンストラクタは引数あり
public function __construct($value)
{
$this->output = 'これは子クラスのコンストラクタです' . $value;
}
}</pre>
<p>これを、例えばControllerクラスなどで呼び出す場合、以下のようになります。</p>
<pre class="code" data-lang="" data-unlink>class SampleController extends Controller
{
public function index()
{
// 親クラスのコンストラクタがよばれる
$data = new ConstructorSampleA();
// 下をコメントアウトすると子クラスのコンストラクタが呼ばれる
// $data = new ConstructorSampleA('hello world');
return view('sample', ['output' => $data->output]);
}
}</pre>
<h3>【オススメ度:★★★☆☆】方法その2:コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タをオーバーライドさせる(1メソッドで分岐)</h3>
<p>方法その1と似ていますが、こちらのやり方では必ず子クラスのコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを呼び、引数の内容によって親クラスのコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを呼ぶ、というやり方です。</p>
<pre class="code" data-lang="" data-unlink>abstract class SampleBase
{
public $output;
// 親クラスのコンストラクタは引数なし
public function __construct()
{
$this->output = 'これは親クラスのコンストラクタです';
}
}
class ConstructorSampleB extends SampleBase
{
// 子クラスのコンストラクタは引数あり(デフォルトはnull)
public function __construct($value = null)
{
if (empty($value)) {
// 親クラスのコンストラクタを呼び出す
parent::__cunstruct();
} else {
// 引数の値があった時に子クラス側の処理を行う
$this->output = 'これは子クラスのコンストラクタです' . $value;
}
}
}</pre>
<h3>【オススメ度:★☆☆☆☆】方法その3:func_num_argsとfunc_get_argsでコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを分岐する</h3>
<p>この方法は、可読性の観点からオススメはしません。また、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>のバージョンにより挙動が変わる可能性があります。</p>
<p>func_num_args関数でコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タに渡した引数の数を取得・チェックし、引数の数に応じて呼び出すコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを変える方法です。<br>
分岐先のメソッドに渡す引数はfunc_get_args関数で取得できます。</p>
<pre class="code" data-lang="" data-unlink>class ConstructorSampleC
{
public $output;
public function __construct()
{
$argCount = func_num_args();
$argList = func_get_args();
// 受け取った引数の数によって、呼び出すメソッドを分岐
switch($argCount)
{
case 1:
$this->construct1($argList[0]);
break;
case 2:
$this->construct2($argList[0], $argList[1]);
break;
case 3:
$this->construct3($argList[0], $argList[1], $argList[2]);
break;
}
}
private function construct1($value)
{
$this->output = 'これは引数1つのコンストラクタです' . $value;
}
private function construct2($value1, $value2)
{
$this->output = 'これは引数2つのコンストラクタです' . $value1 . $value2;
}
private function construct3($value1, $value2, $value3)
{
$this->output = 'これは引数3つのコンストラクタです' . $value1 . $value2 . $value3;
}
}
class SampleController extends Controller
{
public function index()
{
// 引数1つの時のコンストラクタ
$data = new ConstructorSampleC('hello');
// 引数2つの時のコンストラクタ
// $data = new ConstructorSampleC('hello', 'world');
// 引数3つの時のコンストラクタ
// $data = new ConstructorSampleC('hello', 'world', 'hoge');
return view('sample', ['output' => $data->output]);
}
}</pre>
<h3>【オススメ度:★★★★☆】方法その4:interfaceを使ってクラスで分岐する</h3>
<p>方法その1では継承を使いました。「同じような処理を行う」が「初期化処理が異なる」場合はinterfaceが有効です。</p>
<p>この例ではfactory関数を用意し分岐条件に応じてそれぞれの初期化を行う、別々のクラスを返すようにします。<br>
継承を使う場合、継承元・継承先でコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タの分岐を行うことで少々複雑さが増します。</p>
<p>主処理が同じ場合は</p>
<ul>
<li>初期化を行ってから主処理を行うクラス</li>
<li>初期化を行わず、主処理を行うクラス</li>
</ul>
<p>にクラスそのものを、分けてしまった方がわかりやすくなるケースがあります。</p>
<pre class="code" data-lang="" data-unlink>interface ISample
{
// 共通で行う主処理
public function execute();
}
class ConstructorSampleD implements ISample
{
public function construct()
{
logger()->debug('引数が0の時のコンストラクタです');
}
public function execute()
{
// クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い
}
}
class ConstructorSampleE implements ISample
{
public function __construct($value)
{
logger()->debug('引数が1の時のコンストラクタです' . $value);
}
public function execute()
{
// クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い
}
}
class ConstructorSampleF implements ISample
{
public function __construct($value1, $value2)
{
logger()->debug('引数が2の時のコンストラクタです' . $value1 . $value2);
}
public function execute()
{
// クラスごとに処理を分けても良いし、共通的な処理を作って呼び出しても良い
}
}
// クラスのインスタンスを作り出すfactoryクラス
class ConstructorSampleFactory
{
public static function createSample($mode)
{
if ($mode === 1) {
// 引数が0の時はConstructorSampleDクラスとしてコンストラクタを実行
return new ConstructorSampleD();
} elseif ($mode === 2) {
// 引数が1つの時はConstructorSampleEクラスとしてコンストラクタを実行
return new ConstructorSampleE('hoge');
} elseif ($mode === 3) {
// 引数が2つの時はConstructorSampleFクラスとしてコンストラクタを実行
return new ConstructorSampleF('hoge', 'fuga');
}
}
}</pre>
<p>この場合、呼び出し元では下記のようになります。</p>
<pre class="code" data-lang="" data-unlink>class SampleController extends Controller
{
public function index()
{
// 引数0の時のコンストラクタ
$data = ConstructorSampleFactory->createSample(1);
// 引数1つの時のコンストラクタ
// $data = ConstructorSampleFactory->createSample(2);
// 引数2つの時のコンストラクタ
// $data = ConstructorSampleFactory->createSample(3);
return view('sample', ['output' => $data->output]);
}
}</pre>
<p>この例ではcreateSample関数の中で、直接コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タに引数を渡していますが、呼び出し元(Controller)から引数を繋げたい場合は、createSample関数へ渡す引数を可変長引数リストにする等で対応可能です。</p>
<h1>まとめ</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PHP">PHP</a>でコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを複数実装する方法をご紹介しました。</p>
<p>他の言語のように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>ができないので、少し不恰好になってしまいますが</p>
<ul>
<li>継承を使い、親クラスと子クラスでコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タを分ける</li>
<li>func_num_args関数、func_get_args関数を使って、処理パターンを分岐させる</li>
<li>interfaceを使ってクラスそのものを分岐し、それぞれで初期化させる</li>
</ul>
<p>まとめるとこの3パターンになります。</p>
<p>是非試してみて下さい。</p>
<hr />
<p>Wizではエンジニアを募集しております。</p>
<p>興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
t-yone3
EMになって雑食になり行動が変わった話
hatenablog://entry/26006613797909151
2021-08-16T15:26:25+09:00
2021-08-16T15:26:25+09:00 はじめまして。エンジニアリングマネージャー(以下EM)の岡本です。今回は私がプレーヤーからマネージャーになって変化した「行動」について書いていきます。 マネージャーになり、基本的に何にでも興味を示し行動するようになりました。 エンジニアに関係ない記事や本でも興味があれば読んでみたり、コミュニケーションが苦手なのに積極的に取るようになったり、課題と感じたらとりあえず何かやってみたり、と何でも好き嫌いなくまず行動するようになりました。 「こんなのできるのが当たり前じゃん!」と思う方もいると思いますが、自分自身、性格と合わない行動が多く不思議に思っており、今回振り返りも兼ねてまとめていきたいと思いま…
<p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sevenium/20210816/20210816152010.jpg" alt="f:id:sevenium:20210816152010j:plain" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p>
<p><br />はじめまして。<br />エンジニア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャー(以下EM)の岡本です。<br />今回は私がプレーヤーからマネージャーになって変化した「行動」について書いていきます。</p>
<p><br />マネージャーになり、基本的に何にでも興味を示し行動するようになりました。</p>
<p>エンジニアに関係ない記事や本でも興味があれば読んでみたり、<br />コミュニケーションが苦手なのに積極的に取るようになったり、<br />課題と感じたらとりあえず何かやってみたり、<br />と何でも好き嫌いなくまず行動するようになりました。</p>
<p>「こんなのできるのが当たり前じゃん!」と思う方もいると思いますが、<br />自分自身、性格と合わない行動が多く不思議に思っており、<br />今回振り返りも兼ねてまとめていきたいと思います。</p>
<h4>経歴&自己紹介</h4>
<p>私は2016年7月に中途でフロントエンドエンジニアとして入社しました。<br />2019年4月にマネージャーになるまではフロントエンド開発をメインでしておりました。</p>
<p>基本的に内気で初対面の人や人前で話すのが苦手です。<br />趣味はゲームやキーボードと完全なインドアタイプです。</p>
<p>こういった人間にEMが務まるのかと心配になりますが、とりあえず振り返っていきましょう!</p>
<h4>【振り返り1】ビジネス意識が変わったから?</h4>
<p>プレーヤーのときはプロダクトを作ること(コードを書きまくること)が楽しく、<br />その先にあるビジネス意識は薄く、設計やトレンドの技術に意識が向いていました。<br />何のために作るのかを理解してプロダクトに落とし込むことはできても、<br />そのプロダクトの利益を最大化するためにエンジニアとして何をすべきかという考えは殆どありませんでした。</p>
<p>しかしマネージャーとなると組織のパフォーマンスの最大化や、<br />さらなる効率化がミッションとなってくるため、<br />徐々にビジネスへの意識というのは高まっていきました。</p>
<p>ビジネスとエンジニアを繋ぐためだったり、<br />ビジネス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トを与えられるエンジニア組織を構築するための勉強をしたりと、<br />とりあえずはインプット量を増やすことを心がけました。</p>
<p>結果、ミッション達成のためにこちらから提案したり、<br />エンジニア組織についての思いをメンバーにアウトプットすることが増えたと感じます。</p>
<p> </p>
<p>自身の成長が一番ではなく、<br />組織としての成功を一番として行動できるような「マインドの変化」が自分の中であったことが、行動の変化に結びついているのかもしれません。</p>
<h4> 【振り返り2】 本を読むようになったから?</h4>
<p>マネジメント能力を強化したく、色々本を読むようになりました。</p>
<ul>
<li>効率的な学び方</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A1%BC%A5%C1%A5%F3">コーチン</a>グ</li>
<li>エンジニア組織</li>
<li>リーダーシップ</li>
<li>ビジネス本</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B7%D0%CD%FD">経理</a></li>
<li>心理学</li>
</ul>
<p>などの本を読んでいて<br />「あの本に書いてあった内容とこの本のこの部分って似てるなー」<br />とか<br />「あの本で言ってたことってこういうことだったのか!」<br />と他の本を読んでいるときに気づくことがよくありました。</p>
<p>全く違う分野の本なのにこういう事が頻繁にあり、<br />「学ぶ」ことに境界線を作っていた自分が恥ずかしいと思うようになりました。</p>
<p> </p>
<p>このように勉強する意識が変わり学びの視野が広がったことで、<br />様々な分野への興味に結びついているのかもしれません。</p>
<p> </p>
<h4>【振り返り3】そもそもコミュニケーション機会が増えたから?</h4>
<p>EMになると様々な部署と連携した課題解決を行うようになるため、<br />今まで関わらなかったビジネスサイドの人やバックオフィスの人と関わりが増えました。<br />また1on1で多くのメンバーと話す機会もあり、<br />基本的に誰かとコミュニケーションを取ることが仕事になってきています。</p>
<p>様々な人とコミュニケーションを取るなか、<br />「ビジネスサイドの人の説明の納得感や語彙力すごいなー」<br />「この人の説明わかりやすいから今度真似してみよう」<br />「メンバーの話し方や表情から色々情報が取れるの大事だしおもしろい」<br />と様々な学びを得たり、コミュニケーションの重要性に気づいたというのが大きいです。</p>
<p>今までは自分がコミュニケーション苦手だからと、<br />積極的にコミュニケーションを取ってきませんでしたが、<br />「コミュニケーションを取っていないからこそコミュニケーションが苦手だった」ということに気づいたのです。</p>
<p> </p>
<p>今はコミュニケーションに対するネガティブな印象は無くなり、<br />話すことで得られる学びの好奇心の方が強くなったため、<br />積極的な行動に変化しているのだと感じます。</p>
<h4>最後に</h4>
<p>一度マネジメントを経験することで目線が変わり、<br />今まで見えなかったもの・見ようとしなかったものが見えてくるようになることで「考え方=思考」が変化していきました。</p>
<p>マネジメントを「経験」することで「思考」が変化し、<br />「思考」が変化することでそれはやがて「行動」へと変化していきます。<br />その結果が今の私を作っているのではないかと今回振り返ってみて感じました。</p>
<p>これらの経験はマネジメントだけでなく、今後プレーヤーのときにも活かせることであると感じます。</p>
<p> </p>
<p>EMになって学んだこと。<br />それは、<strong>「自分自身で壁や境界を作らず、何にでも興味を持ちインプットし思考し続けること」</strong>です。<br />雑食に何でも学ぶ姿勢が自身を成長させてくれるのかもしれません。</p>
<p> <br />株式会社Wizでは雑食に学ぶエンジニアを募集しています!<br />↓↓↓興味ある方はぜひご覧ください!↓↓↓<br /><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
sevenium
TypeScript InterfaceとType Aliasについて
hatenablog://entry/26006613794072396
2021-08-06T11:10:24+09:00
2021-08-06T11:10:24+09:00 はじめに 皆様こんにちは、フロントエンドの松本です。 本日はTypeScriptのInterfaceとType Aliasについてお話していきたいと思います。 オブジェクトの宣言時などはどちらを使えばいいのか、頭を悩ませるトピックの一つでもあると思います。 それでは早速、両者の具体的な違いをみていきましょう。 型の宣言 // Interface interface Book { title: string page: number } // Type Alias type Book = { page: number } Interfaceは構造を定義するものなのでType Aliasと違って=…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yukiji_03/20210805/20210805141123.jpg" alt="f:id:yukiji_03:20210805141123j:plain" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>はじめに</h2>
<p>皆様こんにちは、フロントエンドの松本です。</p>
<p>本日はTypeScriptの<code>Interface</code>と<code>Type Alias</code>についてお話していきたいと思います。</p>
<p>オブジェクトの宣言時などはどちらを使えばいいのか、頭を悩ませるトピックの一つでもあると思います。</p>
<p>それでは早速、両者の具体的な違いをみていきましょう。</p>
<h2>型の宣言</h2>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// Interface</span>
<span class="synStatement">interface</span> Book <span class="synIdentifier">{</span>
title: <span class="synType">string</span>
page: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synComment">// Type Alias</span>
<span class="synStatement">type</span> Book <span class="synStatement">=</span> <span class="synIdentifier">{</span>
page: <span class="synType">number</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>Interface</code>は構造を定義するものなので<code>Type Alias</code>と違って<code>=</code>が不要になっており、見た目としてはほとんど一緒です。</p>
<p>また、 <code>Type Alias</code>は右辺に任意の型を指定できるという点で汎用的です。</p>
<p><code>Interface</code>の場合、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%ED%A5%C3%A5%AF%A5%B9">ブロックス</a>コープでなければならないので、次のような<code>Type Alias</code>を<code>Interface</code>に書き直す方法はありません。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> A <span class="synStatement">=</span> <span class="synType">number</span>
<span class="synStatement">type</span> B <span class="synStatement">=</span> A | <span class="synType">string</span>
</pre>
<h2>宣言のマージ</h2>
<p>同名の<code>Interface</code>は 型がマージされます(宣言のマージ)</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">interface</span> Book <span class="synIdentifier">{</span>
title: <span class="synType">string</span>
<span class="synIdentifier">}</span>
<span class="synComment">// Bookに page: number をマージ</span>
<span class="synStatement">interface</span> Book <span class="synIdentifier">{</span>
page: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synStatement">const</span> book: Book <span class="synStatement">=</span> <span class="synIdentifier">{</span>
title: <span class="synConstant">'TypeScript'</span><span class="synStatement">,</span>
page: <span class="synConstant">360</span>
<span class="synIdentifier">}</span>
</pre>
<p>これを<code>Type Alias</code> に書き直すとエラーが起こります。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink>
<span class="synStatement">type</span> Book <span class="synStatement">=</span> <span class="synIdentifier">{</span>
title: <span class="synType">string</span>
<span class="synIdentifier">}</span>
<span class="synStatement">type</span> Book <span class="synStatement">=</span> <span class="synIdentifier">{</span>
page: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synComment">// //Duplicate identifier 'Book'</span>
</pre>
<h2>拡張性</h2>
<p><code>Interface</code> は拡張性が高いです。</p>
<p><code>extends</code>を使うことで、継承したサブインターフェースを作成できます。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">interface</span> Movie <span class="synIdentifier">{</span>
title: <span class="synType">string</span>
time: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synComment">//Movieを継承</span>
<span class="synStatement">interface</span> Drama <span class="synStatement">extends</span> Movie <span class="synIdentifier">{</span>
season: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synStatement">const</span> kaigaiDrama: Drama <span class="synStatement">=</span> <span class="synIdentifier">{</span>
title: <span class="synConstant">'The Walking Dead'</span><span class="synStatement">,</span>
time: <span class="synConstant">60</span><span class="synStatement">,</span>
season: <span class="synConstant">10</span>
<span class="synIdentifier">}</span>
</pre>
<p>また、<code>Interface</code> は <code>Type Alias</code>をextendsすることも出来ます。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// Type Alias</span>
<span class="synStatement">type</span> Movie <span class="synIdentifier">{</span>
title: <span class="synType">string</span>
time: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synComment">//Movieを継承</span>
<span class="synStatement">interface</span> Drama <span class="synStatement">extends</span> Movie <span class="synIdentifier">{</span>
season: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synStatement">const</span> kaigaiDrama: Drama <span class="synStatement">=</span> <span class="synIdentifier">{</span>
title: <span class="synConstant">'The Walking Dead'</span><span class="synStatement">,</span>
time: <span class="synConstant">60</span><span class="synStatement">,</span>
season: <span class="synConstant">10</span>
<span class="synIdentifier">}</span>
</pre>
<p><code>Type Alias</code>は拡張こそできないものの、<code>交差型</code>を利用して既存のタイプを組み合わせることができます。</p>
<pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">type</span> Rice <span class="synStatement">=</span> <span class="synIdentifier">{</span>
kind: <span class="synConstant">'白米'</span> | <span class="synConstant">'玄米'</span>
gram: <span class="synType">number</span>
<span class="synIdentifier">}</span>
<span class="synStatement">type</span> Egg <span class="synStatement">=</span> <span class="synIdentifier">{</span>
size: <span class="synConstant">'S'</span> | <span class="synConstant">'M'</span> | <span class="synConstant">'L'</span>
<span class="synIdentifier">}</span>
<span class="synStatement">type</span> TamagokakeGohan <span class="synStatement">=</span> Rice & Egg
<span class="synStatement">const</span> tkg: TamagokakeGohan <span class="synStatement">=</span> <span class="synIdentifier">{</span>
kind: <span class="synConstant">'白米'</span><span class="synStatement">,</span>
gram: <span class="synConstant">250</span><span class="synStatement">,</span>
size: <span class="synConstant">'L'</span>
<span class="synIdentifier">}</span>
</pre>
<h2>まとめ</h2>
<p><code>Type Alias</code>と<code>Interface</code>はほとんど同じことができ、違いは小さなものです。</p>
<p><code>Type Alias</code>は型に名前をつけるもの、<code>Interface</code>は構造を定義するものです。</p>
<p>例えばオブジェクトの宣言時などは<code>Interface</code>を使用するなどして、ケースバイケースで使い分けると良いと思います。</p>
<p>個人的には<code>Type Alias</code>で書いた方が安心できる派です。</p>
<p>(<code>Interface</code>は宣言のマージにより、意図しないところで型が変化する恐れがあるので)</p>
<p>最後になりますが、Wizではエンジニアを募集中です。</p>
<p>興味のある方は是非覗いてみてください↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
yukiji_03
NextAuth.jsでカスタムログインページを実装する方法
hatenablog://entry/26006613793369614
2021-08-05T13:51:45+09:00
2021-08-05T13:51:45+09:00 こんにちは、フロントエンドエンジニアの松尾です。 最近はNext.jsが盛り上がっており、エコシステムも充実してきています。 その中に、NextAuth.jsという認証機能を簡単に実装できるライブラリがあり非常に便利です。 next-auth.js.org 本記事では、NextAuth.jsを使用したカスタムログインページの実装方法について説明したいと思います。 NextAuth.jsとは? NextAuth.jsとは、Next.jsに簡単に認証機能を実装できるライブラリです。 アカウント登録も必要なく、ライブラリをインストールするだけで実装できます。 また、メールアドレス認証に加えて、Goo…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mt_816/20210804/20210804192049.png" alt="f:id:mt_816:20210804192049p:plain" width="1200" height="628" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、フロントエンドエンジニアの松尾です。</p>
<p>最近はNext.jsが盛り上がっており、エコシステムも充実してきています。</p>
<p>その中に、NextAuth.jsという認証機能を簡単に実装できるライブラリがあり非常に便利です。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnext-auth.js.org%2F" title="NextAuth.js" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://next-auth.js.org/">next-auth.js.org</a></cite></p>
<p>本記事では、NextAuth.jsを使用したカスタムログインページの実装方法について説明したいと思います。</p>
<h2>NextAuth.jsとは?</h2>
<p>NextAuth.jsとは、Next.jsに簡単に認証機能を実装できるライブラリです。</p>
<p>アカウント登録も必要なく、ライブラリをインストールするだけで実装できます。</p>
<p>また、メールアドレス認証に加えて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a>・<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a>・<a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>といったサービスの<a class="keyword" href="http://d.hatena.ne.jp/keyword/OAuth%C7%A7%BE%DA">OAuth認証</a>を組み込むことが可能です。</p>
<p>NextAuth.jsでログインする場合、以下のようにNextAuth.js専用のログインページに遷移する形となっており、自由にページをカスタマイズできません。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mt_816/20210803/20210803152037.png" alt="f:id:mt_816:20210803152037p:plain" width="1100" height="752" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>認証機能を特定のページに組み込みたい場合も多いのではないでしょうか?</p>
<p>そこで、カスタムログインページの実装方法について説明したいと思います。</p>
<h2>実装方法</h2>
<p>まず初めに、<code>next-auth</code>をインストールしてください。</p>
<pre class="code" data-lang="" data-unlink>npm install next-auth</pre>
<p>次に、NextAuth.jsの<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>ルートを作成します。</p>
<p><code>/pages/api/auth/[...nextauth].js</code>を作成し、以下を記述してください。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> NextAuth from <span class="synConstant">'next-auth'</span>
<span class="synStatement">import</span> Providers from <span class="synConstant">'next-auth/providers'</span>
<span class="synComment">// NextAuth.js関数に渡すオプション</span>
<span class="synStatement">const</span> options = <span class="synIdentifier">{</span>
providers: <span class="synIdentifier">[</span>
Providers.Credentials(<span class="synIdentifier">{</span>
<span class="synComment">// NextAuthの認証関数。credentialsにログイン情報が格納される。</span>
authorize: async credentials => <span class="synIdentifier">{</span>
<span class="synStatement">if</span> (
<span class="synComment">// ログインID・パスワードは環境変数にて設定する。</span>
credentials.login === process.env.NEXT_PUBLIC_LOGIN_ID &&
credentials.password === process.env.NEXT_PUBLIC_PASSWORD
) <span class="synIdentifier">{</span>
<span class="synComment">// ログイン成功後ユーザー情報を返却する。値はsessionに格納される。</span>
<span class="synStatement">return</span> Promise.resolve(<span class="synIdentifier">{</span> name: <span class="synConstant">'admin'</span> <span class="synIdentifier">}</span>)
<span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span>
<span class="synComment">// ログイン失敗後認証を拒否し、エラーメッセージを返却する。</span>
<span class="synStatement">return</span> Promise.resolve(<span class="synStatement">null</span>)
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>,
<span class="synIdentifier">}</span>),
<span class="synIdentifier">]</span>,
<span class="synComment">// ログインページを指定する。今回はトップページのため'/'を指定。</span>
pages: <span class="synIdentifier">{</span>
signIn: <span class="synConstant">'/'</span>,
<span class="synIdentifier">}</span>,
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> (req, res) => NextAuth(req, res, options)
</pre>
<p>最後に、認証機能を実装するページに以下を記述します。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> useRouter <span class="synIdentifier">}</span> from <span class="synConstant">'next/router'</span>
<span class="synStatement">import</span> <span class="synIdentifier">{</span> getCsrfToken, useSession, signOut <span class="synIdentifier">}</span> from <span class="synConstant">'next-auth/client'</span>
<span class="synStatement">const</span> LoginPage = (<span class="synIdentifier">{</span> csrfToken <span class="synIdentifier">}</span>) => <span class="synIdentifier">{</span>
<span class="synStatement">const</span> <span class="synIdentifier">{</span> error <span class="synIdentifier">}</span> = useRouter().query
<span class="synStatement">const</span> <span class="synIdentifier">[</span> session <span class="synIdentifier">]</span> = useSession()
<span class="synStatement">return</span> (
<div>
<h1>カスタムログインページ</h1>
<span class="synIdentifier">{</span>session ? (
<span class="synComment">// ログイン状態の場合。ユーザー名、ログアウトボタンを表示。</span>
<>
<div>ユーザー:<span class="synIdentifier">{</span>session.user?.name<span class="synIdentifier">}</span></div>
<button onClick=<span class="synIdentifier">{</span>signOut<span class="synIdentifier">}</span>>ログアウト</button>
</>
) : (
<span class="synComment">// ログアウト状態の場合。入力フォームを表示。</span>
<form method=<span class="synConstant">'post'</span> action=<span class="synConstant">'/api/auth/callback/credentials'</span>>
<input name=<span class="synConstant">'csrfToken'</span> type=<span class="synConstant">'hidden'</span> defaultValue=<span class="synIdentifier">{</span>csrfToken<span class="synIdentifier">}</span> />
<label>
<div>ログインID</div>
<input name=<span class="synConstant">'login'</span> />
</label>
<label>
<div>パスワード</div>
<input name=<span class="synConstant">'password'</span> type=<span class="synConstant">'password'</span> />
</label>
<div>
<button type=<span class="synConstant">'submit'</span>>ログイン</button>
</div>
<span class="synIdentifier">{</span><span class="synComment">/* ログイン失敗後、エラーメッセージを表示。*/</span><span class="synIdentifier">}</span>
<span class="synIdentifier">{</span>error && <div>ログインID又はパスワードが間違っています。</div><span class="synIdentifier">}</span>
</form>
)<span class="synIdentifier">}</span>
</div>
)
<span class="synIdentifier">}</span>
<span class="synComment">// POSTリクエスト(サインイン・サインアウトなど)に必要なCSRFトークンを返却する。</span>
<span class="synStatement">export</span> <span class="synStatement">const</span> getServerSideProps = async context => <span class="synIdentifier">{</span>
<span class="synStatement">return</span> <span class="synIdentifier">{</span>
props: <span class="synIdentifier">{</span>
csrfToken: await getCsrfToken(context),
<span class="synIdentifier">}</span>,
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
<span class="synStatement">export</span> <span class="synStatement">default</span> LoginPage
</pre>
<p>ログインに成功すると、useSession関数の<code>session</code>にユーザー情報が格納されます。</p>
<p>以下のように、<code>session</code>にユーザー情報が含まれているか否かで表示を切り替えています。</p>
<p>▼ ログイン前</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mt_816/20210804/20210804110407.png" alt="f:id:mt_816:20210804110407p:plain" width="366" height="238" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>▼ ログイン後</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mt_816/20210804/20210804110414.png" alt="f:id:mt_816:20210804110414p:plain" width="366" height="164" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>また、認証失敗時に関しては、URLパラメータの<code>error</code>を参照し、エラーメッセージを表示させています。</p>
<p>▼ ログイン失敗時</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mt_816/20210804/20210804135835.png" alt="f:id:mt_816:20210804135835p:plain" width="366" height="170" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>まとめ</h2>
<p>以上NextAuth.jsを使用したカスタムログインページ実装についての説明でした。</p>
<p>今回は簡易的な認証機能の例でしたが、NextAuth.jsは、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a>・<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a>・<a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>といったサービスの認証機能の組み込みや、データベース・メール送信サービスとの連携も可能となっています。</p>
<p>NextAuth.jsは無料で使用できますので、興味ある方は是非お試しください!</p>
<p>最後になりますが、Wizではエンジニアを募集中です。</p>
<p>興味のある方は是非覗いてみてください↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
mt_816
npm packageの公開の手順
hatenablog://entry/26006613791161977
2021-07-29T17:27:44+09:00
2021-07-29T17:27:44+09:00 npmモジュール(npm パッケージ)を作成しデプロイしてオープンソースとして使えるところまでやってみたいと思います。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20210727/20210727170909.png" alt="f:id:thunder_fury:20210727170909p:plain" width="1000" height="420" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2>はじめに</h2>
<p>皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪<br></p>
<p>npmモジュール(npm パッケージ)を作成しデプロイして<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>として使えるところまでやってみたいと思います。</p>
<h2>npm (Node Package Manager)</h2>
<p>開始する前に、簡単にnpmについて調べてみたいと思います。</p>
<p>npmの役割はパッケージ管理ツールです。
ソフトウェアを管理し、インストールをサポートしてくれます。
この記事を書いた時点では公開のパッケージ数<b>約1,688,642</b>個以上のnpmパッケージが公開されており現在もまだnpmのパッケージが増えています。</p>
<h2>npm インストール</h2>
<p>Node.jsをインストールするとnpmも同時にインストールがされます。
下記のリンクでNode.jsのインストールが簡単にできます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnodejs.org%2Fja%2F" title="Node.js" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://nodejs.org/ja/">nodejs.org</a></cite></p>
<h2>package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a></h2>
<p><code>package.json</code>を生成するためにはプロジェクトのフォルダから<code>npm init</code>のコマンドを叩く必要があります。
<code>Package.json</code>は開発に必要な情報などを記載する場所であり、必要なパッケージをインストールする時に自動で記載をしてくれます。</p>
<h2>package-lock.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a></h2>
<p><code>package-lock.json</code>は各パッケージの依存関係の管理をするファイルです。</p>
<p><code>npm init</code>を実行した時には生成されませんが、このnpmパッケージをインストールしたり、変更、削除などの操作を行うと生成されます。</p>
<h2>npmユーザー登録</h2>
<p>npm公式ページで登録することができます。
お名前とメールアドレス、パスワードを入力した後、加入したメールアドレスに本人認証メールが届くので承認したらnpmパッケージをデプロイすることができます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.npmjs.com%2F" title="npm" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.npmjs.com/">www.npmjs.com</a></cite></p>
<h2>npmパッケージ開発</h2>
<h3>npm 初期化</h3>
<p>プロジェクトのフォルダ(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>)を作成してコマンドを叩きます。</p>
<pre class="code" data-lang="" data-unlink>npm init -y</pre>
<p>package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>が生成されるので下記のように設定を行います。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">@[npmid]/[パッケージ名]</span>", <span class="synError">// <-追加</span>
"<span class="synStatement">version</span>": "<span class="synConstant">1.0.0</span>",
"<span class="synStatement">description</span>": "<span class="synConstant">どんなパッケージのかの説明</span>", <span class="synError">// <-追加</span>
"<span class="synStatement">main</span>": "<span class="synConstant">index.js</span>",
"<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
"<span class="synStatement">test</span>": "<span class="synConstant">echo </span><span class="synSpecial">\"</span><span class="synConstant">Error: no test specified</span><span class="synSpecial">\"</span><span class="synConstant"> && exit 1</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">repository</span>": <span class="synSpecial">{</span> <span class="synError">// <-追加</span>
"<span class="synStatement">type</span>": "<span class="synConstant">git</span>",
"<span class="synStatement">url</span>": "<span class="synConstant">リポジトリのURL</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">bugs</span>": <span class="synSpecial">{</span> <span class="synError">// <-追加</span>
"<span class="synStatement">url</span>": "<span class="synConstant">リポジトリのissuesのURL</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">homepage</span>": "<span class="synConstant">リポジトリのreadmeURL</span>", <span class="synError">// <-追加</span>
"<span class="synStatement">keywords</span>": <span class="synSpecial">[</span>
"<span class="synConstant">キーワード</span>", <span class="synError">// <-追加</span>
"<span class="synError">keyword</span>" <span class="synError">// <-追加</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">author</span>": "<span class="synConstant">開発者</span>", <span class="synError">// <-追加</span>
"<span class="synStatement">license</span>": "<span class="synError">MIT</span>" <span class="synError">// <-変更</span>
<span class="synSpecial">}</span>
</pre>
<p>注意点としては<code>license</code>の設定ですが、自分は<code>MIT</code>によくします。
<code>MIT</code>とは、「コードは好きに使っても良いけど、公開されたコードを使って何か問題が起きた場合、作者は責任とらないよ」というlicenseで、
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>には<code>MIT</code>ライセンスをよく使うらしいです。 他にもいろいろなlicenseがありますので調べてみてください。</p>
<hr />
<h3>npmパッケージ作成</h3>
<p>rootにindex.jsを作成します。
<br>
下記は簡単挨拶文を出力する簡単なサンプルコードです。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">export</span> <span class="synStatement">class</span> Test <span class="synIdentifier">{</span>
constructor(name) <span class="synIdentifier">{</span>
<span class="synIdentifier">this</span>.name = name
<span class="synIdentifier">}</span>
action() <span class="synIdentifier">{</span>
<span class="synStatement">return</span> `Hello $<span class="synIdentifier">{this</span>.name<span class="synIdentifier">}</span>`
<span class="synIdentifier">}</span>
<span class="synIdentifier">}</span>
</pre>
<p>その他最低限必要なファイルを追加してモジュールのアップロードの準備を完了します。</p>
<pre class="code" data-lang="" data-unlink>root/
├── index.js
├── .gitignore
├── .npmignore
├── README.md
├── LICENSE
└── package.json</pre>
<h3>npmログイン</h3>
<p>ログインは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リのrootからコマンドを叩きます。</p>
<pre class="code" data-lang="" data-unlink>> npm login
Username: 登録したID
Password:
Email: (this IS public) 登録したメールアドレス
Logged in as youngsoohan on https://registry.npmjs.org/.</pre>
<p>ログインが成功されたらアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンが発行されコマンドでデプロイすることができます。
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンはマイページで確認することができます。</p>
<h3>npm デプロイ</h3>
<p>デプロイは上記の<code>package.json</code>に記載したnameがパッケージ名になります。
<br>
<b>約1,688,642</b>パッケージの中から被らないパッケージ名で公開しなければいけません。<br>
<br>
そのため<code>@userid</code>を頭につけると被ることはないので一般的には<code>@userid</code>をつけてデプロイをすることが多いです。</p>
<p>例)<code>@npmid/modal</code>のような感じです。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">name</span>": "@<span class="synSpecial">[</span><span class="synError">npmid</span><span class="synSpecial">]</span>/<span class="synSpecial">[</span>パッケージ名<span class="synSpecial">]</span>"
</pre>
<p>バージョンも設定する必要があり、<code>npm init -y</code>の時、defaultで1.0.0が記されます。
<br>
自分は正式のリリースとして問題なければ1.0.0でアップを行い、その前までの開発段階では0.0.1から設定して公開するようにしてます。
<br>
今回はサンプルとして公開し後ほど削除するので1.0.0で公開しました。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">version</span>": "<span class="synConstant">1.0.0</span>",
</pre>
<p>公開コマンド(下記は無料プランのコマンドです。)</p>
<pre class="code" data-lang="" data-unlink>npm publish --access public</pre>
<p>npmのマイページに接続したらちゃんと公開されています!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20210728/20210728140437.png" alt="f:id:thunder_fury:20210728140437p:plain" width="1200" height="471" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>npmのデプロイ完了しましたー🔥</p>
<h3>公開したパッケージ使ってみる</h3>
<pre class="code" data-lang="" data-unlink>npm i @npmid/test</pre>
<pre class="code" data-lang="" data-unlink>import { Test } '@npmid/test'
const test = new Test(`npm`)
console.log(test.action())</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/thunder_fury/20210728/20210728180556.png" alt="f:id:thunder_fury:20210728180556p:plain" width="662" height="156" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>npmにちゃんと挨拶をしていますので問題なく公開はされているようです。</p>
<h3>npm パッケージ削除</h3>
<p>注意点としては最後に消したいバージョンを書かないといけないのと、時間過ぎてしまうと自分で消すことができずnpmに問い合わせをしないといけないようなので必要でなければすぐ消しましょう⚡️。</p>
<pre class="code" data-lang="" data-unlink>npm unpublish @userid/test@1.0.0</pre>
<h2>最後に</h2>
<p>今回の内容はサンプルとしての内容ですが実際にはフォームのバリデーションのパッケージを開発し公開しました。
初めて開発をして色々調べながらですがとても勉強になりました。
<br>
実際公開したパッケージをどんどんプロジェクトに導入し<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>削減を目指して行きたいと思っております。</p>
<hr />
<p>最後になりますが、Wizではエンジニアを募集中です!</p>
<p>興味のある方は是非覗いてみてください!↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
thunder_fury
【Laravel】ローカルスコープから考える要求と意図
hatenablog://entry/26006613791512681
2021-07-29T16:08:07+09:00
2021-07-29T16:08:07+09:00 Laravelのローカルスコープをとおして、「要求」を「意図」に変換しその「意図」に名付けをおこなうという、プログラミングそのものを考える。
<p>こんにちは。バックエンドエンジニアの河内です。</p>
<p>LaravelのEloquent ORMに<a href="https://laravel.com/docs/8.x/eloquent#query-scopes">クエリスコープ</a>という機能があります。
その中に、<a href="https://laravel.com/docs/8.x/eloquent#local-scopes">ローカルスコープ</a>という機能があって、それを使うと頻繁に利用するクエリ条件を1箇所にアクセスしやすい形でまとめることができます。</p>
<p>このローカルスコープを使うと、コードが非常に読み下しやすくなるんですね。それはなぜなんだろう、というお話です。</p>
<h1>ローカルスコープ使い方</h1>
<p>公式からの抜粋ですが、たとえば「人気のあるアクティブユーザー」だけをDBから取得したい場合、以下のように「人気のある」スコープ(=scopePopular)と「アクティブ」スコープ(=scopeActive)を用意します。</p>
<p>ここで想定されている仕様については、「人気のある」とは「票数が100より大きい」であり、「アクティブ」とは「アクティブフラグが立っている」…と読み取れます。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synType">namespace</span> App\Models;
<span class="synPreProc">use</span> Illuminate\Database\Eloquent\Model;
<span class="synType">class</span> User <span class="synType">extends</span> Model
<span class="synSpecial">{</span>
<span class="synComment">/**</span>
<span class="synComment"> * Scope a query to only include popular users.</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">\Illuminate\Database\Eloquent\Builder $query</span>
<span class="synComment"> * </span><span class="synPreProc">@return </span><span class="synComment">\Illuminate\Database\Eloquent\Builder</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> scopePopular<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">query</span><span class="synSpecial">)</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">query</span><span class="synType">-></span>where<span class="synSpecial">(</span><span class="synConstant">'votes'</span>, <span class="synConstant">'>'</span>, <span class="synConstant">100</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synComment">/**</span>
<span class="synComment"> * Scope a query to only include active users.</span>
<span class="synComment"> *</span>
<span class="synComment"> * </span><span class="synPreProc">@param </span><span class="synComment">\Illuminate\Database\Eloquent\Builder $query</span>
<span class="synComment"> * </span><span class="synPreProc">@return </span><span class="synComment">\Illuminate\Database\Eloquent\Builder</span>
<span class="synComment"> */</span>
<span class="synType">public</span> <span class="synPreProc">function</span> scopeActive<span class="synSpecial">(</span><span class="synStatement">$</span><span class="synIdentifier">query</span><span class="synSpecial">)</span>
<span class="synSpecial">{</span>
<span class="synStatement">return</span> <span class="synStatement">$</span><span class="synIdentifier">query</span><span class="synType">-></span>where<span class="synSpecial">(</span><span class="synConstant">'active'</span>, <span class="synConstant">1</span><span class="synSpecial">)</span>;
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<p>これらのスコープを利用するには、以下のように書きます(チェーンできます)。利用時に、scope<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%EC%A5%D5%A5%A3%A5%C3%A5%AF%A5%B9">プレフィックス</a>は不要で、scopeを取り去ったあとの単語を小文字で呼び出します。</p>
<ul>
<li>scopePopular → popular</li>
<li>scopeActive → active</li>
</ul>
<p>ですね。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
:
<span class="synStatement">$</span><span class="synIdentifier">users</span> <span class="synStatement">=</span> User<span class="synStatement">::</span>popular<span class="synSpecial">()</span><span class="synType">-></span>active<span class="synSpecial">()</span>;
</pre>
<p>逆に、スコープを利用しないと以下のような同じような条件を何度も書くことになります。</p>
<pre class="code lang-php" data-lang="php" data-unlink><span class="synSpecial"><?php</span>
<span class="synStatement">:</span>
<span class="synStatement">$</span><span class="synIdentifier">users</span> <span class="synStatement">=</span> User<span class="synStatement">::</span>where<span class="synSpecial">(</span><span class="synConstant">'votes'</span>, <span class="synConstant">'>'</span>, <span class="synConstant">100</span><span class="synSpecial">)</span><span class="synType">-></span>where<span class="synSpecial">(</span><span class="synConstant">'active'</span>, <span class="synConstant">1</span><span class="synSpecial">)</span>;
</pre>
<p>たとえば、「人気」の基準が、「票数が200より大きい」という仕様に変更になった場合、個別に書いて回った<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を書き換える旅が始まってしまいます。</p>
<p>処理をまとめるのはプログラムの基本ですが、スコープで条件が書かれていると、読み下しやすいのもポイントです。</p>
<h1>要求と意図</h1>
<p>なぜ読み下しやすいのでしょうか。それは、たとえばscopePopularについていえば、「票数が100より大きい」ユーザーを取得するという<b>論理的な「要求」が、「人気のある」という「意図」である、という位置付けに変換されたから</b>…だと考えます。</p>
<p>ここでの「要求」と「意図」という単語は、<a href="https://qiita.com/hirokidaichi/items/61ad129eae43771d0fc3">スケールする要求を支える仕様の「意図」と「直交性」</a>という記事の文脈から援用しました。</p>
<p>この「要求」と「意図」、わりと順不同でどちらが発端にもなるのかな、と思っています。</p>
<h2>意図発</h2>
<p>たとえば、「人気のある」ユーザーだけをトップページに表示したい、という「意図」発で依頼がやってくることも往々にしてあるかと思います。</p>
<p>この場合、「人気のある」とはどういうことなのか…ということを依頼者(DDD<a href="#f-3bda3dfe" name="fn-3bda3dfe" title="参照記事にもDDDの話が登場しますね">*1</a>の文脈では<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>エキスパート)と協議して「要求」を煮詰めることになります。</p>
<h2>要求発</h2>
<p>逆に、「要求」発の場合、まさに「意図」を見出だす必要があります。これは依頼者が「意図」を意識的に持っていることもあれば持っていないこともあり、持っていなかった場合、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DE">プログラマ</a>側が抽出する必要があります。</p>
<p>「サイトのトップページに『票数が100より大きい』ユーザーの降順に表示したいんだよね」という要求があったときに、「つまり、『人気のある』ユーザーですよね」という意図を見出だし名付ける…ローカルスコープのメソッド名はまさに、その意図についての名付けの好例なのだと思います。</p>
<h1>まとめ</h1>
<p>Laravelのローカルスコープは、「要求」を「意図」に変換しその「意図」に名付けをおこなっている行為でした。</p>
<p>このローカルスコープの例に限らず、およそプログラミング自体がそういうものなのだ、ということは、DDDなどの設計思想に触れるにつれ痛感します。</p>
<p>また、「意図」の名前は、客観的な「要求」と違い、主観的なより生き生きとしたものになってくるかと思います<a href="#f-a5e84d79" name="fn-a5e84d79" title="リーダブルコードでいえば「カラフル」な単語で表現できないか、ということになるのかと思います">*2</a>。</p>
<p>そういう「意図」で編まれたプログラムは見通しのよいものになるのだと思っています。</p>
<h1>最後に</h1>
<p>Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
<div class="footnote">
<p class="footnote"><a href="#fn-3bda3dfe" name="f-3bda3dfe" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">参照記事にもDDDの話が登場しますね</span></p>
<p class="footnote"><a href="#fn-a5e84d79" name="f-a5e84d79" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">リーダブルコードでいえば「カラフル」な単語で表現できないか、ということになるのかと思います</span></p>
</div>
wz-tkch
Puppeteerを使ったツールの構成について
hatenablog://entry/26006613788433269
2021-07-26T17:41:31+09:00
2021-07-26T17:41:31+09:00 こんにちは、フロントエンドエンジニアの高橋です。 先日Puppeteerを使った業務効率化のツールを作成しました。 その時参考にさせていただいた記事と一緒にその構成についてまとめたいと思います! Puppeteerをこれから触る方に少しでもお役に立てればと思います…! 何を作るのか あまりオープンにできないのですが、webでの操作を半自動化するツールです! 作りたいものの流れとしては以下の通りです。 必要な情報をフォームで入力 データをどこかにためておく 入力した値を元にPuppeteerを動かす 結果をSlackで通知する では構成についてまとめていきたいと思います。 構成 早速ですが、以下…
<p>こんにちは、フロントエンドエンジニアの高橋です。</p>
<p>先日Puppeteerを使った業務効率化のツールを作成しました。
その時参考にさせていただいた記事と一緒にその構成についてまとめたいと思います!</p>
<p>Puppeteerをこれから触る方に少しでもお役に立てればと思います…!</p>
<h1>何を作るのか</h1>
<p>あまりオープンにできないのですが、webでの操作を半自動化するツールです!
作りたいものの流れとしては以下の通りです。</p>
<ul>
<li>必要な情報をフォームで入力</li>
<li>データをどこかにためておく</li>
<li>入力した値を元にPuppeteerを動かす</li>
<li>結果をSlackで通知する</li>
</ul>
<p>では構成についてまとめていきたいと思います。</p>
<h1>構成</h1>
<p>早速ですが、以下のような構成になっております!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173754.png" alt="アーキテクチャ" width="1042" height="932" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>順番に何をやっているかをまとめていきます。</p>
<h3>①フォームからGASを動かす</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173818.png" alt="1" width="490" height="250" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h5>フォーム作成</h5>
<p>まずは必要情報を書き込むためのフォームを作成します。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a> Formを使うことも考えましたが、データを加工する必要がありました。
次のGAS上やCloudFunctions上でもできますが処理が複雑になってしまうため、送信前に加工することにしました。</p>
<p>個人的にReactが好きなのでReactで作成し、フォームはReact Hook Formで作成しました。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>削減のために<a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>は書かず<a class="keyword" href="http://d.hatena.ne.jp/keyword/Chakra">Chakra</a> UIを使用しています。</p>
<p>React Hook Form と <a class="keyword" href="http://d.hatena.ne.jp/keyword/Chakra">Chakra</a> UIを組み合わせると本当に爆速でフォームが出来上がります。実は<a href="https://chakra-ui.com/guides/integrations/with-hook-form">公式</a>でもサンプルが載っていますので気になる方はぜひご確認ください!</p>
<h5>GAS連携</h5>
<p>おなじみのGASで<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>を公開します。
詳細は以下の記事を参照ください!</p>
<h5>①で参考にした/使用したサイト</h5>
<p><a href="https://react-hook-form.com/form-builder">React Hook Form formbuilder</a></p>
<p><a href="https://chakra-ui.com/docs/getting-started:embed:cite">Chakra UI公式</a></p>
<p><a href="https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba">今から10分ではじめる Google Apps Script(GAS) で Web API公開</a></p>
<h3>②GASから<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%EC%A5%C3%A5%C9%A5%B7%A1%BC%A5%C8">スプレッドシート</a>に保存</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173838.png" alt="2" width="224" height="516" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>GASの<a href="https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app">SpreadsheetAppクラス</a>を使用してSpreadsheetを更新します。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">// シート取得</span>
<span class="synStatement">const</span> ss = SpreadsheetApp.openById(SpreadsheetApp.getActiveSpreadsheet().getId());
<span class="synComment">// dataというシートを取得</span>
<span class="synStatement">const</span> dataSheet = ss.getSheetByName(<span class="synConstant">"data"</span>);
<span class="synComment">// データ(res)をシートに入れる</span>
await dataSheet.appendRow(<span class="synIdentifier">[</span>res<span class="synIdentifier">]</span>);
</pre>
<h5>②で参考にしたサイト</h5>
<p><a href="https://qiita.com/negito6/items/c64a7a8589faaffcfdcf">Google Apps Script で Spreadsheet にアクセスする方法まとめ</a></p>
<p><a href="https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app">Spreadsheet-app</a></p>
<h3>③GASからCloudFunctionsに保存している関数を呼び出す</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173855.png" alt="3" width="486" height="252" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こちらも<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>連携できますので、上記のシート追加の後にCloudFunctionsの<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>を呼び出します。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synStatement">const</span> url = <span class="synConstant">'CloudFunctionsのAPI URL'</span>
<span class="synStatement">const</span> result = await UrlFetchApp.fetch(url, option)
</pre>
<p>GAS上ではHTTPリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト用にUrlFetchAppというClassが用意されているので、そちらを使用します。</p>
<h5>③で参考にしたサイト</h5>
<p><a href="https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app">url-fetch-app</a></p>
<h3>④⑤CloudFunctionsを<a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>アクセスコネクタ経由で動かす</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173914.png" alt="4-7" width="228" height="796" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h5>関数を用意する</h5>
<p>一番大変な部分ですが、全てを語ると尽きないので以下の参照記事をご確認いただければと思います!</p>
<h5><a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>アクセスコネクタ設定をする</h5>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>とは<code>Virtual Private Cloud</code>の略で、プライベート仮想ネットワーク空間のことです。アクセスコネクタを設定をすることで<a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>に接続することができます。</p>
<p>こちらを使用した経緯は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/IP%A5%A2%A5%C9%A5%EC%A5%B9">IPアドレス</a>の固定のためです。
今回Puppeteerで操作するサイトが<a class="keyword" href="http://d.hatena.ne.jp/keyword/IP%A5%A2%A5%C9%A5%EC%A5%B9">IPアドレス</a>で制限をかけていたりするので、こちらの設定が必須でした。(これに気づけずかなり時間を浪費しました。。)</p>
<p>料金もある程度かかってしまいますので、事前に料金も試算しておいた方がいいかと思います!</p>
<h5>④⑤で参考にしたサイト</h5>
<ul>
<li>Puppeteer</li>
</ul>
<p><a href="https://github.com/puppeteer/puppeteer">公式</a></p>
<p><a href="https://github.com/puppeteer/puppeteer/issues/5674">バージョンによってエラーが出る</a></p>
<p><a href="https://github.com/puppeteer/puppeteer/issues/1922">ヘッドレスモード時のみエラーが出てしまう時</a></p>
<p><a href="https://dev.to/sonyarianto/practical-puppeteer-how-to-upload-a-file-programatically-4nm4">imageUploadについて</a></p>
<ul>
<li>Cloud Functions</li>
</ul>
<p><a href="https://cloud.google.com/functions/docs/concepts/exec?hl=ja">CloudFunctions実行環境</a></p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/VPC">VPC</a>アクセスコネクタ設定</li>
</ul>
<p><a href="https://cloud.google.com/vpc/pricing?hl=ja#serverless-vpc-pricing">公式 料金について</a></p>
<p><a href="https://www.isoroot.jp/blog/3238/">VPCアクセスコネクタ 設定方法</a></p>
<h3>⑥⑦結果(成功 or 失敗)を返す</h3>
<p>最後にSlackで結果を通知する必要があるので、関数内で処理を分ける必要があります。
今回はtry~catchで実装しました。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synStatement">try</span> <span class="synIdentifier">{</span>
<span class="synStatement">const</span> browser = await puppeteer.launch(options)
<span class="synStatement">const</span> page = await browser.newPage()
:
await browser.close()
<span class="synComment">// 完了時の処理</span>
<span class="synIdentifier">}</span><span class="synStatement">catch</span>(e)<span class="synIdentifier">{</span>
<span class="synComment">//例外が発生した場合の処理</span>
<span class="synIdentifier">}</span>
</pre>
<h5>⑥⑦で参考にしたサイト</h5>
<p><a href="https://qiita.com/tpyamamoto/items/35a8dc4e0ffc310cd65d">Pupeteerを使うときは try~finallyでbrowserをcloseする。</a></p>
<h3>⑧結果をSlackで通知</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sotq17/20210726/20210726173940.png" alt="8" width="496" height="256" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>最後に<a href="https://api.slack.com/">Slackとの連携</a>をしていきます。
登録の流れは下記の記事を参考にしていただければと思います。</p>
<p>関数内からSlackへメッセージを送るには<code>@slack/web-api</code>というライブラリが便利なのでそちらを使用します。</p>
<p>以下のように書けばメッセージを送ることができます。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> <span class="synIdentifier">{</span>WebClient<span class="synIdentifier">}</span> = require(<span class="synConstant">"@slack/web-api"</span>);
:
<span class="synStatement">const</span> slackClient = <span class="synStatement">new</span> WebClient(<span class="synConstant">'slackのtoken'</span>);
<span class="synStatement">const</span> slackParams = <span class="synIdentifier">{</span>
channel: <span class="synConstant">'#チャンネル名'</span>,
text: <span class="synConstant">'テキスト'</span>
<span class="synIdentifier">}</span>;
await slackClient.chat.postMessage(slackParamsFinish);
</pre>
<p>かなり簡単に書くことができました!
こちらを先程のtry~catchに反映させていきます。</p>
<pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synStatement">try</span> <span class="synIdentifier">{</span>
<span class="synStatement">const</span> browser = await puppeteer.launch(options)
<span class="synStatement">const</span> page = await browser.newPage()
await browser.close()
<span class="synComment">// 処理完了メッセージ送る</span>
<span class="synStatement">const</span> slackParamsFinish = <span class="synIdentifier">{</span>
channel: <span class="synConstant">'#チャンネル名'</span>,
text: <span class="synConstant">'テキスト'</span>
<span class="synIdentifier">}</span>;
await slackClient.chat.postMessage(slackParamsFinish);
<span class="synIdentifier">}</span><span class="synStatement">catch</span>(e)<span class="synIdentifier">{</span>
<span class="synComment">//例外が発生した場合の処理</span>
<span class="synStatement">const</span> slackParamsError = <span class="synIdentifier">{</span>
channel: <span class="synConstant">'#チャンネル名'</span>,
text: <span class="synConstant">'テキスト'</span>
<span class="synIdentifier">}</span>;
await slackClient.chat.postMessage(slackParamsError);
<span class="synIdentifier">}</span>
</pre>
<p>こちらでSlackへの連携も完了です!</p>
<h5>⑧で参考にしたサイト</h5>
<p><a href="https://qiita.com/kou_pg_0131/items/56dd81f2f4716ca292ef">Slack APIと連携する</a></p>
<p><a href="https://qiita.com/kou_pg_0131/items/31728a4b7b7133198207">Slack APIを使用してメッセージを送信する</a></p>
<h1>最後に</h1>
<p>難しいツールではなかったのですが、初挑戦の部分も多く所々で苦戦していました。相談に乗っていただいたメンバーには感謝です。
構成に関しても改善できる点があると思うので、何か思うことがあればコメントいただけますと幸いです…!</p>
<p>現在Wizではエンジニアを募集中です。
興味のある方はぜひ覗いてみてください!↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
sotq17
リモート環境におけるエンジニアの新人研修と新人メンターの話
hatenablog://entry/26006613787044117
2021-07-19T11:43:16+09:00
2021-07-19T11:43:16+09:00 リモート環境下で行われたフロントエンドエンジニア新人研修について、その内容と振り返りに加えて、エンジニア歴約1年半の自分がメンターとして教育に携わった所感を記したいと思います。
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kdm012/20210716/20210716114128.jpg" alt="f:id:kdm012:20210716114128j:plain" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>こんにちは、フロントエンドエンジニア小玉です。<br/></p>
<p>本記事は、今年度4月〜6月までリモート環境下で行われたフロントエンドエンジニア新人研修について、<br/>
その内容と振り返りに加えて、エンジニア歴約1年半の自分がメンターとして教育に携わった所感を記したいと思います。</p>
<h2>研修概要</h2>
<p><b> 研修目的</b>:
<u><b>『<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>を伴わないサイトの運用ができるようなレベルになる』</b></u><br/>
<b>研修対象</b>:新卒社員1名(メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>)<br/>
<b>研修実施者</b>:メンター2人(うち一人が小玉) オブザーバー1名<br/>
<b>研修期間</b>:2021年4月〜6月<br/>
<b>研修場所</b>:oVice(オンラインオフィス)or GoogleMeet</p>
<h2>研修内容</h2>
<p>新卒社員が入社してからの大まかな研修の流れとして、以下のようになっています。</p>
<ol>
<li>会社概要研修</li>
<li>技術研修</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/OJT">OJT</a></li>
</ol>
<h3>会社概要研修</h3>
<p>こちらでは自分が所属している部署(エンジニア組織)が会社においてどのような役割を担っているのかを理解すること、また社会人としての基礎的なマナーを身につけることを目的としています。</p>
<h3>技術研修</h3>
<p>以下のような内容を座学と実践的な課題を交えて研修して行きます。</p>
<p><b>□Webの基礎知識<br/></b></p>
<ul>
<li>Webページが表示されるまでの仕組み</li>
</ul>
<p><b>□開発環境について<br/></b></p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>の設定</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/github">github</a>について</li>
<li>pug/stylus</li>
</ul>
<p><b>□フロントエンドの基礎知識<br/></b></p>
<ul>
<li>HTML(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%AF%A5%A2%A5%C3%A5%D7">マークアップ</a>/ペーパーコーディング)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>基礎</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>管理について</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/CSS">CSS</a>設計</li>
<li>LPコーディング</li>
<li>gitフロー</li>
</ul>
<p>また、技術研修の中ではスキルの習得に加えてフロントエンドとしての心構えや振る舞いについても研修します。</br>
エンジニアとして仕事をしていると、どうしてもわからない課題やエラーに直面し、思ったように作業が進まないことがあるかと思います。そんな時に上司やチームのメンバーに報告・連絡・相談することの大切さや時間管理の重要性などを研修します。</p>
<h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/OJT">OJT</a></h3>
<p>メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>に対しメンターが一人付き、メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>は実際の案件をメンターと一緒にこなしていきます。<br/>
その中では、他人の書いたコードを読み解くことができるようになることや、
複数人で1つのプロジェクトを運用できるようになることが求められます。</p>
<h2>研修の振り返り</h2>
<h3>リモート環境下での研修</h3>
<p>今回実施した研修では一度も実際にあって研修を行うことはなく、全てオンラインでの研修となり、<br/>
その中で以下のような点に注意するべきだと感じました。</p>
<ul>
<li>雑談の時間を積極的に取る</li>
<li>日々の振り返りシートなどを活用する</li>
</ul>
<p><b>【雑談の時間を積極的に取る】</b><br/>
顔の見えない研修となると、淡々と座学や実習を進めがちになってしまいます。<br/>
メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>がどのようなリアクションをとっているのかは声からでしか判断できないため、<br/>
仮に課題に詰まってしまった時にメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>が質問のしやすい雰囲気を作る必要があります。
<br/></p>
<p>私たちの研修の中では朝イチから研修にすぐ入るわけではなく、<br/>
仕事と関係のない雑談をしてから研修に入るようにしていました。</p>
<p><b>【日々の振り返りシートなどを活用する】</b><br/>
雑談を取り入れることに通ずる部分はありますが、<br/>
メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>の表情や仕草から課題への理解度は測りきれないのが正直なところです。<br/>
そこはメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>本人にアウトプットしてもらう仕組みが必要です。</p>
<p>私たちは、日々の研修後にメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>に振り返りシートを記載してもらい、それに対しメンターがコメントを返すようにしていました。そこから次の研修時不足分を補うようなカリキュラムに変更するという柔軟な対応ができたかと思います。</p>
<h3>メンター制度について</h3>
<p>今回の研修はメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>1人に対し、メンターが2人付く体制で実施しました。
初めて教育をする側の立場になり感じたこととして、『<b><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>チング < <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A1%BC%A5%C1%A5%F3">コーチン</a>グの意識</b>』が重要だと感じました。<br/>
どのような意図でコードを書いたのか、何に悩んでいるのか、どうしたらできるのか、しっかり<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D2%A5%A2%A5%EA">ヒアリ</a>ングし、すぐに回答を与えるのではなく、考える方法や何かヒントを与えることでメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>のさらなる理解が望めると思いました。</p>
<h2>まとめと今後について</h2>
<p>リモート環境下での研修内容と、その振り返りに加えメンターをやってみた所感をまとめてみました。<br/>
メン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>との密なコミュニケーションを取り<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>安全性を担保した研修が実施できるよう今後も施策を考えて行きたいと思います。<br/>
また、新卒社員を研修カリキュラムを事前に作成し研修をしっかり行うのが組織として今回が初めてだったこともありメン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C6%A5%A3%A1%BC">ティー</a>やメンターの評価制度も改善する余地があります。
引き続きみんながハッピーになれる研修、組織にできるようがんばります。</p>
<p>今回あげた研修内容とは別に、新人教育の一環としてリアルタイムコーディング大会を開いたりもしています。<br/>
詳細はこちら↓</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.012grp.co.jp%2Fentry%2Fteam-event-coding" title="コーディングで天下を目指せ! − 社内チームでの取り組み − - Wiz テックブログ " class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.012grp.co.jp/entry/team-event-coding">tech.012grp.co.jp</a></cite></p>
<p>Wizではエンジニアを募集しています。興味のある方、ぜひご覧ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcareers.012grp.co.jp%2Fengineer" title="Wiz for developer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://careers.012grp.co.jp/engineer">careers.012grp.co.jp</a></cite></p>
kdm012