tag:blogger.com,1999:blog-39914304454585352102024-03-16T10:10:49.349+09:00狛ログオフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.comBlogger232125tag:blogger.com,1999:blog-3991430445458535210.post-7289712527025792582023-07-25T15:20:00.002+09:002023-07-27T15:09:31.538+09:00iPhone(iOS16以降の端末)のブラウザでのinput[type="time"]のレイアウトをCSSで修正する。<p> </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj8UPI2b290RH9VXFtNWrWF-4npn-ozanoqyqykihM-XCK6qazFS0MPvVj3q6X9-1KVeNSIvHrpcDxIEmSW8viLjQE8LnoewP2fHIbsgQF5B_TQmxjm4NBkHCB1MAYsIlBdtmrzvDYTpXkLBvx8X-8fn_xbrTeL1uneTejJdvA8csiU1M83osNfLyXrfQHJ/s502/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99%E3%82%B5%E3%83%A0%E3%83%8D%E3%82%A4%E3%83%AB108.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="322" data-original-width="502" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj8UPI2b290RH9VXFtNWrWF-4npn-ozanoqyqykihM-XCK6qazFS0MPvVj3q6X9-1KVeNSIvHrpcDxIEmSW8viLjQE8LnoewP2fHIbsgQF5B_TQmxjm4NBkHCB1MAYsIlBdtmrzvDYTpXkLBvx8X-8fn_xbrTeL1uneTejJdvA8csiU1M83osNfLyXrfQHJ/s320/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99%E3%82%B5%E3%83%A0%E3%83%8D%E3%82%A4%E3%83%AB108.png" width="320" /></a></div><br /><p></p><p>こんにちは、オフィス狛 デザイン部のSatoです。</p>
<br />
<br />
<p>先日、モバイルで利用するWebシステムのHTMLを作成した際に、時間を入力する必要があるフォームが必要になりました。
<br />
そこで、input[type="time"]を使いました。</p>
<p>HTMLの作成後に、「iPhone端末で表示すると、時間入力欄(input[type="time"])の入力後の文字が上揃えになっているので、他のに入力欄と同じ縦中央揃えに修正してほしい」という報告をいただきました。<br />
指摘いただいた箇所を私の方でもiPhone端末で確認すると、時間入力欄(input[type="time"])だけで入力後の文字の行揃えも中央揃えになっていて、他の入力欄に合わせる為に文字揃えも修正が必要になってきました。</p><p>
</p><div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQspP455DG55LSTsC0R4IiWDqwES5nrkkH_Ma92Qp_FP1tZJbb1_v6pawzb7y9kmUcVBLI_qSCoAek4TOnlTeOxNf_nis0b8KIyJhuWitSwPcZ7qmffCA8kUlEkE_Qj2jAo3QhPRWQSNkgTeLU7pRWd-Ys3e3LCNZMBjajO-mQOCkv8sZ3ka3hHH-5zLGu/s1280/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99%E7%94%BB%E5%83%8F202307241.png" style="display: block; padding: 1em 0px; text-align: center;"><img alt="" border="0" data-original-height="563" data-original-width="1280" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQspP455DG55LSTsC0R4IiWDqwES5nrkkH_Ma92Qp_FP1tZJbb1_v6pawzb7y9kmUcVBLI_qSCoAek4TOnlTeOxNf_nis0b8KIyJhuWitSwPcZ7qmffCA8kUlEkE_Qj2jAo3QhPRWQSNkgTeLU7pRWd-Ys3e3LCNZMBjajO-mQOCkv8sZ3ka3hHH-5zLGu/s600/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99%E7%94%BB%E5%83%8F202307241.png" width="600" /></a></div>
<br />
<p>そこで時間を入力するinputにclassをつけ、そのclassで余白をつけ行揃えを左に修正しました。<br />
実機で修正ができていることを確認!これで解決!のはずが、<br />
「iOS16以降の端末のブラウザでは不具合が修正されていません!」という報告をいただきとても困りました😢</p>
<p>私が修正確認に利用した端末のOSバージョンはiOS15でしたので、どうやらiOS16以降iPhone端末のブラウザではinputにclassをつけるだけでは修正が良い感じに反映されていないようです😢💦<br />
非常にジミ〜な不具合ですが、解決に時間が掛かった為解決法を共有いたします。</p>
<br />
<br />
<p>修正には、「<span style="color: #ffa400;">::-webkit-date-and-time-value</span>」という疑似要素を使います。<br />
これをinput[type="time"]につけてあげると、iOSで修正適用されなかった修正がうまく適用されます。<br />
今回の場合は、下記のような記載になります👇</p>
<pre class="prettyprint lang-css">input[type="time"]::-webkit-date-and-time-value {
padding-top: 10px;
text-align: left !important;
}
</pre>
<p>「::-webkit-date-and-time-value」の擬似要素を利用する方法は、input[type="date"]にCSSが上手く反映されない場合でも使えるようです。<br /><a href="https://developer.apple.com/forums/thread/691305">参考:https://developer.apple.com/forums/thread/691305</a><br />
iOSのSafariだけ日付の入力欄のデザインが他のinputや他ブラウザと違って困った際は、是非お試しください!</p>
<br />
<br />
<p>デモを用意しましたので、良ければこちらもiOS16以降のiPhone端末で表示して確認してみてください👇</p>
<p class="codepen" data-height="300" data-slug-hash="bGQMVjL" data-user="officekoma_sato" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/officekoma_sato/pen/bGQMVjL">
iOS端末でのtype="time"の文字のレイアウトを他のinputと同じにする</a> by sato (<a href="https://codepen.io/officekoma_sato">@officekoma_sato</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>※端末固有の不具合のため、デベロッパーツールなどの開発ツールのモバイルモードで表示した場合は差異がわからないかと思います。<br />
iOS16以降のiPhone端末のブラウザでお試しください!</p>
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?lang=css&skin=sunburst"></script>
<p></p><p></p>オフィス狛 デザイン部 Satohttp://www.blogger.com/profile/09225615773755838034noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-50677761580867656932023-07-25T14:39:00.002+09:002023-07-25T16:38:42.120+09:00【Laravel】GA4を使用して、Google Analyticsの値を取得する方法。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipskyguczA8XecgcPoiC9d4gsVQgG6ZVZRjrvDLAzX7-2EKPwtttJxezFivl5yzWpo6Sow6UjRkbaZMVojL3uXR6eB1TK9aHNTevOvcYpbYIodyKpe_wlfG4et50PcekOvtP67jDSKIGBorcDn1NovQjE5rBYCdUe-kZuKdvyFJ5CwvSzAze0jotCTgpaK/s2048/AdobeStock_276356125.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1366" data-original-width="2048" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipskyguczA8XecgcPoiC9d4gsVQgG6ZVZRjrvDLAzX7-2EKPwtttJxezFivl5yzWpo6Sow6UjRkbaZMVojL3uXR6eB1TK9aHNTevOvcYpbYIodyKpe_wlfG4et50PcekOvtP67jDSKIGBorcDn1NovQjE5rBYCdUe-kZuKdvyFJ5CwvSzAze0jotCTgpaK/s320/AdobeStock_276356125.jpeg"/></a></div>
<br>
オフィス狛 技術部の mmm(むー)です。<br><br>
突然ですが、Google Analytics を使用していますか?<br>
弊社では、Laravel のプロジェクトにて、Google Analytics で取得した値を集計する処理があります。先日、Google Analytics の古いバージョンであるUA(Universal Analytics)から、GA4(Google Analytics 4)へ移行しましたので、その時の備忘録を残しておこうと思います。<br><br>
※UA(Universal Analytics)は2023年7月1日にサービスが終了しました。<br>
(参考:<a href="https://support.google.com/analytics/answer/2790010?hl=ja">ユニバーサル アナリティクスについて</a>)<br>
<h2>前提条件</h2>
Google Analytics 4 の設定が完了していること。<br><br>
<div style="font-weight: bold">■ Laravel バージョン</div>
<pre><code>$ php artisan -V
Laravel Framework 8.27.0
</code></pre><br>
<div style="font-weight: bold">■ Comporser バージョン</div>
<pre><code>$ composer --version
Composer version 2.4.1 2022-08-20 11:44:50
</code></pre><br>
<h2>google/analytics-data のインストール</h2>
google/analytics-data をインストールします。
<pre><code>composer require google/analytics-data
</code></pre><br>
Google Analytics API を実行するためのコードを記載します。
<pre><code>use Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
use Google\Analytics\Data\V1beta\DateRange;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;
use Google\Analytics\Data\V1beta\OrderBy;
use Google\Analytics\Data\V1beta\OrderBy\MetricOrderBy;
// GAコンソール画面にてサービスアカウントの認証情報を確認し、取得してください。
// ファイル名と格納場所は一例となりますので、ご自身の環境に合わせて値を修正してください。
$jsonPath = storage_path('app/analytics/service-account-credentials.json');
// 認証用のライブラリで getenv が使用されているため、putenv の使用が必須のようです。
putenv("GOOGLE_APPLICATION_CREDENTIALS=".$jsonPath);
// GAのコンソール画面にてプロパティIDの値を確認し、ご自身の環境に合わせて値を修正してください。
$propertyId = 12345;
$client = new BetaAnalyticsDataClient();
// Google Analytics API を実行します。(GA4)
$response = $client->runReport([
'property' => "properties/{$propertyId}",
// 取得期間
'dateRanges' => [
new DateRange([
'start_date' => '2023-07-01',
'end_date' => '2023-07-02',
])
],
// ディメンション
'dimensions' => [new Dimension(['name' => 'pagePath'])],
// 指標
'metrics' => [new Metric(['name' => 'screenPageViews'])],
// ソート
'orderBys' => [
new OrderBy([
'metric' => new MetricOrderBy(
[
'metric_name' => 'screenPageViews'
]
),
'desc' => true
])
],
]);
// 取得した値を処理します。例となりますので、具体的な処理については省略します。
foreach ($response->getRows() as $row) {
// ディメンションの値
foreach ($row->getDimensionValues() as $v) {
$pagePath = $v->getValue();
}
// 指標の値
foreach ($row->getMetricValues() as $v) {
$screenPageViews = $v->getValue();
}
}
</code></pre><br>
<div style="font-weight: bold">■補足(コードについて)</div>
service-account-credentials.json には以下のような値の設定が必要となります。ご自身の環境に合わせて値を設定してください。
<pre><code>// app/analytics/service-account-credentials.json
{
"type": "service_account",
"project_id": "testapi",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
"client_id": "1234",
"auth_uri": "...",
"token_uri": "...",
"auth_provider_x509_cert_url": "...",
"client_x509_cert_url": "..."
}
</code></pre><br>
ディメンションと指標は、公式ページを参考にご自身が必要なものを設定してください。上記の例では、各ページごとのビューを取得しています。<br>
(参考:<a href="https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema?hl=ja">API のディメンションと指標</a> )<br><br>
<div style="font-weight: bold">■補足(エラーが発生した場合)</div>
この修正をおこなっている時に遭遇したエラーについて、記載します。<br><br>
<div style="font-weight: bold">1. Google Analytics Data API が無効になっているエラー</div>
下記エラーが表示されている場合、Google Analytics Data API を使用したことがなく、無効になっていることが原因ですので、メッセージに記載されている URL にアクセスして API を有効にしてください。
(「..12345..」の部分は本来プロジェクトのIDとなります)
<pre><code>Google Analytics Data API has not been used in project ..12345.. before or it is disabled.
Enable it by visiting https://console.developers.google.com/apis/api/analyticsdata.googleapis.com/overview?project=..12345.. then retry.
If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
</code></pre><br>
<div style="font-weight: bold">2. bcmath がインストールされていないエラー</div>
下記エラーが表示されている場合、bcmath が入っていないことが原因ですので、サーバーにてインストールを行なってください。(Apache を使用している場合、リロードも行なってください。)
<pre><code>Call to undefined function Google\Protobuf\Internal\bccomp()
</code></pre><br>
<div style="font-weight: bold">■参考サイト</div>
・<a href="https://cloud.google.com/php/docs/reference/analytics-data/latest">Google Analytics Data for PHP</a><br>
・<a href="https://github.com/googleapis/google-cloud-php/blob/main/AUTHENTICATION.md">google-cloud-php</a><br>
・<a href="https://developers.google.com/analytics/devguides/reporting/data/v1?hl=ja">Analytics Data API の概要</a><br><br>
以上となります。<br>
参考にして頂ければ幸いです。<br><br>
<!-- 編集禁止(ここから) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/darcula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- 編集禁止(ここまで) -->オフィス狛 技術部 mmmhttp://www.blogger.com/profile/02929539829077086029noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-75930269957651367702023-02-27T13:20:00.002+09:002023-02-28T11:51:36.284+09:002023年オフィス狛の年賀状の制作秘話。<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8gX8zLp1dYbkSGOpVHxvJkivVGBxgwfw3Fli-3mL_pH_Ny-CORwu_Zg6dIjWGPpsbEm3kabPiicAOM_DH5X9vej0Max-cOugqpzO-cfq53j2vmXC71APkZHQuM2EivvfOKnrQCD9TwPtvEwoZpnqavvKejDffdpj0WTV2-pHci2cwfYLYkznL7VCLzg/s750/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201@3x.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="480" data-original-width="750" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8gX8zLp1dYbkSGOpVHxvJkivVGBxgwfw3Fli-3mL_pH_Ny-CORwu_Zg6dIjWGPpsbEm3kabPiicAOM_DH5X9vej0Max-cOugqpzO-cfq53j2vmXC71APkZHQuM2EivvfOKnrQCD9TwPtvEwoZpnqavvKejDffdpj0WTV2-pHci2cwfYLYkznL7VCLzg/s320/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201@3x.png" width="320" /></a></div><p><br /></p><p>出遅れたオフィス狛 デザイン部のSatoです💦<br />
毎年恒例(?)の年賀状の記事を出さないまま2月になってしまいました!<br />
言い訳になってしまいますが、オフィス狛社員全員私含め2023年1月から2月にかけ色々とてんてこまいでした😭💦<br />
遅れてしまいましたが、2023年もどうぞよろしくお願いいたします!</p>
<p>
2月も終わりそうですが、デザイン部の年明け初めての記事ですので去年同様、今年の年賀状のおはなしをしようと思います。<br />
<b>↓去年の年賀状の記事はこちら</b>
<iframe class="hatenablogcard" frameborder="0" scrolling="no" src="https://hatenablog-parts.com/embed?url=https://blog.officekoma.co.jp/2022/01/2022.html" style="height: 175px; max-width: 680px; width: 100%;" title="%title%"></iframe>
</p><p><br /></p>
<h2 style="text-align: center;"><b>2023年の年賀状はこちら!</b></h2>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzffjMoHg8LY1av90UuxIHzEB_dYwKBqLdrpsyDPahw1EGa3KYkMRK_DPMHzbqHOGr93giS16L1fohqi3RLb7eRZP6MolLSDweu5SskUJeSHFDXrdr4ZtlNZp9s0wRTwVNJXrUV_YgMrg2o7QWKhLAHoHf8x8SP-Dl4dbL0TbSMgh8Z-1GUcRPw2aM7Q/s1680/2023%E5%B9%B4%E5%B9%B4%E8%B3%80%E7%8A%B6@2x.png" style="display: block; padding: 1em 0px; text-align: center;"><img alt="" border="0" data-original-height="1680" data-original-width="1134" height="600" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzffjMoHg8LY1av90UuxIHzEB_dYwKBqLdrpsyDPahw1EGa3KYkMRK_DPMHzbqHOGr93giS16L1fohqi3RLb7eRZP6MolLSDweu5SskUJeSHFDXrdr4ZtlNZp9s0wRTwVNJXrUV_YgMrg2o7QWKhLAHoHf8x8SP-Dl4dbL0TbSMgh8Z-1GUcRPw2aM7Q/s600/2023%E5%B9%B4%E5%B9%B4%E8%B3%80%E7%8A%B6@2x.png" /></a></div>
<p>
例年通り弊社サイトやSNSなどのアイコンなどで登場している<a href="https://www.officekoma.co.jp/company/komachan.html">こまちゃんとここまちゃん</a>のイラストです。<br />2023年の干支は卯(うさぎ)!ウサギといえば月でお餅つきをしているウサギ!<br />
……ということでこまちゃんにはウサギさんとお餅つきにチャレンジしてもらいました!<br />
小柄なここまちゃんに餅つきはちょっと危険かな?杵で潰れちゃいそう?と思ったので、添えるだけになりました。
</p><p><br /></p><p><br /></p>
<p>そして、今年の年賀状のラフです。</p>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijUjeJpN-afNvlC5iLc_ZxlIaGIYMWUsZ2u3uv8ISS6JCpNV0S0HrrMC7MAWs02EZufnXs4CKDBGsYgGcx4tQt7xR9I2DGSbHBNSjCI4vnc6_-liw8beGd7QFHgvyHqabJP_37v_pAcack12jOmnxgZavV2ORUPT-C0ppntIFG_AFlP5wv80x6fj2GTQ/s1171/2023%E5%B9%B4%E8%B3%80%E7%8A%B6.png" style="display: block; padding: 1em 0px; text-align: center;"><img alt="" border="0" data-original-height="424" data-original-width="1171" height="232" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijUjeJpN-afNvlC5iLc_ZxlIaGIYMWUsZ2u3uv8ISS6JCpNV0S0HrrMC7MAWs02EZufnXs4CKDBGsYgGcx4tQt7xR9I2DGSbHBNSjCI4vnc6_-liw8beGd7QFHgvyHqabJP_37v_pAcack12jOmnxgZavV2ORUPT-C0ppntIFG_AFlP5wv80x6fj2GTQ/w640-h232/2023%E5%B9%B4%E8%B3%80%E7%8A%B6.png" width="640" /></a></div>
<p>個人的には②の鏡餅とウサギ耳こまちゃん&ここまちゃんが結構気に入っています。<br />
ここまちゃんの( ´ ω ` )←顔が良いと思ったのですが、ウサギ分がたりなかったのかもですね。<br />
③は宇宙猫のパロディです。背景は<a href="https://unsplash.com/ja/%E5%86%99%E7%9C%9F/0o_GEzyargo" target="_blank">フリー素材の宇宙の写真</a>です</p>
<p><br /></p><p>そして、①を清書したものが今年の年賀状はになります</p>
<p>ラフの時点で丸い窓状にイラストを切り抜きすることは決めていたのですが、実際に切り抜きしてみると寂しく感じたので、どうにか装飾がつけられないかな?と考えてみて、お正月飾りとしてポピュラーな「しめ飾り」を入れてみました。<br />
実は私、このお正月飾りが「しめ飾り」という名前だということを、今回年賀状イラストを描く際に初めて知りました!</p>
<p><br /></p>
<p>もっと「少しでも重心がずれたらこまちゃんが転びそう!」と見る人もハラハラするような躍動感のあるイラストにしたかったのですが、イメージよりも50%躍動感オフされたようなイラストになってしまいました💦<br />
今後は動きのある絵を練習せねばと思いました!</p><p>そのためには遠近含めたパース、構図練り、デッサン…etc色々練習が必要そうです!</p>オフィス狛 デザイン部 Satohttp://www.blogger.com/profile/09225615773755838034noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-51100403203853441082022-11-01T09:27:00.003+09:002022-11-01T09:27:23.723+09:00「iOSのSafariでheight:100vhが上手くいかない!」の問題をCSSで解決する。<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl6gnGE3HQTmNs8cJ2K8paQT7ulLroH1bzL4BxMir0i4EV6fGO4L2dPNBFOL9Qhw6XlRP0g6uAW2UvQzo9pUroPo2qrxVichvkI_NRUhpI0huKmXY3xEIQ7lqumBp-XlRVJ4bpjxi4xeFSxpkdS8sXUdR8w2fIZ1teXOijzEbVQXeh1IJKhNbBcK_Nkw/s750/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%2010%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC@3x.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="480" data-original-width="750" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl6gnGE3HQTmNs8cJ2K8paQT7ulLroH1bzL4BxMir0i4EV6fGO4L2dPNBFOL9Qhw6XlRP0g6uAW2UvQzo9pUroPo2qrxVichvkI_NRUhpI0huKmXY3xEIQ7lqumBp-XlRVJ4bpjxi4xeFSxpkdS8sXUdR8w2fIZ1teXOijzEbVQXeh1IJKhNbBcK_Nkw/s320/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%2010%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC@3x.png" width="320" /></a></div><p>こんにちは、オフィス狛 デザイン部のSatoです。</p><p><br /></p><p><span aria-label="CSS セレクタ" class="selector" color="var(--color-text-disabled)" style="box-sizing: border-box; min-height: 0px; min-width: 0px;">私がモバイル用サイトをコーディングする際、諸事情によりbodyタグにclassをつけられないことが多いです。</span></p><p><span aria-label="CSS セレクタ" class="selector" color="var(--color-text-disabled)" style="box-sizing: border-box; min-height: 0px; min-width: 0px;">ですので、divタグで全体的な内容を囲んで</span>背景色を設定していたりします。</p><p><span aria-label="CSS セレクタ" class="selector" color="var(--color-text-disabled)" style="box-sizing: border-box; min-height: 0px; min-width: 0px;"><br /></span></p><p><span aria-label="CSS セレクタ" class="selector" color="var(--color-text-disabled)" style="box-sizing: border-box; min-height: 0px; min-width: 0px;">そうなると、背景色をページごとに設定が必要サイトでSafariなどのiOSのブラウザで要素が少ないページを</span>表示した場合に、<span aria-label="CSS セレクタ" class="selector" color="var(--color-text-disabled)" style="box-sizing: border-box; min-height: 0px; min-width: 0px;">下に余白ができたり余白をなくすために</span>height:100vhを設定すると不要なスクロールができてしまうことが多々ありました。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuzqOnDN9vEFelD9CCPb1ZZcwPls69ci8JEz2sYeDgEtLEVcVb9MiI2hzqeC_ysN8y_KYAVKXFn-0KgITnmif0IIm6pN5cVyMDiCLVUsmO1fl2kAndNob6P1W3FTHVzxdOY8CId7NohRpDxEXhLeWQhq-Fo0bBdVuqjUfwRucRgAsoEdCQCvQIzgE7tw/s1600/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC%202.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="904" data-original-width="1600" height="362" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuzqOnDN9vEFelD9CCPb1ZZcwPls69ci8JEz2sYeDgEtLEVcVb9MiI2hzqeC_ysN8y_KYAVKXFn-0KgITnmif0IIm6pN5cVyMDiCLVUsmO1fl2kAndNob6P1W3FTHVzxdOY8CId7NohRpDxEXhLeWQhq-Fo0bBdVuqjUfwRucRgAsoEdCQCvQIzgE7tw/w640-h362/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC%202.png" width="640" /></a></div><p>不要なスクロールをなくすにはJSを書く必要がありましたが、CSSの新要素で解決できるようになったので、ご紹介します。</p><p><br /></p><p><br /></p><p><br /></p><p>CSSの新要素、それは新しく追加された単位であるsvh・lvh・dvhです。</p><p>これら3つとも、ビューポートを基準にした単位ですがモバイルサイトをコーディングする際に困っていた部分を解決できる仕様になっています。</p><p><br /></p><p><span style="color: #ffa400;"><b>★svhはスモールビューポートです。</b></span></p><p>表示領域の高さが最小の際のサイズが基準になります。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRnItWjiiE4IIenMSohkKC8tUPsmOu9_oo-ag4XtFxnk6AuCLUEFvys_wjYTsrAkgq4J0OT_BB7hbXzuId0NtHh_rDDDEun4Ab_QJ0NG6IBcKAzHhzcwJGxF2cKzp4yGUTfNNiKBuBtHThR7RIfapaoB8RPD6_d0BKpGD-TfcP9KDUAdpeX7m_hk-RPw/s1600/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="904" data-original-width="1600" height="362" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRnItWjiiE4IIenMSohkKC8tUPsmOu9_oo-ag4XtFxnk6AuCLUEFvys_wjYTsrAkgq4J0OT_BB7hbXzuId0NtHh_rDDDEun4Ab_QJ0NG6IBcKAzHhzcwJGxF2cKzp4yGUTfNNiKBuBtHThR7RIfapaoB8RPD6_d0BKpGD-TfcP9KDUAdpeX7m_hk-RPw/w640-h362/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC.png" width="640" /></a></div><p>height:100svhを設定すると、表示領域の高さが最小の時(アドレスバーが表示されている時)の高さになります。</p><p><br /></p><p><b><span style="color: #ffa400;">★lvhはラージビューポートです。</span></b></p><p>表示領域の高さが最大の際のサイズが基準になります。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwvSoeHbrz6HxRa4wJTKDHD2IV3JJyU1DU8X6UDXtxgGOUtXZ9lqqfcXgS54MTjH32G7OVCR5XuEJLymJ-ZyfgPCvjp0x5AfdPpte9bmn7GuAu72XjNi1op7m560-4qooCu8vg0cuMr4BNXvhSViieeXv29TX8R78um3Vb3Qul8NWNJXYOa2hbbS7zug/s1600/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="904" data-original-width="1600" height="362" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwvSoeHbrz6HxRa4wJTKDHD2IV3JJyU1DU8X6UDXtxgGOUtXZ9lqqfcXgS54MTjH32G7OVCR5XuEJLymJ-ZyfgPCvjp0x5AfdPpte9bmn7GuAu72XjNi1op7m560-4qooCu8vg0cuMr4BNXvhSViieeXv29TX8R78um3Vb3Qul8NWNJXYOa2hbbS7zug/w640-h362/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201.png" width="640" /></a></div><p>height:100lvhを設定すると、表示領域の高さが最大の時(アドレスバーの表示が小さい時)の高さになります。</p><p><br /></p><p><b><span style="color: #ffa400;">★dvhはダイレクトビューポートです。</span></b></p><p>ブラウザの表示領域の動きに対し、動的に対応してくれます。この単位は今までの悩みを解決してくれるすごい単位です!</p><p>height:100dvhを設定すると、ブラウザの表示項目の表示・非表示に合わせサイズをフレキシブルに変更してくれます。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWI9y_9-ARLoYH0O4SnZSuZFjVAJ_SSLEtac5X6kvnyfLBX1LnwdGj4mysieFtJAvaI8e6L6izMWvEN9gnyMCN9ump8_AmUVKpsesSfUbtuhUM83y2IPhNM3bCSAeR_WMVzw3C5ass7mowNhQiTPe4Y2vQDeOtd-EA3FX4aZT8cUC46vH3rcIM4fyfSQ/s1600/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC%203.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="904" data-original-width="1600" height="362" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWI9y_9-ARLoYH0O4SnZSuZFjVAJ_SSLEtac5X6kvnyfLBX1LnwdGj4mysieFtJAvaI8e6L6izMWvEN9gnyMCN9ump8_AmUVKpsesSfUbtuhUM83y2IPhNM3bCSAeR_WMVzw3C5ass7mowNhQiTPe4Y2vQDeOtd-EA3FX4aZT8cUC46vH3rcIM4fyfSQ/w640-h362/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%201%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC%203.png" width="640" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><br /></div><p>そして、<span style="color: #ffa400;">min-height:100dvh;</span>を設定することで、ブラウザの表示項目の表示・非表示に合わせサイズをフレキシブルに変更してくれるので画面の要素が少ない際は余分なスクロールが発生しない、かつ内容が画面に収まらない場合は下まで背景色がつくようになります!</p><p><br /></p><p>どの単位も、今後スマートフォンサイトをコーディングする際に絶対に役に経ちますので、覚えておきたいですね。</p>オフィス狛 デザイン部 Satohttp://www.blogger.com/profile/09225615773755838034noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-74795766632928351212022-10-29T11:42:00.000+09:002022-10-31T09:41:54.007+09:00Mavenビルドの「Error code 501, HTTPS Required」エラーに対応する。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZju-4Z11uylOH27UgmiKfEfpCkyHl56jGEVotCL8xcHsVUp8P-tiEr5jQwc6spOCEfOsq21GwYKc9tYlFdBJmv-5W062Yll17QSYXzEoNU2VXaN-Upj7xicOocFIJM1Br9z5lpu2aVpNcth0fYM6QPDI0o0GdNVM-xOWApEiOsUoBOppJoFOXuSpJ8g/s385/Fotolia_238569932_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="311" data-original-width="385" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZju-4Z11uylOH27UgmiKfEfpCkyHl56jGEVotCL8xcHsVUp8P-tiEr5jQwc6spOCEfOsq21GwYKc9tYlFdBJmv-5W062Yll17QSYXzEoNU2VXaN-Upj7xicOocFIJM1Br9z5lpu2aVpNcth0fYM6QPDI0o0GdNVM-xOWApEiOsUoBOppJoFOXuSpJ8g/s320/Fotolia_238569932_XS.jpg"/></a></div>
<br />
オフィス狛 技術部のJoeです。<br />
先日、担当しているJavaのプロジェクトでMavenのビルドを行ったところ、下記のエラーが発生しました。<br />
<br />
<pre class="prettyprint linenums">
[ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: Could not transfer artifact org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE from/to central (http://repo1.maven.org/maven2/): Failed to transfer http://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/1.5.4.RELEASE/spring-boot-dependencies-1.5.4.RELEASE.pom. Error code 501, HTTPS Required
</pre>
<br />
Maven Central リポジトリからアーティファクトをダウンロードできずに、「501」でエラーとなっています。<br />
<br />
原因は、2020年1月15日から、Maven の Central リポジトリ へのHTTP経由による接続のサポートが終了し、HTTPS経由での接続が必須となりました。(ちょっと古い情報ですが)<br />
そのため、このままですと、Central リポジトリ への接続が拒否され、ビルドが失敗します。<br />
<br />
解決方法を調査してみると、いくつか対応策が出てきますが、私が解決できた方法をご紹介します。<br />
<br />
【環境】<br />
・Spring Tool Suite(Windows):4.15.3<br />
・Maven:3.8.4<br />
<br />
※環境の状態によっては、下記のエラーが発生する場合もありますが、同じ対応で解決できました<br />
<pre class="prettyprint linenums">
[ERROR] Failed to execute goal on project web: Could not resolve dependencies for project [プロジェクト]: Failed to collect dependencies at [アーティファクトID]: Failed to read artifact descriptor for [アーティファクトID]: org.springframework.boot:spring-boot-dependencies:pom:1.5.4.RELEASE was not found in https://oss.sonatype.org/content/repositories/snapshots during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of sonatype-snapshots has elapsed or updates are forced
</pre>
<br />
<h3>① エラーとなったアーティファクトのバージョンを最新版に上げる</h3>
私の利用していたアーティファクト「doma-spring-boot-starter」は、バージョンを最新版(1.1.1 → 1.6.0)にすることでエラーが解消し、アーティファクトをダウンロードできました。<br />
※アーティファクトによっては、最新バージョンとしても今回の事象が解消されていない可能性もあるかと思います<br />
<br />
バージョンの上げ方は、<a href="http://search.maven.org/"><u>Central リポジトリ</u></a>で、ご利用のアーティファクトを検索、最新バージョンを確認いただき、pom.xmlファイルで、対象のアーティファクトの「<version>タグ」で指定下さい。<br />
<br />
<h3>② Maven Central リポジトリのURLを指定する</h3>
①でエラーが解消されない場合や、事情によりバージョンを変更できない場合は、pom.xmlファイルに、下記のとおり、Maven Central リポジトリのURLを明示的に指定することで、エラーを解消することが出来ました。<br />
<pre class="prettyprint linenums">
<repositories>
<repository>
<id>maven</id>
<name>Maven Central</name>
<url>https://repo.maven.apache.org/maven2/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</pre>
<br />
※調査してみると、「<id>タグ」と「<name>タグ」には、下記を指定することで解消できるとの情報もありましたが、私の環境ではエラーを解消できませんでした。<br />
<pre class="prettyprint linenums">
<id>central</id>
<name>Central Repository</name>
</pre>
<br />
<br />
①、②でpom.xmlファイルを編集し、ビルドしてもエラーが解消されない場合は、プロジェクトの更新(プロジェクトを右クリック > Maven > プロジェクトの更新)し、再度ビルドすることもお試しください。<br />
<br />
以上です。<br />
同じエラーが発生した場合に、お役に立てば幸いです。<br />
<br />
オフィス狛 技術部 Joehttp://www.blogger.com/profile/04239261021335514104noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-64526028269967600772022-10-27T13:37:00.001+09:002022-10-31T09:42:03.559+09:00【Angular】画面遷移前に確認ダイアログ風のポップアップを表示する。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiV9C3twpOGUBh-ZYvaB6yN8JGrtFdWR292ztUe8zvYi3QVqRY5V_dcGERmiJPhM1oloESo7HfBtmZdMukQERaJW3ymdSesRjuoTW_mxbdkIJ-oPVU3OUXn3HEJaFbbmm0HyBktujCKR0YBHw96QMhcmomNKmxssTA1_jHwcTBgXhAtKqPpOoGUcv-mMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiV9C3twpOGUBh-ZYvaB6yN8JGrtFdWR292ztUe8zvYi3QVqRY5V_dcGERmiJPhM1oloESo7HfBtmZdMukQERaJW3ymdSesRjuoTW_mxbdkIJ-oPVU3OUXn3HEJaFbbmm0HyBktujCKR0YBHw96QMhcmomNKmxssTA1_jHwcTBgXhAtKqPpOoGUcv-mMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
今回は画面遷移前に表示する下図のような確認ダイアログ風のポップアップ(以降、確認ポップアップ)を作成していこうと思います。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgz4CFqw6b6mtCf-pCJDGtDacSRLBhU_7CXdyZtmizEKj6K_5WT6kCxRZK8r4Nfbm2uf_NlnSPlMtKItUDuyBGmBZbQPwcrZqfvqAIl6-ft0g7FknKEufZ9yN8X-x_JaS3UUn456_aTtd9W6T6SrShTidAK1bhpvrMOaxIBVA7sM9aP4gRTZa7cQV1crg/s746/%E7%A2%BA%E8%AA%8D%E3%83%9B%E3%82%9A%E3%83%83%E3%83%95%E3%82%9A%E3%82%A2%E3%83%83%E3%83%95%E3%82%9A_%E3%82%AD%E3%83%A3%E3%83%95%E3%82%9A%E3%83%81%E3%83%A3.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="400" data-original-height="380" data-original-width="746" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgz4CFqw6b6mtCf-pCJDGtDacSRLBhU_7CXdyZtmizEKj6K_5WT6kCxRZK8r4Nfbm2uf_NlnSPlMtKItUDuyBGmBZbQPwcrZqfvqAIl6-ft0g7FknKEufZ9yN8X-x_JaS3UUn456_aTtd9W6T6SrShTidAK1bhpvrMOaxIBVA7sM9aP4gRTZa7cQV1crg/s400/%E7%A2%BA%E8%AA%8D%E3%83%9B%E3%82%9A%E3%83%83%E3%83%95%E3%82%9A%E3%82%A2%E3%83%83%E3%83%95%E3%82%9A_%E3%82%AD%E3%83%A3%E3%83%95%E3%82%9A%E3%83%81%E3%83%A3.png"/></a></div>
<br />
1からこのような仕組みを作るのは大変なので、「Simple Modal Module」(ngx-simple-modal)というプラグインを使いたいと思います。<br />
<br />
※「Simple Modal Module」のインストールや使い方の細かい説明は、弊社メンバーの記事(<a href="https://blog.officekoma.co.jp/2020/10/angularsimple-modal-module.html" target="_blank"><u>【Angular・【Simple Modal Module】モーダルを手軽に実装する。</u></a>)をご覧ください。<br />
<br />
当記事では、以前の記事で使用した「アカウント登録機能」に実装する形で、実践的な説明をしようと思います。<br />
と言うことで、是非、以前の記事も参照ください<br />
<br />
参照1:<a href="https://blog.officekoma.co.jp/2022/08/angular_8.html" target="_blank"><u>【Angular】独自エラーチェック(カスタムバリデーション)を作成する。</u></a><br />
参照2:<a href="https://blog.officekoma.co.jp/2022/08/angular_5.html" target="_blank"><u>【Angular】コンポーネントの設計(画面ごとの設計)について。</u></a><br />
参照3:<a href="https://blog.officekoma.co.jp/2022/08/angular_4.html" target="_blank"><u>【Angular】エラーメッセージの管理について考える。</u></a><br />
<br />
<h3>(1)どのコンポーネントでポップアップを呼び出すべきか</h3>
まず、どのコンポーネントでポップアップを呼び出すべきか考えてみます。<br />
親(Container)なのか、子(Presentation)なのか・・・・<br />
以前の記事で、こんな定義をしたかと思います。<br />
<br />
<span style="font-size: large;color: #AB47BC;">「Container」として分類するもの</span><br />
<ul>
<li>画画面の状態保持に関すること(NgRxのStore操作など)</li>
<li>画面に入力した値の業務チェック(API呼び出しが必要なもの、など)</li>
<li>API(バックエンド処理)の呼び出し</li>
<li>画面遷移</li>
</ul>
<br />
<span style="font-size: large;color: #AB47BC;">「Presentation」として分類するもの</span><br />
<ul>
<li>画面表示、及び表示内容の制御(エラー時など)</li>
<li>画面に入力した値のバリデーションチェック</li>
<li>画面に入力した値の関連チェック、及び業務チェック</li>
</ul>
<br />
<br />
という事で、ちょっと悩みどころではあるのですが、呼び出しは「子(Presentation)」の方が良いと思います。<br />
確認ポップアップの「OKボタン=画面遷移する」、「キャンセルボタン=画面遷移しない」と考えると、確認ポップアップとしての動きは、あくまで「画面遷移するかどうかの関連チェック」に過ぎない、というのが理由です。<br />
他にも、「キャンセルを押された時に画面の表示を変えたい」という要件があった場合、確認ポップアップを親(Container)で呼び出すと、かなり複雑になってしまうから、というのも理由の1つです。(今回はそのような要件はないですが)<br />
<br />
<h3>(2)確認ポップアップの作成</h3>
呼び出す場所は決まりましたので、呼び出す「確認ポップアップ」用のコンポーネントを作成しようと思います。<br />
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。<br />
<br />
今回は、以下のように作成します。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ shared/
└ modal/
└ containers/
└ confirmation-dialog/
└ confirmation-dialog.component.html
└ confirmation-dialog.component.ts
</pre>
<br />
では、それぞれ実装を見ていきましょう。まずは、テンプレート(View)から。<br />
<span id="confirmationDialogComponentHtml" style="font-size: small;color: #9E9E9E;">[confirmation-dialog.component.html]</span>
<pre class="prettyprint linenums">
<div class="message">
<div class="modal-dialog modal-content">
<div class="modal-header">
<div class="modal-title"></div>
<button type="button" class="close" aria-label="閉じる" (click)="onClickCancel()">
<span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<p class="text-center">{{ message }}</p>
</div>
<div class="modal-footer p-0">
<button class="btn btn2" (click)="onClickCancel()">キャンセル</button>
<button class="btn btn1 btn-block" type="submit" (click)="onClickOk()">OK</button>
</div>
</div>
</div>
</pre>
特筆すべきところは特に無いですが、このコンポーネントを汎用的に利用する為に、表示するメッセージは「{{ message }}」で変数を展開する形にしています。<br />
<br />
続いて、コンポーネントの実装です。<br />
<span id="confirmationDialogComponentTs" style="font-size: small;color: #9E9E9E;">[rconfirmation-dialog.component.ts]</span>
<pre class="prettyprint linenums">
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { SimpleModalComponent } from 'ngx-simple-modal';
export interface ConfirmModel {
message: string;
}
@Component({
selector: 'koma-confirmation-dialog',
templateUrl: './confirmation-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmationDialogComponent
extends SimpleModalComponent<ConfirmModel, boolean>
implements ConfirmModel, OnInit {
message: string;
constructor() {
super();
}
ngOnInit() {}
onClickCancel() {
this.result = false;
this.close();
}
onClickOk() {
this.result = true;
this.close();
}
}
</pre>
<br />
少し詳細を説明しています。<br />
まず、下記の記載でポップアップに表示するメッセージを呼び出し元で設定出来るようにしています。<br />
<pre class="prettyprint linenums">
export interface ConfirmModel {
message: string;
}
</pre>
<br />
あと重要なのは、「ngx-simple-modal」のクラス(SimpleModalComponent)を継承し、先程作った「ConfirmModel」を実装する、と言う事です。<br />
<pre class="prettyprint linenums">
export class ConfirmationDialogComponent
extends SimpleModalComponent<ConfirmModel, boolean>
implements ConfirmModel, OnInit, OnDestroy {
</pre>
<br />
「OKが押されたか、キャンセルが押されたか」は、「result」にTrue、Falseを設定することで、呼び出し側へ知らせます。<br />
<pre class="prettyprint linenums">
onClickCancel() {
this.result = false;
this.close();
}
onClickOk() {
this.result = true;
this.close();
}
</pre>
<br />
これで確認ポップアップが作成できました。次はこの確認ポップアップを呼び出してみましょう。<br />
<br />
<div style="padding: 10px; margin-bottom: 10px; border: 1px dashed #333333;">
<span id="registerComponentHtml" style="font-size: small;">※モジュール(NgModule)ファイルの記載は省略していますので、適宜、追加したコンポーネントをモジュールでdeclarationsしておいてください。</span><br />
<span id="registerComponentHtml" style="font-size: small;">※モジュール(NgModule)ファイルでの「SimpleModalModule(ngx-simple-modal)」のインポートについてですが、共通的に使用されることを考えると、「app.module.ts」などでインポートした方が良いと思います。</span><br />
</div>
<br />
<h3>(3)確認ポップアップの呼び出し</h3>
では早速、先程作成した確認ポップアップの呼び出しを実装して行きます。<br />
まずは、呼び出し側の子のテンプレート(View)を実装します。<br />
<br />
<span id="registerComponentHtml" style="font-size: small;color: #9E9E9E;">[register.component.html](form部分のみ抜粋)</span>
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div class="form-input">
<div>
<label>携帯電話番号<span>必須</span></label>
<input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
[ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
<ng-container *ngIf="v.mobilePhoneNumberInvalid">
<p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorFormat" class="error-message">{{ message('msg_error_cellphone_number') }}</p>
</ng-container>
</div>
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(任意)"
[ngClass]="{'alert-danger' : v.nameInvalid}">
<ng-container *ngIf="v.nameInvalid">
<p class="error-message">{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}</p>
</ng-container>
</div>
</div>
<footer class="footer fixed-bottom d-flex">
<button type="button" (click)="onClickPrev()">戻る</button>
<button [disabled]="formRegister.invalid" type="submit">確認</button>
</footer>
</form>
</pre>
<br />
特筆すべき部分は特になく、単純に下記の部分を追加しているだけです。<br />
<pre class="prettyprint linenums">
<button type="button" (click)="onClickPrev()">戻る</button>
</pre>
これは単純ですね、「戻る」ボタンを追加しただけ、になります。<br />
<br />
では、次は子コンポーネントに戻るボタンを押した時のアクションを実装して行きます。<br />
<span id="registerComponentTs" style="font-size: small;color: #9E9E9E;">[register.component.ts]</span>
<pre class="prettyprint linenums">
import { Component, OnInit, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { CustomValidators } from '@app/shared/validator/custom-validators';
import { RegisterValidator } from './register.validator';
import { AccountViewSaveModel } from '../../models/account';
import { getMessage } from '@app/shared/message/error-messages';
import { SimpleModalService } from 'ngx-simple-modal';
import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';
@Component({
selector: 'koma-register',
templateUrl: './register.component.html',
})
export class RegisterComponent implements OnInit, OnDestroy {
// Input・Outputの定義。
@Input() accountViewSave$: Observable<AccountViewSaveModel>;
@Output() formBack = new EventEmitter();
@Output() formSubmit = new EventEmitter<AccountViewSaveModel>();
mobilePhoneNumberMaxLength = 11;
nameMaxLength = 20;
registerSubscription: Subscription = new Subscription();
formRegister: FormGroup = this.formBuilder.group({
mobilePhoneNumber: [
'',
[
Validators.required,
Validators.maxLength(this.mobilePhoneNumberMaxLength),
CustomValidators.mobilePhoneNumberValidator,
],
],
name: ['', [Validators.maxLength(this.nameMaxLength)]],
});
constructor(
private formBuilder: FormBuilder,
public v: RegisterValidator,
private simpleModalService: SimpleModalService,
) {}
ngOnInit(): void {
// バリデーション設定
this.v.formGroup = this.formRegister;
this.registerSubscription.add(
// 画面初期値設定
this.accountViewSave$.subscribe(value => {
if (value) {
this.formRegister.controls.mobilePhoneNumber.setValue(
value.mobilePhoneNumber,
);
this.formRegister.controls.name.setValue(value.name);
}
}),
);
}
ngOnDestroy(): void {
// ダイアログ削除
this.simpleModalService.removeAll();
// サブスクリプション解除
this.registerSubscription.unsubscribe();
}
onClickPrev(): void {
// 確認ダイアログの表示
this.simpleModalService
.addModal(ConfirmationDialogComponent, {
message: getMessage('msg_confirm_screen_transition'),
})
.subscribe(result => {
if (result) {
this.formBack.emit();
}
});
}
// 画面でSubmitが発生した時の処理
onSubmit(): void {
if (this.formRegister.valid) {
// バリデーションエラーが発生していない場合
const formModel = {
mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value,
name: this.formRegister.controls.name.value,
} as AccountViewSaveModel;
this.formSubmit.emit(formModel);
}
}
message(messageId: string, ...args: any[]): string {
return getMessage(messageId, ...args);
}
}
</pre>
<br />
中身を細かく見ていきましょう。<br />
まずは、「ngx-simple-modalのSimpleModalService」と、先程作成した確認ポップアップをインポートしています。<br />
<pre class="prettyprint linenums">
import { SimpleModalService } from 'ngx-simple-modal';
import { ConfirmationDialogComponent } from '@app/shared/modal/containers/confirmation-dialog/confirmation-dialog.component';
</pre>
<br />
戻るボタンを押された場合の画面遷移の制御は、親コンポーネント側で行うので、親コンポーネントへのイベント登録(定義)も行います。
<pre class="prettyprint linenums">
@Output() formBack = new EventEmitter();
</pre>
<br />
そして、インポートした「SimpleModalService」はDIする必要があります。
<pre class="prettyprint linenums">
constructor(
private formBuilder: FormBuilder,
public v: RegisterValidator,
private simpleModalService: SimpleModalService,
) {}
</pre>
<br />
次に「戻るボタン」を押した時の処理です。ここで確認ポップアップを表示します。<br />
<pre class="prettyprint linenums">
onClickPrev(): void {
// 確認ダイアログの表示
this.simpleModalService
.addModal(ConfirmationDialogComponent, {
message: getMessage('msg_confirm_screen_transition'),
})
.subscribe(result => {
if (result) {
this.formBack.emit();
}
});
}
</pre>
<br />
ざっくり説明すると、<br />
<ul>
<li>SimpleModalServiceのaddModalに自作コンポーネントを指定して、さらに自作コンポーネントのmessageに、表示用の文言を設定している</li>
<li>ポップアップの戻りは、subscribeで受け取り設定する(resultにTrue、Falseが設定されて戻ってくる)</li>
</ul>
と言う感じですね。<br />
Trueの場合(確認ポップアップで「OK」が押された場合)、親画面へイベントを委譲しています。<br />
<br />
<h3>(4)親コンポーネントの実装</h3>
確認ポップアップの表示とは直接関係ないですが、親コンポーネントも実装しておきましょう。<br />
まずはテンプレート(View)に、子のOutput(formBack)と親のイベント(onClickBack)の紐付けを追加します。<br />
<br />
<span id="accountRegisterComponentHtml" style="font-size: small;color: #9E9E9E;">[account-register.component.html]</span>
<pre class="prettyprint linenums">
<koma-register
(formBack)="onClickBack()"
(formSubmit)="onSubmit($event)"
[accountViewSave$]="accountViewSave$"
></koma-register>
</pre>
<br />
続いて、コンポーネント側の実装です。<br />
<span style="font-size: small;color: #9E9E9E;">[account-register.component.ts]</span>
<pre class="prettyprint linenums">
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Router } from '@angular/router';
import { AccountViewSaveModel } from '../../models/account';
import * as fromAccount from '../../store/reducers';
import * as AccountActions from '../../store/actions/account.actions';
@Component({
selector: 'koma-account-register',
templateUrl: './account-register.component.html',
})
export class AccountRegisterComponent implements OnInit {
// Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
// 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));
constructor(public store: Store<fromAccount.State>, private router: Router) {}
ngOnInit(): void {}
onClickBack(): void {
// ホーム画面に遷移する(戻る)
this.router.navigateByUrl('/home/top');
}
onSubmit(formModel: AccountViewSaveModel): void {
// 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
this.store.dispatch(
AccountActions.setRegisterPostData({ data: formModel }),
);
// 確認画面へ遷移する
this.router.navigateByUrl('/account/register-confirm');
}
}
</pre>
<br />
「NgRxのStore」などが使われていますが、ここは一旦スルーしてください。(いずれ、NgRxの実装方法も記事にしようと思います。)<br />
ここでは、画面で入力された値はStore(と言う場所)に格納されている、ぐらいの理解でOKです。<br />
(重要なのは、「データ保存方式」ではなく、あくまで「戻るボタンの制御」なので)<br />
<br />
と言う事で、中身を見ていくと・・・・先程、子側と紐付けたイベント(onClickBack)が実装されていますね。<br />
画面遷移するだけの処理ですが、URLがそのまま固定文字列で記載されているのが気になります。<br />
こちらも今回の記事の本質では無いですが、URLの固定文字列も定数化しちゃいましょう。<br />
<br />
(2)でも説明したように、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置します。<br />
今回は、以下のように作成します。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ shared/
└ constant/
└ page-constants.ts
</pre>
<br />
実装は下記のようになります。<br />
<span id="pageConstantsTs" style="font-size: small;color: #9E9E9E;">[page-constants.ts]</span>
<pre class="prettyprint linenums">
export namespace url {
export const HOME_TOP = '/home/top';
export const ACCOUNT_REGISTER_CONFIRM = '/account/register-confirm';
}
</pre>
<br />
では、先程の固定文字列を定数に変更してみます。<br />
下記が、最終的な親のコンポーネントとなります。<br />
<span id="accountRegisterComponentTs" style="font-size: small;color: #9E9E9E;">[account-register.component.ts]</span>
<pre class="prettyprint linenums">
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Router } from '@angular/router';
import * as PageConstants from '@app/shared/constant/page-constants';
import { AccountViewSaveModel } from '../../models/account';
import * as fromAccount from '../../store/reducers';
import * as AccountActions from '../../store/actions/account.actions';
@Component({
selector: 'koma-account-register',
templateUrl: './account-register.component.html',
})
export class AccountRegisterComponent implements OnInit {
// Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
// 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));
constructor(public store: Store<fromAccount.State>, private router: Router) {}
ngOnInit(): void {}
onClickBack(): void {
// ホーム画面に遷移する(戻る)
this.router.navigateByUrl(PageConstants.url.HOME_TOP);
}
onSubmit(formModel: AccountViewSaveModel): void {
// 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
this.store.dispatch(
AccountActions.setRegisterPostData({ data: formModel }),
);
// 確認画面へ遷移する
this.router.navigateByUrl(PageConstants.url.ACCOUNT_REGISTER_CONFIRM);
}
}
</pre>
<br />
<h3>(5)メッセージを追加する</h3>
最後に確認ポップアップに送る為のメッセージ定義もしておきましょう。<br />
<br />
<span id="errorMessagesTs" style="font-size: small;color: #9E9E9E;">[error-messages.ts]</span>
<pre class="prettyprint linenums">
export const errorMessages: { [key: string]: string } = {
msg_error_field_required: '{0}は必ず入力してください。',
msg_error_field_max: '{0}は{1}文字以内で入力してください。',
msg_confirm_screen_transition:
'現在の入力中の情報は破棄されます。前の画面に戻りますか?',
};
function formatMessage(msg: string, ...args: any[]): string {
return msg.replace(/\{(\d+)\}/g, (m, k) => {
return args[k];
});
}
export function getMessage(messageId: string, ...args: any[]): string {
return formatMessage(errorMessages[messageId], ...args);
}
</pre>
<br />
これで、全ての実装が完了しました。<br />
この応用(むしろ、今回の方が応用ですが)で、「アラート表示ポップアップ」や、「完了メッセージ表示ポップアップ」など、色々作れると思います。<br />
<br />
ぜひ、色々カスタマイズしてみてください!<br />
<br />
<br />
オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-53422531729057978372022-10-27T12:46:00.003+09:002022-10-31T09:41:59.457+09:00【AWS】プライベートサブネット内のLambda関数でKMSを利用する(NAT Gatewayとエンドポイントについて)<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0NMl-k0oFOXbTMxL1R20bmrmL3TarwyPFVf8_VWgQa4tn7gMftCz9GzS4-UplI8_gV0R7tVvsEKcdvtc4C6lnwdMgl3CN3LEUR4jT8ZXcxUiU86WBAd_le0YQPlDvvOCayUaeQzSw9Nx0yE7rLSJ_e4hiN4q-jbEqoxNQPoSzzqNjngNgNnEnqA1TbA/s438/Fotolia_201012742_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="274" data-original-width="438" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0NMl-k0oFOXbTMxL1R20bmrmL3TarwyPFVf8_VWgQa4tn7gMftCz9GzS4-UplI8_gV0R7tVvsEKcdvtc4C6lnwdMgl3CN3LEUR4jT8ZXcxUiU86WBAd_le0YQPlDvvOCayUaeQzSw9Nx0yE7rLSJ_e4hiN4q-jbEqoxNQPoSzzqNjngNgNnEnqA1TbA/s320/Fotolia_201012742_XS.jpg"/></a></div>
<br />
オフィス狛 技術部のJoeです。<br />
担当のプロジェクトで、AWSのプライベートサブネット内のLambda関数で、環境変数の複合化を行うため、AWS KMSを利用する必要がありました。<br />
インターネットゲートウェイとルーティングできないプライベートサブネット内のLambda関数が、VPC外のサービスであるAWS KMSを利用するためには、2つの方法があります。<br />
<br />
<h2>方法と設定</h2>
<h3>1.NAT Gatewayを作成する</h3>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi76cv0pAZESQ1fW2uMorRwPmbd2plpkvj_jhBOnRUGtKS1RvL3AeNSg649Vc9_jS3W0OCJ0ZCKKTqzQ75VWy5tAMvgyYckB8ptOOfSzElzmx01r4fUFUoFUu6Fdd71Y1mtRcSPcdVehm7Ri6MMYfRBP7tfXkwc128kvxbv4T3OCruQTp9Cts9LkGc8Q/s2544/NAT%20Gateway.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="600" data-original-height="1132" data-original-width="2544" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi76cv0pAZESQ1fW2uMorRwPmbd2plpkvj_jhBOnRUGtKS1RvL3AeNSg649Vc9_jS3W0OCJ0ZCKKTqzQ75VWy5tAMvgyYckB8ptOOfSzElzmx01r4fUFUoFUu6Fdd71Y1mtRcSPcdVehm7Ri6MMYfRBP7tfXkwc128kvxbv4T3OCruQTp9Cts9LkGc8Q/s600/NAT%20Gateway.png"/></a></div>
<br />
パブリックサブネットにNAT Gatewayを作成し、インターネット経由でKMSに接続します。<br />
<br />
<b>【NAT Gatewayの設定手順】</b><br />
※NAT Gateway 以外は、既に構築済みの前提として、手順は省略させていただきます<br />
<br />
① VPCのサービスから、NAT Gateway を作成します。<br />
<div style="padding: 10px; margin-bottom: 10px; border: 1px dotted #333333;">
・名前:任意の名前を指定します。<br />
・サブネット:NAT Gatewayを作成するパブリックサブネット を選択します。<br />
・接続タイプ:「パブリック」を選択します。<br />
・Elastic IP 割り当て ID:任意のElastic IP アドレスを選択します。<br />
※「Elastic IP を割り当て」ボタンで、新たなElastic IP を割り当てることも可能です<br />
</div>
<br />
② プライベートサブネットのルートテーブルのルートに、①で作成したNAT Gateway を指定します。<br />
<div style="padding: 10px; margin-bottom: 10px; border: 1px dotted #333333;">
・送信先:「0.0.0.0/0」を指定します。<br />
・ターゲット:①で作成したNAT GatewayのID(「nat-…」)を指定します。<br />
</div>
<br />
■注意点<br />
・Elastic IP アドレスは、AWS アカウントごとに各リージョンで 5 つのデフォルト制限がありますので、既に5つ割り当てている場合、制限の増加をリクエストする必要があります。<br />
<br />
<br />
<h3>2.エンドポイントを作成する(PrivateLink)</h3>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZpOxDVP4np99XGajaJ31-bymTDLqCIMM5dX76oHWjknvDSrXvs2Ue_kTygmNO9AlQldT_FNykUtPq-qNdJgiHkRLJIGz7Qq9aOz00iskV3GZKcVBq-1f6E5iaFzJdLhwSOISYSbDZUfE_A6NEBBQ4tYLGJG3aTapq0Dwp46E2-aVVpcc618pBgG39zA/s1774/PrivateLink.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="400" data-original-height="1218" data-original-width="1774" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZpOxDVP4np99XGajaJ31-bymTDLqCIMM5dX76oHWjknvDSrXvs2Ue_kTygmNO9AlQldT_FNykUtPq-qNdJgiHkRLJIGz7Qq9aOz00iskV3GZKcVBq-1f6E5iaFzJdLhwSOISYSbDZUfE_A6NEBBQ4tYLGJG3aTapq0Dwp46E2-aVVpcc618pBgG39zA/s400/PrivateLink.png"/></a></div>
KMS用のエンドポイント(インターフェイスエンドポイント)を作成し、プライベートリンクでKMSに接続します。<br />
<br />
<b>【エンドポイントの設定手順】</b><br />
※エンドポイント以外は、既に構築済みの前提として、手順は省略させていただきます<br />
<br />
① VPCのサービスからエンドポイントを作成します。<br />
<div style="padding: 10px; margin-bottom: 10px; border: 1px dotted #333333;">
・名前タグ : 任意の名前を指定します。<br />
・サービスカテゴリ : 「AWS のサービス」を選択します。<br />
・サービス : 「com.amazonaws.ap-northeast-1.kms」を選択します。<br />
・VPC : ご自身の環境の、VPC を選択します。<br />
・DNS 名を有効化 : プライベート DNS 名を有効にする場合、チェックしてください。<br />
・DNS レコードの IP タイプ : DNS 名を有効にした場合、IPv4 or IPv6 を選択ください。<br />
・サブネット : ご自身の環境の、プライベートサブネット を選択します。<br />
・IP アドレスタイプ : 選択したサブネットのIP アドレスタイプから、IPv4 or IPv6 を選択ください。<br />
・セキュリティグループ : 任意のセキュリティグループを指定します。<br />
・ポリシー : 今回は「フルアクセス」としますが、制限したい場合「カスタム」を選択し、ポリシーを設定します。<br />
</div>
<br />
■注意点<br />
・エンドポイントに設定するセキュリティグループは、インバウンドルールに「HTTPS」を許可する必要があります。<br />
<br />
<br />
<h2>NAT Gatewayとエンドポイントのどちらを利用したほうが良いのか</h2>
<br />
ご利用に環境による場合もあるかもしれませんが、今回のように、新たにプライベートサブネット内のLambdaやEC2から、AWSのサービス(KMS、S3など)を利用する場合は、
下記の理由などから、エンドポイントを作成し、プライベートリンクで接続が良さそうです。<br />
<br />
ただし、対象の AWS サービスで VPC エンドポイントが利用できない場合もありますので、こちらで事前にご確認ください。<br />
<a href="https://docs.aws.amazon.com/ja_jp/vpc/latest/privatelink/integrated-services-vpce-list.html"><u>AWS PrivateLink と統合できる AWS のサービス</u></a><br />
<br />
<h3>1.セキュリティ</h3>
NAT Gatewayは、インターネット経由で接続しますが、プライベートリンクを利用すると、トラフィックをインターネットに公開することなく、AWS のサービスに接続できます。<br />
そのため、ブルートフォース攻撃やDDos攻撃、その他の脅威に晒される危険が軽減されます。<br />
また、セキュリティグループを関連付けることで、アクセス制御も可能です。<br />
<br />
<h3>2.料金</h3>
料金については、<a href="https://aws.amazon.com/blogs/architecture/reduce-cost-and-increase-security-with-amazon-vpc-endpoints/"><u>AWS アーキテクチャ ブログ</u></a>を見ますと、エンドポイント(インターフェイスエンドポイント)は、「インターネット ゲートウェイへのトラフィックを回避し、NAT ゲートウェイ、NAT インスタンス、またはファイアウォールの維持に関連するコストが発生するのを回避することで、ネットワーク パスを最適化できます。」との記載があります。<br />
また、NAT Gatewayと「1つ当たりの料金」と「処理データ 1 GB あたりの料金」を比較すると、料金は約6分の1で、AWSのコストを削減できる可能性があります。<br />
<a href="https://aws.amazon.com/jp/vpc/pricing/"><u>NAT Gatewayの料金</u></a><br />
<a href="https://aws.amazon.com/jp/privatelink/pricing/"><u>エンドポイントの料金</u></a><br />
<br />
<h3>3.管理</h3>
管理については、エンドポイントは設定手順が少なく、構成も分かり易いため、個人的には管理し易いと思いました。<br />
<br />
<br />
以上です。<br />
今回は、インターフェイスエンドポイント(プライベートリンク)についてご紹介しましたが、エンドポイントは他にも「ゲートウェイロードバランサーのエンドポイント」、「ゲートウェイエンドポイント」がありますので、機会があればご紹介したいと思います。<br />
<br />
オフィス狛 技術部 Joehttp://www.blogger.com/profile/04239261021335514104noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-48861446614861475812022-10-26T10:52:00.001+09:002022-10-26T10:52:16.145+09:00【Laravel】created_atの日付フォーマットを変更する方法。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_Qi_7IABlKVfzJt6l5hp9Hi61-0fAd5Pn4nArCxSof-OG4ndbuahocoF_Y9_ZuiM5KBXzJ67rU4632m8toiFdEqgy69KwOtbM0quMsfC670pjKxu_zf3DXDQ5y9qOo6IO7E-Mg0qy1wU70eWCT_Gnx7JxVhhW6FQaF2HuC5zMjf2M1XMpYmmurngirA/s2048/AdobeStock_276356125.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1366" data-original-width="2048" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_Qi_7IABlKVfzJt6l5hp9Hi61-0fAd5Pn4nArCxSof-OG4ndbuahocoF_Y9_ZuiM5KBXzJ67rU4632m8toiFdEqgy69KwOtbM0quMsfC670pjKxu_zf3DXDQ5y9qOo6IO7E-Mg0qy1wU70eWCT_Gnx7JxVhhW6FQaF2HuC5zMjf2M1XMpYmmurngirA/s320/AdobeStock_276356125.jpeg"/></a></div>
こんにちは、オフィス狛 技術部のmmm(むー)です。<br><br>
以前、Laravelのプロジェクトで、日付のフォーマットを編集したかったのですが、レスポンスの項目名が<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span> の場合、うまく変換できず少しはまりました。<br><br>
その時、調べたことについて記事に残します。<br><br>
<h2>前提条件</h2>
<div style="font-weight: bold">■Laravel バージョン</div>
<pre><code>$ php artisan -V
Laravel Framework 8.27.0
</code></pre><br><br>
<div style="font-weight: bold">■SQL Server バージョン</div>
<pre><code>SELECT @@VERSION;
Microsoft SQL Server 2017 (RTM-CU30) (KB5013756) - 14.0.3451.2 (X64) Jun 22 2022 18:20:15 Copyright (C) 2017 Microsoft Corporation Developer Edition (64-bit) on Linux (Ubuntu 18.04.6 LTS)
</code></pre><br><br>
<h2>1. したかったこと</h2>
当初、SQLでフォーマットを指定して、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span> カラムの値をレスポンスしたかったのですが、期待した形の値が取得できませんでした。<br>
※コードは必要な箇所以外、簡易化して記載します。<br>
※<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">127</span>は「ISO 8601 (タイム ゾーン Z)」となります。<br>
<pre><code>class ConditionsController extends Controller
{
public function index(Request $request)
{
$conditions = Condition::select(
DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
)->get()->first();
return response()->json($conditions);
}
}
/*
■結果
{
"created_at": "2022-09-07T02:48:46.947000Z"
}
*/
</code></pre><br>
本来、「2022-09-07T11:48:46.947」のようなフォーマットになる想定でしたが、<br>
<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span>の値は「2022-09-07T02:48:46.947000Z」でした。<br><br>
一方、下記は全く同じ書き方ですが、取得するカラム名が <span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span> ではない場合、期待通りにフォーマットを変換することができました。<br>
<pre><code>class ConditionsController extends Controller
{
public function index(Request $request)
{
$conditions = Condition::select(
DB::raw("CONVERT(VARCHAR, created_at, 127) as test") // 別名でtestを設定
)->get()->first();
return response()->json($conditions);
}
}
/*
■結果
{
"test": "2022-09-07T11:48:46.947"
}
*/
</code></pre><br>
<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span>カラムに、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">test</span>という別名を付けると、値は「2022-09-07T11:48:46.947」でした。<br><br>
<h2>2. 原因と解決策</h2>
調査した結果、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">created_at</span>と<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">updated_at</span>はデフォルトの場合、EloquentがCarbonインスタンスへ変換していることがわかりました。<br>
Modelから返ってきた値をCarbonのフォーマットを使用して編集するように修正しました。
<pre><code>class ConditionsController extends Controller
{
public function index(Request $request)
{
$conditions = Condition::select(
DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
)->get()->first();
$response['created_at'] = $conditions['created_at']->toIso8601String(); // 変換処理を追記
return response()->json($response);
}
}
/*
■結果
{
"created_at": "2022-09-07T11:48:46+09:00"
}
*/
</code></pre><br>
今回は<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">
toIso8601String()</span> を使用しましたが、基本的なフォーマットは下記ページに用意されています。<br>
参考:<a href="https://carbon.nesbot.com/docs/#api-conversion">https://carbon.nesbot.com/docs/#api-conversion</a><br><br>
<h2>3. 補足</h2>
今回はControllerでフォーマットの修正を行いましたが、Modelで下記のように、属性を指定してフォーマットを指定することもできます。
<pre><code>protected $casts = [
'created_at' => 'datetime:Y/m/d',
];
</code></pre><br>
さらに、Laravelで<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">
get...Attribute</span> (...は加工したい項目名)と記載して、フォーマットを指定することも可能です。
<pre><code>public function getCreatedAtAttribute($value)
{
return Carbon::parse($value)->format('Y/m/d');
}
</code></pre><br>
上記は一例となります。Laravelでは様々な機能が用意されているので、もっといろんな方法があると思います。<br><br>
参考になりましたら、幸いです。<br>
<!-- 編集禁止(ここから) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/darcula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- 編集禁止(ここまで) -->オフィス狛 技術部 mmmhttp://www.blogger.com/profile/02929539829077086029noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-88081786469956887052022-10-26T10:49:00.000+09:002022-10-26T10:49:05.658+09:00【Laravel】プログレスバーが表示されるコマンド実行の処理を作成する方法。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_Qi_7IABlKVfzJt6l5hp9Hi61-0fAd5Pn4nArCxSof-OG4ndbuahocoF_Y9_ZuiM5KBXzJ67rU4632m8toiFdEqgy69KwOtbM0quMsfC670pjKxu_zf3DXDQ5y9qOo6IO7E-Mg0qy1wU70eWCT_Gnx7JxVhhW6FQaF2HuC5zMjf2M1XMpYmmurngirA/s2048/AdobeStock_276356125.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1366" data-original-width="2048" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_Qi_7IABlKVfzJt6l5hp9Hi61-0fAd5Pn4nArCxSof-OG4ndbuahocoF_Y9_ZuiM5KBXzJ67rU4632m8toiFdEqgy69KwOtbM0quMsfC670pjKxu_zf3DXDQ5y9qOo6IO7E-Mg0qy1wU70eWCT_Gnx7JxVhhW6FQaF2HuC5zMjf2M1XMpYmmurngirA/s320/AdobeStock_276356125.jpeg"/></a></div>
こんにちは。オフィス狛 技術部のmmm(むー)です。<br><br>
担当しているプロジェクトで、Laravelのコマンドラインで実行するバッチ処理がいくつかあるのですが、ユーザー数が日に日に増えていき実行時間が長くなっていきました。<br><br>
普段は自動で実行しているので特に問題ないのですが、先日テストのため手動でバッチを実行しないといけないことがあり、<br><br>
(この処理いつ終わるんだろう・・・・)<br><br>
となってしまいました😖<br><br>
処理を実行して放置すれば良いとしても、目安もわからず、都度確認するのは大変だと思いますので、今回はコマンド実行の処理とプログレスバーの作成方法をご紹介いたします。<br><br>
<h2>前提条件</h2>
<div style="font-weight: bold">■Laravel バージョン</div>
<pre><code>$ php artisan -V
Laravel Framework 8.27.0
</code></pre><br><br>
<h2>1. コマンドを作成する</h2>
コマンドを作成します。今回は例として、ランキング作成の処理としました。
<pre><code># php artisan make:command [コマンド名]
$ php artisan make:command CreateRankingCommand
</code></pre><br>
自動で作成されたファイルを確認すると下記のようになっています。
<pre><code>// app/Console/Commands/CreateRankingCommand.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CreateRankingCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
return 0;
}
}
</code></pre><br>
まず、2つの項目を設定します。<br>
1. <span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">$signature</span>に、コマンド名を設定します。ここに設定した名前を使用して、コマンドラインで処理を実行することになります。<br>
2. <span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">$description</span> に、このコマンドの説明を記載します。<br><br>
修正すると、以下のようになりました。<br>
<pre><code> /**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:create_ranking'; // 修正しました
/**
* The console command description.
*
* @var string
*/
protected $description = 'ランキングを作成します。'; // 修正しました
</code></pre><br>
次に、コマンドが実行できるか確認します。<br><br>
処理内容は<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">handle()</span>内に本来記載しますが、初期状態から何も修正していないので、特にエラー等が表示されなければ問題ありません。
<pre><code># php artisan command:[コマンド名]
$ php artisan command:create_ranking
</code></pre><br>
また、以下のようにコマンドが作成できているか確認することもできます。<br>
<pre><code>$ php artisan list | grep command:
command:create_ranking ランキングを作成します。
</code></pre><br><br>
<h2>2. プログレスバーを作成する</h2>
コマンドが実行できることは確認できたので、処理とプログレスバーを作成します。<br><br>
処理内容は、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">handle()</span>メソッドの中に記載します。<br>
大変簡素ですが、今回はただループするだけの処理を作成します。<br>
<pre><code>public function handle()
{
$count = 10;
for ($i = 1; $i <= $count; $i++) {
// 本来、ここで何か処理を行います
}
return 0;
}
</code></pre><br>
ここにプログレスバーを表示する処理を追記していきます。<br>
コードの説明としては、下記①②③のコメントに記載した通りです。
<pre><code>public function handle()
{
$count = 10;
// ①処理の総ステップ数を指定します
$progressBar = $this->output->createProgressBar($count);
for ($i = 1; $i <= $count; $i++) {
// 本来、ここで何か処理を行います
// ②プログレスバーの表示を1つ分進めます
$progressBar->advance();
}
// ③プログレスバーの表示を完了状態にします
$progressBar->finish();
return 0;
}
</code></pre><br>
処理を実行すると下記のようになります。<br>
※この処理は中身がないため、すぐ終わります。
<pre><code>$ php artisan command:create_ranking
10/10 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
</code></pre><br><br>
とても簡単に処理の進捗度を表示できるようになりました!<br><br>
以上となります。<br>
参考にして頂ければ幸いです。<br><br>
<!-- 編集禁止(ここから) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/darcula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- 編集禁止(ここまで) -->オフィス狛 技術部 mmmhttp://www.blogger.com/profile/02929539829077086029noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-6343486195776307612022-10-03T15:10:00.001+09:002022-10-03T15:10:10.035+09:00【Python/Flask】AWSのALB(プロキシ)経由でWebシステムにアクセスしたらHTTPSがHTTPに書き換わって盛大にハマった話。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPq6YTiV7tzBcVFLTMBhR7Y0DvDNBzo-OLCan94aw9hoLuJwcKh8GeLEmYnsWFD8bUIZdwNDmRXLl6XEnmut9QH5c6Kf9VV-1L9w28GI8Mh8IIn9KBIJ_J3nGTLZ7aCIP0i3KViwn1lHD-0evFd8_Q6PYOSReLrn-l8udt07ar_NAx7D8gH3Xw9RPI3w/s424/Fotolia_121084901_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="283" data-original-width="424" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPq6YTiV7tzBcVFLTMBhR7Y0DvDNBzo-OLCan94aw9hoLuJwcKh8GeLEmYnsWFD8bUIZdwNDmRXLl6XEnmut9QH5c6Kf9VV-1L9w28GI8Mh8IIn9KBIJ_J3nGTLZ7aCIP0i3KViwn1lHD-0evFd8_Q6PYOSReLrn-l8udt07ar_NAx7D8gH3Xw9RPI3w/s320/Fotolia_121084901_XS.jpg"/></a></div>
技術部のyuckieee(ゆっきー)です。<br/>
今回は、PythonのFlaskフレームワークを使用し、Webシステム構築をした際にハマった事象について、解決策と合わせて、ご紹介しようと思います。<br/>
<br/>
<h3><u>■Webシステム概要</u></h3>
発生事象の説明をする前に、まずは今回開発を行ったWebシステムの概要について共有しておきます。超ザックリとした概要ではありますが、以下のような構成・仕様となっていました。<br/>
<br/>
【システム構成】<br/>
・ALB(ロードバランサー)<br/>
・EC2(Web/APサーバ)<br/>
- OS:RedHatLinux8.x(EC2)<br/>
- Web/AP:Apache 2.4.xx(mod_wsgiでFlaskと連携)<br/>
- 言語:Python 3.8<br/>
- フレームワーク:Flask<br/>
・通信プロトコル<br/>
- クライアント⇔ALB:HTTPS(TCP/443※)<br/>
※HTTPで接続された場合でもALB側でHTTPSに変換して、リクエスト自体は受け付ける<br/>
- ALB⇔EC2:HTTP(TCP/80)<br/>
<br/>
(構成イメージ)
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGE3Bb3RDO4-iUaObzPzD6jbjaJ_wXnCs1dBnfgEhQPZQuvhCon7FyUFqXm3SlEV5w_7wfoyHU-cgtFG3yjSCjaiqfIdLK-js2jp3_ceBNtrysSLEYdjLqVytYpC4LM6OgkCdeMbQe0kUjWGkc_Gq1NdHxfflh3GdC43wPN79JVm18Dy2qUeU42RB8UQ/s774/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-10-02%2021.04.19.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="400" data-original-height="394" data-original-width="774" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGE3Bb3RDO4-iUaObzPzD6jbjaJ_wXnCs1dBnfgEhQPZQuvhCon7FyUFqXm3SlEV5w_7wfoyHU-cgtFG3yjSCjaiqfIdLK-js2jp3_ceBNtrysSLEYdjLqVytYpC4LM6OgkCdeMbQe0kUjWGkc_Gq1NdHxfflh3GdC43wPN79JVm18Dy2qUeU42RB8UQ/s400/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-10-02%2021.04.19.png"/></a></div>
<br/>
【Webシステムの仕様】<br/>
・BASIC認証を使用して、ログイン認証を行う<br/>
・ログイン認証後、トップページでユーザ情報をセッションに格納して他ページで使用する<br/>
・他ページはユーザ情報必須のため、トップページ以外へのダイレクトアクセス※は非許可<br/>
※ブラウザにURLを直接入力したり、お気に入りからアクセスした場合など<br/>
・ダイレクトアクセス検知時は、トップページに強制遷移させ、必ずトップページ経由とさせる<br/>
<br/>
(画面遷移イメージ)
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSts0P1vGy5Xm6rJx94sBM_HaEHRnZku8zZ2NPlJBb6sL7S-1a5fCxJSmg0g6wwAZUap-AzlAOVCF-ugMulqr6ugUR42InM9ckuqTZzZCyoVV_aydGjrpyTgjPlveapRdBXP0pTwo1zSat0MpruFt73zhr6_sacEAmsesEhch-S2isHWgSARGjQgPeZw/s996/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2017.56.04.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="400" data-original-height="880" data-original-width="996" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSts0P1vGy5Xm6rJx94sBM_HaEHRnZku8zZ2NPlJBb6sL7S-1a5fCxJSmg0g6wwAZUap-AzlAOVCF-ugMulqr6ugUR42InM9ckuqTZzZCyoVV_aydGjrpyTgjPlveapRdBXP0pTwo1zSat0MpruFt73zhr6_sacEAmsesEhch-S2isHWgSARGjQgPeZw/s400/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2017.56.04.png"/></a></div>
<br/>
<h3><u>■発生した事象</u></h3>
Webシステム開発が完了し、システム構成で説明した通りの通信経路となるよう、Webシステムにアクセスする際のURLを以下のように変更しました。(URLはイメージです)<br/>
<br/>
【開発時】<b><font color="FF0000">http</font></b>://websystem.com/<br/>
※Webシステムのあるサーバに直接アクセスするために設定されたWebシステムのURL<br/>
↓<br/>
【開発完了後】<b><font color="FF0000">https</font></b>://alb.websystem.com/<br/>
※AWSのALBを介してアクセスするために設定されたWebシステムのURL<br/>
<br/>
そして、動作確認をしようと【開発完了後】のURLにアクセスしたのですが、通常アクセス時のとおりトップページから他ページに遷移しようとしても、トップページへの強制遷移が発生し、他ページに遷移ができない状態に陥りました。<br/>
明日からユーザ側で試験利用と言っているのに、軽くパニックです(笑)<br/>
<br/>
(画面遷移イメージ)
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidxpzATQpgfzDZweOL7hJxigJ3kY13S9BNy9KsLdNufpX2ugFIFEkYGiMCmMlxrAnrO44a28IjZcs5z32uvhdMQ4VMf2zN2aMscUGENff1ca_BY3CDmHjL8hyidunkpz8tg1wmNIJ3pQa5tfdKB8N2emilvPGiX-wIZAhqKfbMUC2qCfVvC2ybkwYzEw/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2018.01.33.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="398" data-original-width="1088" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidxpzATQpgfzDZweOL7hJxigJ3kY13S9BNy9KsLdNufpX2ugFIFEkYGiMCmMlxrAnrO44a28IjZcs5z32uvhdMQ4VMf2zN2aMscUGENff1ca_BY3CDmHjL8hyidunkpz8tg1wmNIJ3pQa5tfdKB8N2emilvPGiX-wIZAhqKfbMUC2qCfVvC2ybkwYzEw/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2018.01.33.png"/></a></div>
<br/>
<h3><u>■直接原因</u></h3>
Webシステムの仕様から考えると、ダイレクトアクセス検知によってトップページに強制転送されているのだろうと感じていました。
そして、その直感は当たっており、この仕様に絡んで以下2つの仕組みにより起こった問題であることが分かりました。<br/>
<h4><b>① 相対URLでの画面遷移</b></h4>
Webシステムのページ遷移に使用するURLは、Flaskの<code class="inline-code">url_for</code>というメソッドを使用しており、<b><u>このメソッドで生成されるURLは相対URLがデフォルト</u></b>となっており、このWebシステムでもデフォルト指定にて使用していました。<br/>
今回の場合、直前のアクセス元であるALBが<code class="inline-code">https → http</code>に書き換えてリクエストを投げてきているため、受け取った<code class="inline-code">http://~</code>から始まる絶対パスを元に、相対指定でURLが作成されて画面遷移されることになりました。<br/>
<h4><b>② HTTPSサイトからHTTPサイト遷移によるリファラ削除</b></h4>
突然何だ?!と思うかもしれませんが、このWebシステムではダイレクトアクセスの検知を、リクエストヘッダ内にあるリファラ(遷移元URL情報)の存在チェックで行っていました。<br/>
想定では、ダイレクトアクセスの場合、リファラには遷移元URLが入っていないため、ここをチェックすることでダイレクトアクセスの判定が可能と考えていたためです。<br/>
ですが、①の画面遷移を受け付けたブラウザは「HTTPSサイト(安全)」から「HTTPサイト(非安全)」への遷移が発生したと検知し、<b><u>セキュリティリスク回避のためリファラの内容を削除</u></b>してリクエストしていました。<br/>
<br/>
その結果、リクエストを受け取ったWebシステムは、リファラなし(=ダイレクトアクセス)と判断し、トップページに強制転送していた訳ですね。<br/>
<br/>
リファラという用語がピンとこない方は、公式ページを参照してみてください。<br/>
<a href=https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Referer>Referer - HTTP - MDN Web Docs</a>
<br/>
<h3><u>■根本原因</u></h3>
ここまで、実際に事象を引き起こした仕組みについて説明してきました。<br/>
ですが、そもそもAWSのALBを経由しなければ、正常に動作していたわけです。なのに、なぜこの事象が発生したのでしょうか?<br/>
<br/>
それは、<u>AWSのALBを経由したことでリクエスト情報が書き換わった</u>ためです。<br/>
<br/>
今回でいうと、ALBからEC2(Web/APサーバ)への転送時に通信プロトコルを<code class="inline-code">https → http</code>に書き換えており、転送リクエストを受け取ったEC2(Web/APサーバ)から見るとリクエスト元はALBなので何も間違ってはいませんし、そういう仕様なのです。<br/>
そのため、開発者は、プロキシ経由が発生すると分かった時点で、クライアントのリクエスト情報を正確に取得できるよう設計考慮が必要でした。<br/>
<br/>
<h3><u>■解決方法</u></h3>
それでは、実際に根本原因に対処していきましょう。<br/>
今回は、クライアントの情報をちゃんと取得できれば解決するわけです。<br/>
でも、リクエスト情報が書き換えられてるのにどうすれば?<br/>
<br/>
ご安心ください!<br/>
<br/>
今回使用したALBの場合、経由前のリクエスト情報が別のヘッダーに退避されています。<br/>
それが<b>「X-Forwarded-xxx」ヘッダー</b>です。<br/>
「X-Forwarded-xxx」ヘッダーには、いくつか種類があり、通信プロトコルが格納されているヘッダーは「X-Fowarded-Proto」となります。
<br/><br/>
<table border="1">
<tr>
<th bgcolor="#aaaaaa">ヘッダー名</th>
<th bgcolor="#aaaaaa">説明</th>
</tr>
<tr>
<td width="130" bgcolor="#eeeeee"><strong>X-Forwarded-For</strong></td>
<td>プロキシ経由時、クライアントのIPアドレスが格納されます。複数プロキシ経由時はカンマ区切りで表記され、プロキシ経由ごとに右端にIPアドレスが追加されていきます。</td>
</tr>
<tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>X-Forwarded-Host</strong></td>
<td>リクエストヘッダー内でクライアントから要求された元のホストを特定するための事実上の標準となっているヘッダーです。</td>
</tr>
<tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>X-Forwarded-Proto</strong></td>
<td>プロキシまたはロードバランサーへ接続するのに使っていたクライアントの通信プロトコル (HTTP または HTTPS) を特定するために事実上の標準となっているヘッダーです。</td>
</tr>
</table>
<br/>
ただし、これらのヘッダーは標準化されたものでなく、書き換え容易且つ、経由するプロキシ(今回はALB)によって挙動が異なる可能性もあるので、ご注意ください。ALBの仕様については以下に記載がありました。<br/>
<a href=https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/x-forwarded-headers.html>HTTP ヘッダーと Application Load Balancer</a><br/>
<br/>
なお、「X-Forwarded-xxx」ヘッダーに関する詳細は以下を参照して確認してみてください。<br/>
※「X-Fowarded-xxx」は、現状「X-Forwarded」ヘッダに移行されるようなので「X-Fowarded」ヘッダーに関するリンクも張っておきます。<br/>
<a href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Forwarded-Proto">X-Forwarded-Proto - HTTP - MDN Web Docs</a><br/>
<a href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Forwarded">Forwarded - HTTP - MDN Web Docs - Mozilla</a><br/>
<a href="https://www.rfc-editor.org/rfc/rfc7239#section-4">RFC 7239 Forwarded HTTP Extension</a><br/>
<br/>
ここまでの説明から、この事象を解決するには「X-Fowarded-xxx」ヘッダーにあるプロトコル情報を使えば良い。というのが、ふんわり頭に浮かんだのではないかと思います。<br/>
それでは、このWebシステムでは、どのように「X-Forwarded-Proto」ヘッダーの値で書き換えればよいのでしょうか?<br/>
自分でゴリゴリ実装することも可能ですが、今回使用したPythonのフレームワークであるFlaskでは、既に対応するミドルウェアが提供されていました。<br/>
それが「X-Forwarded-For Proxy Fix」というミドルウェアです。<br/>
<br/>
<a href="https://werkzeug.palletsprojects.com/en/2.2.x/middleware/proxy_fix/">X-Forwarded-For Proxy Fix - Werkzeug</a><br/>
<br/>
このミドルウェアを使用することで、経由(信頼)するプロキシ数に応じてリクエスト元情報の補正を行うことが可能です。
使用方法などの詳細は、上記公式ページを参照して、確認してみてください。<br/>
具体的な実装例は以下となります。<br/>
<br/>
<pre class="prettyprint linenums lang-python">
from werkzeug.middleware.proxy_fix import ProxyFix
def app():
~略~
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
~略~
</pre>
<br/>
インスタンス作成時に今回問題となった「X-Forwareded-Proto」ヘッダの値を使用して、通信プロトコル情報が補正されるように設定します。
公式を参照すると、「X-Fowareded-Proto」ヘッダーを補正したい場合は、引数に<code class="inline-code">x_proto=(信頼する)プロキシ経由数</code>の形式で指定すれば良いようです。<br/>
ヘッダーごとに引数が異なるようなので、詳細は公式参照ください。<br/>
<br/>
上記を設定することで、無事問題は解決することが出来ました。<br/>
<br/>
<b><おまけ></b><br/>
対処方法を色々と探していたのですが、他の方法もあるようでした。<br/>
プロキシによっては「X-Forwarded-xxx」ヘッダーが設定されていないなどもあると思うので、参考までに紹介しておきます。
ただし、ヘッダーが取得可能な時は、わざわざ手間を増やしたり、セキュリティリスク高める必要はないと思うので、推奨は致しません。<br/>
<br/>
<b><u>・直接原因① Flaskが認識するリクエスト元プロトコルを"HTTPS"固定に変更する</u></b><br/>
Flaskのインスタンス生成時、リクエスト元のプロトコル情報を直接"HTTPS"(固定)に書き換えることで対処する方法です。以下、実装例です。<br/>
<pre class="prettyprint linenums lang-python">def create_app():
...省略...
class SchemeFix:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
environ['wsgi.url_scheme'] = 'https'
return self.app(environ, start_response)
app = Flask(__name__)
app.wsgi_app = SchemeFix(app.wsgi_app)
return app
</pre>
<br/>
<b><u>・直接原因② リファラが削除されないように設定を変更する</u></b><br/>
直接原因②はリファラが削除されるのが問題なので、削除されないようにすればいいじゃん!という対策です。対象のWebページ(HTML)に対して、以下のようなメタ情報を定義してリファラの送信制限を緩和させます。
<pre class="prettyprint linenums"><meta name="referrer" content="origin-when-crossorigin"></pre>
<br/>
上記<code class="inline-code">content="origin-when-crossorigin"</code>の記載が該当箇所になります。<br/>
この設定の場合、同一のプロトコル水準 (HTTP→HTTP, HTTPS→HTTPS) で同一オリジンのリクエストを行う場合はオリジン、パス、クエリー文字列が送信され、オリジン間リクエストや安全性の低下する移動先 (HTTPS→HTTP) ではオリジンのみを送信します。<br/>
今回のダイレクトアクセスチェックでは、リファラの値有無を確認しているため、オリジンのみでも値があれば事象は解消可能となります。<br/>
全ての情報が必要な場合は<code class="inline-code">content="unsafe-url"</code>を設定することでリファラを取得可能ですが、安全性の面から非推奨となっているため、使用は回避するべきだと思います。<br/>
<br/>
<a href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Referrer-Policy">Referrer-Policy - HTTP - MDN Web Docs</a><br/>
<br/>
<h3><u>■まとめ</u></h3>
Webシステムだけでなく、インフラも含めた全体的な視点でシステム構築をすることの難しさと面白さを知りました。<br/>
徐々に仕組みを理解し、謎が解けた時は本当に楽しいですね。<br/>
今後も幅広い領域の経験を積みながら、レベルアップしていければと思います。<br/>
<!--編集禁止(ここから)-->
<style>
.inline-code {
background: #f0f5f9;
border: 1px solid #e6edf3;
padding: .04em .3em;
margin: 0 .2em;
border-radius: 3px;
line-height: 1.4;
font-size: .85em;
}
</style>
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/default.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<script>
hljs.initHighlightingOnLoad();
hljs.initLineNumbersOnLoad();
</script>
<link href="/styles/shThemeEmacs.css" rel="Stylesheet" type="text/css"></link>
<!--編集禁止(ここまで)-->
オフィス狛 技術部 yuckieeehttp://www.blogger.com/profile/18033915954934523504noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-42108478130022420772022-10-03T13:59:00.003+09:002022-10-03T14:45:51.720+09:00Visual Studioでデバッグ時に「ID XXXXXのプロセスは実行されていません」のエラーからの復帰方法。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHTJKHelZxrD5LPxpMcwfQYm6QYxe_9lMvdnxeZzi4Zr6JBGSmBZr1Kxk105G6bl-4gA1sYArWxgTl6OYPG4ya_gR2g-1nj_LINn4zjOJg88J_EBkeI8g4snWF-DjwqVAUrfIJv8CkEaZ5DfIU0eQprUXg9sd1j5crF_ZIkuOm1zps_Rb-YykxM-t1Sw/s447/Fotolia_231814718_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="268" data-original-width="447" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHTJKHelZxrD5LPxpMcwfQYm6QYxe_9lMvdnxeZzi4Zr6JBGSmBZr1Kxk105G6bl-4gA1sYArWxgTl6OYPG4ya_gR2g-1nj_LINn4zjOJg88J_EBkeI8g4snWF-DjwqVAUrfIJv8CkEaZ5DfIU0eQprUXg9sd1j5crF_ZIkuOm1zps_Rb-YykxM-t1Sw/s320/Fotolia_231814718_XS.jpg"/></a></div>
オフィス狛 技術部のHammarです。<br />
<br />
先日Visual StudioでIIS Expressからデバッグ時に、急に「ID XXXXXのプロセスは実行されていません」と表示されるようになり、全くデバッグが出来なくなりました。<br />
いろいろ先人の復帰方法を見ながら試してみても、どうもうまくいかず軽くハマっていたんですが、意外と簡単な方法で復帰したので、お知らせしたいと思います。<br />
<br />
<h3>実行環境<hr style="margin:0px;"></h3>
<ul>
<li>Windows10</li>
<li>Visual Studio 2017</li>
- IIS Express(Google Chrome)よりデバッグ
</ul>
<br />
<h3>事象<hr style="margin:0px;"></h3>
先日までは普通にデバッグ開始ボタンよりデバッグできていて、ある日同じようにデバッグ開始したところ、起動してブラウザは開くんですが、「このサイトにアクセスできません」のエラーが発生して、上手く接続できていない状態になっていました。<br />
その時、Visual Studioもデバッグモードになっているものの、設定したデバッグポイントが、「ブレークポイントは、現在の設定ではヒットしません。このドキュメントのシンボルが読み込まれていません。」と表示されてしまっていました。<br />
なので、一旦デバッグを停止し、もう一度デバッグを開始したところ、下記のように「ID XXXXXのプロセスは実行されていません」が表示され、以降はデバッグをやり直しても、Visual Studioを起動しなおしても同エラーが出続けてしまう状態に陥ってしまいました。<br />
<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4zF-ejE8aR3I-Ejhxj89rpIpKFdPahN9Rze6P1K86RCdEZfW83epr3aXntJp2Q0AyLr71g3NS9CeWrzF0l0dA1Y8gV-dQaW7NF-YhTqiA0pOa9VOKM_2hjClw9eaR_8WRE3-pXsnBX8Ii9z8GI5aV7RZha1-nI7CXap427VEFsLnXxc_dirgryEh8/s282/%E3%83%96%E3%83%AD%E3%82%B0%E7%94%A8%E3%82%A8%E3%83%A9%E3%83%BC%E8%A1%A8%E7%A4%BA.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="155" data-original-width="282" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4zF-ejE8aR3I-Ejhxj89rpIpKFdPahN9Rze6P1K86RCdEZfW83epr3aXntJp2Q0AyLr71g3NS9CeWrzF0l0dA1Y8gV-dQaW7NF-YhTqiA0pOa9VOKM_2hjClw9eaR_8WRE3-pXsnBX8Ii9z8GI5aV7RZha1-nI7CXap427VEFsLnXxc_dirgryEh8/s320/%E3%83%96%E3%83%AD%E3%82%B0%E7%94%A8%E3%82%A8%E3%83%A9%E3%83%BC%E8%A1%A8%E7%A4%BA.png"/></a></div>
<h3>解消方法<hr style="margin:0px;"></h3>
調べていくと、似たような状態になっている記事をいくつか見つけたので、手っ取り早くできる方法を2つ3つやってみたのですが、どうも解決しませんでした。<br />
(「IIS Expressサーバーを起動できません。」というエラーと一緒に発生している記事の内容を主に試してみたので、ちょっと今回の事象とは微妙に違ったのかもしれません)<br />
で、調べをすすめていくと、簡単に解消できた方法が以下の内容です。
<br />
<h4>■csprojファイルの下記3行を削除する</h4>
<pre class="prettyprint linenums">
<DevelopmentServerPort>xxxxx</DevelopmentServerPort>
<DevelopmentServerVPath></DevelopmentServerVPath>
<IISUrl>http://localhost:xxxxx/</IISUrl>
</pre>
<br />
上記はデバッグ時に起動するIIS ExpressのURLや、仮想開発サーバーのポート、パスを設定している部分なのですが、こちら3行を削除して、プロジェクトをリロードすれば、なんとあっさりデバッグが正常にできるようになりました。<br />
ちなみに、上記を削除しても、再度デバッグ実行したタイミングで、記述が自動的に再作成されるので、特に問題は無いようです。
<br />
<br />
また本エラーの解消方法調べていくと、結構対応方法が多様でバラバラなので、もしかしたら一概に、この方法で解決します!とはならないかもしれないのですが、とりあえず簡単に試せる方法ではあるので、もし同様のエラーに陥った方は、まずは一旦試していただければと思います。<br /><br />
オフィス狛 技術部 Hammarhttp://www.blogger.com/profile/10127726650091942134noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-42460369035654779202022-10-03T13:59:00.000+09:002022-10-03T14:45:49.088+09:00node.jsでnockライブラリを使って外部API呼び出しをモック化させる。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwyCGQ1SEiFBJ5EZU1nk6NcGhN2-cnRIDlZiDMU0FMIXNXp4QrCWvpm-jAfVujlxWZZM6PoEavWX569zj8IWSbRrGZl58neW16oikw_GGlQnFBYKVoK9tF3fYG2CI4LgVIKooMhuBBE_ltBn8IblvhAGWyRp8Vqp7svEjiS802MNMdGdz_0r0usNFd3w/s371/Fotolia_61107255_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" height="320" data-original-height="371" data-original-width="323" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwyCGQ1SEiFBJ5EZU1nk6NcGhN2-cnRIDlZiDMU0FMIXNXp4QrCWvpm-jAfVujlxWZZM6PoEavWX569zj8IWSbRrGZl58neW16oikw_GGlQnFBYKVoK9tF3fYG2CI4LgVIKooMhuBBE_ltBn8IblvhAGWyRp8Vqp7svEjiS802MNMdGdz_0r0usNFd3w/s320/Fotolia_61107255_XS.jpg"/></a></div>
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js?skin=sunburst&lang=js"></script>
オフィス狛 技術部のHammarです。<br />
<br />
node.jsでAPIを実装しているプロジェクトにユニットテストを実施することになり、いろいろと自動化させています。<br />
このユニットテストにおいて、外部のAPIを呼び出す処理は、実際に外部APIを呼ばずに、APIモックを作成してテストするということになり、今回<b>nock</b>というライブラリを使って簡単にモックを作成ができたので、ご紹介したいと思います。<br /><br />
<h3>実行環境<hr style="margin:0px;"></h3>
※ユニットテストではjestを使います
<ul>
<li>Node.js v12.14.0</li>
<li>jest v27.5.1</li>
<li>nock v13.2.4</li>
</ul>
<br />
<br />
以下代表的なGETとPOSTのテストについてサンプルを書いてみます。<br />
<h3>GETの場合<hr style="margin:0px;"></h3>
テスト対象となるプログラムの例として、以下GETリクエストのプログラムを作成します。<br />
<b>■testGetRequest.js</b>
<pre class="prettyprint lang-js">
const request = require('request');
exports.getUserInfo = async () => {
// リクエスト作成
const requests = {
url: 'https://test.com/users',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
json: true,
};
// リクエスト送信
return new Promise((resolve, reject) => {
request(requests, async (error, response, body) => {
if (error) {
reject(error);
} else if (response.statusCode !== 200) {
reject(response);
} else {
resolve(body);
}
});
});
};
</pre>
上記をnockを使ったテストコードを書くと以下のようになります。<br />
<b>■testGetRequest.test.js</b>
<pre class="prettyprint">
const nock = require('nock');
describe('ユーザー情報取得のTest', () => {
let resData;
beforeEach(() => {
// レスポンス
resData = {
user_id: '1',
user_name: 'テスト太郎',
};
});
afterEach(() => {
nock.cleanAll();
jest.clearAllMocks();
});
it('ユーザー情報取得リクエスト', async () => {
// APIリクエストのモック
nock('https://test.com')
.get('/users')
.reply(200, resData);
const repos = await testGetRequest.getUserInfo();
expect(repos.user_id).toEqual('1');
expect(repos.user_name).toEqual('テスト太郎');
expect(nock.isDone()).toBe(true);
});
</pre>
上記の<i>nock('https://test.com').get('/users')</i
>が、テスト対象プログラム(testGetRequest.js)の<i>getUserInfo()</i>で呼んでいるAPIリクエストのパスと一致、かつHTTPメソッドも同じにすることによって、<i>resData</i>に記述したモックするレスポンス内容を返すことができるようになります。<br />
ちなみに<i>nock.isDone()</i>で、リクエストをしたかどうかを判定できます。<br />
<br />
続いて、POSTリクエストの例が以下になります。<br />
<h3>POSTの場合<hr style="margin:0px;"></h3>
<b>■testPostRequest.js</b>
<pre class="prettyprint">
const request = require('request');
exports.entryUserInfo = async (userId, userName) => {
// リクエストBODY
const bodyData = {
user_id: userId,
user_name: userName,
};
// リクエスト作成
const requests = {
url: 'https://test.com/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: bodyData,
json: true,
};
// リクエスト送信
return new Promise((resolve, reject) => {
request(requests, async (error, response, body) => {
if (error) {
reject(error);
} else if (response.statusCode !== 200) {
reject(response);
} else {
resolve(body);
}
});
});
};
</pre>
上記のテストコードを書くと以下のようになります。<br />
<b>■testPostRequest.test.js</b>
<pre class="prettyprint">
const nock = require('nock');
describe('ユーザー情報登録のTest', () => {
let userId;
let userName;
let reqBody;
let resData;
beforeEach(() => {
userId = '1';
userName = 'テスト太郎'
reqBody = {
user_id: userId,
user_name: userName,
};
// レスポンス
resData = {
message: '登録完了しました',
};
});
afterEach(() => {
nock.cleanAll();
jest.clearAllMocks();
});
it('ユーザー情報登録リクエスト', async () => {
// APIリクエストのモック
nock('https://test.com')
.post('/users', reqBody)
.reply(200, resData);
const repos = await testPostRequest.entryUserInfo(userId, userName);
expect(repos.message).toEqual('登録完了しました');
expect(nock.isDone()).toBe(true);
});
});
</pre>
GETの時と同様に、リクエストのパスとHTTPメソッドを一致させることでモック作成が可能となります。<br />
POSTの場合は、上記<i>.post('/users', reqBody)</i
>でリクエストボディの内容を設定できます。<br />
ちなみに、テストを実行した後は、<i>nock.cleanAll()</i>でモックをクリアする必要がありますので、忘れずに。<br />
<br />
上記のように、思っていた以上に楽にAPIリクエストのモックを作成することが出来るようになりました。<br />
<a href="https://github.com/nock/nock" target="_blank" rel="nofollow">公式の情報</a
>を確認すると、さらに細かくモック設定できそうなので、細かなテストにも対応できそうです!<br /><br />
オフィス狛 技術部 Hammarhttp://www.blogger.com/profile/10127726650091942134noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-15922832505903184352022-09-30T15:21:00.003+09:002022-10-04T17:51:40.673+09:00Photoshopの「書き出し形式」で数倍に書き出した時に画質が劣化してしまうオブジェクト・劣化しないオブジェクトの違い。<div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzRZvRUZ3xD-jQ6TltS_kH4I8EFsQZxevAzO1yZjHrxHW-SY40zAb90ybjlFnPo6werKHcRdINnkq0fTG7nf7AAu-F5YXsCw2xYFXyLOetvaU8n31BtJe5juXI9hQvXa_-Q4wiqqFV8YUmOoWohzKVCnKxsXoggEc3g1CFazt6qlePm2W2AJHnfHyA4A/s500/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%206%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="320" data-original-width="500" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzRZvRUZ3xD-jQ6TltS_kH4I8EFsQZxevAzO1yZjHrxHW-SY40zAb90ybjlFnPo6werKHcRdINnkq0fTG7nf7AAu-F5YXsCw2xYFXyLOetvaU8n31BtJe5juXI9hQvXa_-Q4wiqqFV8YUmOoWohzKVCnKxsXoggEc3g1CFazt6qlePm2W2AJHnfHyA4A/s320/%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9B%E3%82%99%E3%83%BC%E3%83%88%E3%82%99%206%20%E3%81%AE%E3%82%B3%E3%83%92%E3%82%9A%E3%83%BC.png" width="320" /></a></div><br /><span style="text-align: left;"><br /></span></div><p style="text-align: left;">こんにちは、オフィス狛 デザイン部のSatoです。</p>
<p><br /></p><p>初歩的な話なのですが、Photoshopの書き出し形式機能で小さい画像を2倍など大きいサイズを指定して書き出すと一部のオブジェクトの画質が劣化し、ぼやけてしまうことがあります。</p>
<p>過去に無知故に2倍(2x)で書き出せるなら…と作成したデータを2xで書き出したところ、画像の一部だけが少し劣化してぼやけてしまったことがありました。</p><p>この時、画像全体の画質が劣化する訳では無く画像の一部だけ劣化するのが不思議で、画質が落ちる箇所・画質が落ちない箇所の差が何なのかが気になりました🤔</p>
<p>今回は、Photoshopの「書き出し形式」で、元サイズより大きい書き出した時に画質が落ちてしまうオブジェクト・綺麗なオブジェクトの違いについて解説しようと思います。</p>
<iframe class="hatenablogcard" frameborder="0" scrolling="no" src="https://hatenablog-parts.com/embed?url=https://blog.officekoma.co.jp/2018/12/photoshop.html" style="height: 175px; max-width: 680px; width: 100%;" title="%title%"></iframe>
<p>Photoshopの書き出し形式機能自体は、私が書いた上記の記事でも解説しています。<br />
合わせて読んでいただけると、書き出し形式機能についての基礎もわかりますよ。
</p>
<div><br /></div><div><br /></div><div><br /></div>
<p>書き出し形式機能を利用して、拡大して書き出した場合に画質が落ちぼやけるオブジェクト・画質が落ちずぼやけないオブジェクトの一覧表を作ってみました。</p>
<p>この一覧表自体、Photoshopの書き出し機能を利用しキャンバスサイズの3倍(3x)のサイズで書き出してみたので、クリックして拡大表示してみてくだい👇</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGDHiHX7p6MIiHT3pdJmZhY3be-zVNtSeYBSvM1Ei7Qgxe3WVZQzmDPSzCapQkAPQQUdHKE6vIBQ7Qe-QGTYOtPXYz-1kPfEqvtbZn8_hoN4OQNBg6mCXAticaTEXVrPJ8R4sSAjBz85xR0v4pvrdSpnwvUl4PyiuSfbY2JQXtZkQwhg-IWqy5Kv8xEQ/s1104/%E6%9B%B8%E3%81%8D%E5%87%BA%E3%81%97@3x.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="1044" data-original-width="1104" height="606" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGDHiHX7p6MIiHT3pdJmZhY3be-zVNtSeYBSvM1Ei7Qgxe3WVZQzmDPSzCapQkAPQQUdHKE6vIBQ7Qe-QGTYOtPXYz-1kPfEqvtbZn8_hoN4OQNBg6mCXAticaTEXVrPJ8R4sSAjBz85xR0v4pvrdSpnwvUl4PyiuSfbY2JQXtZkQwhg-IWqy5Kv8xEQ/w640-h606/%E6%9B%B8%E3%81%8D%E5%87%BA%E3%81%97@3x.png" width="640" /></a></div>
<p>同じ画像内でも、こんなに画質が変わってきます。</p><p>ぼやけないカテゴリの画像は画質が落ちていませんが、ぼやけるカテゴリに入れている画像はかなり画質が劣化してしまいにじんでしまっています😞</p><div><br /></div>
<p>さて、この画質が劣化する・しないの違いは何かというと……。<br /><b><span style="color: #ffa400;">スマートオブジェクト</span></b>以外は、「<b><span style="color: #ffa400;">ラスタ形式のオブジェクト</span></b>」か「<b><span style="color: #ffa400;">ベクタ形式のオブジェクト</span></b>」かです。</p>
<div><br /></div>
<p>ラスタ形式のオブジェクトは、色のついたピクセルで作られたデータで、拡大するとぼやけてしまいます。</p><p>ベクタ形式のオブジェクトは、点と曲線の数値で作られたデータで、拡大してもぼやけません。</p>
<p>ラスタ形式とベクタ形式についての解説は、アドビの<a href="https://helpx.adobe.com/jp/photoshop-elements/key-concepts/raster-vector.html">ラスター&ベクトル</a>が分かりやすいと思います。ぜひ読んでみてください。</p>
<div><br /></div><div><br /></div>
<p>しかし、上の説明で<br />
「Photoshopのようなグラフィックソフトで作成するオブジェクトは全部ラスタ形式のはずでは?」<br />
と疑問に思われる方もいらっしゃるかもしれません。</p>
<p>実はグラフィックソフトのPhotoshopでも、ベクタ形式のオブジェクトを扱っています!</p><p><br /></p>
<div><div>Photoshopのベクタ形式のオブジェクトをご紹介します👇</div>
<p>ペンツールやシェイプツール(図形ツール・カスタムシェイプ)などで製図した図形「<b><span style="color: #ffa400;">シェイプ</span></b>」<br />シェイプで製図した際に作られるレイヤー「シェイプレイヤー」は下記のような見た目になります。</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzxsAFFQ__JROV3SHOqgLaOf9DpaPZSqB-WUkX9OhlrEUb8Q2gMoXgS8n-1XfAvKb2TVrfD6R2MA8pR8EckTQDYgNk_D6rsrax4NeYFFDAqnl3-mXCenKKOV1DT81QnLaJHcPnIiPhovgno8JSxYDupFx7H9vxVuUpEuKpbbKbCUs3LwUzmvfdWHwRbg/s642/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.10.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="84" data-original-width="642" height="42" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzxsAFFQ__JROV3SHOqgLaOf9DpaPZSqB-WUkX9OhlrEUb8Q2gMoXgS8n-1XfAvKb2TVrfD6R2MA8pR8EckTQDYgNk_D6rsrax4NeYFFDAqnl3-mXCenKKOV1DT81QnLaJHcPnIiPhovgno8JSxYDupFx7H9vxVuUpEuKpbbKbCUs3LwUzmvfdWHwRbg/w320-h42/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.10.png" width="320" /></a></div><br /><p></p>
<p><br /></p><p>字ツールで文字を打ち込んだ際に作成される「<span style="color: #ffa400;"><b>テキスト</b></span>」<br />テキストを入力した際に作られるレイヤー「テキストレイヤー」は下記のような見た目になります。
</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAghx3PTbd0FwOVefzZHz2MotRMQRivqYntch2VgR55Xl2L1irmKrSYzu4lJoOA6sTB5b-sZGzJlu8PNZvUCKjue6vdtkjHFeqrR30j7jZlyrK2bbP86oOkUWssLrz9H6nODOZcfeJykB9izNQ_iz9koxLsxmO9bz5VfgPpA1KI4jqSMB54razgU7dtw/s640/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.12.10.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="86" data-original-width="640" height="43" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAghx3PTbd0FwOVefzZHz2MotRMQRivqYntch2VgR55Xl2L1irmKrSYzu4lJoOA6sTB5b-sZGzJlu8PNZvUCKjue6vdtkjHFeqrR30j7jZlyrK2bbP86oOkUWssLrz9H6nODOZcfeJykB9izNQ_iz9koxLsxmO9bz5VfgPpA1KI4jqSMB54razgU7dtw/s320/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.12.10.png" width="320" /></a></div><br /><p><br /></p>
<div>
<p>Illustratorで作成したオブジェクトをPhotoshopのレイヤーにコピー&ペーストした際に出るペースト形式選択で、スマートオブジェクトを選択した際に作成される「<b><span style="color: #ffa400;">ベクトルスマートオブジェクト</span></b>」</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsz9FHsXfuGODL2hCBO9hq76mMXa3SuEw57BrYl-GaPz40KnO7qFNNJJQE44IZXSEElnR8u4cyLlYH68527Xxeg5kco2VdmF222nvm8Dp-WksrbQWz2dQXVVB7Pa_AbPDxCLHgeU0slSg7B2VSntZoC8IRzIiRQ6YgqvxezUcV46BMvMP5iWwdJKRgDQ/s596/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2013.44.51.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="538" data-original-width="596" height="361" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsz9FHsXfuGODL2hCBO9hq76mMXa3SuEw57BrYl-GaPz40KnO7qFNNJJQE44IZXSEElnR8u4cyLlYH68527Xxeg5kco2VdmF222nvm8Dp-WksrbQWz2dQXVVB7Pa_AbPDxCLHgeU0slSg7B2VSntZoC8IRzIiRQ6YgqvxezUcV46BMvMP5iWwdJKRgDQ/w400-h361/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2013.44.51.png" width="400" /></a></div></div>
<div>
<p><br /></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgHlf9jnN-lTE0FBYn6rHKH4sFPKTX1FRrG1put7ot2CLrsvR8PBrqKn0awVg5pXP2zuCzjNkR7Edaz0PnfB0_srSQ_Q0chpa8frJEQg5-3UoP0ipOHqcj_P-Q1AUv54oYc4C3pLusabxNtmZx0OzcKDtq-VUE3ClTPzQpYmYw1sHVHaHsOGS9IsLPf5g/s648/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.46.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><br /></a></div><div><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgHlf9jnN-lTE0FBYn6rHKH4sFPKTX1FRrG1put7ot2CLrsvR8PBrqKn0awVg5pXP2zuCzjNkR7Edaz0PnfB0_srSQ_Q0chpa8frJEQg5-3UoP0ipOHqcj_P-Q1AUv54oYc4C3pLusabxNtmZx0OzcKDtq-VUE3ClTPzQpYmYw1sHVHaHsOGS9IsLPf5g/s648/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.46.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><br /></a><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgHlf9jnN-lTE0FBYn6rHKH4sFPKTX1FRrG1put7ot2CLrsvR8PBrqKn0awVg5pXP2zuCzjNkR7Edaz0PnfB0_srSQ_Q0chpa8frJEQg5-3UoP0ipOHqcj_P-Q1AUv54oYc4C3pLusabxNtmZx0OzcKDtq-VUE3ClTPzQpYmYw1sHVHaHsOGS9IsLPf5g/s648/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.46.png" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" data-original-height="96" data-original-width="648" height="59" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgHlf9jnN-lTE0FBYn6rHKH4sFPKTX1FRrG1put7ot2CLrsvR8PBrqKn0awVg5pXP2zuCzjNkR7Edaz0PnfB0_srSQ_Q0chpa8frJEQg5-3UoP0ipOHqcj_P-Q1AUv54oYc4C3pLusabxNtmZx0OzcKDtq-VUE3ClTPzQpYmYw1sHVHaHsOGS9IsLPf5g/w400-h59/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-26%2016.11.46.png" width="400" /></a></div></div><p>ベクトルスマートオブジェクトをペーストした際に作られるレイヤー「ベクトルスマートオブジェクトレイヤー」は下記のような見た目になります。</p><p><br /></p><p><br /></p><p>以上、3つのオブジェクトは全てベクタ形式のオブジェクトです。</p><div><br /></div><p>また、ラスタ形式ではありませんが、「<b><span style="color: #ffa400;">元の画像のサイズが書き出したサイズより大きいスマートオブジェクト</span></b>」は縮小しても元のサイズのデータを保持している為、劣化しません。</p><p>
しかし、スマートオブジェクトはあくまで元のサイズのデータを保持しているだけですので、「元の画像のサイズが書き出したサイズより小さいスマートオブジェクト」は劣化してしまいます。<br />
スマートオブジェクトも完璧という訳ではないのです。</p>
<div><br /></div><div><br /></div><div>まとめると、</div><div>「<b><span style="color: #ffa400;">シェイプレイヤー</span></b>」「<span style="color: #ffa400;"><b>テキストレイヤー</b></span>」「<b><span style="color: #ffa400;">ベクトルスマートオブジェクトレイヤー</span></b>」「<b><span style="color: #ffa400;">元の画像のサイズが書き出したサイズより大きいスマートオブジェクト</span></b>」</div><div>以上の4つのオブジェクトであれば、ラスタライズしない限りは画質が落ちる事なく等倍で書き出すことが可能です。</div></div><div><br /></div><div>逆に「通常レイヤーのオブジェクト」や「元の画像のサイズが書き出したサイズよりも小さいスマートオブジェクト」などのオブジェクトが含まれるキャンバスを等倍で書き出すのは、画質が落ちてしまうので、やめた方が良いでしょう。</div><div><br /></div><div>Photoshopを使われる方は、覚えておいて損はないと思います🙌</div>オフィス狛 デザイン部 Satohttp://www.blogger.com/profile/09225615773755838034noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-9997801726533003162022-09-30T10:24:00.001+09:002022-09-30T10:28:38.069+09:00【Sourcetree】カスタムアクションで特定コミット間の差分ファイルをサクッと抽出する。(Mac版)<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj_05bkhf_yOKHvyGfRvIgvCX2QXNleN02hPEqc9lf-TRK17_Uc0V3_n-UmxdIaw2fL2IBwL0S4jOrZDwnEHF1lgBQk2l63TalPoN2IENMvIFk-Z_FH3XWBQ6XaASVt3yWjcqwrE1FZbKofJcy1nGkM-iaMG361uYIErRdIp-Mo53UTZI9m5tiq4610A/s410/Fotolia_117787839_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="293" data-original-width="410" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgj_05bkhf_yOKHvyGfRvIgvCX2QXNleN02hPEqc9lf-TRK17_Uc0V3_n-UmxdIaw2fL2IBwL0S4jOrZDwnEHF1lgBQk2l63TalPoN2IENMvIFk-Z_FH3XWBQ6XaASVt3yWjcqwrE1FZbKofJcy1nGkM-iaMG361uYIErRdIp-Mo53UTZI9m5tiq4610A/s320/Fotolia_117787839_XS.jpg"/></a></div>
<br/>
技術部のyuckieee(ゆっきー)です。<br/>
色々なプロジェクトで開発を行っていて、ちょいちょい発生する作業で面倒だなって思っていることがありました。それは納品物やリリース対象物の準備です。<br/>
<br/>
何かというと、運用保守中に追加開発などが発生した場合に、プログラムの差分ファイルのみを納品物やリリース対象として準備する必要があります。<br/>
これ、中々に面倒くさいんですよね。間違えたら大変だし、毎回ドキドキしちゃいます(笑)<br/>
<br/>
そこで、出来るだけ間違いが起こらないように自動化出来ないか探してみた結果、私がいつも使っているGit管理ツール「Sourcetree」のカスタムアクションを試してみたら良さげだったので、ご紹介しようと思います。<br/>
<br/>
<h3><u>■作成したカスタムアクションの概要</u></h3>
まずはイメージ共有のため、今回作成したカスタムアクションの概要を説明します。<br/>
ざっくりした動作仕様としては「Sourcetreeで現在選択されているリポジトリ-ブランチに存在するコミット間の差分ファイルを取得する。」です。
利用イメージ(方法)は以下のとおり。<br/>
<br/>
[利用イメージ(方法)]<br/>
Sourcetreeの該当ブランチの履歴から差分ファイルをとりたいコミットを選択のうえ、カスタムアクションを実行します。
差分を取るためにコミットは2つ選択。ただし、1つだけ選択した場合でも、選択したコミットから最新コミットまでを対象と実行します。<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj94Wr4BFfKiKRL3a26FBjQONSLRI5qn683cx_vfIVscpe7wuK9w8JBD-n-x8cYs3ZA3kSS8dpv7Ls352mOTO-dliRPLVQlUBqFfdgx7UdWuxOlU8SOfoQbUkvvEHGb4-tmdu-NWcg0WqWSPNWtA8KS9bY7L5Rm0UPHzehnSEAY3osnl743X5WDnO4j2g/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2017.53.16.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="530" data-original-width="717" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj94Wr4BFfKiKRL3a26FBjQONSLRI5qn683cx_vfIVscpe7wuK9w8JBD-n-x8cYs3ZA3kSS8dpv7Ls352mOTO-dliRPLVQlUBqFfdgx7UdWuxOlU8SOfoQbUkvvEHGb4-tmdu-NWcg0WqWSPNWtA8KS9bY7L5Rm0UPHzehnSEAY3osnl743X5WDnO4j2g/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2017.53.16.png"/></a></div>
実行時は、経過が分かるようにダイアログにログが表示されます。<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_oUktxJAT6Dj0TQ1prYkFFrhySdMjeeebitWnTs6o0PoKf2cgguNe-WGT5gTy1iNuneoWHMRrCzHQNnCiJqXTFwhX9kfpZ29w6nF1WJh_Sma-KjVZ6X5nYAGGE8KtWo-d6AC9xrsV1wTclciPXYqo7T8CNsIG8_H7OhEcLI1vO6MicAF9M7mkRJdG3g/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2017.54.10.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="630" data-original-width="637" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_oUktxJAT6Dj0TQ1prYkFFrhySdMjeeebitWnTs6o0PoKf2cgguNe-WGT5gTy1iNuneoWHMRrCzHQNnCiJqXTFwhX9kfpZ29w6nF1WJh_Sma-KjVZ6X5nYAGGE8KtWo-d6AC9xrsV1wTclciPXYqo7T8CNsIG8_H7OhEcLI1vO6MicAF9M7mkRJdG3g/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2017.54.10.png"/></a></div>
実行後、ログ記載の出力先に差分ファイル(zip)、差分ファイル一覧、実行結果ログが格納されます。<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiISe4iufNogg2ypTUlQNB5rNrPGQExzrzLYKrhsZn1Orde8I6PlOnuHwzYCYqPdHCjYLoiOH2E5F3cgW15k2Hj_Dwj6pHpZ2-8-XPRP0k2uP2bsbQypjhlODXeCgQFpUmpHSPr_0MTevPRrq5n2k9r0b2qPXAqistZop7d0sg25VyhKs3UC6s1XiKguQ/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2011.35.24.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="510" data-original-width="1574" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiISe4iufNogg2ypTUlQNB5rNrPGQExzrzLYKrhsZn1Orde8I6PlOnuHwzYCYqPdHCjYLoiOH2E5F3cgW15k2Hj_Dwj6pHpZ2-8-XPRP0k2uP2bsbQypjhlODXeCgQFpUmpHSPr_0MTevPRrq5n2k9r0b2qPXAqistZop7d0sg25VyhKs3UC6s1XiKguQ/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-29%2011.35.24.png"/></a></div>
以上です!<br/>
<br/>
<h3><u>■カスタムアクションの実装方法</u></h3>
実装完了までの大まかな流れは以下のとおりです。<br/>
<br/>
<b>[実装の流れ]</b><br/>
① 呼び出しスクリプト作成<br/>
② カスタムアクション登録<br/>
③ 動作確認<br/>
<br/>
それではサクッと詳細の説明に入ります。<br/>
<br/>
<h4><b><u>① 呼び出しスクリプト作成</u></b></h4>
最初にカスタムアクションで呼び出されるスクリプトを作成します。<br/>
今回はシェルスクリプト(sh)で作成し、実際に作成したコードは以下のとおりです。(スクリプトの引数は②で説明しますが、<code class="inline-code">$1</code>にリポジトリ名、<code class="inline-code">$2</code>と<code class="inline-code">$3</code>にコミットIDが指定されています)
<pre class="prettyprint linenums lang-bash">
#!/bin/sh
#########################################
# ファイル名: export_diff.sh
# 処理内容 : 該当コミット間の差分ファイル出力
#########################################
#########################################
# 変数定義
#########################################
repo_name=$(basename "$(pwd)")
branch_name=$(git rev-parse --abbrev-ref @)
file_name="$(basename "$(pwd)")_diff.zip"
list_name="$(basename "$(pwd)")_diff_list.log"
today="$(date +"%Y%m%d")"
dir_name="export_diff/$repo_name/$branch_name/$today"
log_file=export_diff_result.log
#########################################
# 出力処理
#########################################
export_diff() {
# リモートブランチと同期を実行した上で、出力対象をダイアログ表示
echo ""
echo "出力を開始します($(date))"
echo "-------------------"
echo "■リモートブランチと同期"
echo "-------------------"
git pull
echo ""
echo "-------------------"
echo "■出力対象"
echo "-------------------"
echo "レポジトリ:${1}"
echo "ブランチ:${branch_name}"
echo ""
echo "-------------------"
# 差分ファイル出力処理
if [ "$3" != "" ]; then
# 2コミット間の差分ファイルを出力
git archive --worktree-attributes -o "$file_name" "$2" $(git diff --name-only --diff-filter=du "$3" "$2")
echo "■差分対象に含まれるコミット"
echo "-------------------"
git log "$3"..."$2" --pretty=format:"%h : %s"
echo ""
elif [ "$2" != "" ]; then
# 選択されたコミットから最新コミットまでを出力
git archive --worktree-attributes -o "$file_name" HEAD $(git diff --name-only --diff-filter=d "$2" HEAD)
echo "■差分対象に含まれるコミット"
echo "-------------------"
git log "$2"...HEAD --pretty=format:"%h : %s"
echo ""
else
# 上記以外の場合は正しく出力できないためエラーとする
echo "【ERROR】指定できるコミット数は1又は2です。"
echo "-------------------"
exit
fi
# 出力された差分ファイルの一覧をリスト出力(ディレクトリのみの表示は除く)
zipinfo -1 "$file_name" | grep -v /$ > "$list_name"
echo ""
echo "-------------------"
echo "■出力ファイル一覧"
echo "-------------------"
cat "$list_name"
echo ""
# 出力されたファイル移動(混乱しないように)
mv "$file_name" ~/"$dir_name"
mv "$list_name" ~/"$dir_name"
echo "-------------------"
echo "■ファイル出力先"
echo "-------------------"
echo "${HOME}/$dir_name"
echo "-------------------"
echo ""
echo "出力が完了しました($(date))"
}
#########################################
# メイン処理
#########################################
if [ $# -gt 3 ]; then
# 選択したコミットが4つ以上の場合は正しく出力できないためエラーとする
echo "【ERROR】指定できるコミット数は1又は2です。"
echo "-------------------"
exit
fi
# 出力ディレクトリを作成
mkdir -p ~/"$dir_name"
# 出力処理を呼び出し
export_diff "$1" "$2" "$3" 2>&1 | tee "${HOME}/$dir_name/$log_file"
</pre>
<br/>
...長い!!!(笑)<br/>
ダイアログ表示やファイル移動等の余分なコードが含まれていますが、重要なのは以下に抜粋したコマンドです。<br/>
<pre class="prettyprint linenums lang-bash">
git archive --format=zip -o release.zip --worktree-attributes "$2" $(git diff --name-only --diff-filter=d "$2" "$3")
</pre>
<br/>
上記は、Gitが提供している「Git管理対象のみを圧縮形式で出力してくれる」コマンドです。<br/>
出力対象の指定がない場合は、指定したブランチ又は、コミット時点のGit管理対象の全ファイルが出力されますが、ファイル名及び、パスを指定することで出力対象ファイルを絞り込むことが出来ます。<br/>
今回は、このファイル名・パス指定に<code class="inline-code">git diff</code>コマンドを使用することで差分ファイルの抽出を実現しました。<br/>
以降、<code class="inline-code">git diff</code>と<code class="inline-code">git archive</code>、それぞれのコマンドについて紹介します。<br/>
<br/>
<b>●<code class="inline-code">git diff</code>コマンド</b><br/>
差分ファイルを抽出するために使用したコマンドです。<br/>
オプションを省いた場合のシンプルな構成は以下のとおりです。<br/>
<pre class="prettyprint linenums lang-bash">
git diff <変更前のコミットID> <変更後のコミットID>
例) git diff 1668413042b61f16f0f353fd66b53cd4c0dbc5d5 2d13b702a783bb2c8252ba07f5410619ee3a0186
</pre>
なお、コミットIDの指定順序が逆転してしまうと「削除したファイル」と「新規作成されたファイル」の識別が逆転してしまう等、その後の出力に影響があるので注意してください。<br/>
<br/>
以降に私が指定が必要そうだと考えたオプションについて補足します。<br/>
<br/>
[コマンドオプション]<br/>
<pre class="prettyprint linenums lang-bash">
--name-only (必須)
</pre>
差分比較結果としてファイル名(パス含む)のみを返す指定です。<br/>
本オプションを指定しない場合、ズラズラとコミット間の比較内容が表示されるのですが、それが<code class="inline-code">git archive</code>に引数として渡ってしまうとエラーになりますので指定必須です。<br/>
<br/>
<pre class="prettyprint linenums lang-bash">
--diff-filter=<出力対象の指定> (必須)
例)--diff-filter=ACMR
</pre>
指定することで、差分取得対象を絞り込むことが出来ます。<br/>
このオプションは、大文字で記述すると「出力対象」を、小文字で記述すると「出力除外対象」を指定することが出来ます。それぞれのオプション文字列の説明を下表にまとめました。<br/>
<br/>
指定可能なオプション文字列
<table border="1">
<tr>
<th bgcolor="#aaaaaa">オプション<br/>文字列</th>
<th bgcolor="#aaaaaa">説明</th>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>A/a</strong></td>
<td>追加されたファイル</td>
</tr>
<tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>C/c</strong></td>
<td>コピーされたファイル</td>
</tr>
<tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>D/d</strong></td>
<td>削除されたファイル</td>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>M/m</strong></td>
<td>ファイル内容又は、ファイル属性が変更されたファイル</td>
</tr>
<tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>R/r</strong></td>
<td>ファイル名が変更されたファイル</td>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>T/t</strong></td>
<td>ファイルタイプ(通常ファイル、シンボリックファイル、サブモジュール等)が変更されたファイル</td>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>U/u</strong></td>
<td>マージされていないファイル(コンフリクトファイル確認時に使用されるようです)</td>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>B/b</strong></td>
<td>ペアリングが壊れているファイル</td>
</tr>
<tr>
<td width="100" bgcolor="#eeeeee"><strong>X/x</strong></td>
<td>不明な変更タイプ(通常は有り得ないようです)</td>
</tr>
</table>
<br/>
今回は差分に「削除されたファイルのパス」が返ってしまうと<code class="inline-code">git archive</code>時に対象無しのエラーになってしまうので「明確に出力対象としたいファイルのみ」を指定しました。<br/>
この指定方法以外に<code class="inline-code">--diff-filter=d</code>とすることで、削除されたファイルを「出力対象外」にすることも可能です。<br/>
<br/>
<b> ●<code class="inline-code">git archive</code>コマンド</b><br/>
指定したコミット時点のGit管理対象ファイルを圧縮出力してくれるコマンドです。(ブランチ指定も可能ですが、Sourcetreeから引数として受け取れないので説明割愛します)<br/>
指定したコミットID時点のファイルが出力対象となりますので、当たり前ではありますが、必ず変更後のコミットIDを指定するようにしてください。<br/>
<pre class="prettyprint linenums lang-bash">
git archive <変更後コミットID>
例) git archive 1668413042b61f16f0f353fd66b53cd4c0dbc5d5
</pre>
<br/>
[コマンドオプション]<br/>
<pre class="prettyprint linenums lang-bash">
--format=<圧縮形式> (任意)
例) --format=zip
</pre>
出力される差分ファイルの圧縮形式として、zip形式又はtar形式が指定可能です。<br/>
このオプションが指定されていない場合は、ファイル名に付与された拡張子から推測され、推測ができない場合はtar形式となるようです。<br/>
今回はzip形式で出力したいため、念のため、フォーマットも指定しておきました。<br/>
<br/>
<pre class="prettyprint linenums lang-bash">
-o <ファイル名> (必須)
※--output=<ファイル名>としても同じ
例) -o export_diff.zip
</pre>
本オプションを指定しない場合は、標準出力(コンソール)に結果が出力されます。<br/>
結果をファイルに出力したい場合は、本オプションを付与した上で、 出力ファイル名称を指定する必要があります。<br/>
<br/>
<pre class="prettyprint linenums lang-bash">
--worktree-attributes(任意)
</pre>
Git管理対象となっているファイルでも、今回の出力対象として含めたくない!というファイルがある場合は、本オプションを使用します。
例えば、本番環境へのリリース対象としてReadMeや環境変数のサンプルファイルなどは不要で、これらを除外したい、と言う時に使います。<br/>
<br/>
なお、除外対象は、リポジトリ直下にある<code class="inline-code">.gitattributesファイル</code>に記述することで認識されます。<br/>
以下のように<code class="inline-code">対象ファイル export-ignore</code>と書き込めばOKです。<br/>
<pre class="prettyprint linenums lang-bash">
README.md export-ignore
*.example export-ignore
</pre>
試してみた感じでは、今回の実装内容であれば、本ファイルをリモートリポジトリにマージしていなくても適用されるようでした。<br/>
<br/>
<pre class="prettyprint linenums lang-bash">
--prefix=<プレフィックス文字列/> (任意)
例)--prefix=prod/
</pre>
今回は使用しませんでしたが、出力されるファイルを特定のディレクトリでラップしたい場合に指定します。 例えば<code class="inline-code">--prefix=prod/</code>と入力しておくと、出力されたzipを解凍した際にprodディレクトリの配下に差分ファイルが格納されることになります。<br/>
試してみたところ<code class="inline-code">--prefix=prod/app1/</code>など、ディレクトリを多階層で指定することもできるようでした。 <br/>
<br/>
ザックリ説明はここまでとなりますが、各コマンドには他にもオプションがありますので、公式ドキュメントを参考にしながら試してみるのも面白いと思います。<br/>
<a href=https://git-scm.com/docs/git-diff>git-diffコマンド (Documentation - Reference)</a><br/>
<a href=https://git-scm.com/docs/git-archive> git-archiveコマンド (Documentation - Reference)</a><br/>
<br/>
なお、スクリプトについては、私のやりたように書いていますが、自分仕様にカスタマイズいただければと思います♪(見やすさ重視で<code class="inline-code">echo</code>の乱れ咲きさせてたり...笑)<br/>
<h4><b><u>② カスタムアクションの登録</u></b></h4>
①で作ったスクリプトを実際にSourceTreeに登録します。<br/>
Sourcetreeを開き、左上の[Sourcetree]>[環境設定...]を押下し、設定画面を開きます。そのうえで、右上にある[>>]>[カスタムアクション]を選択してカスタムアクションの定義画面を開き、左下の[追加]ボタンを押下してください。<br/>
<br/>
[注意事項]<br/>
Sourcetree日本語版では、不具合(確認したところ、4.1及び、4.2.0は駄目でした)により上記の流れで画面を開いた場合、カスタムアクションが非活性となっており選択できないようです!(笑)<br/>
選択できない場合は、対処法として2パターン有り、1つ目が言語を英語に切り替えてSourcetreeの再立ち上げを行う方法、2つ目がトップ画面で任意のリポジトリ右クリックメニューから[カスタムアクション]>[編集...]を選択する方法です。どちらかお好きな方をお試しください。<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEir-WvNiWgAyf_fxPKaq0sMKccpeVNYPlWOAS9nLSlghcKO1S1HB-_SMXXTFX-4C_mHtMXAye1sL8c1Ljb9kp82h3-MTWUA6WjhtWHIwFbmoE75cnokl3Pw_Hzo03fX7yAUtZe3tIls6twNeeDBQbjreWydoIDCyuZgBoxNh-cOAd_b1SqX2LxgdvSkSg/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-16%2018.52.14.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="395" data-original-width="672" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEir-WvNiWgAyf_fxPKaq0sMKccpeVNYPlWOAS9nLSlghcKO1S1HB-_SMXXTFX-4C_mHtMXAye1sL8c1Ljb9kp82h3-MTWUA6WjhtWHIwFbmoE75cnokl3Pw_Hzo03fX7yAUtZe3tIls6twNeeDBQbjreWydoIDCyuZgBoxNh-cOAd_b1SqX2LxgdvSkSg/s1600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-16%2018.52.14.png"/></a></div>
<br/>
登録画面が開きますので、必要事項を入力し[OK]を押下して、登録を完了させます。<br/>
<br/>
【登録内容】<br/>
メニューキャプション:差分出力(zip) ※1<br/>
別のウィンドウで開く:チェックオフorオン どちらでも可 ※2<br/>
フル出力を表示:チェックオン ※3<br/>
実行するスクリプト:①で作成したスクリプトを選択<br/>
パラメータ:$REPO $SHA ※4<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi28ZCIOBkq-4SJj63rlHOkQ837ktFVuGsLdFO6aRPPKEijxygK-stqITrjM4ITHOwFbhIiLhAqTlJ3XaBTF5_5FCQjEtf4A4r7_oEPtAdy-nTEBlb7dJKibS77fh4Y4SRPfzSANMynrfoAhsCL1QAH-Zc7_FoSk2xK7fCAdIOImg2aUh4ZNHs7MmymJQ/s1064/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-16%2019.25.03.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="600" data-original-height="528" data-original-width="1064" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi28ZCIOBkq-4SJj63rlHOkQ837ktFVuGsLdFO6aRPPKEijxygK-stqITrjM4ITHOwFbhIiLhAqTlJ3XaBTF5_5FCQjEtf4A4r7_oEPtAdy-nTEBlb7dJKibS77fh4Y4SRPfzSANMynrfoAhsCL1QAH-Zc7_FoSk2xK7fCAdIOImg2aUh4ZNHs7MmymJQ/s600/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-16%2019.25.03.png"/></a></div>
※1:メニューに表示される名称なので、お好きなものを指定してください。<br/>
※2:選択したファイルをVSCodeなどの別ウィンドウで表示したい場合は、このチェックをオンにします。なお、このチェックをオンにすることでダイアログも別ウィンドウとなるのでダイアログの拡大縮小が可能になります。<br/>
※3:チェックがオフの場合は、エラーのみがダイアログ表示されます。今回は全て表示したいのでチェックをオンにします。<br/>
※4:$SHAについては、複数コミットを選択した場合、コミットタイミングが新しいコミットIDから順に、選択した全てのコミットIDがスクリプトの引数として渡されます。(順番の選択順には依らないようです)<br/>
<br/>
<h4><b><u>③ 動作確認</u></b></h4>
最後に動作確認を行います。<br/>
今回登録したカスタムアクションは、できるだけ汎用的に作っているので、どのリポジトリ、ブランチでも対応可能(な、はず)ですので、皆さんも動作確認してみてください。<br/>
実行方法は、「■作成したカスタムアクションの概要」と同様の流れになるため、説明は割愛しますが、コミットの選択方法について、少しだけ補足します。<br/>
<br/>
[実行時の補足事項]<br/>
Sourcetreeのデフォルトでは、履歴に「すべてのブランチ」のコミットが表示されています。<br/>
この場合、親ブランチの差分を取得しようとした際に、間違って未マージの子ブランチのコミットを選択してしまっても、それらが差分出力対象となっているようでした。(これは<code class="inline-code">--diff-filter=u</code>としても結果は変わりませんでした)<br/>
<br/>
そのため、履歴の表示フィルターを使用して「現在のブランチ」のみの表示にしておくと、ミスがなくて良いのかなと思います。<br/>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgV4abvDd0tTw01C2WF62NBrFNqbob_bQJMai0jexokX5P402wpu-2o7Ph7i6HJYfwgMLeSDfr_vp5ccqQvvEWfH5TnM12gdVChm2xuWfTFgjfmOPRelxtWxaz_MlaGCc4LhNiyD-SKtWrT9lOb6Lqd77L-TwROB_oi3d8c3bN8ECBx0kaUj0CxZwjkvg/s1104/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2011.35.00.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="400" data-original-height="480" data-original-width="1104" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgV4abvDd0tTw01C2WF62NBrFNqbob_bQJMai0jexokX5P402wpu-2o7Ph7i6HJYfwgMLeSDfr_vp5ccqQvvEWfH5TnM12gdVChm2xuWfTFgjfmOPRelxtWxaz_MlaGCc4LhNiyD-SKtWrT9lOb6Lqd77L-TwROB_oi3d8c3bN8ECBx0kaUj0CxZwjkvg/s400/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202022-09-22%2011.35.00.png"/></a></div>
<br/>
ここまでで、カスタムアクションの実装は完了となります!<br/>
これでサクッと差分ファイルの抽出ができるようになったので、これからのリリース準備作業も捗りそうです♪<br/>
ただし、自動化しても、間違いがないかのダブルチェックは怠らず!!<br/>
<br/>
<h3><u>■まとめ</u></h3>
Sourcetreeやgitには、沢山の便利機能やコマンドがあるのに活用できていないなぁ、と、改めて感じました。<br/>
特に今回使ったSourcetreeのカスタムアクションは、利用の幅が広く、もっと色々な応用がききそうだなと感じたため、継続して色々と試してみたいと思います。<br/>
<!--編集禁止(ここから)-->
<style>
.inline-code {
background: #f0f5f9;
border: 1px solid #e6edf3;
padding: .04em .3em;
margin: 0 .2em;
border-radius: 3px;
line-height: 1.4;
font-size: .85em;
}
</style>
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/default.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<script>
hljs.initHighlightingOnLoad();
hljs.initLineNumbersOnLoad();
</script>
<link href="/styles/shThemeEmacs.css" rel="Stylesheet" type="text/css"></link>
<!--編集禁止(ここまで)-->
オフィス狛 技術部 yuckieeehttp://www.blogger.com/profile/18033915954934523504noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-15887389624865764252022-09-30T10:24:00.000+09:002022-09-30T10:28:44.694+09:00laravel-analyticsを使用して、Universal Analyticsの値を取得する方法。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzKqtLhaKf2Cjaw3OnkkffzY7mgOTho5LMkAzBmts-zvp-nHdBNM1JIzw7hhVEOgPzi3StYGjCATOy2IhOdvkuVZsPKZN7qMueHNDe__7VjpLsgfTX4Dc-HOnL3KijZglAR2b9cIMPd1W1DSC0z1tQxPGLdhaMBdDJpnWBzLXAHQknVJA3XjRS2E8bcQ/s2048/AdobeStock_276356125.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="1366" data-original-width="2048" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzKqtLhaKf2Cjaw3OnkkffzY7mgOTho5LMkAzBmts-zvp-nHdBNM1JIzw7hhVEOgPzi3StYGjCATOy2IhOdvkuVZsPKZN7qMueHNDe__7VjpLsgfTX4Dc-HOnL3KijZglAR2b9cIMPd1W1DSC0z1tQxPGLdhaMBdDJpnWBzLXAHQknVJA3XjRS2E8bcQ/s320/AdobeStock_276356125.jpeg"/></a></div>
<br>
オフィス狛 技術部のmmm(むー)です。<br><br>
今回、LaravelからUniversal Analyticsの値を取得する必要があり、調査したため記事に残します。<br><br>
※ちなみに、Universal Analyticsは2023年7月1日にサービスが終了されるので、時間に余裕があり移行できる場合、Google Analytics 4を使用しましょう。<br>
<br>
<h2>前提条件</h2>
Universal Analyticsの設定が完了していること<br><br>
<div style="font-weight: bold">■Laravel バージョン</div>
<pre><code>$ php artisan -V
Laravel Framework 8.27.0
</code></pre><br>
<div style="font-weight: bold">■Comporser バージョン</div>
<pre><code>$ composer --version
Composer version 2.4.1 2022-08-20 11:44:50
</code></pre><br>
<br>
<h2>1. laravel-analyticsをインストールする</h2>
<a href="https://github.com/spatie/laravel-analytics">laravel-analytics</a>をインストールします。<br>
<pre><code># laravel-analytics(バージョン 4.0.1)をインストールする
$ composer require spatie/laravel-analytics 4.0.1
</code></pre><br>
<div style="font-weight: bold">■補足</div>
・ 2022/09/07現在、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">laravel-analytics</span>の最新バージョンは <span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">4.1.0</span>ですが、今回のプロジェクトのLaravelのバージョンが8で対応していなかったので、1つ下のバージョンの <span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">4.0.1</span>をインストールしています。<br>
・Laravel 9を使用している場合、(試していませんが)最新バージョンを使用できるので、バージョン指定なしで、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">composer require spatie/laravel-analytics</span>を代わりに実行してください。<br><br>
<div style="font-weight: bold">■エラーが起きた場合</div>
・composerのバージョンが1でメモリエラーになる場合、composerを2に更新すると解決することがあるようです。<br>
<pre><code># comporserのバージョンを「1」から「2」に更新する
$ docker exec -it yanase-ybs_app_1 composer self-update --2
</code></pre><br>
・また関連してインストールされるパッケージが多く、私の環境の場合、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">google/apiclient-services</span>をインストールしている最中にタイムアウトしました。その場合、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">laravel-analytics</span>をインストールするコマンドをもう一度実行すると、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">google/apiclient-services</span>だけ差分でインストールできます。<br>
<pre><code># laravel-analyticsをインストールした際のエラーログ
$ composer require spatie/laravel-analytics 4.0.1
〜省略〜
- Installing symfony/cache (v5.4.11): Extracting archive
- Installing spatie/laravel-package-tools (1.12.1): Extracting archive
- Installing google/apiclient (v2.12.1): Extracting archive
- Installing spatie/laravel-analytics (4.0.1): Extracting archive
103/104 [===========================>] 99% Install of google/apiclient-services failed
In Filesystem.php line 314:
Could not delete /work/vendor/composer/e8a27dbc/googleapis-google-api-php-client-services-28c4208/src/SASPortalTesting/Resource:
</code></pre><br>
<br>
<h2>2. laravel-analyticsを使用するための設定を行う</h2>
下記コマンドを実行すると、設定ファイル(<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">config/analytics.php</span>)が作成されます。
<pre><code># 設定ファイル(config/analytics.php)を作成する
$ php artisan vendor:publish --provider="Spatie\Analytics\AnalyticsServiceProvider"
</code></pre><br>
<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">config/analytics.php</span>の中身を見ると下記の様になっています。
<pre><code>// config/analytics.php
return [
/*
* The view id of which you want to display data.
*/
'view_id' => env('ANALYTICS_VIEW_ID'),
/*
* Path to the client secret json file. Take a look at the README of this package
* to learn how to get this file. You can also pass the credentials as an array
* instead of a file path.
*/
'service_account_credentials_json' => storage_path('app/analytics/service-account-credentials.json'),
/*
* The amount of minutes the Google API responses will be cached.
* If you set this to zero, the responses won't be cached at all.
*/
'cache_lifetime_in_minutes' => 60 * 24,
/*
* Here you may configure the "store" that the underlying Google_Client will
* use to store it's data. You may also add extra parameters that will
* be passed on setCacheConfig (see docs for google-api-php-client).
*
* Optional parameters: "lifetime", "prefix"
*/
'cache' => [
'store' => 'file',
],
];
</code></pre><br>
まず、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">view_id</span>を設定する必要があるので、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">.env</span> に値をUniversal Analyticsの画面で取得した値を記載してください。
(下記は例となりますので、ご自身の環境に合わせて値を修正してください。)
<pre><code>// .env
ANALYTICS_VIEW_ID=1234567
</code></pre><br>
次に、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">service_account_credentials_json</span>を設定する必要があるので、<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">app/analytics/</span>フォルダの配下に<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">service-account-credentials.json</span>ファイルを作成して、Universal Analyticsの画面で取得した値を記載してください。
(下記は例となりますので、ご自身の環境に合わせて値を修正してください。)
<pre><code>// app/analytics/service-account-credentials.json
{
"type": "service_account",
"project_id": "testapi",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
"client_id": "1234",
"auth_uri": "...",
"token_uri": "...",
"auth_provider_x509_cert_url": "...",
"client_x509_cert_url": "..."
}
</code></pre><br>
<br>
<h2>3. Universal Analyticsの値を取得する</h2>
下記に、一例ですが値を取得する方法を記載します。
<pre><code>namespace App\Console\Commands;
use Illuminate\Console\Command;
use Analytics;
use Spatie\Analytics\Period;
class TestCommand extends Command
{
// 例1:
// 1日分の訪問者数とページビューを取得します。
// こちらはlaravel-analyticsが用意しているメソッドとなります。
$data1 = Analytics::fetchVisitorsAndPageViews(Period::days(1));
// 例2:
// 期間を指定して、7日分の訪問者数とページビューを取得します。
// ちなみにライブラリ内で日付に変換されるので、時間は設定できません。
$periodFrom = Carbon::now()->subDay(7);
$periodTo = Carbon::now();
$period = Period::create($periodFrom, $periodTo);
$data2 = Analytics::fetchVisitorsAndPageViews($period);
// 例3:
// 取得する指標、取得項目、ソート順をカスタムしたい場合は、performQuery()を使用してください。
// 指標(合計イベント数)
$metrics = 'ga:totalEvents';
// 取得項目(イベントラベル)、ソート(合計イベント数の降順)
$item = [
'dimensions' => 'ga:eventLabel',
'sort' => '-ga:totalEvents',
];
$data3 = Analytics::performQuery(Period::days(3), $metrics, $item);
}
</code></pre><br>
<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">fetchVisitorsAndPageViews()</span>以外のメソッドについては、<a href="https://github.com/spatie/laravel-analytics">公式ページ</a>を確認してみてください。<br>
公式が用意しているメソッドで、項目が取得できない場合は<span style="background-color: #99dcd89e; border-radius: 4px; padding: 1px 2px; font-size: 90%;">performQuery()</span>メソッドを使用してください。<br>
指定する項目は、下記から必要なものを指定してください。<br>
■参考サイト:<a href="https://ga-dev-tools.web.app/dimensions-metrics-explorer/">https://ga-dev-tools.web.app/dimensions-metrics-explorer/</a><br><br>
以上となります。<br>
参考にして頂ければ幸いです。<br><br>
<!-- 編集禁止(ここから) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/darcula.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- 編集禁止(ここまで) -->オフィス狛 技術部 mmmhttp://www.blogger.com/profile/02929539829077086029noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-60634538237222809712022-08-08T15:08:00.001+09:002022-08-08T15:08:49.399+09:00【Angular】独自エラーチェック(カスタムバリデーション)を作成する。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
今回は、独自エラーチェック(カスタムバリデーション)を作成します。<br />
プログラムは、以前の記事で使用した「アカウント登録機能」を使用したいと思いますので、下記の記事も参照ください。<br />
<br />
参照1:<a href="https://blog.officekoma.co.jp/2022/08/angular_5.html" target="_blank"><u>【Angular】コンポーネントの設計(画面ごとの設計)について。</u></a><br />
参照2:<a href="https://blog.officekoma.co.jp/2022/08/angular_4.html" target="_blank"><u>【Angular】エラーメッセージの管理について考える。</u></a><br />
<br />
<h3>(1)登録画面の実装を確認</h3>
まずは、現在の登録画面のコンポーネントを見てみます。<br />
コード量が多いので、全体は<a href="https://blog.officekoma.co.jp/2022/08/angular_5.html#registerComponentTs" target="_blank"><u>こちら(register.component.ts)</u></a>で確認ください<br />
今回カスタムバリデーションを追加するのは、こちらの携帯番号(mobilePhoneNumber)部分です。<br />
<pre class="prettyprint linenums">
mobilePhoneNumberMaxLength = 11;
nameMaxLength = 20;
// (中略)
formRegister: FormGroup = this.formBuilder.group({
mobilePhoneNumber: ['',[Validators.required,Validators.maxLength(this.mobilePhoneNumberMaxLength)]],
name: ['', [Validators.maxLength(this.nameMaxLength)]],
});
</pre>
<br />
しかし、これだけだと、「<span style="font-size: large">18011112222</span>」のような「<span style="font-size: large">0</span>」始まりではない番号はエラーになりません。<br />
このチェックを独自で作成しようと思いますが、<br />
その前に、一応テンプレート側の記載も見ておこうと思います。<br />
<br />
こちらもコード量が多いので、全体は<a href="https://blog.officekoma.co.jp/2022/08/angular_5.html#registerComponentHtml" target="_blank"><u>こちら(register.component.html)</u></a>で確認ください<br />
下記に、今回関係する部分だけ記載しておきます。<br />
<pre class="prettyprint linenums">
<div>
<label>携帯電話番号<span>必須</span></label>
<input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
[ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
<ng-container *ngIf="v.mobilePhoneNumberInvalid">
<p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
</ng-container>
</div>
</pre>
<br />
<h3>(2)カスタムバリデーションを作成する</h3>
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。<br />
今回のカスタムバリデーションも、共通的に使われるものなので、、以下のように作成します。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ shared/
└ validator/
└ custom-validators.ts
</pre>
<br />
では、携帯電話番号のチェックを実装しようと思います。<br />
<span style="font-size: small;color: #9E9E9E;">[custom-validators.ts]</span><br />
<pre class="prettyprint linenums">
import { FormControl } from '@angular/forms';
export class CustomValidators {
/**
* 携帯電話番号かどうか判定
* @param control Formのコントロール
*/
static mobilePhoneNumberValidator(control: FormControl) {
const dateObj = control.value;
// 当メソッドでは、電話番号は空文字で登録することも許可する
if (dateObj === '') {
return null;
}
const regexp = new RegExp('^(0{1}\\d{10})');
if (
typeof dateObj === 'undefined' ||
dateObj === null ||
!regexp.test(dateObj)
) {
return { mobilePhoneNumberFormat: true };
}
return null;
}
}
</pre>
<br />
「<span style="font-size: large">当メソッドでは、電話番号は空文字で登録することも許可する</span>」と言うコメントの部分が特殊なのですが、例えば、「任意の項目」でこのバリデーションを使いたくなった場合、そのまま使うと「未入力」でもエラーになってしまうので、未入力はエラーとしないようにしています。<br />
どっちにしても、必須かどうかは、通常のバリデーションで行なっているので、そちらに任せる、と言う感じです。<br />
それと、「携帯電話番号かどうか」はかなり適当に記載していますので、ご了承ください(今回は、その説明が本質では無いので)<br />
<br />
ちょっと分かり難い(勘違いしやすい)のですが、「エラーになるパターンは『<span style="font-size: large">true</span>』を返却し、エラーとしない場合『<span style="font-size: large">null</span>』を返却しています」
<h3>(3)作成したカスタムバリデーションを使う</h3>
では、作成したカスタムバリデーションを実際に使ってみます。<br />
「register.component.ts」に実装していきますが、まずは先ほどのカスタムバリデーションをimportします。<br />
<pre class="prettyprint linenums">
import { CustomValidators } from '@app/shared/validator/custom-validators';
</pre>
<br />
importしたカスタムバリデーションは、通常のバリデーションと同じ流れで定義します。<br />
<pre class="prettyprint linenums">
formRegister: FormGroup = this.formBuilder.group({
mobilePhoneNumber: [
'',
[
Validators.required, // ←通常のバリデーション
Validators.maxLength(this.mobilePhoneNumberMaxLength), // ←通常のバリデーション
CustomValidators.mobilePhoneNumberValidator, // ←カスタムバリデーション
],
],
name: ['', [Validators.maxLength(this.nameMaxLength)]],
});
</pre>
<br />
とても簡単ですね、次は、別ファイルにしている「register.validator.ts」にも追記します。<br />
こちらは、全文そのまま記載しようと思います。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Injectable()
export class RegisterValidator {
private form: FormGroup;
constructor() {}
set formGroup(form: FormGroup) {
this.form = form;
}
get mobilePhoneNumberInvalid() {
return (
this.form.controls['mobilePhoneNumber'].invalid &&
(this.form.controls['mobilePhoneNumber'].dirty ||
this.form.controls['mobilePhoneNumber'].touched)
);
}
get mobilePhoneNumberHasErrorRequired() {
return this.form.controls['mobilePhoneNumber'].hasError('required');
}
get mobilePhoneNumberHasErrorMaxLength() {
return (
!this.form.controls['mobilePhoneNumber'].hasError('required') &&
this.form.controls['mobilePhoneNumber'].hasError('maxlength')
);
}
get mobilePhoneNumberHasErrorFormat() {
return (
!this.form.controls['mobilePhoneNumber'].hasError('required') &&
this.form.controls['mobilePhoneNumber'].hasError(
'mobilePhoneNumberFormat',
)
);
}
get nameInvalid() {
return (
this.form.controls['name'].invalid &&
(this.form.controls['name'].dirty || this.form.controls['name'].touched)
);
}
get nameHasErrorMaxLength() {
return this.form.controls['name'].hasError('maxlength');
}
}
</pre>
<br />
追記した部分は以下の通りです。
<pre class="prettyprint linenums">
get mobilePhoneNumberHasErrorFormat() {
return (
!this.form.controls['mobilePhoneNumber'].hasError('required') &&
this.form.controls['mobilePhoneNumber'].hasError(
'mobilePhoneNumberFormat',
)
);
}
</pre>
この「mobilePhoneNumberHasErrorFormat」は、テンプレート側で使うことになります。<br />
では、そのテンプレート側も変更しようと思います。<br />
<br />
<pre class="prettyprint linenums">
<div>
<label>携帯電話番号<span>必須</span></label>
<input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
[ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
<ng-container *ngIf="v.mobilePhoneNumberInvalid">
<p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorFormat" class="error-message">{{ message('msg_error_cellphone_number') }}</p>
</ng-container>
</div>
</pre>
<br />
下記が追加した部分です。今回、新たにエラーメッセージも追加しています。<br />
(エラーメッセージの管理については、「<a href="https://blog.officekoma.co.jp/2022/08/angular_4.html" target="_blank"><u>【Angular】エラーメッセージの管理について考える。</u></a>」を参照ください)<br />
<pre class="prettyprint linenums">
<p *ngIf="v.mobilePhoneNumberHasErrorFormat" class="error-message">{{ message('msg_error_cellphone_number') }}</p>
</pre>
<br />
これで、独自エラーチェック(カスタムバリデーション)を実装できました。<br />
ある程度規模の大きいプロジェクトになると、結構な数のカスタムバリデーションを作る事になるかと思いますので、参考にして頂ければ幸いです。<br />
<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-55489596611486811672022-08-05T15:32:00.005+09:002022-08-08T12:14:51.519+09:00【Angular】コンポーネントの設計(画面ごとの設計)について。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
<a href="https://blog.officekoma.co.jp/2022/08/angular.html"><u>前回</u></a>、「機能内の画面構成(コンポーネント構成)設計」について記載したので、今回は、「<span style="font-size: large;color: #AB47BC;">画面ごとのコンポーネント設計</span>」を記載しようと思います。<br />
<br />
それでは、<a href="https://blog.officekoma.co.jp/2022/08/angular.html"><u>前回</u></a>の続きとして、『アカウント登録処理』から「登録情報入力画面」について、コンポーネントの設計を説明しようと思います。<br />
<br />
<h3>(1)前回のおさらい</h3>
「アカウント登録処理」として、以下のような画面が必要と想定されます。<br />
<br />
<span style="font-size: large;color: #AB47BC;">①登録情報入力画面</span><br />
<span style="font-size: large;color: #AB47BC;">②入力情報確認画面</span><br />
<span style="font-size: large;color: #AB47BC;">③登録完了画面</span><br />
<br />
画面遷移としては以下になります。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDZZ2bvClKGuV9Xe1txTE0dzGydjU7BmVkMQm5N4xFy2-1-nGuQws5wJ9xUTQEi4ak9Va_uatIfkvNVJvuY4J726IFxCH4VawxbEHXb8SR_roiB7hgIQ_TtkvxOLV8SdhaAHSR1Kx1zyIGu7r576kX2EfU1xAHKqmOYFdGrFP1iWXfyEfwQv2zwRs-sQ/s1600/%E7%94%BB%E5%83%8F%EF%BC%91.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="136" data-original-width="1058" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDZZ2bvClKGuV9Xe1txTE0dzGydjU7BmVkMQm5N4xFy2-1-nGuQws5wJ9xUTQEi4ak9Va_uatIfkvNVJvuY4J726IFxCH4VawxbEHXb8SR_roiB7hgIQ_TtkvxOLV8SdhaAHSR1Kx1zyIGu7r576kX2EfU1xAHKqmOYFdGrFP1iWXfyEfwQv2zwRs-sQ/s1600/%E7%94%BB%E5%83%8F%EF%BC%91.png"/></a></div>
<br />
そして、ディレクトリ構成は以下となります。(一部省略)<br />
<pre class="prettyprint linenums">
src/
└ app/
└ account/
└ containers/
├ account/
├ account-register
│ └ account-register.component.html
│ └ account-register.component.spec.ts
│ └ account-register.component.ts
├ account-register-confirm
│ └ ・・・
└ account-register-complete
└ ・・・
</pre>
<br />
図で表すと、以下のようになります。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGcJo81-ZQ4dWUf2Dlb9JPivSo7zyAv2EwsMxcr7aci-ne9o4U2EWeNb0hEVPJmfXGYN4rHR_OiETIQRZP957bDFTCKnNwMzNL3-TroTW3Vv7jB7r_X31lIdknNioH1nbesAsMT0slXQIKO4ryyG9nVSxWG2uWGxmlukk-iFmMoDWU8mg-c1owobrf3g/s1600/%E7%94%BB%E5%83%8F%EF%BC%93.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="488" data-original-width="1748" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGcJo81-ZQ4dWUf2Dlb9JPivSo7zyAv2EwsMxcr7aci-ne9o4U2EWeNb0hEVPJmfXGYN4rHR_OiETIQRZP957bDFTCKnNwMzNL3-TroTW3Vv7jB7r_X31lIdknNioH1nbesAsMT0slXQIKO4ryyG9nVSxWG2uWGxmlukk-iFmMoDWU8mg-c1owobrf3g/s1600/%E7%94%BB%E5%83%8F%EF%BC%93.png"/></a></div>
<br />
<h3>(2)画面単位でのディレクトリ構成を考える</h3>
画面単位で考えた時、「登録情報入力画面」にはどんな機能が含まれるでしょうか?<br />
<br />
<ul>
<li>画面の表示</li>
<li>入力項目のチェック(バリデーション、業務的な関連チェックなど)</li>
<li>入力項目を次の画面へ持ち越す為の準備</li>
<li>確認画面から戻って来た場合は、入力した値を再現</li>
</ul>
<br />
などなど、色々ありますね。<br />
もっと複雑な機能であれば、「遷移先を入力値によって分岐させる」や、「APIを呼び出し、入力用の補足情報を取得する」・・・・なんて事もあるかもしれません。<br />
<br />
このような多種多様な処理を以下の「containers」配下のコンポーネントだけでやるとしたら、コンポーネントが肥大化してしまうと思います。
<pre class="prettyprint linenums">
src/
└ app/
└ account/
└ containers/
└ account-register
└ account-register.component.ts ← 肥大化する
</pre>
<br />
そこで、「画面の表示」「入力項目のチェック」など、View(ビュー)側に属する処理については、「presentations」ディレクトリに分けることにします。<br />
<br />
と言うことで「presentations」ディレクトリとファイルを追加します。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ account/
└ containers/
└ presentations/
└ register/
└ register.component.html
└ register.component.spec.ts
└ register.component.ts
└ register.validator.ts
</pre>
<br />
※「 register.validator.ts」について<br />
弊社のプロジェクトでは、Validationは出来る限りViewとコンポーネントからは切り離して別ファイルで管理するようにしています。(Validationについては、以前の記事「<a href="https://blog.officekoma.co.jp/2022/08/angularformvalidation.html"><u>【Angular】FormのValidationの書き方を簡略化させる。</u></a>」を参照ください。)<br />
<br />
<h3>(3)「Container」と「Presentation」の切り分け方</h3>
「Container」と「Presentation」の切り分けですが、弊社では以下のように定義付けしています。<br />
<br />
<span style="font-size: large;color: #AB47BC;">「Container」として分類するもの</span><br />
<ul>
<li>画画面の状態保持に関すること(NgRxのStore操作など)</li>
<li>画面に入力した値の業務チェック(API呼び出しが必要なもの、など)</li>
<li>API(バックエンド処理)の呼び出し</li>
<li>画面遷移</li>
</ul>
<br />
<span style="font-size: large;color: #AB47BC;">「Presentation」として分類するもの</span><br />
<ul>
<li>画面表示、及び表示内容の制御(エラー時など)</li>
<li>画面に入力した値のバリデーションチェック</li>
<li>画面に入力した値の関連チェック、及び業務チェック</li>
</ul>
<br />
「関連チェック」というのは、例えば「AとBが選択されているときは、Cは必須となる」とか、です。<br />
両方に含まれている「業務チェック」が、判断難しいところかな、と思います。<br />
クライアント内で完結出来るチェックであれば「Presentation」。完結出来ないものは「Container」、という考えで良いと思います。<br />
<br />
アカウント登録で言うと、「入力されたメールアドレスが既に使用されているかどうか」のチェックは、クライアント内では完結出来ず、バックエンドの処理(API等)を呼び出す必要があると思いますので、「Container」側に記載する必要があります。<br />
<br />
さて、今の状態を図で表すと、以下のようになります。(登録情報入力画面のみ)<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGQ_YdIAhB-sNJP5I4bt9GZPiZ9DYOkCBUm0a1GYW-WlHZE1BDEcX3joJREEGSPSvh0OJUsl5Ot-02vkbP1RvEm9XShP3W2AtvSq0uKUw8vd9CT7vgsNrEwIbB8VzTrEWAuI_XLuFDVISKNnrZLAGqgH1AC0TEhThDQRkwAt1aUAXDAMJ9vSqYWVKwvA/s1600/%E7%94%BB%E5%83%8F%EF%BC%95.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="652" data-original-width="1072" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGQ_YdIAhB-sNJP5I4bt9GZPiZ9DYOkCBUm0a1GYW-WlHZE1BDEcX3joJREEGSPSvh0OJUsl5Ot-02vkbP1RvEm9XShP3W2AtvSq0uKUw8vd9CT7vgsNrEwIbB8VzTrEWAuI_XLuFDVISKNnrZLAGqgH1AC0TEhThDQRkwAt1aUAXDAMJ9vSqYWVKwvA/s1600/%E7%94%BB%E5%83%8F%EF%BC%95.png"/></a></div>
<br />
<h3>(4)「Container」と「Presentation」間のデータ・処理受け渡し方法を考える</h3>
ここで、分かりやすいように、「Container」を<span style="font-size: large">親</span>、「Presentation」を<span style="font-size: large">子</span>として、話を進めようと思います。<br />
親と子の間では、データ・処理のやり取りが必要になります。<br />
<br />
コンポネート間のデータ・処理のやり取りについては、いくつか方法がありますが、<br />
今回は、「<span style="font-size: large;color: #AB47BC;">@Input()、@Output()</span>」を使います。<br />
<br />
参考サイト(本家):<a href="https://angular.jp/guide/inputs-outputs"><u>ディレクティブとコンポーネントの親子間でのデータ共有</u></a>
<br />
<br />
<h3>(5)親子間でやり取りが必要なデータ・処理を考える</h3>
方式が決まったところで、親子間で、どんなデータ・処理のやり取りが必要か考えます。<br />
登録情報入力画面の内部処理は、以下の流れになると思います。<br />
<br />
<span style="font-size: large">1. 画面初期表示</span><br />
※確認画面から戻ってきた場合は、画面の入力値を復元<br />
→親から子へ、復元用のデータを送る(@Input())<br />
<span style="font-size: large">2. 画面の各フィールドへの値入力(バリデーション)</span><br />
<span style="font-size: large">3. Submitによって、入力値を引き継ぎつつ、確認画面へ遷移</span><br />
→入力値を引数に、子から親へ処理を引き継ぐ(@Output())<br />
<br />
上記を実現したいのですが、ここで、<br />
<ul>
<li>親子間でのデータ</li>
<li>次画面(確認画面)に送るデータ</li>
</ul>
両方を兼ね備えたデータモデルを作成しようと思います。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ account/
└ containers/
└ presentations/
└ models/
└ account.ts
</pre>
<br />
中身は下記のようにします。<br />
<pre class="prettyprint linenums">
// アカウント登録用画面間保持データモデル
export class AccountViewSaveModel {
mobilePhoneNumber: string;
name: string;
}
</pre>
とりあえず、電話番号と名前を項目として用意しました。この辺はあくまで例なので適当です。<br />
<br />
続いて、親と子のコンポーネントの中身を実装します。<br />
<br />
<h3>(6)親子間のデータ・処理のやり取りを実装する</h3>
まずは親コンポーネントの実装です。<br />
<span style="font-size: small;color: #9E9E9E;">[account-register.component.ts]</span><br />
<pre class="prettyprint linenums">
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Router } from '@angular/router';
import { AccountViewSaveModel } from '../../models/account';
import * as fromAccount from '../../store/reducers';
import * as AccountActions from '../../store/actions/account.actions';
@Component({
selector: 'koma-account-register',
templateUrl: './account-register.component.html',
})
export class AccountRegisterComponent implements OnInit {
// Storeから入力情報を取得する(確認画面から戻ってきた場合には値が入っている)
// 取得値の型はObservableになるので、変数の末尾に「$」を付ける(お約束みたいなもの)
accountViewSave$ = this.store.pipe(select(fromAccount.getDataRegisterPost));
constructor(public store: Store<fromAccount.State>, private router: Router) {}
ngOnInit(): void {}
onSubmit(formModel: AccountViewSaveModel): void {
// 入力情報をStoreに格納して(=引き継ぎ情報として)、遷移後の画面でも使えるようにする。
this.store.dispatch(
AccountActions.setRegisterPostData({ data: formModel }),
);
// 確認画面へ遷移する
this.router.navigateByUrl('/account/register-confirm');
}
}
</pre>
<br />
唐突に「NgRxのStore」が出てきましたが、ここは一旦スルーしてください。(いずれ、NgRxの実装方法も記事にしようと思います。)<br />
ここでは、画面で入力された値はStore(と言う場所)に格納されている、ぐらいの理解でOKです。<br />
(今回重要なのは、「データ保存方式」ではなく、あくまで「データ受け渡し部分」なので)<br />
<br />
細かい説明も実装内のコメントとして記載しましたので、参考にお願いします。<br />
<br />
続いて、子コンポーネントの実装です。<br />
<span id="registerComponentTs" style="font-size: small;color: #9E9E9E;">[register.component.ts]</span><br />
<pre class="prettyprint linenums">
import { Component, OnInit, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { RegisterValidator } from './register.validator';
import { AccountViewSaveModel } from '../../models/account';
import { getMessage } from '@app/shared/message/error-messages';
@Component({
selector: 'koma-register',
templateUrl: './register.component.html',
})
export class RegisterComponent implements OnInit, OnDestroy {
// Input・Outputの定義。
@Input() accountViewSave$: Observable<AccountViewSaveModel>;
@Output() formSubmit = new EventEmitter<AccountViewSaveModel>();
mobilePhoneNumberMaxLength = 11;
nameMaxLength = 20;
registerSubscription: Subscription = new Subscription();
formRegister: FormGroup = this.formBuilder.group({
mobilePhoneNumber: ['',[Validators.required,Validators.maxLength(this.mobilePhoneNumberMaxLength)]],
name: ['', [Validators.maxLength(this.nameMaxLength)]],
});
constructor(private formBuilder: FormBuilder, public v: RegisterValidator) {}
ngOnInit(): void {
// バリデーション設定
this.v.formGroup = this.formRegister;
this.registerSubscription.add(
// 画面初期値設定
this.accountViewSave$.subscribe(value => {
if (value) {
this.formRegister.controls.mobilePhoneNumber.setValue(
value.mobilePhoneNumber,
);
this.formRegister.controls.name.setValue(value.name);
}
}),
);
}
ngOnDestroy(): void {
// サブスクリプション解除
this.registerSubscription.unsubscribe();
}
// 画面でSubmitが発生した時の処理
onSubmit(): void {
if (this.formRegister.valid) {
// バリデーションエラーが発生していない場合
const formModel = {
mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value,
name: this.formRegister.controls.name.value,
} as AccountViewSaveModel;
this.formSubmit.emit(formModel);
}
}
message(messageId: string, ...args: any[]): string {
return getMessage(messageId, ...args);
}
}
</pre>
ちょっと記載量が多いのですが、あくまで「親子間のデータやり取り」部分についてのみ説明します。<br />
その他、FormBuilder, FormGroup, Validatorsあたりは、以前の記事などを参照ください。<br />
<br />
参照:<a href="https://blog.officekoma.co.jp/2022/08/angularformvalidation.html"><u>FormのValidationの書き方を簡略化させる</u></a><br />
<br />
まずはInput・Outputの定義です。
<br />
<pre class="prettyprint linenums">
@Input() accountViewSave$: Observable<AccountViewSaveModel>;
@Output() formSubmit = new EventEmitter<AccountViewSaveModel>();
</pre>
Inputは、親からデータが来る為の受け口になるので、分かりやすく同じ変数名にします。<br />
また、型も同じ「Observable」にします。<br />
Outputについては、「処理を引き継ぐ」為のお決まりの「EventEmitter」を使います。<br />
<br />
続いて、ngOnInitで、親からのデータをFormに反映する処理をSubscribeします。<br />
<pre class="prettyprint linenums">
// 画面初期値設定
this.accountViewSave$.subscribe(value => {
if (value) {
this.formRegister.controls.mobilePhoneNumber.setValue(
value.mobilePhoneNumber,
);
this.formRegister.controls.name.setValue(value.name);
}
}),
</pre>
<br />
Subscribeする事で、親からのデータが来れば、常にアクティブに画面表示が更新されることになります。<br />
<br />
最後が、画面でSubmitが発生した場合です。<br />
<pre class="prettyprint linenums">
// 画面でSubmitが発生した時の処理
onSubmit(): void {
if (this.formRegister.valid) {
// バリデーションエラーが発生していない場合
const formModel = {
mobilePhoneNumber: this.formRegister.controls.mobilePhoneNumber.value,
name: this.formRegister.controls.name.value,
} as AccountViewSaveModel;
this.formSubmit.emit(formModel);
}
}
</pre>
<br />
バリデーションエラーが発生していない場合、画面で入力された値を Model にセットし、その Model を引数に Emit します。
Emitする事で、親側に処理を引数ごと引き渡すことになります。<br />
<br />
続いて、親のテンプレート(View)を実装します。<br />
<span style="font-size: small;color: #9E9E9E;">[account-register.component.html]</span><br />
<pre class="prettyprint linenums">
<koma-register
[accountViewSave$]="accountViewSave$"
(formSubmit)="onSubmit($event)"
></koma-register>
</pre>
左辺が子の変数(処理)、右辺が親の変数(処理)となります。<br />
ここで、親子を結び付けています。<br />
<br />
最後に、子のテンプレート(View)を実装します。<br />
<span id="registerComponentHtml" style="font-size: small;color: #9E9E9E;">[register.component.html](form部分のみ抜粋)</span><br />
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div class="form-input">
<div>
<label>携帯電話番号<span>必須</span></label>
<input formControlName="mobilePhoneNumber" type="tel" inputmode="numeric" class="form-control" placeholder="携帯電話番号を入力"
[ngClass]="{'alert-danger' : v.mobilePhoneNumberInvalid}" autofocus>
<ng-container *ngIf="v.mobilePhoneNumberInvalid">
<p *ngIf="v.mobilePhoneNumberHasErrorRequired" class="error-message">{{ message('msg_error_field_required', '携帯電話番号') }}</p>
<p *ngIf="v.mobilePhoneNumberHasErrorMaxLength" class="error-message">{{ message('msg_error_field_max', '携帯電話番号', mobilePhoneNumberMaxLength) }}</p>
</ng-container>
</div>
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(任意)"
[ngClass]="{'alert-danger' : v.nameInvalid}">
<ng-container *ngIf="v.nameInvalid">
<p class="error-message">{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}</p>
</ng-container>
</div>
</div>
<footer>
<button [disabled]="formRegister.invalid" type="submit">確認</button>
</footer>
</form>
</pre>
こちらに関しては、特に「親子間のデータやり取り」について意識している部分は無いですね。<br />
<br />
これで、<br />
<br />
<span style="font-size: large">1. 画面初期表示</span><br />
※確認画面から戻ってきた場合は、画面の入力値を復元<br />
→親から子へ、復元用のデータを送る(@Input())<br />
<span style="font-size: large">2. 画面の各フィールドへの値入力(バリデーション)</span><br />
<span style="font-size: large">3. Submitによって、入力値を引き継ぎつつ、確認画面へ遷移</span><br />
→入力値を引数に、子から親へ処理を引き継ぐ(@Output())<br />
<br />
が実現出来たことになります。(長かった・・・・・)<br />
最終的に図で表すと下記のようになります。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQTMeEhShUrjBhsDbj6TfD5u7OpcN5mpk6bkBjv4MJsMmc8b9CJ3BsyZFA61AhbP_heaRVvbf4g7xC4053m3A1OexS4Rg4nmW13UbNIh6tA5k5DDzWyujlX9Y6sRg_ODshqysckWQBRmBXuov0Z6Dri21mMHVBH3-K-IMzU9v2kSCPtVRn7lMTUDK55Q/s1600/%E7%94%BB%E5%83%8F%EF%BC%96.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="804" data-original-width="1130" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQTMeEhShUrjBhsDbj6TfD5u7OpcN5mpk6bkBjv4MJsMmc8b9CJ3BsyZFA61AhbP_heaRVvbf4g7xC4053m3A1OexS4Rg4nmW13UbNIh6tA5k5DDzWyujlX9Y6sRg_ODshqysckWQBRmBXuov0Z6Dri21mMHVBH3-K-IMzU9v2kSCPtVRn7lMTUDK55Q/s1600/%E7%94%BB%E5%83%8F%EF%BC%96.png"/></a></div>
<br />
今回は、コンポーネント設計(と実装)を紹介しました。<br />
もっと複雑な画面になると、設計方法や実装も違ってきますので、それはまた別途記事にしようと思います。<br />
<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-88491331966209417892022-08-04T15:48:00.000+09:002022-08-04T15:48:22.112+09:00【Angular】エラーメッセージの管理について考える。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
以前「<a href="https://angular.jp/guide/form-validation"><u>FormのValidationの書き方を簡略化させる</u></a>」と言う記事を書きましたが、その時、特にValidationエラーのメッセージについては触れませんでした。<br />
今回、Validationエラーも含めた「<span style="font-size: large;color: #AB47BC;">メッセージ管理</span>」について考えて行こうと思います。<br />
以前の記事をまだ見ていない方は、是非、目を通して頂けると幸いです。<br />
<br />
ちなみに、今回の考え方と実装は、Angularに限らず(TypeScriptやJavaScriptを使っているフレームワークであれば)、同じように使えるかな、と思います。<br />
<br />
<h3>(1)そもそも、何が問題なのか</h3>
前回、最終的にテンプレート(View)側は下記のようになりました。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
[ngClass]="{'alert-danger' : v.nameInvalid}">
<ng-container *ngIf="v.nameInvalid">
<ng-container *ngIf="v.nameHasErrorRequired">
<p class="error-message">お名前は必ず入力してください。</p>
</ng-container>
<ng-container *ngIf="v.nameHasErrorMaxLength">
<p class="error-message">お名前は10文字以内で入力してください。</p>
</ng-container>
</ng-container>
</div>
<button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button>
</form>
</pre>
<br />
これの何が問題になるのでしょうか・・・・?<br />
<br />
例えば、必須入力のエラーである「<span style="font-size: large;color:#c60055;">xxxは必ず入力してください。</span>」ですが、内容を「<span style="font-size: large;color:#c60055;">xxxの入力は必須です。</span>」に変えたい、となった場合を考えます。<br />
テンプレート(View)側を変えれば良いのでしょうが、例えば、必須項目が数十個あった場合はどうでしょうか?<br />
そして、この画面だけでなく、他の画面でも必須項目があった場合はどうでしょうか?<br />
なるべく修正箇所は少なく済ませたいですが、どうしても対応する量が多くなってしまいます。<br />
<br />
<h3>(2)メッセージを1箇所で管理する</h3>
まあ、1箇所って言ってしまうと極端ですが、なるべくまとめて管理する、が良いと思います。<br />
<br />
※例えば、メッセージにも「正常」「エラー」「ワーニング」など種別があると思いますので、それを全て1箇所で管理すると、今度は管理するファイルが肥大化することになるので、種別毎に管理した方が良い・・・とか、ですね。<br />
<br />
早速、まとめて管理するファイルを作ろうと思います。<br />
弊社のプロジェクトでは、「app」配下に「shared」と言うディレクトリを作り、その中にプロジェクトで共通的に使うものを配置しています。<br />
今回は、以下のように作成します。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ shared/
└ message/
└ error-messages.ts
</pre>
<br />
見ての通り、今回は、あくまで「エラー用メッセージ」として管理します。<br />
<br />
<h3>(3)メッセージ管理処理の仕様について</h3>
さて、管理用のファイルを作ったところで、実装はどのようにしましょう。<br />
パッと思い付く要求仕様としては、<br />
<br />
<ul>
<li>複数のメッセージを定義可能。</li>
<li>メッセージは、差し込みが可能。</li>
<li>メッセージ毎にユニークなKeyを持ち、Keyからメッセージを取得可能。</li>
</ul>
<br />
と言うところですかね。<br />
<br />
特に今回は「メッセージは、差し込みが可能。」が重要です。<br />
必須項目であれば、「{0}は必ず入力してください。」と定義して、「{0}」の部分を名前だったり、電話番号だったり、郵便番号だったり・・・・と動的に差し込み出来れば、メッセージの定義は1つだけで済みます。<br />
<br />
では、要求仕様を満たす実装をします。<br />
<span style="font-size: small;color: #9E9E9E;">[error-messages.ts]</span><br />
<pre class="prettyprint linenums">
export const errorMessages: { [key: string]: string } = {
msg_error_field_required: '{0}は必ず入力してください。',
msg_error_field_max: '{0}は{1}文字以内で入力してください。',
};
function formatMessage(msg: string, ...args: any[]): string {
return msg.replace(/\{(\d+)\}/g, (m, k) => {
return args[k];
});
}
export function getMessage(messageId: string, ...args: any[]): string {
return formatMessage(errorMessages[messageId], ...args);
}
</pre>
<br />
ざっくりと説明します。<br />
まず、他の処理からこの「メッセージ管理処理」を使いたい時は、メッセージの Key と、差し込みたい文字列を引数に getMessage を呼ぶことになります。<br />
<pre class="prettyprint linenums">
export function getMessage(messageId: string, ...args: any[]): string {
return formatMessage(errorMessages[messageId], ...args);
}
</pre>
<br />
getMessage の中で、formatMessage が呼ばれ、ここで、Keyに該当するメッセージを取得しつつ、差し込み文字列を置換している、と言う感じです。<br />
<pre class="prettyprint linenums">
function formatMessage(msg: string, ...args: any[]): string {
return msg.replace(/\{(\d+)\}/g, (m, k) => {
return args[k];
});
}
</pre>
<br />
メッセージの定義自体は、連想配列として、Key・Valueで定義しています。<br />
<pre class="prettyprint linenums">
export const errorMessages: { [key: string]: string } = {
msg_error_field_required: '{0}は必ず入力してください。',
msg_error_field_max: '{0}は{1}文字以内で入力してください。',
};
</pre>
<br />
<h3>(4)メッセージ管理処理を使ってみる</h3>
では、実際にメッセージ管理処理を使ってみます。<br />
まずは、コンポーネント側の実装です。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.ts]</span><br />
<pre class="prettyprint linenums">
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RegisterValidator } from './register.validator';
import { getMessage } from '@app/shared/message/error-messages';
// (中略)
export class RegisterComponent implements OnInit {
// (中略)
formRegister: FormGroup = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(10)]],
});
// (中略)
message(messageId: string, ...args: any[]): string {
return getMessage(messageId, ...args);
}
</pre>
<br />
コンポーネント側で「error-messages.ts」のimportを行い、<br />
メッセージ管理処理の「getMessage」を呼び出す「message()」を追加しました。<br />
<br />
この「message()」を呼び出すのは、テンプレート側です。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
[ngClass]="{'alert-danger' : v.nameInvalid}">
<ng-container *ngIf="v.nameInvalid">
<ng-container *ngIf="v.nameHasErrorRequired">
<p class="error-message">{{ message('msg_error_field_required', 'お名前') }}</p>
</ng-container>
<ng-container *ngIf="v.nameHasErrorMaxLength">
<p class="error-message">{{ message('msg_error_field_max', 'お名前', 10)}}</p>
</ng-container>
</ng-container>
</div>
<button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button>
</form>
</pre>
<br />
元々、メッセージを固定で記載していた部分を以下のように変更しています。
<pre class="prettyprint linenums">
{{ message('msg_error_field_required', 'お名前') }}
{{ message('msg_error_field_max', 'お名前', 10)}}
</pre>
<br />
テンプレート側とコンポーネント側で「10」を2回定義しているのが気になりますね。ここは定数宣言して1回の定義で済むようにしましょう。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.ts]</span><br />
<pre class="prettyprint linenums">
nameMaxLength = 10;
formRegister: FormGroup = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(this.nameMaxLength)]],
});
</pre>
<br />
テンプレート側も変えておきます。
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
{{ message('msg_error_field_max', 'お名前', nameMaxLength)}}
</pre>
<br />
これで全ての対応が完了しました。<br />
<br />
今後、仮に「xxxは必ず入力してください。」を「xxxの入力は必須です。」に変えたいとなっても、<br />
全項目・全画面確認する必要はなく、「error-messages.ts」を変更すれば、一律変更出来ることになりました。<br />
<br />
<h3>(5)デメリットも把握しておく</h3>
当然ながら、汎用的に使われているメッセージについて、「特定の1箇所だけメッセージだけ変えたい」と言う要望に対しては、対応が難しくなります。<br />
まあ、そんな事はあまり無い思いますが、それよりも、実際に弊社が抱えている悩みとしては・・・・・<br />
<span style="font-size: large;color:#c60055;">デザイナーが作ったhtmlをAngular側に反映する時に苦労する。</span><br />
があります。<br />
<br />
この方式は結局のところ、テンプレート側にロジックを記載している事になるのですが、デザイナーとしては、そんなこと知ったこっちゃないので、元のままメッセージ直書きしたhtmlを渡してきます。<br />
画面のレイアウトに変更があった場合など、プログラマーが気を付ける必要がある、と言う事が、デメリットと言えばデメリットなのかもしれません。
<br />
<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-33407499552986201332022-08-03T13:30:00.002+09:002022-08-05T15:04:48.157+09:00【Angular】コンポーネントの設計(機能内の画面構成設計)について。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
今回は、弊社内のAngularプロジェクトでのコンポーネントの設計のルールを記載しようと思います。<br />
<br />
2記事に分けて「コンポーネントの設計」についてお届けしますが、今回は「<span style="font-size: large;color: #AB47BC;">機能内の画面構成設計</span>」となります。<br />
(次回は「画面ごとの設計」になります)<br />
<br />
それでは、今回は、『アカウント登録処理』を例に、コンポーネントの設計を説明しようと思います。<br />
<br />
<h3>(1)まずは必要な画面と、画面遷移を考える</h3>
「アカウント登録処理」として、以下のような画面が必要と想定されます。<br />
<br />
<span style="font-size: large;color: #AB47BC;">①登録情報入力画面</span><br />
<span style="font-size: large;color: #AB47BC;">②入力情報確認画面</span><br />
<span style="font-size: large;color: #AB47BC;">③登録完了画面</span><br />
<br />
当然、画面遷移としては以下になりますね。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEii1DSBf94d8RInegg1ttoXWG7ozYGaECrEiPzISbp87sdt8wwtmLlQDafg5RYEEvHknXPdyuZPCdGPBR74oSVdzh-Sbwg5mfl4-PJcFex6oADdKEbF_9GTDWE9hnp_cyFHsUNkPSCeYAMlh6i_gDyK91-5cRXiWkbvaHEwYPhR3vplhGAf9ZAn7zpDvw/s1600/%E7%94%BB%E5%83%8F%EF%BC%91.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="136" data-original-width="1058" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEii1DSBf94d8RInegg1ttoXWG7ozYGaECrEiPzISbp87sdt8wwtmLlQDafg5RYEEvHknXPdyuZPCdGPBR74oSVdzh-Sbwg5mfl4-PJcFex6oADdKEbF_9GTDWE9hnp_cyFHsUNkPSCeYAMlh6i_gDyK91-5cRXiWkbvaHEwYPhR3vplhGAf9ZAn7zpDvw/s1600/%E7%94%BB%E5%83%8F%EF%BC%91.png"/></a></div>
<br />
<h3>(2)機能としてのディレクトリ構成を考える</h3>
では、次は「アカウント登録処理」として、ディレクトリ構成を考えます。<br />
各画面の設計の前に、「機能」としての設計をする、って事ですね。<br />
<br />
まず、何も機能が無いAngularプロジェクトだと、<br />
<pre class="prettyprint linenums">
src/
├ app/
├ app-routing.module.ts
├ app.component.html
├ app.component.ts
└ app.module.ts
</pre>
となっています。<br />
<br />
ここに「アカウント登録処理」を追加するとしたら、以下のようになります。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ account/
├ containers/
│ └ account/
│ └ account.component.html
│ └ account.component.spec.ts
│ └ account.component.ts
├ account-routing.module.ts
└ account.module.ts
</pre>
いきなり「containers」ディレクトリが出てきましたが、後ほど説明しますので、一旦スルーしてください。<br />
ここでは、「account」と言う「アカウント登録処理」を示すコンポーネントを作った、と言うことになります。<br />
図で表すと、<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitYf4sUW63K4QoruoChncI0yeYbz5vJD-jlhT2TEjWrNlzsMikH3MMezXp7GBCScnpcI7QCzE797KZ6FJOyEL7b16kBupKkSvWQuQiW6QvHiCPxye4a6DV0cHlVH25zZ7to4C9HvAVxjclny8pCXKi-Bi5duodA7NNpLVF-kzxN_IKptgOTANXfsRJTw/s1600/%E7%94%BB%E5%83%8F%EF%BC%92.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="380" data-original-width="982" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitYf4sUW63K4QoruoChncI0yeYbz5vJD-jlhT2TEjWrNlzsMikH3MMezXp7GBCScnpcI7QCzE797KZ6FJOyEL7b16kBupKkSvWQuQiW6QvHiCPxye4a6DV0cHlVH25zZ7to4C9HvAVxjclny8pCXKi-Bi5duodA7NNpLVF-kzxN_IKptgOTANXfsRJTw/s1600/%E7%94%BB%E5%83%8F%EF%BC%92.png"/></a></div>
となります。これだけだと、「??」って感じですが、とりあえず先へ進みます。<br />
<br />
<h3>(3)各画面のディレクトリ構成を考える</h3>
続いて、各画面のディレクトリ構成を考えてみます。<br />
画面が3つだから、ディレクトリも3つで、コンポーネントとしても3つ・・・はい、最初はその考えで大丈夫です。<br />
Angularに限らず、コンポーネント設計で重要なのは、まずは、ざっと大きめな区分けで考えておいて、後から細分化していく、です。<br />
<br />
と言うことで、以下のような構成になります。<br />
<pre class="prettyprint linenums">
src/
└ app/
└ account/
└ containers/
├ account/
├ account-register/
│ └ account-register.component.html
│ └ account-register.component.spec.ts
│ └ account-register.component.ts
├ account-register-confirm/
│ └ account-register-confirm.component.html
│ └ account-register-confirm.component.spec.ts
│ └ account-register-confirm.component.ts
└ account-register-complete/
└ account-register-complete.component.html
└ account-register-complete.component.spec.ts
└ account-register-complete.component.ts
</pre>
<br />
図で表すと、<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhV6MttxorzTzVFCW44He-SCWhkUI07qi9gjiVS1qykTCLZiXjeG0OIUhPKRIjJhrLsNvLBP8CC40NaeavWF4Fv1D-pVOm7-05YxFjdH3hZ5xpx7ziv7HPTRY-B5HxfxPajsi0s_uMcuHEUI7b8j7pdVYXySY-ooQQCIi7h5z38hU85BrykPu61Epqaeg/s1600/%E7%94%BB%E5%83%8F%EF%BC%93.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="488" data-original-width="1748" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhV6MttxorzTzVFCW44He-SCWhkUI07qi9gjiVS1qykTCLZiXjeG0OIUhPKRIjJhrLsNvLBP8CC40NaeavWF4Fv1D-pVOm7-05YxFjdH3hZ5xpx7ziv7HPTRY-B5HxfxPajsi0s_uMcuHEUI7b8j7pdVYXySY-ooQQCIi7h5z38hU85BrykPu61Epqaeg/s1600/%E7%94%BB%E5%83%8F%EF%BC%93.png"/></a></div>
こんな感じですね。<br />
なんとなくイメージが出来るようになって来ました。<br />
<br />
ついでなので、ルーティングの設定も記載しておきます。<br />
<span style="font-size: small;color: #9E9E9E;">[app-routing.module.ts]</span><br />
<pre class="prettyprint linenums">
{
path: 'account',
loadChildren: () =>
import('./account/account.module').then(m => m.AccountModule),
},
</pre>
<br />
<span style="font-size: small;color: #9E9E9E;">[account-routing.module.ts]</span><br />
<pre class="prettyprint linenums">
const routes: Routes = [
{
path: '',
component: AccountComponent,
children: [
{ path: '', redirectTo: 'register' },
{
path: 'register',
component: AccountRegisterComponent,
},
{
path: 'register-confirm',
component: AccountRegisterConfirmComponent,
},
{
path: 'register-complete',
component: AccountRegisterCompleteComponent,
},
],
},
];
</pre>
<br />
上記を元に、先ほどの図にURLを記載すると、以下のようになります。<br />
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjI2A__35BIWDK5TKAyn6r1l5M9Aty_42162uz96t8Ug3XafRKd5lt7JJ8-nd_9lSE0IcmPUn5b3jKPIQize1gJGiES4E8V4YnST3lTrBo6AOl0pySTclsEOldRCbXn1p7B5Yvanl4L67nYQZvkOGJzkr_V8_zSFglzw8_UYx4U25Wbb6nIPL9B0GnAVw/s1600/%E7%94%BB%E5%83%8F%EF%BC%94.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="468" data-original-width="1748" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjI2A__35BIWDK5TKAyn6r1l5M9Aty_42162uz96t8Ug3XafRKd5lt7JJ8-nd_9lSE0IcmPUn5b3jKPIQize1gJGiES4E8V4YnST3lTrBo6AOl0pySTclsEOldRCbXn1p7B5Yvanl4L67nYQZvkOGJzkr_V8_zSFglzw8_UYx4U25Wbb6nIPL9B0GnAVw/s1600/%E7%94%BB%E5%83%8F%EF%BC%94.png"/></a></div>
<br />
今回はここまで。<br />
<br />
次回は、さらに細分化して「画面ごとのコンポーネント設計」を見て行こうと思います。<br />
<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-55621059954555938352022-08-02T09:54:00.000+09:002022-08-02T09:54:50.795+09:00【Angular】FormのValidationの書き方を簡略化させる。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s346/Fotolia_107330960_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="346" data-original-width="346" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKuSDl4NJAgWObqIJ_B9jKLkcVKJL7pki43pO4Eajk4mflTZTpeYwFhdxoCo_hgiWOBiICXydHp2mymmvlOtgk2hoHhz4QoCDSecEGPMUf4R5Vsp7AcIvYbgOCsg5gTda4sdxSdOhd20460__uQE8MsiB2IA-GULpLj8OMvtRJBJNc7Ad-Ybz7Ie6uMw/s320/Fotolia_107330960_XS.jpg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
Angularに限ったことでは無いですが、Form の Validation って、記載が煩雑になりますよね。<br />
弊社内のAngularプロジェクトでは、出来る限り記載を楽にしようと日々試行錯誤しています。<br />
<br />
今回は、<a href="https://angular.jp/guide/form-validation"><u>本家サイト</u></a>のValidationの説明を参考に、<br />
<span style="font-size: large;color: #AB47BC;">入力必須で最大10文字の「名前」項目</span><br />
を作ってみます。<br />
<br />
<h3>(1)とりあえずバリデーションを作ってみる</h3>
まずはテンプレート側を作ってみます。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)">
<ng-container *ngIf="name.invalid && (name.dirty || name.touched)">
<ng-container *ngIf="name.errors?.['required']">
<p class="error-message">お名前は必ず入力してください。</p>
</ng-container>
<ng-container *ngIf="name.errors?.['maxLength']">
<p class="error-message">お名前は10文字以内で入力してください。</p>
</ng-container>
</ng-container>
</div>
<button [disabled]="formRegister.invalid" type="submit">登録</button>
</form>
</pre>
<br />
続いて、コントロール(コンポーネント)側を作ります。(抜粋)<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.ts]</span><br />
<pre class="prettyprint linenums">
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// (中略)
export class RegisterComponent implements OnInit {
// (中略)
formRegister: FormGroup = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(10)]],
});
constructor(
private formBuilder: FormBuilder,
) {}
get name() { return this.formRegister.get('name'); }
ngOnInit(): void {
// (後略)
</pre>
<br />
ここだけ見ると、何て簡単なんだ!って思うのですが・・・・・<br />
<br />
<h3>(2)エラー時に背景色を変える</h3>
では、ここで、バリデーションエラーの時に、input項目の背景色を変えたい、となったとします。<br />
テンプレート側の修正をしましょう。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
[ngClass]="{'alert-danger' : name.invalid && (name.dirty || name.touched)}">
</pre>
「ngClass」を追加しました。<br />
<span style="font-size: small;color: #9E9E9E;">※ngClassの利用方法については、<a href="https://blog.officekoma.co.jp/2019/08/classangular.html"><u>以前の記事</u></a>を参照ください。</span><br />
<br />
「<span style="font-size: large;color: #AB47BC;">name.invalid && (name.dirty || name.touched)</span>」の部分、<br />
同じ条件なのに、また記載する事に・・・・<br />
<br />
例えば、「名前」の他に、「住所」、「電話番号」などが増えていくと、<br />
その都度、記載していく事に・・・・テンプレートがゴチャゴチャしていきますね。<br />
特に、「<span style="font-size: large;color: #AB47BC;"> (xxxx.dirty || xxxx.touched)</span>」の記載が大量になります。<br />
<br />
「デザイナーとの分業?知ったこっちゃねぇ」なら良いですが、普通は分業していると思うのでhtml(デザイン)に修正が入ると、どんどんAngularに取り込むのが辛くなります。<br />
<br />
「関心の分離」と言うことで、バリデーションはバリデーション専用のクラスを作り、それをコントロール(コンポーネント)側にDIする形にしてみようと思います。<br />
<br />
<h3>(3)バリデーションの処理を分ける</h3>
まずは、バリデーション専用のクラスを新規で作成します。<br />
<span style="font-size: small;color: #9E9E9E;">[register.validator.ts]</span><br />
<pre class="prettyprint linenums">
import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Injectable()
export class RegisterValidator {
private form: FormGroup;
constructor() {}
set formGroup(form: FormGroup) {
this.form = form;
}
get nameInvalid() {
return (
this.form.controls['name'].invalid &&
(this.form.controls['name'].dirty || this.form.controls['name'].touched)
);
}
get nameHasErrorRequired() {
return this.form.controls['name'].hasError('required');
}
get nameHasErrorMaxLength() {
return this.form.controls['name'].hasError('maxlength');
}
}
</pre>
上記クラスは、モジュールで定義をしてDI出来るようにしておいてください<br />
<pre class="prettyprint linenums">
@NgModule({
providers: [
RegisterValidator,
],
</pre>
<br />
続いて、コントロール(コンポーネント)側でDIします。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.ts]</span><br />
<pre class="prettyprint linenums">
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RegisterValidator } from './register.validator';
// (中略)
export class RegisterComponent implements OnInit {
// (中略)
formRegister: FormGroup = this.formBuilder.group({
name: ['', [Validators.required, Validators.maxLength(10)]],
});
constructor(
private formBuilder: FormBuilder,
public v: RegisterValidator,
) {}
ngOnInit(): void {
// バリデーション設定
this.v.formGroup = this.formRegister;
// (後略)
</pre>
<br />
<span style="font-size: large;color: #AB47BC;">get name() { return this.formRegister.get('name'); }</span><br />
は不要になるので、削除しています。<br />
<br />
コンストラクタで、
<pre class="prettyprint linenums">
constructor(
private formBuilder: FormBuilder,
public v: RegisterValidator,
) {}
</pre>
のように定義して、DIを行います。 public にしているのは、このオブジェクトをテンプレート側で使いたい為です。<br />
また、変数名はなるべく短い方が、テンプレート側がゴチャゴチャしません<br />
<br />
「ngOnInit」で、Formの設定内容をバリデーションクラスに送るのも忘れずに。<br />
<pre class="prettyprint linenums">
ngOnInit(): void {
// バリデーション設定
this.v.formGroup = this.formRegister;
</pre>
<br />
<h3>(4)テンプレート側の記載を変える</h3>
最後はテンプレート側を変更します。<br />
<span style="font-size: small;color: #9E9E9E;">[register.component.html]</span><br />
<pre class="prettyprint linenums">
<form [formGroup]="formRegister" (ngSubmit)="onSubmit()" (keydown.enter)="$event.preventDefault()">
<div>
<label>お名前</label>
<input formControlName="name" type="text" class="form-control" placeholder="お名前を入力(必須)"
[ngClass]="{'alert-danger' : v.nameInvalid}">
<ng-container *ngIf="v.nameInvalid">
<ng-container *ngIf="v.nameHasErrorRequired">
<p class="error-message">お名前は必ず入力してください。</p>
</ng-container>
<ng-container *ngIf="v.nameHasErrorMaxLength">
<p class="error-message">お名前は10文字以内で入力してください。</p>
</ng-container>
</ng-container>
</div>
<button class="btn btn1 ml-auto" [disabled]="formRegister.invalid" type="submit">登録</button>
</form>
</pre>
<br />
「<span style="font-size: large;color: #AB47BC;">name.invalid && (name.dirty || name.touched)</span>」の条件を「<span style="font-size: large;color: #AB47BC;">v.nameInvalid</span>」で判断出来るので、かなり記載がスッキリします。<br />
<br />
「v.nameHasErrorRequired」などについても、実際の処理は、バリデーション専用のクラス側にあるので、処理の分離も出来ています。<br />
<br />
という事で、今回はAngularのValidationの書き方を簡略化してみました。<br />
<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-57408842490681088512022-05-23T11:26:00.000+09:002022-05-23T11:29:37.211+09:00【Red Hat Linux 8.2】ActiveDirectoryユーザでWebアプリのBASIC認証しようとしたらハマった話。<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhg5a0BDpr_bCbEpP3SR0rlE-bWdw_zDcLjddl203IsMYoET4d9S7C8Nni8tlPviUbuSWlSEnkLRpO9YjmcpT9dAkbpAzkBJiHBfY5EWYq1_SCytEUSm4JaSBqrA28tXNmFJ63QdDZ-BLVwtdzUtEakHS758DwJqJ4JM7pT2DLPTU_DmO5zDbOONKi9Hg/s447/Fotolia_231814718_XS.jpg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="268" data-original-width="447" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhg5a0BDpr_bCbEpP3SR0rlE-bWdw_zDcLjddl203IsMYoET4d9S7C8Nni8tlPviUbuSWlSEnkLRpO9YjmcpT9dAkbpAzkBJiHBfY5EWYq1_SCytEUSm4JaSBqrA28tXNmFJ63QdDZ-BLVwtdzUtEakHS758DwJqJ4JM7pT2DLPTU_DmO5zDbOONKi9Hg/s320/Fotolia_231814718_XS.jpg"/></a></div>
技術部のyuckieee(ゆっきー)です!<br/>
最近、RedHatLinux上でWebアプリ(Web/APサーバ設定含む)を行ったのですが、その際にハマったエラーの解決策をメモしておきます。ググっても、RedHatLinux公式のカスタマーポータル等で探しても、中々それらしき記述がなく、めちゃくちゃ時間を潰しました。<br/>
<h3><u>■発生事象</u></h3>
今回は、外部ActiveDirecotry上に設定されたID/パスワードを使用して、WebアプリのBASIC認証を行う形をとっていました。<br/>
ですが、何度認証を行ってもBASIC認証ダイアログが出続けログインが出来ません。<br/>
SSSDのログには、BASIC認証を行ったタイミングで以下のようなエラーが表示されていました。<br/>
<pre class="prettyprint linenums lang-json">
March 07 00:00:00 httpd [999999]: pam_sss(webapp:auth): authentication success; logname= uid=0 euid=0 tty= ruser= rhost= user=user_name
March 07 00:00:00 httpd [999999]: pam_sss(webapp:account): Access denied for user user_name: 6 (Permission denied)
</pre>
内容を見ると、認証は成功するのに該当ユーザの権限が無いためにエラーとなっているように見えます。ただ、ActiveDirectoryにはユーザは間違いなく登録されてており、該当サーバへのアクセスが許可されたグループに所属、現にSSHでは接続できていました。謎です。<br/>
<h3><u>■発生環境</u></h3>
[構成]<br/>
OS:RedHatLinux 8.2<br/>
Web/Apサーバ:Apache 2.4.xx<br/>
- BASIC認証のため、mod_authnz_pamモジュール使用<br/>
- WebアプリはPython3.xを使用して構築(Apacheと連携させるため、mod_httpdを使用) <br/>
認証:PAM<br/>
ActiveDirecotry連携:SSSD 2.x<br/>
<br/>
具体的なBASIC認証の設定は以下サイトを参考に行いました。<br/>
[参考]<a href="https://www.adelton.com/apache/mod_authnz_pam/">Apache module mod_authnz_pam</a><br/>
※pamの設定のみ以下のように変更して、ローカルユーザでの認証もできるようにしていました。<br/>
<pre class="prettyprint linenums lang-json">
#%PAM-1.0
auth include system-auth
account include system-auth
session include system-auth
</pre>
<h3><u>■事象分析</u></h3>
エラー内容より「<code class="inline-code">pam_sss</code>によるwebapp(参考サイトでtlwikiとなっていたPAMサービス名)の<code class="inline-code">account</code>チェックにおいてアクセス権限エラーが出ている」と言うことは分かります。<br/>
そのため、<code class="inline-code">pam_sss</code>の<code class="inline-code">account</code>を呼び出している箇所をコメントアウトしてみました。
<pre class="prettyprint linenums lang-json">
/etc/pam.d/system-auth
#account [default=bad success=ok user_unknown=ignore] pam_sss.so ←こちらコメントアウト
</pre>
<br/>
まさかですが、これでBASIC認証自体は可能となりました。<br/>
<br/>
同じ<code class="inline-code">pam_sss</code>を使用しているであろうSSH接続では発生していないことから、Webアプリ限定のエラーだと分かります。
ちなみに<code class="inline-code">account</code>は以下の役割を果たしているそうです。(<a href="https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system-level_authentication_guide/pam_configuration_files">10.2. PAM 設定ファイルについて</a>)
<pre class="lang-json"><code>account: このモジュールインターフェースは、アクセスが許可されることを確認します。
たとえば、ユーザーアカウントの有効期限が切れたか、または特定の時間にユーザーがログインできるかどうかを確認します。
</code></pre>
アクセス許可の確認と書いてあるとおり、コメントアウトすることでGPOベースのアクセス制御が外れた状態になることが分かりました。<br/>
というか、このGPOベースのアクセス制御の設定に問題が有りました。<br/>
<br/>
<h3><u>■発生原因</u></h3>
SSSDのバージョンアップに際し、セキュリティ向上の観点からデフォルトでActiveDirectoryのGPOベースのアクセス制御が有効となったようです。<br/>
さらに、本アクセス制御対象はデフォルト以外のPAMサービスがデフォルトで「拒否」状態となっており、デフォルトではない「Webアプリ用PAMサービス」は「拒否」となり、GPOベースのアクセス制御自体が許可されていない状態でした。<br/>
エラーメッセージは、ユーザではなく、PAMサービスに対する権限エラーだったのですね。<br/>
<br/>
[参考]<br/>
<a href="https://www.ibm.com/support/pages/cannot-log-clearcase-wan-server-after-enabling-sssd-based-authentication-windows-active-directory-red-hat-linux-8x">Cannot log in to ClearCase WAN server after enabling SSSD-based authentication to Windows Active Directory on Red Hat Linux 8.x.</a><br/>
<a href="https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html-single/integrating_rhel_systems_directly_with_windows_active_directory/index">2.6.3. SSSD の GPO ベースのアクセス制御の設定</a>
<h3><u>■解決方法</u></h3>
解決方法としては以下2パターンが考えられます。<br/>
<b>①GPOベースのアクセス制御時、デフォルト以外のPAMサービスの扱いを「拒否」から「許可」に設定変更(=記載追加)する。</b><br/>
以下のとおり<code class="inline-code">sssd.conf</code>のADドメインのセクションに記載を追加し、SSSDサービスを再起動することで対応。
<pre class="prettyprint linenums lang-json">
/etc/sssd/sssd.conf
ad_gpo_default_right=permit
</pre>
<br/>
<b>②GPOベースのアクセス制御時、作成したPAMサービスを「許可」するよう設定追加する。</b><br/>
以下のとおり<code class="inline-code">sssd.conf</code>のADドメインのセクションに記載を追加し、SSSDサービスを再起動することで対応。
<pre class="prettyprint linenums lang-json">
/etc/sssd/sssd.conf
ad_gpo_map_interactive = + webapp
</pre>
<br/>
解決策として①だと影響範囲が大きそうでしたので②を採用しました。
<h3><u>■まとめ</u></h3>
今回、ActiveDirectoryやSSSD設定が自分の担当ではなかったことで原因調査がかなり難航しました。<br/>
振り返りとしては、早期にSSSDのデバッグログ出力設定変更などの相談をしておくと良かったのかなとも思いましたが、原因箇所が特定出来ない中では中々難しいですね。<br/>
<br/>
個人的には良い経験となったと思いますので、この内容を今後に活かしていければ良いなと考えています。<br/>
<!--編集禁止(ここから)-->
<style>
.inline-code {
background: #f0f5f9;
border: 1px solid #e6edf3;
padding: .04em .3em;
margin: 0 .2em;
border-radius: 3px;
line-height: 1.4;
font-size: .85em;
}
</style>
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/default.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
<script>
hljs.initHighlightingOnLoad();
hljs.initLineNumbersOnLoad();
</script>
<link href="/styles/shThemeEmacs.css" rel="Stylesheet" type="text/css"></link>
<!--編集禁止(ここまで)-->
オフィス狛 技術部 yuckieeehttp://www.blogger.com/profile/18033915954934523504noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-91726735984056844212022-05-16T13:46:00.000+09:002022-05-16T13:46:40.362+09:00Angular ユニットテスト エラー(NgRxのReducerで発生する「TypeError: Cannot read properties of undefined (reading 'xxxxx')」)の対応。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s4243/AdobeStock_136308523.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="3536" data-original-width="4243" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s320/AdobeStock_136308523.jpeg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
<a href="https://blog.officekoma.co.jp/2022/05/angular-nullinjectorerror-no-provider.html"><u>前回</u></a>、<a href="https://blog.officekoma.co.jp/2022/04/angular-nullinjectorerror-no-provider.html"><u>前々回</u></a>に引き続き、Angularでユニットテスト作成する時のエラー対応です。<br />
今回はNgRx関連のエラーです。<br />
<pre class="prettyprint linenums">
error properties: Object({ longStack: 'TypeError: Cannot read properties of undefined (reading 'xxxxx')
at http://localhost:9876/_karma_webpack_/webpack:/src/app/koma/store/reducers/index.ts:xx:xx
</pre>
みたいなエラーが出てしまった場合の対応です。<br />
<br />
・・・・ええ、言いたいことは分かります。<br />
<span style="font-size: large;color: #7b1fa2;">undefinedって出ているんだから、該当箇所見ればすぐ分かるじゃん。</span><br />
って事ですよね?<br />
<br />
まあ、仰る通りなのですが、エラーとなっている箇所は、コンポーネント側ではなく、NgRxのReducer(index.ts)で、エラーも微妙な位置なので、どう修正すれば良いのか、分からないんですよね・・・・。<br />
<br />
ちなみに、コンポーネント側は、<br />
<span style="font-size: small;color: #9E9E9E;">[koma-confirm.component.ts]</span><br />
<pre class="prettyprint linenums">
export class KomaConfirmComponent implements OnInit, OnDestroy {
komaConfirmSubscription: Subscription = new Subscription();
saveData: KomaViewSaveModel;
constructor(
private router: Router,
public store: Store<fromKoma.State>
) {}
ngOnInit(): void {
// 画面間保持項目の退避
this.komaConfirmSubscription.add(
this.store
.pipe(select(fromKoma.getDataRegisterPost))
.subscribe(model => {
if (model) {
this.saveData = model;
} else {
this.router.navigateByUrl('/koma/home');
}
}),
);
}
ngOnDestroy(): void {
this.komaConfirmSubscription.unsubscribe();
}
}
</pre>
となっています。<br />
<br />
良くある登録系機能(入力→確認→完了)の「確認画面」を想定して頂くと分かりやすいです。<br />
入力画面で入力された値をStoreに格納し、それを確認画面(のngOnInit)で取得しようとしています。<br />
もし、Storeからデータを取得出来ない場合、不正な遷移を行ったと見做し、ホーム画面に遷移させています。<br />
<br />
なので、<br />
<pre class="prettyprint linenums">
this.store
.pipe(select(fromKoma.getDataRegisterPost))
</pre>
が何となく怪しいのは分かるのですが、Reducer(index.ts)側は、<br />
<br />
<span style="font-size: small;color: #9E9E9E;">[/src/app/koma/store/reducers/index.ts]</span><br />
<pre class="prettyprint linenums">
export const getKomaState = createSelector(
getKomaFeatureState,
(state: KomaFeatureState) => state.koma, // ←ここがエラーになってる。
);
export const getDataRegisterPost = createSelector(
getKomaState,
fromKoma.getDataRegisterPost, // ←せめてここでエラーになっていれば・・・・
);
</pre>
上記の「<span style="font-size: large;color: #7b1fa2;">state.koma</span>」の部分がエラーとなっていて、内容としては、<br />
<pre class="prettyprint linenums">
TypeError: Cannot read properties of undefined (reading 'koma')
</pre>
と出ています。<br />
<br />
せめて、<br />
<pre class="prettyprint linenums">
fromKoma.getDataRegisterPost,
</pre>
の部分でエラーになっていれば、エラー原因の当たりがすぐにつくのですが・・・・<br />
<br />
では、このエラーの対応をしていきたいと思います。<br />
<br />
テストファイルの方を見ていきましょう。<br />
<span style="font-size: small;color: #9E9E9E;">[koma-confirm.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { KomaConfirmComponent } from './koma-confirm.component';
describe('KomaConfirmComponent', () => {
let component: KomaConfirmComponent;
let fixture: ComponentFixture<KomaConfirmComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [KomaConfirmComponent],
providers: [
provideMockStore(),
],
imports: [
RouterTestingModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KomaConfirmComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
</pre>
DIの設定だけはしている感じですね。<br />
<br />
<a href="https://blog.officekoma.co.jp/2022/05/angular-nullinjectorerror-no-provider.html"><u>前回</u></a>は、「StoreのdispatchでActionの実行をテストする」だったので、特に意識しなかったのですが、<br />
Storeからデータを取得する、という場合は、「selector」と、取得するデータを定義する必要があります。<br />
<br />
<span style="font-size: small;color: #9E9E9E;">[koma-confirm.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import * as fromKoma from '../../store/reducers';
import { KomaConfirmComponent } from './koma-confirm.component';
describe('KomaConfirmComponent', () => {
let component: KomaConfirmComponent;
let fixture: ComponentFixture<KomaConfirmComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [KomaConfirmComponent],
providers: [
provideMockStore({
selectors: [
{ selector: fromKoma.getDataRegisterPost,
value: {
komaId: '1',
komaData: '1111',
}
},
]
}),
],
imports: [
RouterTestingModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KomaConfirmComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
</pre>
<br />
provideMockStoreの定義に引数として、selectorとvalueを設定しています。<br />
<br />
これで、エラー(TypeError: Cannot read properties of undefined (reading 'xxxxx'))は出なくなると思います。<br />
<br />
では、ちゃんとテストケースを追加していこうと思います。<br />
<br />
まず、書き換えたいのが、<br />
provideMockStoreの定義に引数として、selectorとvalueを設定している部分です。<br />
今、そこを追加したのに・・・変えるんかい!って感じですね。<br />
<br />
後々、テストケースが追加になった時に、selector内の値の入れ替えを楽にしたいので、<br />
provideMockStoreの引数で設定するのではなく、テストクラス内で使えるように変えていこうと思います。<br />
テストケースも追加していきます。<br />
<span style="font-size: small;color: #9E9E9E;">[koma-confirm.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import * as fromKoma from '../../store/reducers';
import { KomaConfirmComponent } from './koma-confirm.component';
import { KomaViewSaveModel } from '../../models/koma';
describe('KomaConfirmComponent', () => {
let component: KomaConfirmComponent;
let fixture: ComponentFixture<KomaConfirmComponent>;
// 全テストケースで利用出来るように、ここで定義
let store: MockStore<fromKoma.State>;
let router: Router;
let mockDataRegisterPostSelector;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [KomaConfirmComponent],
providers: [
provideMockStore(),
],
imports: [
RouterTestingModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KomaConfirmComponent);
// DI後のインスタンスを取得
router = TestBed.inject(Router);
store = TestBed.inject(MockStore);
// データの初期値を設定
mockDataRegisterPostSelector = store.overrideSelector(
fromAccount.getDataRegisterPost, {
komaId: '1',
komaData: '1111'
}
);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('ngOnInitのテスト', () => {
it('画面引き継ぎ情報が無い場合、ホーム画面へ遷移すること', () => {
// 処理のモックを作成
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
// データにnullを設定
mockDataRegisterPostSelector.setResult(null);
// テスト対象のメソッドを呼び出す
component.ngOnInit();
// navigateByUrlが、'/koma/home'を引数として呼ばれているか確認
expect(navigateByUrlSpy).toHaveBeenCalledWith('/koma/home');
});
it('画面引き継ぎ情報がある場合、データを退避すること', () => {
// 処理のモックを作成
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
// データを再設定
const formModel: KomaViewSaveModel = {
komaId: '2',
komaData: '2222'
};
mockDataRegisterPostSelector.setResult(formModel);
// テスト対象のメソッドを呼び出す
component.ngOnInit();
// 退避されたデータの中身をチェック
expect(component.saveData).not.toBeUndefined();
expect(component.saveData).not.toBeNull();
expect(component.saveData.komaId).toBe('2');
expect(component.saveData.komaData).toBe('2222');
// navigateByUrlが、呼ばれていないか確認
expect(navigateByUrlSpy).not.toHaveBeenCalled();
});
});
});
</pre>
<br />
ポイントとしては、「overrideSelector」によって、selectorを再定義している部分<br />
<pre class="prettyprint linenums">
mockDataRegisterPostSelector = store.overrideSelector(
// (中略)
);
</pre>
と、値を再設定する
<pre class="prettyprint linenums">
mockDataRegisterPostSelector.setResult(formModel);
</pre>
の部分ですね。<br />
<br />
これで、Store内のデータを設定し、テストをすることが可能になりました!<br />
<br />
今後も、Angularでユニットテストについて、ブログに書いて行こうと思います。<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-63395754574067097132022-05-13T13:54:00.002+09:002022-05-13T13:54:49.351+09:00Angular ユニットテスト エラー( NullInjectorError: No provider for Store )の対応。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s4243/AdobeStock_136308523.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="3536" data-original-width="4243" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s320/AdobeStock_136308523.jpeg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
<a href="https://blog.officekoma.co.jp/2022/04/angular-nullinjectorerror-no-provider.html"><u>前回</u></a>に引き続き、Angularでユニットテスト作成時のエラー対応です。<br />
今回もDI関連のエラーです。<br />
<pre class="prettyprint linenums">
NullInjectorError: R3InjectorError(DynamicTestModule)[Store -> Store]:
NullInjectorError: No provider for Store!
</pre>
上記のエラーが出てしまった場合の対応です。<br />
<br />
これは、NgRxのStoreを使用している場合に出てくるエラーとなります。<br />
NgRxの<a href="https://ngrx.io/guide/store/testing"><u>公式サイト</u></a>を見ると、コンポーネント側のテスト記載のサンプルがあるので、詳細はそちらを見るとして、とりあえず、エラーを無くしてみます。<br />
まずは、コンポーネント側のロジックを確認してみましょう。<br />
<br />
例として、よくある「入力画面(から確認画面)」を挙げてみます。<br />
「入力画面で、入力した値をSubmitによって、Storeに保存し、確認画面へ引き継ぐ」って感じですね。<br />
<span style="font-size: small;color: #9E9E9E;">[input.component.ts]</span><br />
<pre class="prettyprint linenums">
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import * as fromInput from '../../store/reducers';
import * as InputActions from '../../store/actions/input.actions';
import { InputViewSaveModel } from '../../models/input';
// (中略)
constructor(
private router: Router,
public store: Store<fromInput.State>,
) {}
// (中略)
onSubmit(inputData: InputViewSaveModel): void {
// 入力情報をStoreに格納して(=引き継ぎ情報として)確認画面へ遷移
this.store.dispatch(InputActions.setRegisterPostData({ data: inputData }));
this.router.navigateByUrl('/koma/confirm');
}
</pre>
<br />
ここではNgRxの仕様については触れませんので、NgRxを知っている前提で進めます。<br />
それと、前回やった「Router」もありますので、一緒にエラーが出ないように対応してみます。<br />
<br />
まずは必要モジュールのインポートと、DIをします。<br />
(Angularでは、DI可能なクラスであれば、コンポーネントのコンストラクタに定義する事で、DIして使用可能になります。)<br />
<span style="font-size: small;color: #9E9E9E;">[input.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { InputComponent } from './input.component';
describe('InputComponent', () => {
let component: InputComponent;
let fixture: ComponentFixture<InputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [InputComponent],
providers: [provideMockStore()],
imports: [RouterTestingModule],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
</pre>
と言う感じになります。<br />
<br />
Storeに関する部分は、<br />
<pre class="prettyprint linenums">
import { provideMockStore } from '@ngrx/store/testing';
</pre>
と、<br />
<pre class="prettyprint linenums">
providers: [provideMockStore()],
</pre>
を追加しています。<br />
先ほどのエラー(NullInjectorError)を無くすだけであれば、実は上記でOKです。<br />
<br />
ただ、当然テストケースは作らないといけないので、ついでにやってみましょう<br />
<span style="font-size: small;color: #9E9E9E;">[input.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import * as fromInput from '../../store/reducers';
import * as InputActions from '../../store/actions/input.actions';
import { InputViewSaveModel } from '../../models/input';
import { InputComponent } from './input.component';
describe('InputComponent', () => {
let component: InputComponent;
let fixture: ComponentFixture<InputComponent>;
// 全テストケースで利用出来るように、ここで定義
let router: Router;
let store: MockStore<fromInput.State>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [InputComponent],
providers: [provideMockStore()],
imports: [RouterTestingModule],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(InputComponent);
// DI後のインスタンスを取得
router = TestBed.inject(Router);
store = TestBed.inject(MockStore);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('ボタンアクションテスト', () => {
it('確認ボタン押下時 Storeにデータを保存し、確認画面へ遷移する事', () => {
// 処理のモックを作成
const dispatchSpy = spyOn(store, 'dispatch');
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
// 画面から入力された値を再現
const formModel: InputViewSaveModel = {
id: '111',
name: 'koma-chan',
address: 'tokyo',
};
// 実行されるアクションを定義(画面からの入力値をStoreへ保存するアクション)
const expectedAction = InputActions.setRegisterPostData({ data: formModel });
// テスト対象のメソッドを呼び出す
component.onSubmit(formModel);
// Storeのdispatchが、アクションを引数として呼ばれているか確認
expect(dispatchSpy).toHaveBeenCalledWith(expectedAction);
// navigateByUrlが、'/koma/confirm'を引数として呼ばれているか確認
expect(navigateByUrlSpy).toHaveBeenCalledWith('/koma/confirm');
});
});
});
</pre>
<br />
ポイントとしては、DIしたクラスをテスト側で使用する為には、<br />
<pre class="prettyprint linenums">
store = TestBed.inject(MockStore);
</pre>
が必要、というところです。<br />
実際のテストの部分は、プログラム内のコメントを参照して頂ければと思いますが、流れ的に、<br />
<br />
<span style="font-size: medium;">① spyOn で、Store の dispatch に対するモックを作成</span><br />
<span style="font-size: medium;">② 画面の入力値を Store に保存するアクションを定義</span><br />
<span style="font-size: medium;">③ コンポーネント側の onSubmit を呼び出す</span><br />
<span style="font-size: medium;">③ expect の toHaveBeenCalledWith で、指定の引数(アクション)で dispatch が呼ばれたかを判定</span><br />
<br />
となっています。<br />
<br />
ちょっと長くなってしまったので、その他のStore関連のテストエラーは別途記事にしようと思います。<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-66833124970914862732022-05-12T16:28:00.001+09:002022-05-12T17:08:29.933+09:00position: absolute;を使って重ねた画像にCSSで乗算の影をつける。<p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQltpto8rjGvgRLr7WhTtQEGt5L0Sq4x2jw5KG0nbo835-8hf_woLjOMFg7Xofdgt30sLiFJNDgoMQNQBV5FGMLuI1b6s-_DYUOIeLBAdsZLgBG6rtRQ-_NgTjpnCvCn479a-g98k7Gp5dzKyl8UN77RSo9GTsk_c2loQuBJPd2yCXRf5XHhpa7kcRbA/s500/20225.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="320" data-original-width="500" height="205" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjQltpto8rjGvgRLr7WhTtQEGt5L0Sq4x2jw5KG0nbo835-8hf_woLjOMFg7Xofdgt30sLiFJNDgoMQNQBV5FGMLuI1b6s-_DYUOIeLBAdsZLgBG6rtRQ-_NgTjpnCvCn479a-g98k7Gp5dzKyl8UN77RSo9GTsk_c2loQuBJPd2yCXRf5XHhpa7kcRbA/s320/20225.png" width="320" /></a></div><br /> <p></p><p>こんにちは、オフィス狛 デザイン部のSatoです。</p><p><br /></p><p>先日、並んで表示されている複数の要素の上に、position: absolute;を使って画像を重ねて配置し、配置した画像に影をつけたいことがありました。</p><p>しかし、配置した画像は別の色の要素を跨いでいるため、普通の方法ではなかなか自然な影にならず困りました。</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYYKURv_CCDWZKP47pPjNb3C8Xe4hwjN4Br5CnKFIExTw7sCysAL63XrWBmkAak9FK0JCWD82uC3heUviG1-UeELURJmvvS0Fc6Axzdz0Lfw4jSbreOnF1UeD8EjAQk3lr1umga5M30fRE5UVARG1wVWeewmtuq3dA8ZNsfd3nrDbJB2xMO_nE1yfP0w/s1040/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="660" data-original-width="1040" height="406" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYYKURv_CCDWZKP47pPjNb3C8Xe4hwjN4Br5CnKFIExTw7sCysAL63XrWBmkAak9FK0JCWD82uC3heUviG1-UeELURJmvvS0Fc6Axzdz0Lfw4jSbreOnF1UeD8EjAQk3lr1umga5M30fRE5UVARG1wVWeewmtuq3dA8ZNsfd3nrDbJB2xMO_nE1yfP0w/w640-h406/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A8.png" width="640" /></a></div><p>そこでCSSの<a href="https://developer.mozilla.org/ja/docs/Web/CSS/mix-blend-mode">ブレンドモード</a>で乗算の影をつけたら自然になるはずと思い試していたのですが、調整に時間がかかってしまいました。</p><p>あまり発生しないケースかもしれませんが、今後の自分用の備忘録も兼ねて解決方法を書いていこうかとおもいます。</p><p><br /></p><h3>上手くいかなかった方法</h3><div><br /></div><p>最初、imgをdivで囲みdrop-shadowを指定してあげれば影ができるはずですし、一緒に「mix-blend-mode: multiply;」を指定してみました。<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfGjnZz1kSxjshv2r4ILNDvpfHjk1SNuPiFvBxzv4jBXUpCwdv4VQSRB1ucG99SUpdlu5vyuaAwnfyJq1RZr-wX3zcWbPourJdJMtkly6dXtKsvLQ9TGvIZu_Tr_ce1W_lUWL-FzDwZF2kolhuj9wUsqrKqjvVEkWfKUd6rFWclBveYC1Ri_TDcu5RuQ/s520/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A81.png" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="330" data-original-width="520" height="254" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfGjnZz1kSxjshv2r4ILNDvpfHjk1SNuPiFvBxzv4jBXUpCwdv4VQSRB1ucG99SUpdlu5vyuaAwnfyJq1RZr-wX3zcWbPourJdJMtkly6dXtKsvLQ9TGvIZu_Tr_ce1W_lUWL-FzDwZF2kolhuj9wUsqrKqjvVEkWfKUd6rFWclBveYC1Ri_TDcu5RuQ/w400-h254/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A81.png" width="400" /></a>予想はできていましたが、影だけではなく画像まで乗算になってしまいました😔</p><p><br /></p><p>それならば、imgタグに擬似要素「::before」をつけて、画像と同じサイズにした影を作りz-indexを使って画像下になるよう重ねてみようと思いました。<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVr_daNslmmfczIgZ9ruX90qAZBBRm7V0SdSy4PoF0NYcsAq02ix9ZEYMwNaUrrRUjfNy6pROGTxEGTPfHphyPLCZsHDX8RnGMj1r8W8subtZgDRBC8xtByJIthYt6Wjum-IlhGFFxJVqm2nqjp-38_TTVumglJelspZ3RloHzffhoZL9H2Td7Z0iE7g/s520/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A82.png" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="330" data-original-width="520" height="254" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVr_daNslmmfczIgZ9ruX90qAZBBRm7V0SdSy4PoF0NYcsAq02ix9ZEYMwNaUrrRUjfNy6pROGTxEGTPfHphyPLCZsHDX8RnGMj1r8W8subtZgDRBC8xtByJIthYt6Wjum-IlhGFFxJVqm2nqjp-38_TTVumglJelspZ3RloHzffhoZL9H2Td7Z0iE7g/w400-h254/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A82.png" width="400" /></a>……影が出てこないので、調べた所「::before」「::after」はimgには指定できないとのこと。初歩的なミスをやらかしてしまいました😔</p><p><br /></p><p>それならば…imgを囲んでいるdivに擬似要素「::before」をつけようとしました。</p><p>影が画像の上に乗ってしまったのでz-indexを指定すると、乗算でなくなってしまいます😔<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMXMK09QkOAFBi5lO36jiVgmmVo8HMl_nspcXOveRKqjFuon_W3wAVee6Z3_wp_L5Gt_CjkVSEv3wYpVXbPCSS7pu1HNRWecBMISKHwmtmA7K-YCaHEGFsNmZuktYso9_4N2iMouN3aIpIn7OH3n5ddbNIErShxxX87SrRlthvxBG9lhOlQOkHkFY50g/s520/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A84.png" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="330" data-original-width="520" height="254" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhMXMK09QkOAFBi5lO36jiVgmmVo8HMl_nspcXOveRKqjFuon_W3wAVee6Z3_wp_L5Gt_CjkVSEv3wYpVXbPCSS7pu1HNRWecBMISKHwmtmA7K-YCaHEGFsNmZuktYso9_4N2iMouN3aIpIn7OH3n5ddbNIErShxxX87SrRlthvxBG9lhOlQOkHkFY50g/w400-h254/%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%995%E6%9C%88%E7%94%A84.png" width="400" /></a><a href="https://standard.shiftbrain.com/blog/mix-blend-mode-and-stacking-context">mix-blend-modeとスタックコンテキスト</a></p><div><span style="text-align: left;">↑こちらのサイト曰くmix-blend-modeは</span>同じスタックコンテキストに属している要素にしか効果がないらしいです。</div><div>ブレンドモードを使いこなすのは難しいですね…。</div><p><br /></p><p><br /></p><h3>上手くいった方法</h3><div><br /></div><p>親要素の中に影用のdivを作り、画像と同じサイズにした影用の要素を画像の下に重なるよう配置した所、ようやく想像した通りの見た目になりました😄</p>
<p class="codepen" data-default-tab="html,result" data-height="300" data-slug-hash="zYpgmVw" data-user="officekoma_sato" style="align-items: center; border: 2px solid; box-sizing: border-box; display: flex; height: 300px; justify-content: center; margin: 1em 0px; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/officekoma_sato/pen/zYpgmVw">
position: absolute;で浮かせた画像に乗算の影をつける</a> by sato (<a href="https://codepen.io/officekoma_sato">@officekoma_sato</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
<p>上記のような感じになりました。</p><p>どちらの色のエリアでも違和感のない自然な影になり満足です✌️</p><p>あまり発生しないケースですし、影色を乗算にしなければ良いのですが、どうしても乗算を使いたいケースがあれば参考にしてみてください。</p>オフィス狛 デザイン部 Satohttp://www.blogger.com/profile/09225615773755838034noreply@blogger.com0tag:blogger.com,1999:blog-3991430445458535210.post-31269629268838547612022-04-28T15:57:00.001+09:002022-04-28T15:57:19.147+09:00Angular ユニットテスト エラー( NullInjectorError: No provider for Router )の対応。<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s4243/AdobeStock_136308523.jpeg" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" width="320" data-original-height="3536" data-original-width="4243" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhkSxFlA2Oh7Ha-58Lm0Dwtlrf0R_IHU_xXD6QjeTzw4WxJOPde3emRYGxc4ub05o6de3p7IAXHeXVZwdCjf8ikOXRuvkP1R_TuYePIFFTriFQt_xeR5Py1XebqkvpTohVmWAhUHTdTpWqVa5MjvcWTRrwORmjpRJeRwtYVtzuVGfDDkGkLScQbyYmwtQ/s320/AdobeStock_136308523.jpeg"/></a></div>
オフィス狛 技術部のKoma(Twitterアカウントの中の人&CEO)です。<br />
<br />
突然ですが、Angularでユニットテストしてますか?<br />
CLIでコンポーネントを作ると、「xxxxxx.component.spec.ts」が作成されますが、<br />
<span style="font-size: medium;">「そのまま何も変えない(ユニットテスト使わない) or 邪魔だから消す」</span>と、なっているかもしれないですね・・・・<br />
まあ、Angularのユニットテスト、割と難しいので、気持ちは分かります。<br />
<br />
という事で、Angularでユニットテストを書いていく上で、良く出るエラーの対処方法を記載していこうと思います。<br />
今回はDI関連のエラーです。<br />
<br />
<pre class="prettyprint linenums">
NullInjectorError: R3InjectorError(DynamicTestModule)[Router -> Router]:
NullInjectorError: No provider for Router!
</pre>
みたいなエラーが出てしまった場合の対応ですね。<br />
メッセージそのままですが、Router絡みのエラーであることが分かります。<br />
<br />
Angularでは、DI可能なクラスであれば、コンポーネントのコンストラクタに定義する事で、DIして使用可能になります。<br />
<span style="font-size: small;color: #9E9E9E;">[hoge.component.ts]</span><br />
<pre class="prettyprint linenums">
import { Router } from '@angular/router';
// (中略)
constructor(private router: Router) {}
// (中略)
onClickBack(): void {
// 「戻る」ボタンを押したら、「Top画面」に遷移する
this.router.navigateByUrl('/hoge/top');
}
</pre>
<br />
・・・つまりテストクラス側でもDIをする必要があるって事ですね<br />
<span style="font-size: small;color: #9E9E9E;">[hoge.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HogeComponent } from './hoge.component';
describe('HogeComponent', () => {
let component: HogeComponent;
let fixture: ComponentFixture<HogeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HogeComponent],
imports: [
RouterTestingModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HogeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
</pre>
って感じになります。<br />
デフォルト(CLIによって作成された状態)から、<br />
<pre class="prettyprint linenums">
import { RouterTestingModule } from '@angular/router/testing';
</pre>
と、<br />
<pre class="prettyprint linenums">
imports: [
RouterTestingModule,
],
</pre>
を追加しています。<br />
<br />
先ほどのエラー(NullInjectorError)を無くすだけであれば、実は上記でOKです。<br />
<br />
ただ、当然テストケースは作らないといけないので、ついでにやってみましょう<br />
<span style="font-size: small;color: #9E9E9E;">[hoge.component.spec.ts]</span><br />
<pre class="prettyprint linenums">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
// Routerをimport
import { Router } from '@angular/router';
import { HogeComponent } from './hoge.component';
describe('HogeComponent', () => {
let component: HogeComponent;
let fixture: ComponentFixture<HogeComponent>;
// 全テストケースで利用出来るように、ここで定義
let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HogeComponent],
imports: [
RouterTestingModule,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HogeComponent);
// DI後のインスタンスを取得
router = TestBed.inject(Router);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('ボタンアクションテスト', () => {
it('戻るボタン押下時 ホーム画面へ遷移する事', () => {
// navigateByUrl処理のモックを作成
const navigateByUrlSpy = spyOn(router, 'navigateByUrl');
// テスト対象のメソッドを呼び出す
component.onClickBack();
// navigateByUrlが、'/hoge/top'を引数として呼ばれているか確認
expect(navigateByUrlSpy).toHaveBeenCalledWith('/hoge/top');
});
});
});
</pre>
<br />
ポイントとしては、DIしたクラスをテスト側で使用する為には、<br />
<pre class="prettyprint linenums">
router = TestBed.inject(Router);
</pre>
が必要、というところです。<br />
実際のテストの部分は、プログラム内のコメントを参照して頂ければと思いますが、流れ的に、<br />
<br />
<span style="font-size: medium;">① spyOn で、navigateByUrl のモックを作成</span><br />
<span style="font-size: medium;">② コンポーネント側の onClickBack を呼び出す</span><br />
<span style="font-size: medium;">③ expect の toHaveBeenCalledWith で、指定の引数で navigateByUrl が呼ばれたかを判定</span><br />
<br />
となっています。<br />
<br />
ちょっと長くなってしまったので、その他のDI関連のテストエラーは別途記事にしようと思います。<br />
<br />オフィス狛 技術部 Komahttp://www.blogger.com/profile/00477808845548612411noreply@blogger.com0