文系プログラマによるTIPSブログ 文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。 2020-12-31T03:24:59+09:00 treeapps Hatena::Blog hatenablog://blog/12704591929886165306 2020年の振り返り hatenablog://entry/26006613672078752 2020-12-31T03:24:59+09:00 2021-05-07T02:32:49+09:00 2020年を振り返りますよ ブログ・SEO 仕事 個人開発 GitHub リングフィットアドベンチャーを買った 辛いスキルランキング 1位 マウンテンクライマー 2位 プランク 3位 スクワット 4位 バンザイスクワット 5位 バンザイプッシュ 思ったより辛いスキル ベントオーバー トライセプス バタバタレッグ これに頼っちゃうスキル スワイショウ リングアゲサゲ アゲサゲコンボ その他 ヨガマットとか フィットボクシング2を買った 汗問題 CV TVの故障と液晶ディスプレイの購入 次世代ゲーム機と対応TV 液晶ディスプレイ WQHDという特殊な解像度の懸念 SwitchでフルHD以上だと画面… <p>2020年を振り返りますよ</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170818/20170818174241.png" alt="f:id:treeapps:20170818174241p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#ブログSEO">ブログ・SEO</a></li> <li><a href="#仕事">仕事</a></li> <li><a href="#個人開発">個人開発</a></li> <li><a href="#GitHub">GitHub</a></li> <li><a href="#リングフィットアドベンチャーを買った">リングフィットアドベンチャーを買った</a><ul> <li><a href="#辛いスキルランキング">辛いスキルランキング</a><ul> <li><a href="#1位-マウンテンクライマー">1位 マウンテンクライマー</a></li> <li><a href="#2位-プランク">2位 プランク</a></li> <li><a href="#3位-スクワット">3位 スクワット</a></li> <li><a href="#4位-バンザイスクワット">4位 バンザイスクワット</a></li> <li><a href="#5位-バンザイプッシュ">5位 バンザイプッシュ</a></li> </ul> </li> <li><a href="#思ったより辛いスキル">思ったより辛いスキル</a><ul> <li><a href="#ベントオーバー">ベントオーバー</a></li> <li><a href="#トライセプス">トライセプス</a></li> <li><a href="#バタバタレッグ">バタバタレッグ</a></li> </ul> </li> <li><a href="#これに頼っちゃうスキル">これに頼っちゃうスキル</a><ul> <li><a href="#スワイショウ">スワイショウ</a></li> <li><a href="#リングアゲサゲ">リングアゲサゲ</a></li> <li><a href="#アゲサゲコンボ">アゲサゲコンボ</a></li> </ul> </li> <li><a href="#その他">その他</a></li> <li><a href="#ヨガマットとか">ヨガマットとか</a></li> </ul> </li> <li><a href="#フィットボクシング2を買った">フィットボクシング2を買った</a><ul> <li><a href="#汗問題">汗問題</a></li> <li><a href="#CV">CV</a></li> </ul> </li> <li><a href="#TVの故障と液晶ディスプレイの購入">TVの故障と液晶ディスプレイの購入</a><ul> <li><a href="#次世代ゲーム機と対応TV">次世代ゲーム機と対応TV</a></li> <li><a href="#液晶ディスプレイ">液晶ディスプレイ</a><ul> <li><a href="#WQHDという特殊な解像度の懸念">WQHDという特殊な解像度の懸念</a></li> <li><a href="#SwitchでフルHD以上だと画面がボヤける問題">SwitchでフルHD以上だと画面がボヤける問題</a></li> <li><a href="#色々考えるのが面倒な人は">色々考えるのが面倒な人は</a></li> </ul> </li> </ul> </li> <li><a href="#総評">総評</a></li> </ul><p><br /> いやー、今年は全然ブログ記事書きませんでした。</p><p>今年の簡単な振り返りと、ブログについて少しだけ書いてみようと思います。</p> <div class="section"> <h3 id="ブログSEO">ブログ・SEO</h3> <p>検索エンジンというか、SEOが理想論を追い求め過ぎ?ていると感じています。</p><p>間違いの無い完璧な記事の発信。正しさの追求。オフィシャル性の追求。画面の表示速度の追求。等、様々な正しさ・清廉潔白さの追求をしていて、もはや企業コンテンツか超人気コンテンツ以外はもう付いていけない・・・というのが今のブログ界隈、というかネット界隈の状態だと思っています。</p><p>一応軽くSEO系ニュースはウォッチしているのですが、新たな仕様が追加される度に「もう無理かな・・」という気持ちが強まるのが正直なところです。</p><p>今後もこのブログはバグの解決系やメモ書き等の軽い感じになる予定です。</p> </div> <div class="section"> <h3 id="仕事">仕事</h3> <p>守秘義務な部分は話せないですが、ざっくり言うと丸一年間教育(IT業界の開発)に関して取り組んでいました。</p><p>開発には全くアサインせず、完全に教育一本です。</p><p>今年はコロナの一年だったので採用がアレで、去年のような教育密度ではなかったので、ドキュメントやツールの整備や管理面を主にやっていました。</p><p>来年コロナがどうなるか解りませんが、今後も開発案件にアサインする事は無いかな?と考えています。どちらかというと管理面を強めていきそうです。</p> </div> <div class="section"> <h3 id="個人開発">個人開発</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.tree-maps.com%2F" title="地図のWEB TOOLの事ならtree-mapsにお任せ! | tree-maps" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.tree-maps.com/">www.tree-maps.com</a></cite></p><p>以前↑のtree-mapsという地図系サービスをリニューアルしようと考えていましたが、今は完全に凍結中です。理由は簡単で、Google Geocoding APIの料金が高過ぎるためです。もはや個人では払えない額です。サイトの作りの悪さもありますが、月5万円請求が来た事もありました・・・</p><p>流石にそんな払えないので、APIの利用に上限を付けたので1日に実行できるジオコーディング件数は劇的に少なくなり、恐らくもう実用に耐えないと思います。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmap.yahoo.co.jp%2Fblog%2Farchives%2F20200116_yolp_close.html" title="【重要】YOLP Web APIにおける一部API・SDK提供終了のお知らせ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://map.yahoo.co.jp/blog/archives/20200116_yolp_close.html">map.yahoo.co.jp</a></cite></p><p>Yahoo陣営も地図はサービス提供終了してしまいました。ジオコーディングは継続して使用できますが、Googleジオコーディングと比較すると1段階精度が浅い住所して取得できず、これは使えるのだろうか、という微妙な感じです(だからこそサービスが継続できるのかもしれませんね)。</p><p>Googleジオコーディングの実質の個人利用の限界、Yahooジオコーディングの精度の悪さ、という事があり、正直tree-mapsの開発に対するモチベーションを完全に失っているのが現状です。</p><p>本当はGoogle・Yahoo・Bing等、複数サービスを横断して使える地図サービスサイトを目指していたのですが、ちょっと無理そうです。。。</p> </div> <div class="section"> <h3 id="GitHub">GitHub</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips" title="treetips - Overview" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips">github.com</a></cite></p><p>プロダクトの開発は全くしていません。特定の技術要素を使ったサンプルプロジェクトをいくつか作りました。</p><p>個人的に今年はTypeScriptの年だったので、TypeScript関連リポジトリばかり作りました。何故サンプルばかり作るのかというと、仕事の教育で初学者に教えなくてはならないので、実際にコードを書いてみて理解しないと教える事ができないから、です。</p><p>余談ですが、たまにIssueが起票されますが、所謂「クソIssue」と言えそうなものも有ります、というか大半がクソIssueです。タイトル無しでスクリーンショットだけを貼り付けてきたり、別のリポジトリのエラーを聞いてきたり、それこのリポジトリと関係無い話だよね?、等々。</p><p>体感95%がクソIssueですね。。。</p><p>前述したようにブログが完全にお通夜状態なため、今後はGitHubでサンプルを作り続ける形になりそうです。</p> </div> <div class="section"> <h3 id="リングフィットアドベンチャーを買った">リングフィットアドベンチャーを買った</h3> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07XV8VSZT/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/512+B+W+XmL._SL500_.jpg" class="hatena-asin-detail-image" alt="リングフィット アドベンチャー -Switch" title="リングフィット アドベンチャー -Switch"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07XV8VSZT/treeapps5-22/">リングフィット アドベンチャー -Switch</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2019/10/18</li><li><span class="hatena-asin-detail-label">メディア:</span> Video Game</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>あまりの運動不足でやば過ぎると感じ、リングフィットアドベンチャーをやり始めました。</p><p>最初は運動負荷 5 から開始(負荷の最大値は30)したのですが、5なのに初日に筋肉痛になり、うっそだろこれ・・・と悶絶していました。</p><p>筋肉痛 -> 頑張る -> 筋肉痛 -> 頑張る、を繰り返し、今は↓こんな感じです。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20201231/20201231005457.jpg" alt="f:id:treeapps:20201231005457j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p><p>現在の運動負荷は24まで到達しました。私は運動ガチ勢ではなくエンジョイ勢ですらなく、無運動勢なので、まあまあ成長したのではないかと思います。ただ、ここから運動負荷を上げるのはもう本当に地獄で、なっかなか上げる気にはならないでしょう。。</p><p>まだクリアはしていませんが、個人的に辛いと感じたスキルのランキングを書いてみます。</p> <div class="section"> <h4 id="辛いスキルランキング">辛いスキルランキング</h4> <div class="section"> <h5 id="1位-マウンテンクライマー">1位 マウンテンクライマー</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/hPE3S55dgFY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=hPE3S55dgFY&feature=youtu.be">www.youtube.com</a></cite></p><p>無っっっっっ理。無理。むり。ムリ。</p><p>一瞬で物凄い心拍が上がり、腕の負担、大きな足の移動、無理過ぎてやばい。さり気なく回数が物凄く多いのも無理に拍車をかける・・</p><p>高威力の範囲スキルなので選びたいけど、これは無理・・</p> </div> <div class="section"> <h5 id="2位-プランク">2位 プランク</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/aN221aCy1jo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/aN221aCy1jo">youtu.be</a></cite></p><p>これも無理。見た目以上に腕と大腿四頭筋に負担がかかります。想像以上の負荷です。やる前は「ふーん。できそう。」とか軽く思ってましたが、いざやってみると2〜3回で潰れてぜぇぜぇはぁはぁしました。</p><p>これも高威力の範囲スキルで選びたいけど、無理・・</p> </div> <div class="section"> <h5 id="3位-スクワット">3位 スクワット</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/RQg61DvKFpM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/RQg61DvKFpM">youtu.be</a></cite></p><p>ただのスクワットだと思いますよね?動画見ると解りますが、腰を落とした状態で数秒キープする動作を何十回もやるんです。回数が物凄く多い系スキルなので、途中休憩を挟まないと完走できないです。</p><p>よくネットで見るスクワットって、キープ無しでせいぜい1セット30回を×3回とかですよね。これはキープする動きを1日に何回もするので、負荷が全然違います。</p> </div> <div class="section"> <h5 id="4位-バンザイスクワット">4位 バンザイスクワット</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/9m91BUaTppA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/9m91BUaTppA">youtu.be</a></cite></p><p>またしてもスクワット系です。これのキツイ点は腕を上げる点です。腕を上げる=肩の筋肉をずっと張り続ける事になります。大掃除で高いところを掃除しているとすぐ疲れますよね?アレをずーーーっとやる感じです。この動画のインストラクターは物凄く綺麗なフォームで腕を上げていますが、実際は斜め45度くらいしか上がらなくなります・・</p> </div> <div class="section"> <h5 id="5位-バンザイプッシュ">5位 バンザイプッシュ</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/yrMe4xwkgoI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/yrMe4xwkgoI">youtu.be</a></cite></p><p>「日常生活でこんな動き絶対しねーよ」系のスキルです。肩の筋肉メインで、腕の筋肉はほぼ使わないので、普段こういう動きに慣れていない人には物凄く辛いスキルです。普段こんな動きする人なんているのだろうか・・・</p> </div> </div> <div class="section"> <h4 id="思ったより辛いスキル">思ったより辛いスキル</h4> <div class="section"> <h5 id="ベントオーバー">ベントオーバー</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/KQdHlgfH8c8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/KQdHlgfH8c8">youtu.be</a></cite></p><p>これ、以外と横っ腹の筋肉の負担が強く、完走はできるけどかなり辛いです。ただ、お腹の横の筋肉にダイレクトに効いてくるので、積極的に選びたいですね。</p> </div> <div class="section"> <h5 id="トライセプス">トライセプス</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/GTPTYY920bE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/GTPTYY920bE">youtu.be</a></cite></p><p>普段こんな動きしねーよ系です。20回を超えた辺りから、あれ腕が上がらない・・・!とジワジワ辛くなるやつです。</p> </div> <div class="section"> <h5 id="バタバタレッグ">バタバタレッグ</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/jRuxP6AjBeU?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/jRuxP6AjBeU">youtu.be</a></cite></p><p>他の足系スキルより足の付根への負担が強いです。ぽっこりお腹改善と書いてますが、お腹より足の付根への負担の方が圧倒的に強く感じます。</p> </div> </div> <div class="section"> <h4 id="これに頼っちゃうスキル">これに頼っちゃうスキル</h4> <div class="section"> <h5 id="スワイショウ">スワイショウ</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/5haOPVsLw7Y?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/5haOPVsLw7Y">youtu.be</a></cite></p><p>あまりに負担が少なくてついつい選びがちなスワイショウ。全体攻撃で且つ1回の動きが非常に短く、短時間で低負荷で全体攻撃できるというスキル。これの誘惑に負けてしまう人は多いでしょう。</p><p>私もこれに逃げがちですが、腕の動に対して逆方向に腰をグリンっ!と回す事で横っ腹の筋肉に負荷をかけてます。</p> </div> <div class="section"> <h5 id="リングアゲサゲ">リングアゲサゲ</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/xyjPQSrbdB0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/xyjPQSrbdB0">youtu.be</a></cite></p><p>範囲1のスキルですが、あまりにも負荷が弱く、疲れた時はこれに逃げがち。</p> </div> <div class="section"> <h5 id="アゲサゲコンボ">アゲサゲコンボ</h5> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/OcdzlIo00Tk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://youtu.be/OcdzlIo00Tk">youtu.be</a></cite></p><p>これも範囲1ですが、思ったより威力も高く、筋トレ系ではなく有酸素系の運動で、動き的には大分緩いです。これも疲れた時に選びがちですね。</p> </div> </div> <div class="section"> <h4 id="その他">その他</h4> <p>バンザイコシフリも簡単な部類ですが、判定が思ったより厳しい?っぽく、思ったより腰を大きく左右しないとbest判定にならないように感じました。</p> </div> <div class="section"> <h4 id="ヨガマットとか">ヨガマットとか</h4> <p>Amazonでヨガマット探すと、180〜190cmのものが多く、こんな長いの敷けねーよ!なので、以下の170cmで厚さ10mmの分厚いものを使っています。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086468D6K/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/41TIQYhzXiL._SL500_.jpg" class="hatena-asin-detail-image" alt="【Amazon限定ブランド】プリマソーレ(primasole) ヨガマット トレーニングマット 【エコ素材PERヨガマット】厚さ10㎜ 滑り止め横じまエンボス加工 ストラップ収納ケース付き ツインカラー ジャンゴーグリーン×ブラック PSS91NH075" title="【Amazon限定ブランド】プリマソーレ(primasole) ヨガマット トレーニングマット 【エコ素材PERヨガマット】厚さ10㎜ 滑り止め横じまエンボス加工 ストラップ収納ケース付き ツインカラー ジャンゴーグリーン×ブラック PSS91NH075"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B086468D6K/treeapps5-22/">【Amazon限定ブランド】プリマソーレ(primasole) ヨガマット トレーニングマット 【エコ素材PERヨガマット】厚さ10㎜ 滑り止め横じまエンボス加工 ストラップ収納ケース付き ツインカラー ジャンゴーグリーン×ブラック PSS91NH075</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2020/04/14</li><li><span class="hatena-asin-detail-label">メディア:</span> スポーツ用品</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>10mmでも騒音は少し気になるので、これを2枚重ねてもいいかもしれませんね。</p><br /> <p>こんな感じでリングフィットアドベンチャーを続けてます。筋肉痛が続いた日はきつかったですが、今のところほぼ毎日継続できています!プロテインを飲むようにしているので、今ではゲーム時間で1時間(現実時間で90分前後)やっても筋肉痛にはならなくなるくらいになりました。</p><p>エアロバイクを漕いでいた事もありましたが、Amazon Primeで映画やアニメを見ながら漕いでいましたが、とにかく単調な動きで継続できなかったのですが、リングフィットアドベンチャーはゲーム性があるおかげで、今まで経験した事が無いレベルで継続できています。</p><p>リング君がいい感じに褒めてくれたり励ましてくれたり、オーバーワークになりそうなタイミングで逐一確認してくれるし、継続させる仕組みが絶妙で、これは仕事の教育面に活かしたいな、と強く感じています。</p> </div> </div> <div class="section"> <h3 id="フィットボクシング2を買った">フィットボクシング2を買った</h3> <p>リングフィットアドベンチャーは自重筋トレで、有酸素運動スキルもありますが、どちらかというと筋トレゲームです。</p><p>筋トレだけだとダイエットとしては足りないので、有酸素運動系のフィットボクシング2を購入しました。</p><p>購入初日は「リングフィットを20分やった後、フィットボクシングを40分やったろ。リングフィットの負荷24だし余裕やろ」とか、クソ舐めたムーブをかまそうと思ったのですが、10分で「ぜぇぜぇ・・・はぁはぁ・・・全然駄目だこれ・・・」となり、初日で背中側の脇の下が筋肉痛になりました。</p><p>筋肉痛は1週間くらい続いてしまい、その回復が終わるまでリングフィットしかできませんでした。。</p><p>筋肉痛の回復後、デイリーを30分で再開し、何とか今も継続しています。ただ、リングフィットより頻度は低く、1〜3日に1回しかできてません。できるだけパンチを強く打つようにこころがけているので、割とすぐ息切れして腕が上がりにくくなります。</p><p>リングフィットで足に負担をかけた後にダッキング・ウィービングをすると、負担は更に上がり、これ毎日やってる人すげぇ・・・と思う程の疲労です。</p> <div class="section"> <h4 id="汗問題">汗問題</h4> <p>フィットボクシングをプレーするにあたり、汗問題は結構注意する必要があります。</p><p>リングフィットと違い腕をブンブン、ダッキング・ウィービングで体をブンブンするので、汗が飛びます。無対策だとカーテンやモニターに汗が飛び散ります。。。</p><p>握っているコントローラーにも当然汗が付着するので、壊れないかヒヤヒヤしていました。流石に対策が必要だと思い、今は頭はタオルを巻き、他は以下を装着してプレーしています。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07DK65DYG/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/51zvgpVfqoL._SL500_.jpg" class="hatena-asin-detail-image" alt="手袋 自転車 グローブ 手袋 メンズ uvカット 手袋 自転車 手袋 薄手 吸汗速乾 紫外線対策 日焼け止め 滑り止め おしゃれ 運転 ドライブ グローブ 換気性 通気性 伸縮 軽くて快適 春夏秋 人気" title="手袋 自転車 グローブ 手袋 メンズ uvカット 手袋 自転車 手袋 薄手 吸汗速乾 紫外線対策 日焼け止め 滑り止め おしゃれ 運転 ドライブ グローブ 換気性 通気性 伸縮 軽くて快適 春夏秋 人気"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07DK65DYG/treeapps5-22/">手袋 自転車 グローブ 手袋 メンズ uvカット 手袋 自転車 手袋 薄手 吸汗速乾 紫外線対策 日焼け止め 滑り止め おしゃれ 運転 ドライブ グローブ 換気性 通気性 伸縮 軽くて快適 春夏秋 人気</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>この自転車・車用グローブは薄手でメッシュ状で少しだけ通気性があります。デイリー30分やってもコントローラーには全然ダメージは無い感じです。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07CNMPJWG/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/41YoAm619RL._SL500_.jpg" class="hatena-asin-detail-image" alt="Sillictor コンプレッション トップス メンズ 長袖 パワーストレッチ アンダー シャツ コンプレッション ウェア [UVカット + 吸汗速乾] 二枚セット 323blk*2-XL" title="Sillictor コンプレッション トップス メンズ 長袖 パワーストレッチ アンダー シャツ コンプレッション ウェア [UVカット + 吸汗速乾] 二枚セット 323blk*2-XL"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07CNMPJWG/treeapps5-22/">Sillictor コンプレッション トップス メンズ 長袖 パワーストレッチ アンダー シャツ コンプレッション ウェア [UVカット + 吸汗速乾] 二枚セット 323blk*2-XL</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> ウェア&amp;シューズ</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>コンプレッションウェアは通気性が非常に高く、腕を動かすと通気し過ぎて「え、こんなに!?」というくらい冷えます。長袖なので汗が飛び散る心配もありません。</p><p>足に関しては柔らか素材のマリンシューズがいいそうですが、私は面倒なので普通に靴下履いてるだけです。</p> </div> <div class="section"> <h4 id="CV">CV</h4> <p>このゲーム、トレーナーが有名声優しかいないので誰にするか迷いますが、カレン/CV.鬼頭明里 と ソフィ/CV.小清水亜美 と ベルナルド/CV.大塚明夫 を気分で変える形で落ち着いてますね。</p><p>特定のトレーナーをランダム選択する機能が欲しいところです。</p> </div> </div> <div class="section"> <h3 id="TVの故障と液晶ディスプレイの購入">TVの故障と液晶ディスプレイの購入</h3> <p>リングフィットとフィットボクシングが少しずつ安定して日課になり、さあ頑張るぞ!、という矢先に12/29の時点でTVが壊れました・・・</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2015%2F06%2F22%2F231345" title="東芝4KテレビJ10Xを購入したので少しだけレビュー - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.bunkei-programmer.net/entry/2015/06/22/231345">www.bunkei-programmer.net</a></cite></p><p>2015/06/22 に東芝の4K・43インチ・アップスケーリング内蔵の液晶TVを購入し、ついに壊れました。年末のこのタイミングでです。</p><p>またTV買うかな〜?と考えて調べてましたが、今年一度もTV番組を視聴していない事に気づいたのと、PS5・XBOXの次世代機がHDMI2.1規格に対応し、4K + 120Hzでゲームができるようになっている事を考慮しておいた方がいい事が解りました。</p> <div class="section"> <h4 id="次世代ゲーム機と対応TV">次世代ゲーム機と対応TV</h4> <p>調査してみると、HDMI2.1・4K・120Hzに対応したTVで、一番小さいサイズがLGの48インチのOLED48CXPJAでした。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0873KRDB1/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/510a0k6ucTL._SL500_.jpg" class="hatena-asin-detail-image" alt="LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル" title="LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0873KRDB1/treeapps5-22/">LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2020/05/26</li><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>ちょっとでか過ぎるのと、昨今のTV番組がやらせ・偏向報道・ニュース番組で芸人が適当な事を言うなどが気になり、一切TV番組を見なくなったので、TV購入はもういいかな?と思い、TVは見送りました。</p> </div> <div class="section"> <h4 id="液晶ディスプレイ">液晶ディスプレイ</h4> <p>任天堂SwitchをPC用の液晶ディスプレイにHDMIで接続すると、普通にゲームがプレーできる事は以前から知っていました。</p><p>しかし、TVと同様HDMI2.1に対応した液晶ディスプレイは先日ようやくAsusが型番すら未定の機種を発表したばかりで、実質対応機種無し!という、完全に時期が悪い状況です。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpc.watch.impress.co.jp%2Fdocs%2Fnews%2F1269846.html" title="ASUS、初のHDMI 2.1認証取得の43型4K/120Hzディスプレイ " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://pc.watch.impress.co.jp/docs/news/1269846.html">pc.watch.impress.co.jp</a></cite></p><p>以下の27インチ・4K・144Hzで実売20万円である事を考慮すると、43インチだと価格がいくらになるか考えたくもないですね・・</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07FZR5X1G/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/51XaSh-kyTL._SL500_.jpg" class="hatena-asin-detail-image" alt="ASUS ゲーミングモニター 27インチ ROG SWIFT PG27UQ(4K/量子ドットIPS/HDR10/4ms/144Hz/G-SYNC/直下型LED/Aura Sync/HDMI/DP)" title="ASUS ゲーミングモニター 27インチ ROG SWIFT PG27UQ(4K/量子ドットIPS/HDR10/4ms/144Hz/G-SYNC/直下型LED/Aura Sync/HDMI/DP)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07FZR5X1G/treeapps5-22/">ASUS ゲーミングモニター 27インチ ROG SWIFT PG27UQ(4K/量子ドットIPS/HDR10/4ms/144Hz/G-SYNC/直下型LED/Aura Sync/HDMI/DP)</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2018/08/24</li><li><span class="hatena-asin-detail-label">メディア:</span> Personal Computers</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>もう完全に時期が悪いとしかいいようが無いので、HDMI2.1対応機種が出揃って価格が安定するまでの繋ぎとして、以下を購入しました。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.dospara.co.jp%2F5shopping%2Fdetail_parts.php%3Fic%3D462179%26lf%3D2" title="ViewSonic VX2758-2KP-MHD-7 (27インチワイド 液晶モニター) ドスパラ限定モデル |パソコン通販のドスパラ【公式】" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.dospara.co.jp/5shopping/detail_parts.php?ic=462179&lf=2">www.dospara.co.jp</a></cite></p><p>27インチ・IPS・WQHD(2560×1440)・144Hz・フレームレス・簡易スピーカー付き・HDMI2.0(144Hz) × 1・DisplayPort(144Hz) × 1 で税込み37,400円でした。</p><p>ドット抜け無し、発色良し、画面綺麗で、ゲーム用途なら中々良いのでは?と思います。ACアダプタもゴツい塊タイプではなく普通の細いコンセントタイプなのもGoodですね。</p> <div class="section"> <h5 id="WQHDという特殊な解像度の懸念">WQHDという特殊な解像度の懸念</h5> <p>WQHDは特殊なサイズで、PS5・XBOX Seriesは勿論WQHDに非対応で1080p(FHD)にダウンスケールされます。また、一般的なAVアンプもWQHDに非対応です。</p><p>最近はゲームをやるよりプログラムの勉強をしている時間の方が圧倒的に長いのと、ゲーム専用モニタとして48インチのTVを置きたくないし、次世代機はそもそも購入できないし、実質任天堂Switch専用ディスプレイならこれでいいかな?と考えて選びました。</p><p>次世代機が普通に購入できるようになって、対応ディスプレイが安定してきたら、改めてその時買い替えを検討しています。</p> </div> <div class="section"> <h5 id="SwitchでフルHD以上だと画面がボヤける問題">SwitchでフルHD以上だと画面がボヤける問題</h5> <p>任天堂Switchは携帯モード720p(1280×720)、TV出力時は1080p(1920×1080)です。1080pも実はネイティブ対応しておらず、NVIDIA Tegraの力で720pを1080pにアップスケールしているだけです。ここから更にWQHD(2560x1440)にアップスケール無しで縦横を引き伸ばして画面が出力される事になるので、当然引き伸ばし過ぎてボヤけた画面になります。</p> <blockquote cite="https://www.4gamer.net/games/990/G999026/20170114010/"> <p>ドック側にあるHDMI出力端子はフルHDの1920×1080ドット,60Hz(60fps)にまで対応。1600×900ピクセルの30fps描画となるゼルダのようなタイトルだと,フルHDへのアップスケールを経て出力することになる。</p> <cite><a href="https://www.4gamer.net/games/990/G999026/20170114010/">https://www.4gamer.net/games/990/G999026/20170114010/</a></cite> </blockquote> <p>実際SwitchをWQHDでリングフィットをプレーしてみたのですが、Switch起動後のゲーム選択画面は明らかにボヤけています。しかし、実際のゲーム画面では以外とボヤけておらず、あれ、以外といけるじゃん、と感じました。勿論ジャギーは普通にあります。</p><p>どうしても気になる方は、以下のようなアップスケール装置を噛ませると良さそうです。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QQSLMWX/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/41Pttj0dvhL._SL500_.jpg" class="hatena-asin-detail-image" alt="ラトックシステム 4K60Hz対応 HDMIアップコンバーター RS-HD2UP-4KA" title="ラトックシステム 4K60Hz対応 HDMIアップコンバーター RS-HD2UP-4KA"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QQSLMWX/treeapps5-22/">ラトックシステム 4K60Hz対応 HDMIアップコンバーター RS-HD2UP-4KA</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2019/04/27</li><li><span class="hatena-asin-detail-label">メディア:</span> Personal Computers</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>ただ、2021年の春頃にSwitchの後継機?次世代機?であるSwitch Proが発表されるかも!という噂があり、それが4Kや120Hzにネイティブ対応する可能性があり、アップスケール装置の購入より後継機への買い替えの方がいいのでは?という懸念があります。</p><p>液晶ディスプレイと同様、完全に時期が悪いですね・・</p> </div> <div class="section"> <h5 id="色々考えるのが面倒な人は">色々考えるのが面倒な人は</h5> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0873KRDB1/treeapps5-22/"><img src="https://m.media-amazon.com/images/I/510a0k6ucTL._SL500_.jpg" class="hatena-asin-detail-image" alt="LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル" title="LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0873KRDB1/treeapps5-22/">LG 48型 4Kチューナー内蔵 有機EL テレビ OLED 48CXPJA Alexa 搭載 2020 年モデル</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2020/05/26</li><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p><p>LGの↑これで大体解決すると思います。価格.com調べでは実売16万円前後です。HDMI2.1、4K、120Hz、eARC対応、アップスケーリング対応で、恐らく諸々の機能全部入りのやつですね。ただ、48インチで横幅が107cmもあるので、そこは要注意です。</p> </div> </div> </div> <div class="section"> <h3 id="総評">総評</h3> <p>今年はリングフィットアドベンチャーばっかりしていた1年でした。</p><p>フィットボクシング2も並行しているので、来年はジワジワ体が締まっていけばいいな、と考えています。</p><p>GitHubの方も引き続き何らかのサンプルを継続してpushしていく予定です。</p><p>ブログに関しては正直はてなブログをやめてJamstack化しようかな?と考えてますが、はてな記法の記事をmarkdown化するプログラムを書くのが嫌過ぎて、中々手が動いていません。誰かコンバーター公開してたりしないかな。。</p><br /> <p>こんな感じで来年も緩く色々継続はしていく予定なので、よろしくお願いします 🙇‍♂️🙇‍♂️🙇‍♂️</p> </div> treeapps TSLintからESLintに雑に移行する hatenablog://entry/26006613564588824 2020-05-09T20:05:55+09:00 2020-05-09T20:39:21+09:00 雑の極みです <p>雑の極みです</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418115102.png" alt="f:id:treeapps:20180418115102p:plain" title="f:id:treeapps:20180418115102p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span class="feature1">ESLint v7.0.0</span>がついに正式リリースされました👏<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Feslint%2Feslint%2Freleases%2Ftag%2Fv7.0.0" title="eslint/eslint" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/eslint/eslint/releases/tag/v7.0.0">github.com</a></cite></p><p>そしてgitリポジトリ見ると解りますが、<span class="feature1">TSLint</span>は既に<span class="feature3">Deprecated</span>なのです😱</p><p>そんな状況のTSLintで騙し騙し継続して使っていましたが(まだTSLintが使われるscaffoldingも結構あります)、v7リリースという事で、これは絶好の移行機会だと思い、早速移行しました。</p><p>また、最近Nest.jsを試しているのですが、Nest.jsは既にTypeScript + ESLintでscaffoldingされたので、それを参考にしています。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#TSLintの削除">TSLintの削除</a><ul> <li><a href="#packagejsonからtslintを削除する">package.jsonからtslintを削除する</a></li> <li><a href="#packagejsonからtslint関連のnpm-scriptを削除する">package.jsonからtslint関連のnpm scriptを削除する</a></li> <li><a href="#tslintjsonを削除する">tslint.jsonを削除する</a></li> </ul> </li> <li><a href="#ESLintの追加">ESLintの追加</a><ul> <li><a href="#packagejsonにtslintを追加する">package.jsonにtslintを追加する</a></li> <li><a href="#packagejsonにeslint関連のnpm-scriptを追加する">package.jsonにeslint関連のnpm scriptを追加する</a></li> <li><a href="#eslintjsを追加する">.eslint.jsを追加する</a></li> </ul> </li> <li><a href="#おまけ">おまけ</a><ul> <li><a href="#TypeScript--Nextjs--Material-UI--Redux--Redux-Saga">TypeScript + Next.js + Material-UI + Redux + Redux Saga</a><ul> <li><a href="#git-repository">git repository</a></li> <li><a href="#live-demo">live demo</a></li> </ul> </li> <li><a href="#TypeScript--Nextjs--Material-UI--Redux--Redux-Toolkit">TypeScript + Next.js + Material-UI + Redux + Redux Toolkit</a><ul> <li><a href="#git-repository-1">git repository</a></li> <li><a href="#live-demo-1">live demo</a></li> </ul> </li> </ul> </li> </ul> <div class="section"> <h3 id="環境">環境</h3> <p>React + TypeScript + Prettier + ESLintの組み合わせの設定になります。</p> </div> <div class="section"> <h3 id="TSLintの削除">TSLintの削除</h3> <div class="section"> <h4 id="packagejsonからtslintを削除する">package.jsonからtslintを削除する</h4> <p>package.jsonを開き、tslintと名の付くものを全てuninstallします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>npm uninstall tslint tslint-config-prettier tslint-config-standard tslint-plugin-prettier </pre> </div> <div class="section"> <h4 id="packagejsonからtslint関連のnpm-scriptを削除する">package.jsonからtslint関連のnpm scriptを削除する</h4> <p>例えばscriptsセクションに以下のようなtslintコマンドが有る場合は全て削除します。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">lint</span>&quot;: &quot;<span class="synConstant">tslint -c ./tslint.json --exclude **/*.d.ts --exclude ./node_modules --project . --fix **/*.tsx --fix **/*.ts</span>&quot;, &quot;<span class="synStatement">tslint-check</span>&quot;: &quot;<span class="synConstant">tslint-config-prettier-check ./tslint.json</span>&quot; <span class="synSpecial">}</span> </pre> </div> <div class="section"> <h4 id="tslintjsonを削除する">tslint.jsonを削除する</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">rm</span> <span class="synSpecial">-rfv</span> tslint.json </pre> </div> </div> <div class="section"> <h3 id="ESLintの追加">ESLintの追加</h3> <div class="section"> <h4 id="packagejsonにtslintを追加する">package.jsonにtslintを追加する</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>npm i <span class="synSpecial">-D</span> eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-import </pre> </div> <div class="section"> <h4 id="packagejsonにeslint関連のnpm-scriptを追加する">package.jsonにeslint関連のnpm scriptを追加する</h4> <p>components,constans...の部分は適宜対象フォルダ名を羅列して下さい。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">lint</span>&quot;: &quot;<span class="synConstant">eslint </span><span class="synSpecial">\&quot;</span><span class="synConstant">{components,constants,hooks,model,pages,store,types}/**/*.{ts,tsx}</span><span class="synSpecial">\&quot;</span><span class="synConstant"> --fix</span>&quot; <span class="synSpecial">}</span>, </pre> </div> <div class="section"> <h4 id="eslintjsを追加する">.eslint.jsを追加する</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat <span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .eslintrc.js</span> <span class="synConstant">module.exports = {</span> <span class="synConstant"> parser: &quot;@typescript-eslint/parser&quot;,</span> <span class="synConstant"> parserOptions: {</span> <span class="synConstant"> project: &quot;tsconfig.json&quot;,</span> <span class="synConstant"> sourceType: &quot;module&quot;,</span> <span class="synConstant"> ecmaFeatures: {</span> <span class="synConstant"> jsx: true,</span> <span class="synConstant"> },</span> <span class="synConstant"> useJSXTextNode: true,</span> <span class="synConstant"> },</span> <span class="synConstant"> plugins: [&quot;@typescript-eslint/eslint-plugin&quot;, &quot;react&quot;],</span> <span class="synConstant"> extends: [</span> <span class="synConstant"> &quot;plugin:@typescript-eslint/eslint-recommended&quot;,</span> <span class="synConstant"> &quot;plugin:@typescript-eslint/recommended&quot;,</span> <span class="synConstant"> &quot;prettier/@typescript-eslint&quot;,</span> <span class="synConstant"> &quot;plugin:react/recommended&quot;,</span> <span class="synConstant"> &quot;prettier/react&quot;,</span> <span class="synConstant"> ],</span> <span class="synConstant"> root: true,</span> <span class="synConstant"> env: {</span> <span class="synConstant"> node: true,</span> <span class="synConstant"> jest: true,</span> <span class="synConstant"> },</span> <span class="synConstant"> rules: {</span> <span class="synConstant"> &quot;@typescript-eslint/interface-name-prefix&quot;: &quot;off&quot;,</span> <span class="synConstant"> &quot;@typescript-eslint/explicit-function-return-type&quot;: &quot;off&quot;,</span> <span class="synConstant"> &quot;@typescript-eslint/no-explicit-any&quot;: &quot;off&quot;,</span> <span class="synConstant"> &quot;@typescript-eslint/no-unused-vars&quot;: [</span> <span class="synConstant"> &quot;error&quot;,</span> <span class="synConstant"> {</span> <span class="synConstant"> argsIgnorePattern: &quot;^_&quot;,</span> <span class="synConstant"> },</span> <span class="synConstant"> ],</span> <span class="synConstant"> },</span> <span class="synConstant">}</span> <span class="synStatement">EOF</span> </pre><p>これで完了です。</p> </div> </div> <div class="section"> <h3 id="おまけ">おまけ</h3> <p>以下のサンプルアプリケーションを公開中です。最近Vercel(旧Zeit)の個人プランが無料化したので、両者ともLiveデモで実際に触る事ができます。</p> <div class="section"> <h4 id="TypeScript--Nextjs--Material-UI--Redux--Redux-Saga">TypeScript + Next.js + Material-UI + Redux + Redux Saga</h4> <div class="section"> <h5 id="git-repository">git repository</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Ftypescript-nextjs-redux-material-ui-example%2Ftree%2F7fd73501db45cf87593d878118b485a01b1a1de3" title="treetips/typescript-nextjs-redux-material-ui-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/typescript-nextjs-redux-material-ui-example/tree/7fd73501db45cf87593d878118b485a01b1a1de3">github.com</a></cite><br /> </p> </div> <div class="section"> <h5 id="live-demo">live demo</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftypescript-nextjs-redux-material-ui-example.now.sh%2F" title="Top page | sample" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://typescript-nextjs-redux-material-ui-example.now.sh/">typescript-nextjs-redux-material-ui-example.now.sh</a></cite><br /> </p> </div> </div> <div class="section"> <h4 id="TypeScript--Nextjs--Material-UI--Redux--Redux-Toolkit">TypeScript + Next.js + Material-UI + Redux + Redux Toolkit</h4> <p>こちらは Redux Toolkitに最近追加された createAsyncThunk , createEntityAdapter といった新機能も使っています。</p> <div class="section"> <h5 id="git-repository-1">git repository</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Ftypescript-nextjs-redux-toolkit-material-ui-example" title="treetips/typescript-nextjs-redux-toolkit-material-ui-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/typescript-nextjs-redux-toolkit-material-ui-example">github.com</a></cite><br /> </p> </div> <div class="section"> <h5 id="live-demo-1">live demo</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftypescript-nextjs-redux-toolkit-material-ui-example.now.sh" title="Top page | sample" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://typescript-nextjs-redux-toolkit-material-ui-example.now.sh">typescript-nextjs-redux-toolkit-material-ui-example.now.sh</a></cite></p> </div> </div> </div> treeapps Ionic React+CapacitorでElectron利用時にstaticがずれる件に対応する hatenablog://entry/26006613527268911 2020-02-28T09:32:36+09:00 2020-03-04T01:57:52+09:00 初見殺しなのでメモです <p>初見殺しなのでメモです</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190616/20190616112954.png" alt="f:id:treeapps:20190616112954p:plain" title="f:id:treeapps:20190616112954p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>Ionic Reactがリリースされたので、早速試してみました。</p><p>今回は Ionic React + Capacitorで成果物をElectronで出力してみたのですが、いきなり洗礼を浴びたので、そのメモを残しておきます。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#staticが見つからない">staticが見つからない</a></li> <li><a href="#対応策">対応策</a><ul> <li><a href="#publicindexhtml">public/index.html</a><ul> <li><a href="#補足">補足</a></li> </ul> </li> <li><a href="#packagejson">package.json</a></li> <li><a href="#ビルドする">ビルドする</a></li> </ul> </li> <li><a href="#おまけ">おまけ</a><ul> <li><a href="#ドキュメント">ドキュメント</a></li> <li><a href="#環境構築から動かすまで">環境構築から動かすまで</a></li> </ul> </li> </ul> <div class="section"> <h3 id="環境">環境</h3> <table> <tr> <th>種別</th> <th>バージョン</th> </tr> <tr> <td>macOS</td> <td>Catalina</td> </tr> <tr> <td>node.js</td> <td>v12.16.0</td> </tr> <tr> <td>ionic-cli</td> <td>v6.1.0</td> </tr> <tr> <td>capacitor-cli</td> <td>v1.5.0</td> </tr> </table> </div> <div class="section"> <h3 id="staticが見つからない">staticが見つからない</h3> <p>ビルドやらcopyやらをし、最終的に <span class="feature1">npx cap open electron</span>すると、以下のようになりました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20200228/20200228090450.png" alt="f:id:treeapps:20200228090450p:plain" title="f:id:treeapps:20200228090450p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> <pre class="code text" data-lang="text" data-unlink>GET file:///static/css/10.4c99b71.chunk.css index.html:1 net::ERR_FILE_NOT_FOUND</pre><p>githubに以下のissueが挙がっていました。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fionic-team%2Fcapacitor%2Fissues%2F639%23issuecomment-496547899" title="Better solution for electron &lt;base&gt; url (or just document it) · Issue #639 · ionic-team/capacitor" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/ionic-team/capacitor/issues/639#issuecomment-496547899">github.com</a></cite><br /> </p> </div> <div class="section"> <h3 id="対応策">対応策</h3> <div class="section"> <h4 id="publicindexhtml">public/index.html</h4> <p>以下をコメントアウトし、hrefに動的に値が入るように %PUBLIC_URL% を指定します。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">base</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;/&quot;</span><span class="synIdentifier"> /&gt;</span> </pre><p>↓</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synComment">&lt;!-- &lt;base href=&quot;/&quot; /&gt; --&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">base</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;%PUBLIC_URL%/&quot;</span><span class="synIdentifier">&gt;</span> </pre> <div class="section"> <h5 id="補足">補足</h5> <p>githubのissueではコメントアウトすれば動く旨が記述されており、確かにコメントアウトすれば動きます。</p><p>しかし、ボタンをクリックしたらalertコンポーネントが表示されるコードでelectronを実行すると以下のエラーが発生しました。</p> <pre class="code txt" data-lang="txt" data-unlink>cannot read property &#39;isproxied&#39; of undefined</pre><p>上記エラーを解消するため、baseのhrefを以下にしたところ、解消しました。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">base</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;%PUBLIC_URL%/&quot;</span><span class="synIdentifier">&gt;</span> </pre> </div> </div> <div class="section"> <h4 id="packagejson">package.json</h4> <p>以下を追加します。追加位置はどこでもいいので、今回は末尾に追加しています。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">description</span>&quot;: &quot;<span class="synConstant">An Ionic project</span>&quot; } </pre><p>↓</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">description</span>&quot;: &quot;<span class="synConstant">An Ionic project</span>&quot;, &quot;<span class="synStatement">homepage</span>&quot;: &quot;<span class="synConstant">.</span>&quot; } </pre> </div> <div class="section"> <h4 id="ビルドする">ビルドする</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>ionic build npx cap copy npx cap open electron </pre><p>さて、結果は・・・</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20200228/20200228091607.png" alt="f:id:treeapps:20200228091607p:plain" title="f:id:treeapps:20200228091607p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>OKです。</p><p>修正前は</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">link</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;/static/xxxx&quot;</span><span class="synIdentifier"> </span><span class="synType">rel</span><span class="synIdentifier">=</span><span class="synConstant">&quot;stylesheet&quot;</span><span class="synIdentifier">&gt;</span> </pre><p>だったのが以下のように「.」始まりに変わり、解決したようです。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">link</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;./static/xxxx&quot;</span><span class="synIdentifier"> </span><span class="synType">rel</span><span class="synIdentifier">=</span><span class="synConstant">&quot;stylesheet&quot;</span><span class="synIdentifier">&gt;</span> </pre><p>👍</p> </div> </div> <div class="section"> <h3 id="おまけ">おまけ</h3> <div class="section"> <h4 id="ドキュメント">ドキュメント</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fionicframework.com%2Fjp%2Fdocs%2Freact%2Foverview" title="Ionic Reactの概要 - Ionic Framework 日本語ドキュメンテーション" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ionicframework.com/jp/docs/react/overview">ionicframework.com</a></cite></p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcapacitor.ionicframework.jp%2Fdocs%2Felectron" title="入門 - Capacitor" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://capacitor.ionicframework.jp/docs/electron">capacitor.ionicframework.jp</a></cite><br /> </p> </div> <div class="section"> <h4 id="環境構築から動かすまで">環境構築から動かすまで</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># ionic-cliをグローバルにインストール</span> npm install <span class="synSpecial">-g</span> @ionic/cli <span class="synComment"># プロジェクトの生成(ここでcapacitorの依存が追加されるので別途install不要)</span> ionic <span class="synStatement">start</span> ionic-react-example tabs <span class="synSpecial">--type=react</span> <span class="synSpecial">--capacitor</span> <span class="synSpecial">--no-git</span> <span class="synComment"># ionic start時に以下のエラーが起きた</span> gyp: No Xcode or CLT version detected! gyp ERR! configure error gyp ERR! stack Error: <span class="synSpecial">`gyp`</span> failed with <span class="synStatement">exit</span> code: <span class="synConstant">1</span> gyp ERR! stack at ChildProcess.onCpExit <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/.nodebrew/node/v12.</span><span class="synConstant">16</span><span class="synSpecial">.</span><span class="synConstant">0</span><span class="synSpecial">/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16</span><span class="synPreProc">)</span> gyp ERR! stack at ChildProcess.emit <span class="synPreProc">(</span><span class="synSpecial">events.js:321:20</span><span class="synPreProc">)</span> gyp ERR! stack at Process.ChildProcess._handle.onexit <span class="synPreProc">(</span><span class="synSpecial">internal/child_process.js:275:12</span><span class="synPreProc">)</span> gyp ERR! System Darwin <span class="synConstant">19</span>.<span class="synConstant">3</span>.<span class="synConstant">0</span> gyp ERR! <span class="synStatement">command</span> <span class="synStatement">&quot;</span><span class="synConstant">/Users/tree/.nodebrew/node/v12.16.0/bin/node</span><span class="synStatement">&quot;</span> <span class="synStatement">&quot;</span><span class="synConstant">/Users/tree/.nodebrew/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js</span><span class="synStatement">&quot;</span> <span class="synStatement">&quot;</span><span class="synConstant">rebuild</span><span class="synStatement">&quot;</span> gyp ERR! cwd /Users/tree/work/ionic-react-example/node_modules/jest-haste-map/node_modules/fsevents gyp ERR! node <span class="synSpecial">-v</span> v12.<span class="synConstant">16</span>.<span class="synConstant">0</span> gyp ERR! node-gyp <span class="synSpecial">-v</span> v5.<span class="synConstant">0</span>.<span class="synConstant">5</span> gyp ERR! not ok <span class="synComment"># Xcodeコマンドラインツールを本体のものに切り替え</span> sudo xcode-select <span class="synSpecial">--switch</span> /Applications/Xcode.app <span class="synComment"># リトライ -&gt; 成功</span> ionic <span class="synStatement">start</span> ionic-react-example tabs <span class="synSpecial">--type=react</span> <span class="synSpecial">--capacitor</span> <span class="synSpecial">--no-git</span> <span class="synComment"># webをビルド</span> ionic build <span class="synComment"># electronの依存等を準備(事前にionic buildが必要)</span> npx cap add electron <span class="synComment"># ionicでbuildした成果物を各プラットフォームassetにコピー</span> npx cap copy <span class="synComment"># Electronで起動</span> npx cap open electron </pre> </div> </div> treeapps PostmanでGlobalsの環境変数をリクエスト結果で更新する hatenablog://entry/26006613487289276 2019-12-21T15:05:13+09:00 2019-12-21T15:05:13+09:00 毎回手作業で更新しなくていいのです <p>毎回手作業で更新しなくていいのです</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170828/20170828014922.png" alt="f:id:treeapps:20170828014922p:plain" title="f:id:treeapps:20170828014922p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>皆大好きPostman。</p><p>Postmanには環境変数があり、各Request設定で環境変数を埋め込む事で、設定を一元管理する事ができます。</p><p>今回はこの環境変数についてのお話です。</p> <ul class="table-of-contents"> <li><a href="#Postmanって何">Postmanって何?</a></li> <li><a href="#環境変数を使ってトークンを一元管理するが">環境変数を使ってトークンを一元管理するが・・・</a></li> <li><a href="#そうだ環境変数を自動更新しよう">そうだ、環境変数を自動更新しよう!</a><ul> <li><a href="#Testsを使ってGlobalsを上書きする">Testsを使ってGlobalsを上書きする</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="Postmanって何">Postmanって何?</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.getpostman.com%2F" title="Postman | The Collaboration Platform for API Development " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.getpostman.com/">www.getpostman.com</a></cite></p><p>(主に)HTTPリクエストを発行するためのツールです。</p><p>ブラウザのアドレスバーだとGETリクエストしかできないし、ターミナルでは都度APIのコマンドを用意する必要があります。</p><p>Postmanはこれらを解決してくれ、更に環境変数でAPIサーバのポート番号やトークン値等を一元管理できたり、javascriptで動的に色々制御できたりするツールです。</p><p>設定のimport/exportも可能なので、gitで管理して全員共通のAPI実行環境を用意する〜、なんて事もできます。</p> </div> <div class="section"> <h3 id="環境変数を使ってトークンを一元管理するが">環境変数を使ってトークンを一元管理するが・・・</h3> <p>例えば「<span class="feature1">データ取得API</span>」が20個有り、それら全てのAPIは<span class="feature1">トークン取得APIによって取得したトークンをAuthenticationヘッダに設定する</span>事で、APIが使用できるとします。<span class="feature3">トークンには有効期限が有り、一定期間で無効</span>になってしまいます。</p><p>最初は、トークン失効後にトークンを再発行し、そのトークンを毎回20種類のAPIに対して1件づつ設定していました。しかしこれは流石に無理があるので、環境変数で一言管理してみました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191221/20191221141421.png" alt="f:id:treeapps:20191221141421p:plain" title="f:id:treeapps:20191221141421p:plain" class="hatena-fotolife" itemprop="image"></span><br /> 画面右上の歯車ボタンをクリックし、Globalsボタンをクリックします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191221/20191221141437.png" alt="f:id:treeapps:20191221141437p:plain" title="f:id:treeapps:20191221141437p:plain" class="hatena-fotolife" itemprop="image"></span><br /> テーブルに変数名と値を入力します。今回はトークン値だけでなくAPIサーバのportも登録してみました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191221/20191221141510.png" alt="f:id:treeapps:20191221141510p:plain" title="f:id:treeapps:20191221141510p:plain" class="hatena-fotolife" itemprop="image"></span><br /> 中括弧2個のヒゲ(mustache)形式で、アドレスバーにもBearerトークン値にも、Globalsに登録した変数が参照可能になりました。以下のような形で埋め込みます。</p> <pre class="code" data-lang="" data-unlink>{{bearerToken}}</pre><p>さて、これで以降は環境変数を変更するだけで全APIにBearerトークン値を設定する事ができました!!</p><p>・・・・トークンの有効期限が短いので、想像していたよりも頻繁に設定の修正が発生してしまいました。一元管理できてはいますが、1日に何回も変更するのは大変です・・・</p> </div> <div class="section"> <h3 id="そうだ環境変数を自動更新しよう">そうだ、環境変数を自動更新しよう!</h3> <p>ここで今回の本題です。</p><p>現状より更に楽をするためには、<span class="feature1">トークン取得APIを実行したら自動的にレスポンスjson内のトークン値をGlobalsの環境変数値に上書きする</span>事ができれば、手動での変更は不要になります。</p> <div class="section"> <h4 id="Testsを使ってGlobalsを上書きする">Testsを使ってGlobalsを上書きする</h4> <p>もっといいやり方がある可能性がありますが、「レスポンス値を使って」を実現できるのがTestsを使った方法しか解らなかったので、Testsで説明します。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191221/20191221143804.png" alt="f:id:treeapps:20191221143804p:plain" title="f:id:treeapps:20191221143804p:plain" class="hatena-fotolife" itemprop="image"></span><br /> リクエストのTestsタブを開き、以下のようにjavascriptを記述します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> json = JSON.parse(responseBody); postman.setGlobalVariable(<span class="synConstant">&quot;bearerToken&quot;</span>, json.accessToken); </pre><p>↑のように記述すると、↓のトークン取得APIのレスポンスjsonからbearerTokenの値を取得し、setGlobalVariableでGlobalsを上書きする事がきるのです。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">accessToken</span>&quot;: &quot;<span class="synConstant">ZDQ1YTkxNTgtZTJkMi00OTU4LTk1ZWItMTM3YzM3NWFhMDg1</span>&quot; <span class="synSpecial">}</span> </pre><p>トークン取得APIのTestsに上記javascriptを記述したら、早速トークン取得APIを実行してみて下さい。取得結果のトークン値でGlobalsの値が上書きされている筈です。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191221/20191221144606.png" alt="f:id:treeapps:20191221144606p:plain" title="f:id:treeapps:20191221144606p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>PostmanでChrome DevToolsを表示する事ができる(という事はpostmanはElectron?)ので、console.log(response) のようなデバッグコードを記述して確認する事もできます。</p><p>公式ドキュメントには、各種実行順序やscriptsについてのドキュメントもありますので、他にも自動化したい等があれば、一度ドキュメントに目を通しておくとよさそうですね!<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flearning.getpostman.com%2Fdocs%2Fpostman%2Fscripts%2Fintro-to-scripts%2F" title="Intro to scripts" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://learning.getpostman.com/docs/postman/scripts/intro-to-scripts/">learning.getpostman.com</a></cite></p> </div> treeapps REALFORCE R2 TKL・HHKB Pro2 TypeS・NIZ 2019 Waterproof Seriesを使ってみた hatenablog://entry/26006613483482873 2019-12-14T21:19:24+09:00 2019-12-17T09:43:09+09:00 今回はキーボードの使用レポートになります <p>今回はキーボードの使用レポートになります</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418125439.png" alt="f:id:treeapps:20180418125439p:plain" title="f:id:treeapps:20180418125439p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>最近Realforce for macテンキーレス、HHKB HYBRIDと、相次いでフラッグシップなキーボードが発表されたので、それに触発されて<span class="feature1">旧版の使用レポート</span>をしてみようと思います。</p><p><span class="feature3">※ 個人の主観によるレビューになります。</span><br /> <span class="feature3">※ ぶっちゃけレビューなので、変に期待させたり、不自然に持ち上げるレビューではないです。</span><br /> </p> <ul class="table-of-contents"> <li><a href="#レビューするにあたって">レビューするにあたって</a></li> <li><a href="#比較対象のスペック">比較対象のスペック</a></li> <li><a href="#スクリーンショット">スクリーンショット</a></li> <li><a href="#静音">静音</a></li> <li><a href="#打鍵音">打鍵音</a><ul> <li><a href="#REALFORCE-R2-TKL">REALFORCE R2 TKL</a></li> <li><a href="#HHKB-Professional2-Type-S">HHKB Professional2 Type-S</a></li> <li><a href="#NIZ-2019-87-IP68-waterproof-series">NIZ 2019 87 IP68 waterproof series</a></li> </ul> </li> <li><a href="#特定のキー押下の気になる点">特定のキー押下の気になる点</a><ul> <li><a href="#REALFORCE-R2-TKL-1">REALFORCE R2 TKL</a><ul> <li><a href="#BackspaceキーReturnキー">Backspaceキー・Returnキー</a></li> <li><a href="#バネ音">バネ音</a></li> </ul> </li> <li><a href="#HHKB-Professional2-Type-S-1">HHKB Professional2 Type-S</a><ul> <li><a href="#左Shiftキー">左Shiftキー</a></li> <li><a href="#Returnキー">Returnキー</a></li> <li><a href="#スペースキー">スペースキー</a></li> </ul> </li> <li><a href="#NIZ-2019-87-IP68-waterproof-series-1">NIZ 2019 87 IP68 waterproof series</a><ul> <li><a href="#バネ音-1">バネ音</a></li> </ul> </li> </ul> </li> <li><a href="#打鍵の重さ">打鍵の重さ</a></li> <li><a href="#打鍵感">打鍵感</a><ul> <li><a href="#REALFORCE-R2-TKL-2">REALFORCE R2 TKL</a></li> <li><a href="#HHKB-Professional2-Type-S-2">HHKB Professional2 Type-S</a></li> <li><a href="#NIZ-2019-87-IP68-waterproof-series-2">NIZ 2019 87 IP68 waterproof series</a></li> </ul> </li> <li><a href="#筐体の作り">筐体の作り</a><ul> <li><a href="#REALFORCE-R2-TKL-3">REALFORCE R2 TKL</a></li> <li><a href="#HHKB-Professional2-Type-S-3">HHKB Professional2 Type-S</a></li> <li><a href="#NIZ-2019-87-IP68-waterproof-series-3">NIZ 2019 87 IP68 waterproof series</a></li> </ul> </li> <li><a href="#本体の重さ">本体の重さ</a></li> <li><a href="#デザインカラーバリエーション">デザイン・カラーバリエーション</a><ul> <li><a href="#REALFORCE-R2-TKL-4">REALFORCE R2 TKL</a></li> <li><a href="#HHKB-Professional2-Type-S-4">HHKB Professional2 Type-S</a></li> <li><a href="#NIZ-2019-87-IP68-waterproof-series-4">NIZ 2019 87 IP68 waterproof series</a></li> </ul> </li> <li><a href="#インターフェース">インターフェース</a><ul> <li><a href="#REALFORCE-R2-TKL-5">REALFORCE R2 TKL</a></li> <li><a href="#HHKB-Professional2-Type-S-5">HHKB Professional2 Type-S</a></li> <li><a href="#NIZ-2019-87-IP68-waterproof-series-5">NIZ 2019 87 IP68 waterproof series</a></li> </ul> </li> <li><a href="#価格">価格</a></li> <li><a href="#HHKBに矢印キーが無い点">HHKBに矢印キーが無い点</a></li> <li><a href="#結局どれ使ってるの">結局どれ使ってるの?</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="レビューするにあたって">レビューするにあたって</h3> <p>私は<span class="feature1">静音性を最重要視</span>しているので、それ前提のレビューになります。</p> </div> <div class="section"> <h3 id="比較対象のスペック">比較対象のスペック</h3> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07NWJPS78/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/31K4BcxND1L._SL160_.jpg" class="hatena-asin-detail-image" alt="東プレ REALFORCE R2 TKL キーボード テンキーレス 英語配列87キー 変荷重 かな無し 静音モデル(アイボリー)リアルフォース R2TLS-USV-IV" title="東プレ REALFORCE R2 TKL キーボード テンキーレス 英語配列87キー 変荷重 かな無し 静音モデル(アイボリー)リアルフォース R2TLS-USV-IV"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07NWJPS78/treeapps5-22/">東プレ REALFORCE R2 TKL キーボード テンキーレス 英語配列87キー 変荷重 かな無し 静音モデル(アイボリー)リアルフォース R2TLS-USV-IV</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07K9DVP46/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41v6aoLgbaL._SL160_.jpg" class="hatena-asin-detail-image" alt="PFU Happy Hacking Keyboard Professional2 Type-S 英語配列/白 PD-KB400WS" title="PFU Happy Hacking Keyboard Professional2 Type-S 英語配列/白 PD-KB400WS"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07K9DVP46/treeapps5-22/">PFU Happy Hacking Keyboard Professional2 Type-S 英語配列/白 PD-KB400WS</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2011/06/15</li><li><span class="hatena-asin-detail-label">メディア:</span> Personal Computers</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07RWWWZKM/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41z-ow8MjoL._SL160_.jpg" class="hatena-asin-detail-image" alt="AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)" title="AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07RWWWZKM/treeapps5-22/">AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <table> <tr> <th>名称 </th> <th>スイッチ方式</th> <th>キースイッチ</th> <th>押下圧</th> <th>APC</th> <th>防水</th> <th>静音モデル</th> </tr> <tr> <td>REALFORCE R2 TKL </td> <td>静電容量無接点</td> <td>専用</td> <td>ALL 30g</td> <td>無し</td> <td>✗</td> <td>○</td> </tr> <tr> <td>HHKB Professional2 Type-S </td> <td>静電容量無接点</td> <td>専用</td> <td>ALL 45g</td> <td>無し</td> <td>✗</td> <td>○</td> </tr> <tr> <td>NIZ 2019 87 IP68 waterproof series</td> <td>静電容量無接点</td> <td>Cherry MX軸</td> <td>ALL 35g</td> <td>無し</td> <td>○ IP68</td> <td>○</td> </tr> </table><p>防水レベルIP68については以下をご覧下さい。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.gizmodo.jp%2F2017%2F01%2Fiphone-8-ip68.html" title="iPhone 8は「IP68」の防水/防塵対応でもっとタフになるかも" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.gizmodo.jp/2017/01/iphone-8-ip68.html">www.gizmodo.jp</a></cite><br /> <a href="https://www.ip68.jp/technicalguide/pdf/PP%20IPtoukyu.pdf">https://www.ip68.jp/technicalguide/pdf/PP%20IPtoukyu.pdf</a><br /> </p> </div> <div class="section"> <h3 id="スクリーンショット">スクリーンショット</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191214/20191214202723.png" alt="f:id:treeapps:20191214202723p:plain" title="f:id:treeapps:20191214202723p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>NIZの白さが際立ってますね!</p><p>文字の刻印位置がRealforce・HHKBとは少し異なる点は不思議です。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191214/20191214202740.png" alt="f:id:treeapps:20191214202740p:plain" title="f:id:treeapps:20191214202740p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3 id="静音">静音</h3> <p>左(打鍵音小)             右(打鍵音大)<br /> REALFORCE > HHKB Type-S >>> NIZ</p> <ul> <li>Realforceは<span class="feature1">低〜中の打鍵音</span>。</li> <li>HHKBは<span class="feature1">中〜高の打鍵音</span>。</li> <li>NIZは<span class="feature1">中の打鍵音</span>。静音モデルですが、<span class="feature1">Realforce・HHKBより打鍵音が明確に大きい</span>です。</li> </ul><p>静音を意識して打鍵すればRealforceが最も静音です。</p><p>HHKBは後述するようによく打鍵する左Shiftが煩いです。</p><p>NIZは全体的に煩いです。</p><p><span class="feature3">※ 煩いといっても静電容量無接点方式+静音モデルの中では、という意味です。</span><br /> </p> </div> <div class="section"> <h3 id="打鍵音">打鍵音</h3> <div class="section"> <h4 id="REALFORCE-R2-TKL">REALFORCE R2 TKL</h4> <p>全体的に静かで打鍵音は低めです。</p><p>打鍵感は全体的に <span class="feature1">ストスト</span> です。</p><p>カチャカチャ音は少しします。</p> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S">HHKB Professional2 Type-S</h4> <p>英数記号キーの打鍵感は <span class="feature1">コトコト</span> です。</p><p>それ以外のキーの打鍵感は <span class="feature1">コンコン・カンカン</span> です。</p><p>カチャカチャ音は少しします。</p> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series">NIZ 2019 87 IP68 waterproof series</h4> <p>打鍵感は <span class="feature1">スコスコ・シャコシャコ</span> です。</p><p>カチャカチャ音はしません。全体的に均一にスコスコ・シャコシャコです。</p> </div> </div> <div class="section"> <h3 id="特定のキー押下の気になる点">特定のキー押下の気になる点</h3> <div class="section"> <h4 id="REALFORCE-R2-TKL-1">REALFORCE R2 TKL</h4> <div class="section"> <h5 id="BackspaceキーReturnキー">Backspaceキー・Returnキー</h5> <p>キーの中で最も音がします。中音でコンコン!みたいな感じです。</p> </div> <div class="section"> <h5 id="バネ音">バネ音</h5> <p>数字キーを強めに打鍵した際にバネの音が少しビヨンビヨン聞こえます。</p> </div> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S-1">HHKB Professional2 Type-S</h4> <div class="section"> <h5 id="左Shiftキー">左Shiftキー</h5> <p><span class="feature3">左Shiftキーが明らかに打鍵音が大きい</span>です。カンカン!と音がしてしまいます。個体差ではなく構造上の問題な気がします。</p><p>左Shiftキーだけ、<span class="feature3">打鍵時に机に衝撃が一番強く伝わり</span>、甲高い音が響きます。以下のHHKB吸振マットを付けていますが、付ける前と変化はほぼ無く煩いです。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07BMWGZWM/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41V-aholcfL._SL160_.jpg" class="hatena-asin-detail-image" alt="バード電子 HHKB吸振マットHG(BT用) KMG-BT PZ-KBKMG-BT" title="バード電子 HHKB吸振マットHG(BT用) KMG-BT PZ-KBKMG-BT"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07BMWGZWM/treeapps5-22/">バード電子 HHKB吸振マットHG(BT用) KMG-BT PZ-KBKMG-BT</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> </div> <div class="section"> <h5 id="Returnキー">Returnキー</h5> <p>左Shiftキーより、Returnキーの方が明らかに静かです。ただ、<span class="feature3">キーの中で2番目に煩い</span>です。</p><p>打鍵時の机への衝撃もそこそこあります。</p> </div> <div class="section"> <h5 id="スペースキー">スペースキー</h5> <p>スペースキーは甲高い音ではなく、低い音です。</p> </div> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series-1">NIZ 2019 87 IP68 waterproof series</h4> <div class="section"> <h5 id="バネ音-1">バネ音</h5> <p>矢印キーや左Ctrlキー打鍵時にバネの音が少しビヨンビヨン聞こえます。</p> </div> </div> </div> <div class="section"> <h3 id="打鍵の重さ">打鍵の重さ</h3> <p>Realforce(ALL30g)、HHKB(ALL45g)、NIZ(ALL35g)なのですが、何故か<span class="feature1">Realforceの30gよりNIZの35gの打鍵の方が軽く感じます</span>。キーストローク差等が要因なのかもしれませんね。</p><p>個人的に押下圧は軽い方が好みなので、HHKBは重く感じますね。</p> </div> <div class="section"> <h3 id="打鍵感">打鍵感</h3> <div class="section"> <h4 id="REALFORCE-R2-TKL-2">REALFORCE R2 TKL</h4> <p>良いと言えば良いのですが、なんでしょう。<span class="feature3">この微妙に制御しきれていない感は</span>、みたいな印象です。</p><p>少しグラつき感があるような(キーが底面に到達するまでに少し斜めにグラつくみたいなイメージです)、微妙な精度の悪さを感じます。</p> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S-2">HHKB Professional2 Type-S</h4> <p>凄く良いです。<span class="feature1">制御しきれている感が強い</span>です。グラつきも不安定感も全く感じません。</p><p>Realforceと比較すると、とにかく打ち心地が良いです。</p> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series-2">NIZ 2019 87 IP68 waterproof series</h4> <p>Realforceと同等くらいで、HHKBより悪いです。</p><br /> <p>完全に私の感覚になりますが、打鍵ミスが起きるのは最多はRealforce、次点でNIZ、最小はHHKBです。</p><p>押下圧と精度の両方が関係しているのかもしれませんね。</p> </div> </div> <div class="section"> <h3 id="筐体の作り">筐体の作り</h3> <p>※ 筐体は本体の箱の部分を指しています</p><p>左(作りが良い)      右(作りが良くない)<br /> HHKB Type-S > NIZ > REALFORCE</p> <div class="section"> <h4 id="REALFORCE-R2-TKL-3">REALFORCE R2 TKL</h4> <p>筐体が薄い?というか、他の2つよりは作りが微妙な気がします。トータル品質が平均点くらいな感じです。</p><p>打鍵時にドッシリとした安定感は無く、HHKB・NIZより安定感を感じません。</p> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S-3">HHKB Professional2 Type-S</h4> <p>HHKBが最も作りが良く感じます。カッチリしているというか、「これは明らかに長期間使える」感が強くします。</p><p>打鍵時の安定感も高く、Realforce・NIZと異なりバネ音もしません。</p> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series-3">NIZ 2019 87 IP68 waterproof series</h4> <p>価格の割に作りが良く感じます。カッチリしてます。</p><p>打鍵時の安定感は高いです。筐体が最も重く筐体に厚みがある?ので安定感が高く感じる気がしています。</p> </div> </div> <div class="section"> <h3 id="本体の重さ">本体の重さ</h3> <table> <tr> <th>名称 </th> <th>重量</th> </tr> <tr> <td>REALFORCE R2 TKL </td> <td>1.1Kg</td> </tr> <tr> <td>HHKB Professional2 Type-S </td> <td>530g</td> </tr> <tr> <td>NIZ 2019 87 IP68 waterproof series</td> <td>1.4 Kg</td> </tr> </table><p><span class="feature3">圧倒的にNIZは重いです</span>。凄くズッシリ重量感があります。持ち運びは絶望的ですが、重さがあるので打鍵時の本体の安定感が高いです。</p> </div> <div class="section"> <h3 id="デザインカラーバリエーション">デザイン・カラーバリエーション</h3> <div class="section"> <h4 id="REALFORCE-R2-TKL-4">REALFORCE R2 TKL</h4> <p>カラーバリエーションは、アイボリー・黒の2種類です。</p><p>全体的に<span class="feature3">昔ながらの業務用アイボリー臭が全開</span>です。薄いアイボリーと薄めなアイボリーのツートンカラーで、<span class="feature3">濃淡にメリハリが無い</span>ため、強い業務臭を発してしまっている気がします。</p><p>昔のRealforceより筐体が角張った事により、多少見栄えは良くなってはいます。</p> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S-4">HHKB Professional2 Type-S</h4> <p>カラーバリエーションは、(主に)アイボリー・黒(墨)の2種類です。</p><p>全体的に昔ながらの業務用アイボリー臭はしますが、薄いアイボリーと<span class="feature1">薄い青みがかったアイボリー</span>なので、Realforceより良く見えます。</p><p>無刻印モデルにすれば余計な文字が消え、デザイン性が高くなります。墨色モデルもホコリや汚れが無い状態を維持できれば、とても綺麗で良い感じです。</p> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series-4">NIZ 2019 87 IP68 waterproof series</h4> <p>カラーバリエーションは、アイボリーの1種類です。</p><p>アイボリーではありますが、<span class="feature1">白と濃い色のアイボリー</span>で、濃淡にメリハリがあり、白基調な見た目が結構いい感じに見えます。</p><p>多分<span class="feature1">白さによって綺麗さが演出できている</span>のだろうと思いました。</p> </div> </div> <div class="section"> <h3 id="インターフェース">インターフェース</h3> <table> <tr> <th>名称</th> <th>USBバージョン</th> <th>USB HUB</th> </tr> <tr> <td>REALFORCE R2 TKL</td> <td>2.0</td> <td>無し</td> </tr> <tr> <td>HHKB Professional2 Type-S</td> <td>2.0</td> <td>有り</td> </tr> <tr> <td>NIZ 2019 87 IP68 waterproof series</td> <td>2.0</td> <td>無し</td> </tr> </table> <div class="section"> <h4 id="REALFORCE-R2-TKL-5">REALFORCE R2 TKL</h4> <p>Realforceは基本的に<span class="feature3">インターフェースに期待してはいけない系</span>です。。。それは最新のRealforce for macでも同じです。</p> </div> <div class="section"> <h4 id="HHKB-Professional2-Type-S-5">HHKB Professional2 Type-S</h4> <p>HHKBはPro2まではUSB2.0ですが、<span class="feature1">最新のHHKB HYBRIDはUSB Type-Cに対応しています</span>。</p> </div> <div class="section"> <h4 id="NIZ-2019-87-IP68-waterproof-series-5">NIZ 2019 87 IP68 waterproof series</h4> <p><span class="feature3">防水仕様という事で、余計な穴や隙間は一切無い</span>ので、USBハブ等は無いです。</p> </div> </div> <div class="section"> <h3 id="価格">価格</h3> <p>おいくら万円?</p> <table> <tr> <th>名称</th> <th>amazon価格</th> </tr> <tr> <td>REALFORCE R2 TKL</td> <td>約 23,500円</td> </tr> <tr> <td>HHKB Professional2 Type-S</td> <td>約 24,500円</td> </tr> <tr> <td>NIZ 2019 87 IP68 waterproof series</td> <td>約 15,000円</td> </tr> </table><p>最も高価なのはHHKB、最も安価なのはNIZ、でした。</p><p>ちなみにテンキー有りよりテンキー無しの方が価格が安いです。</p> </div> <div class="section"> <h3 id="HHKBに矢印キーが無い点">HHKBに矢印キーが無い点</h3> <p>HHKBのUS配列は昔から矢印キーが存在せず、右下のFnキーとReturnキー隣の上下左右の同時押しで矢印を再現するアレです。</p><p>暫く使ってみましたが、<span class="feature3">やっぱり辛い!</span>という結論に至りました。</p><p>例えばブラウザやVSCodeのタブの前後移動は、私は option + cmmand + 左右の矢印キー でやっていますが、ボタンを4個押さないといけないのが大変で、「あの時はFn押す、この時はFn押さない」みたいな頭の切り替えが頻繁に起き、これを無意識で行うには修行が足りませんでした・・・</p><p>HHKBのこの配列は「ホームポジションが崩れにくい」とよく言われていますが、私はHHKBで矢印を打鍵しようとすると絶対ホームポジションが大きく崩れてしまうので、結局「ホームポジションとは」みたいになってしまいました。どうせ崩れるなら独立矢印キーが有った方が楽でいいですね。</p><br /> <p>また、通以上配列の矢印キーは以下のような山型なのに対し、</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191214/20191214200534.png" alt="f:id:treeapps:20191214200534p:plain" title="f:id:treeapps:20191214200534p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>HHKBは以下のようにひし形で、しかも↑と↓の縦軸がズレている点と、↓が下部に配置されているせいで指が楽に自然に曲がる形状にフィットしていない点も辛い要因でした。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20191214/20191214200207.png" alt="f:id:treeapps:20191214200207p:plain" title="f:id:treeapps:20191214200207p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3 id="結局どれ使ってるの">結局どれ使ってるの?</h3> <p>自宅ではローテションさせてます。使用頻度が高い順だと、1位:Realforce、2位:NIZ、3位:HHKB、になります。</p><p>HHKBは矢印の辛さ、左Shiftキーのカンカン音、押下圧の重い点が敬遠理由で、NIZは打鍵音が煩いのが敬遠理由です。</p><p>消去法でRealforceになっているのが現状ですが、HHKB・NIZとか比較して作りの甘さを徐々に感じ始めてきています。</p><br /> <p>NIZは防水仕様で筐体を洗える事は大きなメリットだと感じています。防水でないと色系の汚れが付着した際に掃除が難しく、ホコリ取りもエアダスター等がいちいち必要ですが、NIZのwaterproof seriesなら洗面所や風呂場で少量の石鹸や洗剤で水洗いできちゃいます。これは本当に楽です。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>キーボード、自分の中での決定版が見つかりませんね。</p><p>個人的に最もバランスがいいのはRealforceですが、掃除が面倒なのと品質が微妙な気がするのが残念ですね。押下圧ALL30gが影響しているのかな🤔</p><p>HHKBはUS配列で矢印キーがあって左Shiftが静かになれば最高ですね。(あとできれば独自のキー配列もやめて欲しい)</p><p>NIZは個人的に実はかなりオススメで、安いのに筐体もしっかりしていて、何より汎用キートップなので、パステルカラーにしたり、キャラクターが立体的に浮き出ているキートップ等、自在に装飾する事ができます。</p><p>変わり種キートップについてはAliExpressに沢山あるので見てみると面白いです。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.aliexpress.com%2Fpopular%2Fkeycap.html" title=" キーキャップが超お買い得– AliExpress モバイルで、世界のキーキャップ セラーの キーキャップが素晴らしい割引価格に " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ja.aliexpress.com/popular/keycap.html">ja.aliexpress.com</a></cite></p><p>複数のキーボードを使って思うのは、キーによって打鍵音が結構異なる点ですね。HHKBは顕著で、左Shiftはコンコン煩いのに右Shiftはそうでもなかったり。NIZは全体的に一定の打鍵音だったり。こういう細かい点も以外と見逃せない点だったりするので、極力実際に店舗で試すのをオススメします(NIZは絶望的だと思いますが)。</p><p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JLGF7D/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41P0%2BSAlYIL._SL160_.jpg" class="hatena-asin-detail-image" alt="PFU HHKB Professional HYBRID Type-S 英語配列/墨、キーボードルーフ(スモーク)付 PD-KB800BS-KBRFHHB" title="PFU HHKB Professional HYBRID Type-S 英語配列/墨、キーボードルーフ(スモーク)付 PD-KB800BS-KBRFHHB"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JLGF7D/treeapps5-22/">PFU HHKB Professional HYBRID Type-S 英語配列/墨、キーボードルーフ(スモーク)付 PD-KB800BS-KBRFHHB</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JMRTK7/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41aOgGur%2BBL._SL160_.jpg" class="hatena-asin-detail-image" alt="PFU HHKB Professional HYBRID Type-S 日本語配列/墨、キーボードルーフ(スモーク)付 PD-KB820BS-KBRFHHB" title="PFU HHKB Professional HYBRID Type-S 日本語配列/墨、キーボードルーフ(スモーク)付 PD-KB820BS-KBRFHHB"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JMRTK7/treeapps5-22/">PFU HHKB Professional HYBRID Type-S 日本語配列/墨、キーボードルーフ(スモーク)付 PD-KB820BS-KBRFHHB</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JN2WZV/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41-GNJQkvQL._SL160_.jpg" class="hatena-asin-detail-image" alt="PFU HHKB Professional HYBRID 英語配列/白、キーボードルーフ(クリアー)付 PD-KB800W-KBRFHHC" title="PFU HHKB Professional HYBRID 英語配列/白、キーボードルーフ(クリアー)付 PD-KB800W-KBRFHHC"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081JN2WZV/treeapps5-22/">PFU HHKB Professional HYBRID 英語配列/白、キーボードルーフ(クリアー)付 PD-KB800W-KBRFHHC</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081K1TCZT/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41BD9MWCBfL._SL160_.jpg" class="hatena-asin-detail-image" alt="PFU HHKB Professional HYBRID 日本語配列/白、キーボードルーフ(クリアー)付 PD-KB820W-KBRFHHC" title="PFU HHKB Professional HYBRID 日本語配列/白、キーボードルーフ(クリアー)付 PD-KB820W-KBRFHHC"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B081K1TCZT/treeapps5-22/">PFU HHKB Professional HYBRID 日本語配列/白、キーボードルーフ(クリアー)付 PD-KB820W-KBRFHHC</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07RWWWZKM/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41z-ow8MjoL._SL160_.jpg" class="hatena-asin-detail-image" alt="AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)" title="AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07RWWWZKM/treeapps5-22/">AKEEYO NiZ キーボード 有線 87キー 静電容量無接点方式 IP68全体防水 静音 レトロデザイン 35g荷重 プログラマブルキーボード 英語配列 PBT素材 キーキャップ レトロ 多機能 キーボード 日本語マニュアル (87Keys)</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QCBJ7VT/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/41q0UqV4OpL._SL160_.jpg" class="hatena-asin-detail-image" alt="東プレ REALFORCE SA for Mac キーボード ホワイト R2SA-JP3M-WH" title="東プレ REALFORCE SA for Mac キーボード ホワイト R2SA-JP3M-WH"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QCBJ7VT/treeapps5-22/">東プレ REALFORCE SA for Mac キーボード ホワイト R2SA-JP3M-WH</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QGJHM96/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/31Y6tnrwnDL._SL160_.jpg" class="hatena-asin-detail-image" alt="東プレ REALFORCE SA for Mac キーボード ブラック R2SA-JP3M-BK" title="東プレ REALFORCE SA for Mac キーボード ブラック R2SA-JP3M-BK"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07QGJHM96/treeapps5-22/">東プレ REALFORCE SA for Mac キーボード ブラック R2SA-JP3M-BK</a></p><ul><li><span class="hatena-asin-detail-label">メディア:</span> エレクトロニクス</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> </div> treeapps react-redux v7.1+TypeScriptでconnect, mapStateToProps, mapDispatchToPropsを撲滅する hatenablog://entry/17680117127201097807 2019-06-16T11:30:35+09:00 2019-06-18T10:15:25+09:00 ついに例の定型文3兄弟を除去する事ができるようになりました〜 <p>ついに例の定型文3兄弟を除去する事ができるようになりました〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190616/20190616112954.png" alt="f:id:treeapps:20190616112954p:plain" title="f:id:treeapps:20190616112954p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Freduxjs%2Freact-redux%2Freleases%2Ftag%2Fv7.1.0" title="reduxjs/react-redux" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/reduxjs/react-redux/releases/tag/v7.1.0">github.com</a></cite></p><p>先日react-reduxがv7.1にアップデートされ、そこでhooks対応の関数がいくつか追加されました。</p><p>今回紹介するものは「<span class="feature1">useSelector</span>」「<span class="feature1">useDispatch</span>」の2つです。</p> <ul class="table-of-contents"> <li><a href="#react-redux-v71の新機能">react-redux v7.1の新機能</a><ul> <li><a href="#useSelector">useSelector</a></li> <li><a href="#useDispatch">useDispatch</a></li> <li><a href="#connect関数は不要になる">connect関数は不要になる</a></li> </ul> </li> <li><a href="#v71とそれ以前のコードの比較">v7.1とそれ以前のコードの比較</a><ul> <li><a href="#v71以前のTypeScript--react-reduxのコード">v7.1以前のTypeScript + react-reduxのコード</a></li> <li><a href="#v71以降のTypeScript--react-reduxのコード">v7.1以降のTypeScript + react-reduxのコード</a></li> </ul> </li> <li><a href="#全部入りのサンプルコード">全部入りのサンプルコード</a></li> <li><a href="#万能ではない点に注意">万能ではない点に注意</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="react-redux-v71の新機能">react-redux v7.1の新機能</h3> <div class="section"> <h4 id="useSelector">useSelector</h4> <p>ざっくり説明すると、mapStateToPropsをhooks対応したものです。</p><p>useSelectorを使う事で、mapStateToPropsを撲滅する事ができるようになります。</p> </div> <div class="section"> <h4 id="useDispatch">useDispatch</h4> <p>ざっくり説明すると、mapDispatchToPropsをhooks対応したものです。</p><p>useDispatchを使う事で、mapDispatchToPropsとbindActionCreatorsを撲滅する事ができるようになります。</p> </div> <div class="section"> <h4 id="connect関数は不要になる">connect関数は不要になる</h4> <p>useSelectorもuseDispatchもhooks apiで実装されているため、HOCベースなconnect関数は不要になります。これは非常に大きい事で、関数のexport周りのコードが凄くすっきりできます。</p> </div> </div> <div class="section"> <h3 id="v71とそれ以前のコードの比較">v7.1とそれ以前のコードの比較</h3> <div class="section"> <h4 id="v71以前のTypeScript--react-reduxのコード">v7.1以前のTypeScript + react-reduxのコード</h4> <p>まずは今までの見慣れたTypeScript + reduxの伝統的なコードです。これでもextends Rect.Componentなコンポーネントでないのでまだマシです。</p><p>mapStateToPropsより下辺りのコードがもう定型文と化していますね。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Button <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> createStyles, withStyles, WithStyles, Theme <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core/styles&quot;</span> <span class="synStatement">import</span> React from <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> connect <span class="synIdentifier">}</span> from <span class="synConstant">&quot;react-redux&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> CounterActions <span class="synIdentifier">}</span> from <span class="synConstant">&quot;../store/actions&quot;</span> <span class="synStatement">const</span> styles = (theme: Theme) =&gt; createStyles(<span class="synIdentifier">{</span> root: <span class="synIdentifier">{}</span>, <span class="synIdentifier">}</span>) <span class="synStatement">interface</span> IProps <span class="synStatement">extends</span> WithStyles&lt;<span class="synStatement">typeof</span> styles&gt; <span class="synIdentifier">{</span> count: number increment: () =&gt; number decrement: () =&gt; number <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">const</span> Redux = (props: IProps) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> <span class="synIdentifier">{</span> classes, count <span class="synIdentifier">}</span> = props <span class="synStatement">const</span> handleIncrement = () =&gt; props.increment() <span class="synStatement">const</span> handleDecrement = () =&gt; props.decrement() <span class="synStatement">return</span> ( &lt;div className=<span class="synIdentifier">{</span>classes.root<span class="synIdentifier">}</span>&gt; &lt;p&gt;<span class="synIdentifier">{</span>count<span class="synIdentifier">}</span>&lt;/p&gt; &lt;Button color=<span class="synConstant">&quot;primary&quot;</span> onClick=<span class="synIdentifier">{</span>handleIncrement<span class="synIdentifier">}</span>&gt;+ 1&lt;/Button&gt; &lt;Button color=<span class="synConstant">&quot;primary&quot;</span> onClick=<span class="synIdentifier">{</span>handleDecrement<span class="synIdentifier">}</span>&gt;- 1&lt;/Button&gt; &lt;/div&gt; ) <span class="synIdentifier">}</span> <span class="synStatement">const</span> mapStateToProps = (state: IInitialState) =&gt; (<span class="synIdentifier">{</span> count: state.counter.count, <span class="synIdentifier">}</span>) <span class="synStatement">const</span> mapDispatchToProps = (dispatch: Dispatch&lt;Action&lt;any&gt;&gt;) =&gt; bindActionCreators(CounterActions, dispatch) <span class="synStatement">export</span> <span class="synStatement">default</span> withStyles(styles)( connect( mapStateToProps, mapDispatchToProps )(Redux as any) ) </pre><p>※ 汚さを際立たせるため、敢えてwithStylesを使っています。</p><p>このコードを見ると私は以下を考えてしまいます。</p> <ul> <li>propsにstateやactionをコピーするせいで、IPropsに本来のプロパティ値以外のreduxでしか使わないものが混入している。</li> <li>withStylesとconnectのHOCを多重ラップしている部分の冗長さ。</li> <li>HOCのラップする順番とかどうなんだっけ?等と考える面倒さ。</li> <li>mapDispatchToPropsの型定義部分の嫌さ。</li> </ul> </div> <div class="section"> <h4 id="v71以降のTypeScript--react-reduxのコード">v7.1以降のTypeScript + react-reduxのコード</h4> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Button <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> createStyles, makeStyles, Theme <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core/styles&quot;</span> <span class="synStatement">import</span> React from <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> useDispatch, useSelector <span class="synIdentifier">}</span> from <span class="synConstant">&quot;react-redux&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> CounterActions <span class="synIdentifier">}</span> from <span class="synConstant">&quot;../store/actions&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> IInitialState <span class="synIdentifier">}</span> from <span class="synConstant">&quot;../store/states&quot;</span> <span class="synStatement">const</span> useStyles = makeStyles((theme: Theme) =&gt; createStyles(<span class="synIdentifier">{</span> root: <span class="synIdentifier">{}</span>, <span class="synIdentifier">}</span>)) <span class="synStatement">const</span> countSelector = (state: IInitialState) =&gt; state.counter.count <span class="synStatement">export</span> <span class="synStatement">default</span> <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> <span class="synStatement">const</span> classes = useStyles(<span class="synIdentifier">{}</span>) <span class="synStatement">const</span> dispatch = useDispatch() <span class="synStatement">const</span> count = useSelector(countSelector) <span class="synStatement">const</span> handleIncrement = () =&gt; dispatch(CounterActions.increment()) <span class="synStatement">const</span> handleDecrement = () =&gt; dispatch(CounterActions.decrement()) <span class="synStatement">return</span> ( &lt;div className=<span class="synIdentifier">{</span>classes.root<span class="synIdentifier">}</span>&gt; &lt;p&gt;<span class="synIdentifier">{</span>count<span class="synIdentifier">}</span>&lt;/p&gt; &lt;Button color=<span class="synConstant">&quot;primary&quot;</span> onClick=<span class="synIdentifier">{</span>handleIncrement<span class="synIdentifier">}</span>&gt;+ 1&lt;/Button&gt; &lt;Button color=<span class="synConstant">&quot;primary&quot;</span> onClick=<span class="synIdentifier">{</span>handleDecrement<span class="synIdentifier">}</span>&gt;- 1&lt;/Button&gt; &lt;/div&gt; ) <span class="synIdentifier">}</span> </pre><p>滅茶苦茶コードがスッキリしましたね!</p><p>今までconnect + mapStateToProps + mapDispatchToPropsの定型文3兄弟は、確かにプレーンで処理が(ある程度)解りやすい面がありましたが、この定型文を覚えたりコピペするのが結構面倒なのは皆思っていた筈なので、これが無くせるのは大きいですね。</p><p>mapStateToPropsはselectorになっただけなのでほとんど変わってないように見えますが、connect関数の引数としての関数だったのが、connectが消えた事で純粋なstateのセレクタになり、より役割が解りやすくなったのではないかと思います。angularのngrxで言うところのcreateSelector()に近い形になりました。</p><p>ngrxのcreateSelector()風に使うなら、セレクタ関数はstoreパッケージに移動すれば、更にコードがスッキリし、役割も更に明確化しそうですね!</p> </div> </div> <div class="section"> <h3 id="全部入りのサンプルコード">全部入りのサンプルコード</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Ftypescript-nextjs-redux-material-ui-example" title="treetips/typescript-nextjs-redux-material-ui-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/typescript-nextjs-redux-material-ui-example">github.com</a></cite></p><p>TypeScript v3.5 + Next.js v8.1 + material-ui v4 + react-redux v7.1に対応したサンプルプロジェクトになります。</p><p>Next.jsなので、勿論SSR対応しており、このサンプルではSSRはpagesディレクトリ配下で使用しています。</p><p>コンポーネント自体は単純なfunctionで記述しておき、functionにgetInitialPropsをstaticメソッドとして定義し、最後にexport defaultする形になります。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">function</span> Redux() <span class="synIdentifier">{</span> <span class="synComment">// snip</span> <span class="synIdentifier">}</span> <span class="synComment">// for SSR</span> Redux.getInitialProps = async ctx =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> pagePayload: IPagePayload = <span class="synIdentifier">{</span> selectedPage: Page.REDUX, <span class="synIdentifier">}</span> ctx.store.dispatch(<span class="synIdentifier">{</span> type: PageActions.changePage.toString(), payload: pagePayload, <span class="synIdentifier">}</span>) <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">default</span> Redux </pre> </div> <div class="section"> <h3 id="万能ではない点に注意">万能ではない点に注意</h3> <blockquote cite="https://react-redux.js.org/next/api/hooks"> <p>Note: The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.<br /> 注:セレクター関数は、複数回、任意の時点で実行される可能性があるため、純粋でなければなりません。</p> <cite><a href="https://react-redux.js.org/next/api/hooks">https://react-redux.js.org/next/api/hooks</a></cite> </blockquote> <p>また、私もまだ触ったばかりで把握しきれてないですが、以下の問題もあるため、よくドキュメントを読んでおいた方が良さそうです。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Freact-redux.js.org%2Fnext%2Fapi%2Fhooks%23usage-warnings" title="Hooks · React Redux" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://react-redux.js.org/next/api/hooks#usage-warnings">react-redux.js.org</a></cite></p><p></p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>hooksにより、相当シンプルにコードが記述できるようになりました。</p><p>もはや定型文と化していたconnect + mapStateToProps + mapDispatchToPropsの3兄弟がいなくなったのは大きいですね。</p><p>今回の機能はまだリリースされたばかり解っていない部分があったりドキュメントが読み込めてないので、随時キャッチアップして以下のリポジトリに反映していきたいと思います!<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Ftypescript-nextjs-redux-material-ui-example" title="treetips/typescript-nextjs-redux-material-ui-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/typescript-nextjs-redux-material-ui-example">github.com</a></cite></p> </div> treeapps material-ui v4+TypeScriptでwithStylesからmakeStylesに移行してシンプルにする hatenablog://entry/17680117127200991537 2019-06-16T04:36:44+09:00 2019-06-16T11:32:34+09:00 makeStylesを使う事でシンプルになりますよ〜 <p>makeStylesを使う事でシンプルになりますよ〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190616/20190616112954.png" alt="f:id:treeapps:20190616112954p:plain" title="f:id:treeapps:20190616112954p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>ついにリリースされたmaterial-ui v4ですが、色々な機能が加わったり、構造がより最適化されたり、是非ともバージョンアップしたいですね。</p><p>今回はスタイルの「<span class="feature1">makeStyles関数</span>」に焦点を当ててみます。</p> <ul class="table-of-contents"> <li><a href="#v3からv4への移行">v3からv4への移行</a><ul> <li><a href="#v3のスタイル設定">v3のスタイル設定</a></li> <li><a href="#v4のスタイル設定">v4のスタイル設定</a></li> </ul> </li> <li><a href="#注意点">注意点</a><ul> <li><a href="#従来のReactComponentをextendsしたコンポーネントでは使えない">従来のReact.Componentをextendsしたコンポーネントでは使えない</a></li> </ul> </li> <li><a href="#typescript--nextjs--material-uiのサンプル">typescript + next.js + material-uiのサンプル</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="v3からv4への移行">v3からv4への移行</h3> <div class="section"> <h4 id="v3のスタイル設定">v3のスタイル設定</h4> <pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synStatement">import</span> React from <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> createStyles, Theme, withStyles, WithStyles <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core/styles&quot;</span> <span class="synStatement">const</span> styles = (theme: Theme) =&gt; createStyles(<span class="synIdentifier">{</span> root: <span class="synIdentifier">{</span> padding: theme.spacing.unit * 2, <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>) <span class="synStatement">interface</span> IProps <span class="synStatement">extends</span> WithStyles&lt;<span class="synStatement">typeof</span> styles&gt; <span class="synIdentifier">{</span> children: React.ReactNode <span class="synIdentifier">}</span> <span class="synStatement">const</span> HelloComponent = (props: IProps) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> <span class="synIdentifier">{</span> classes, children <span class="synIdentifier">}</span> = props <span class="synStatement">return</span> (&lt;div className=<span class="synIdentifier">{</span>classes.root<span class="synIdentifier">}</span>&gt;<span class="synIdentifier">{</span>children<span class="synIdentifier">}</span>&lt;/div&gt;) <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">const</span> Hello = withStyles(styles)(HelloComponent) </pre><p>FC(functional component)内からstyles定数のrootに触れるようにするため、IPropsのインターフェースにstylesを継承させる事で、classes経由でrootに触れるようにしています。</p><p>IPropsにWithStylesを継承するとclasses.rootと書けるようになるのは、WithStylesクラスのプロパティにclassesがあるためです。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fmui-org%2Fmaterial-ui%2Fblob%2Fe61dc01a299f5b795ab404d05fb684a312ebfe2a%2Fpackages%2Fmaterial-ui%2Fsrc%2Fstyles%2FwithStyles.d.ts%23L38" title="mui-org/material-ui" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/mui-org/material-ui/blob/e61dc01a299f5b795ab404d05fb684a312ebfe2a/packages/material-ui/src/styles/withStyles.d.ts#L38">github.com</a></cite></p><p>そしてwithStyles(styles) の部分がHOC(higher order component)になっていて、hooksが登場した今となっては、関数でラップしているのが冗長です。</p> </div> <div class="section"> <h4 id="v4のスタイル設定">v4のスタイル設定</h4> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">import</span> React from <span class="synConstant">&quot;react&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> Box <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core&quot;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> createStyles, Theme, makeStyles <span class="synIdentifier">}</span> from <span class="synConstant">&quot;@material-ui/core/styles&quot;</span> <span class="synStatement">const</span> useStyles = makeStyles((theme: Theme) =&gt; createStyles(<span class="synIdentifier">{</span> root: <span class="synIdentifier">{</span> padding: theme.spacing(2), <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>)) <span class="synStatement">interface</span> IProps <span class="synIdentifier">{</span> children: React.ReactNode <span class="synIdentifier">}</span> <span class="synStatement">export</span> <span class="synStatement">const</span> Hello = <span class="synIdentifier">function</span>(props: IProps) <span class="synIdentifier">{</span> <span class="synStatement">const</span> <span class="synIdentifier">{</span> children <span class="synIdentifier">}</span> = props <span class="synStatement">const</span> classes = useStyles(props) <span class="synStatement">return</span> (&lt;Box className=<span class="synIdentifier">{</span>classes.root<span class="synIdentifier">}</span>&gt;<span class="synIdentifier">{</span>children<span class="synIdentifier">}</span>&lt;/Box&gt;) <span class="synIdentifier">}</span> </pre><p>IPropsの呪文のようなextendsが消え、withStylesのHOCが消えた事で単なるfunctionにする事ができ、かなりシンプルになりました。</p> </div> </div> <div class="section"> <h3 id="注意点">注意点</h3> <div class="section"> <h4 id="従来のReactComponentをextendsしたコンポーネントでは使えない">従来のReact.Componentをextendsしたコンポーネントでは使えない</h4> <p>例えば以下のようなライフサイクルメソッドを持つコンポーネントです。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">class</span> Hello <span class="synStatement">extends</span> React.Component&lt;IProps, IState&gt; <span class="synIdentifier">{</span> constructor(props: IProps) <span class="synIdentifier">{</span> <span class="synStatement">super</span>(props) <span class="synIdentifier">this</span>.state = <span class="synIdentifier">{</span> open: <span class="synConstant">true</span>, <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> render( <span class="synStatement">return</span> (&lt;div&gt;hello&lt;/div&gt;) ) <span class="synIdentifier">}</span> </pre><p>これに対してmakeStylesを適用しようとすると、以下のようになります。<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190616/20190616040147.png" alt="f:id:treeapps:20190616040147p:plain" title="f:id:treeapps:20190616040147p:plain" class="hatena-fotolife" itemprop="image"></span></p> <blockquote> <p>Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:<br /> 1. You might have mismatching versions of React and the renderer (such as React DOM)<br /> 2. You might be breaking the Rules of Hooks<br /> 3. You might have more than one copy of React in the same app<br /> See <a href="https://fb.me/react-invalid-hook-call">https://fb.me/react-invalid-hook-call</a> for tips about how to debug and fix this problem.</p> </blockquote> <p>要は「<span class="feature3">function componentで使えよな!</span>」と怒られています。</p><p>という事で従来のReact.Componentを継承した形では使えず、FC化する必要があります。</p><p>この従来のstateをもつcomponentをFC化しようとすると、React Hooksを使う事になります。今回はスタイルの話なのでそこは割愛します。</p><p>更に言うと、makeStylesを使い、React HooksでFC化し、先日リリースされたhooks対応されたreact-redux v7でconnect関数(HOC)を排除すると、connect関数を排除する事もでき、より単純なfunction化に近づきます。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Freact-redux.js.org%2Fapi%2Fhooks" title="Hooks · React Redux" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://react-redux.js.org/api/hooks">react-redux.js.org</a></cite><br /> </p> </div> </div> <div class="section"> <h3 id="typescript--nextjs--material-uiのサンプル">typescript + next.js + material-uiのサンプル</h3> <p>以前より以下のgitリポジトリを最新化していっています。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Ftypescript-nextjs-redux-material-ui-example" title="treetips/typescript-nextjs-redux-material-ui-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/typescript-nextjs-redux-material-ui-example">github.com</a></cite></p><p>2019/06時点では以下に対応しているサンプルとなります。</p> <ul> <li>TypeScript v3.5</li> <li>Next.js v8.1</li> <li>React v16.8</li> <li>material-ui v4</li> <li>redux v4</li> <li>react-redux v7</li> </ul><p>まだTSLintを使っていたりreact-redux v7のhooksも使っていないですが、一部可能な部分はmakeStylesを使う形に更新しています。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>material-uiは3〜7日おきくらいにバージョンアップを繰り返しており、物凄い勢いで進化している凄いプロダクトです。</p><p>進化が速いので、twitterでmaterial-uiの更新情報をこまめにキャッチアップし、更新に追従していった方が無難かもしれませんね。</p> </div> treeapps Java+MyBatisでMySQLのLOAD DATA LOCAL INFILEを実行できるようにする hatenablog://entry/17680117127190643160 2019-06-08T14:44:30+09:00 2019-06-08T23:42:37+09:00 実は簡単にできるのでした〜 <p>実は簡単にできるのでした〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418131549.png" alt="f:id:treeapps:20180418131549p:plain" title="f:id:treeapps:20180418131549p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>MyBatisというか、ORM経由でLOAD DATA等の特殊なDMLを実行したかったり、複数行のSQLを1定義で実行したい時って結構あったりしますよね。今回はそれに対応してみます。</p> <ul class="table-of-contents"> <li><a href="#情報のおさらい">情報のおさらい</a><ul> <li><a href="#MySQLのLOAD-DATAとLOAD-DATA-LOCALの違い">MySQLのLOAD DATAとLOAD DATA LOCALの違い</a></li> <li><a href="#LOAD-DATAはオプション未設定では実行できなくなった">LOAD DATAはオプション未設定では実行できなくなった</a></li> <li><a href="#未設定状態のアプリケーションでLOAD-DATAすると発生するエラー">未設定状態のアプリケーションでLOAD DATAすると発生するエラー</a></li> </ul> </li> <li><a href="#解決策">解決策</a><ul> <li><a href="#setLocalInfileInputStreamを使う">setLocalInfileInputStreamを使う</a></li> <li><a href="#対応するJDBCのオプションが有る">対応するJDBCのオプションが有る</a></li> </ul> </li> <li><a href="#おまけ">おまけ</a><ul> <li><a href="#MyBatisのmapperのxml内で複数のステートメントを記述したい">MyBatisのmapperのxml内で複数のステートメントを記述したい</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="情報のおさらい">情報のおさらい</h3> <p>解説の前に情報が混乱しないよういくつか整理しておきます。</p> <div class="section"> <h4 id="MySQLのLOAD-DATAとLOAD-DATA-LOCALの違い">MySQLのLOAD DATAとLOAD DATA LOCALの違い</h4> <table> <tr> <th>種別</th> <th>挙動</th> </tr> <tr> <td>LOAD DATA INFILE</td> <td>「リモートに有る」CSV・TSV等をMySQLに直接読み込む</td> </tr> <tr> <td>LOAD DATA LOCALLOAD DATA LOCAL INFILE</td> <td>「ローカルに有る」CSV・TSV等をMySQLに転送して読み込む</td> </tr> </table><p>Amazon RDSやCloud SQL等のマネージドサービスの場合はそのDBサーバのファイルシステムに触れないため、基本的に「LOAD DATA」は使用できないので、ほとんどの場合「LOAD DATA LOCAL」使用する事になります。</p> </div> <div class="section"> <h4 id="LOAD-DATAはオプション未設定では実行できなくなった">LOAD DATAはオプション未設定では実行できなくなった</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F5.6%2Fja%2Fload-data-local.html" title="MySQL :: MySQL 5.6 リファレンスマニュアル :: 6.1.6 LOAD DATA LOCAL のセキュリティーの問題" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/refman/5.6/ja/load-data-local.html">dev.mysql.com</a></cite></p><p>いろいろ書かれていますが、要は以下です。</p> <ul> <li>今まで認可処理が無かったから、参照権限さえ持っていればリモートのファイルを読み取れて危なかったぞ。</li> <li>クライアントとサーバの両方でLOAD DATA LOCALを許可するオプションを付けないと実行できないようにしたぞ。</li> <li>クライアントかサーバのどちらかのオプションが未設定の場合は「<span class="feature3">ERROR 1148: The used command is not allowed with this MySQL version</span>」エラーを返すぞ。</li> </ul><p>という事です。サーバ側の設定としては、サーバ側のmy.cnfに「<span class="feature1">local-infile=1</span>」を設定します。</p><p>クライアント側は「<span class="feature1">mysql --local-infile=1 -hホスト名 -uユーザ名 -p DB名</span>」というようにオプション設定して接続します。</p><p>しかしここで一つ問題が生じます。</p><p><span class="feature3">mysqlクライアントを介さないアプリケーションからLOAD DATAする場合に「--local-infile=1」を指定するにはどうするか?</span>という問題が起きます。</p><p>実際のユースケースとしては、単純にJDBCドライバからLOAD DATAするには?という事ですね。</p> </div> <div class="section"> <h4 id="未設定状態のアプリケーションでLOAD-DATAすると発生するエラー">未設定状態のアプリケーションでLOAD DATAすると発生するエラー</h4> <p>以下は、Java, Spring boot, MyBatisのアプリケーションからLOAD DATA LOCALを実行した場合のStackTraceの一部です。</p> <pre class="code lang-java" data-lang="java" data-unlink>; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: The used command is not allowed with <span class="synType">this</span> MySQL version at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:<span class="synConstant">93</span>) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:<span class="synConstant">72</span>) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:<span class="synConstant">81</span>) at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:<span class="synConstant">73</span>) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:<span class="synConstant">446</span>) at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:<span class="synConstant">294</span>) at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:<span class="synConstant">67</span>) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:<span class="synConstant">58</span>) Caused by: java.sql.SQLSyntaxErrorException: The used command is not allowed with <span class="synType">this</span> MySQL version at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:<span class="synConstant">120</span>) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:<span class="synConstant">97</span>) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:<span class="synConstant">122</span>) at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:<span class="synConstant">955</span>) at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:<span class="synConstant">372</span>) </pre><p>エラーのポイントは「ClientPreparedStatement.execute」でしょうか。「<span class="feature3">クライアント側で</span>PreparedStatementを実行しようとして失敗した」と言っているわけです。これが正に「--local-infile=1」オプションが無いから発生したエラーなのです。</p> </div> </div> <div class="section"> <h3 id="解決策">解決策</h3> <p>では解決策を調べていきます。</p><p>結論から言うと、「<span class="feature1">MyBatisで対応するのではなくJDBCドライバで対応する</span>」形になります。</p><p>アプリケーションからはJDBCドライバを使用してMySQLサーバに接続しており、mysqlクライアントを介していません。従って、JDBCドライバで何とかするしかありません。</p> <div class="section"> <h4 id="setLocalInfileInputStreamを使う">setLocalInfileInputStreamを使う</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fspullara%2Fmysql-connector-java%2Fblob%2Fcc5922f6712c491d0cc46e846ae0dc674c9a5844%2Fsrc%2Fmain%2Fjava%2Fcom%2Fmysql%2Fjdbc%2FStatement.java%23L61-L86" title="spullara/mysql-connector-java" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/spullara/mysql-connector-java/blob/cc5922f6712c491d0cc46e846ae0dc674c9a5844/src/main/java/com/mysql/jdbc/Statement.java#L61-L86">github.com</a></cite></p><p>ピンポイントでいきなりメソッドが出てきましたが、要はこれが使われれば、「--local-infile=1」を指定したのと同じ状態でMySQLサーバに接続した事になります。</p><p>しかしこのご時世、直接JDBCドライバのメソッドを呼びたくありません。</p> </div> <div class="section"> <h4 id="対応するJDBCのオプションが有る">対応するJDBCのオプションが有る</h4> <p>JDBCドライバにはズバリそれに該当するオプションがあるのです。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Fconnector-j%2F8.0%2Fen%2Fconnector-j-reference-configuration-properties.html" title="MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3 Configuration Properties" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html">dev.mysql.com</a></cite><br /> </p> <blockquote> <p> allowLoadLocalInfile</p><p>Should the driver allow use of 'LOAD DATA LOCAL INFILE...'?</p><p>Default: false</p><p>Since version: 3.0.3</p> </blockquote> <blockquote> <p>allowUrlInLocalInfile</p><p>Should the driver allow URLs in 'LOAD DATA LOCAL INFILE' statements?</p><p>Default: false</p><p>Since version: 3.1.4 </p> </blockquote> <p>LOAD DATAをファイルパス指定する場合は「<span class="feature1">allowLoadLocalInfile</span>」を、URL指定する場合は「<span class="feature1">allowUrlInLocalInfile</span>」を有効にするだけで対応できます。</p><p>このオプションは以下のようにURL属性で指定します。</p> <pre class="code txt" data-lang="txt" data-unlink>jdbc:mysql://${host}:${port}/${schema}?allowLoadLocalInfile=true</pre><p>これで無事、mysqlクライアントを介さずにアプリケーションから直接LOAD DATA LOCAL INFILEを実行できるようになります。</p><p>allowLoadLocalInfileは何故デフォルト値がfalseで無効なのか?と一瞬思いますが、冒頭のMySQLの公式リファレンスのセキュリティ問題に合わせ、初期値は無効にしていると思われます。</p> </div> </div> <div class="section"> <h3 id="おまけ">おまけ</h3> <div class="section"> <h4 id="MyBatisのmapperのxml内で複数のステートメントを記述したい">MyBatisのmapperのxml内で複数のステートメントを記述したい</h4> <p>例えば以下のようにセミコロンが複数ある、つまり複数ステートメントをMyBatisのmapperのxmlで、1度のSQLで実行してみます。</p> <pre class="code lang-xml" data-lang="xml" data-unlink><span class="synComment">&lt;?</span><span class="synType">xml version</span>=<span class="synConstant">&quot;1.0&quot;</span><span class="synType"> encoding</span>=<span class="synConstant">&quot;UTF-8&quot;</span><span class="synType"> </span><span class="synComment">?&gt;</span> <span class="synIdentifier">&lt;!</span><span class="synStatement">DOCTYPE</span> mapper <span class="synStatement">PUBLIC</span> <span class="synConstant">&quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;</span> <span class="synConstant">&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;mapper </span><span class="synType">namespace</span>=<span class="synConstant">&quot;hoge&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;select </span><span class="synType">id</span>=<span class="synConstant">&quot;hoge&quot;</span><span class="synIdentifier">&gt;</span> select 1; select 2; <span class="synIdentifier">&lt;/select&gt;</span> <span class="synIdentifier">&lt;/mapper&gt;</span> </pre><p>すると以下のエラーが発生します。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synError">###</span> Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version <span class="synStatement">for</span> the right syntax to use near <span class="synConstant">'</span><span class="synError">select 2</span><span class="synConstant">'</span> at line <span class="synConstant">2</span> ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version <span class="synStatement">for</span> the right syntax to use near <span class="synConstant">'</span><span class="synError">select 2</span><span class="synConstant">'</span> at line <span class="synConstant">2</span> at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:<span class="synConstant">234</span>) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:<span class="synConstant">72</span>) at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:<span class="synConstant">73</span>) at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:<span class="synConstant">446</span>) Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version <span class="synStatement">for</span> the right syntax to use near <span class="synConstant">'</span><span class="synError">select 2</span><span class="synConstant">'</span> at line <span class="synConstant">2</span> at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:<span class="synConstant">120</span>) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:<span class="synConstant">97</span>) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:<span class="synConstant">122</span>) at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:<span class="synConstant">955</span>) at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:<span class="synConstant">372</span>) </pre><p>これもJDBCドライバのオプションが問題で、「allowMultiQueries」の初期値がfalseなので、シンタックスエラー扱いになっています。</p> <blockquote> <p> allowMultiQueries</p><p>Allow the use of ';' to delimit multiple queries during one statement (true/false). Default is 'false', and it does not affect the addBatch() and executeBatch() methods, which rely on rewriteBatchStatements instead.</p><p>Default: false</p><p>Since version: 3.1.1 </p> </blockquote> <p>allowMultiQueries=trueをJDBCドライバのURLに指定すれば複数ステートメントの実行は可能になります。</p><p>しかしこれを有効にすると、SQLインジェクションを行う際に複数ステートメントが実行できてしまい、最悪沢山の攻撃を1度に受けてしまう可能性があります。もしallowMultiQueries=falseであれば、仮に1度に複数ステートメントを実行しようとしても、syntax error扱いにする事ができ、多少安全になります。</p><p>また、複数ステートメントを許可してしまうと、便利だからと使いがちになり、1定義の責務を増やしてしまうのと同時に共通化の妨げにもなります。</p><p>以上を考慮したうえでallowMultiQueriesを有効にするか決めたいですね。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>この問題は他のORMを使っても起き得る問題なので、覚えておくと便利そうです。</p><p>なお、<span class="feature1">LOAD DATA LOCAL INFILEは暗黙的なコミットを発生させずトランザクションが有効になる</span>ので、バッチ処理等で積極的に使っていきたいですね!<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F5.6%2Fja%2Fimplicit-commit.html" title="MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.3 暗黙的なコミットを発生させるステートメント" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/refman/5.6/ja/implicit-commit.html">dev.mysql.com</a></cite></p> </div> treeapps macのAutomatorでhomebrewのコマンドをFinderのサービスから実行できるようにする hatenablog://entry/17680117127094149689 2019-04-30T06:38:07+09:00 2019-05-01T17:28:35+09:00 homebrewでインストールしたコマンドをFinder上で画面ポチポチで実行可能にしてみましょう〜 <p>homebrewでインストールしたコマンドをFinder上で画面ポチポチで実行可能にしてみましょう〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418114029.png" alt="f:id:treeapps:20180418114029p:plain" title="f:id:treeapps:20180418114029p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> <ul class="table-of-contents"> <li><a href="#モチベーション">モチベーション</a></li> <li><a href="#RARの解凍をサービス化してFinderからポチポチできるようにする">RARの解凍をサービス化してFinderからポチポチできるようにする</a><ul> <li><a href="#homebrewでp7zipをインストールする">homebrewでp7zipをインストールする</a></li> <li><a href="#Automatorからサービスを作成する">Automatorからサービスを作成する</a></li> <li><a href="#単数複数のrarファイルをサービスで解凍するGIFアニメ">単数・複数のrarファイルをサービスで解凍するGIFアニメ</a></li> </ul> </li> <li><a href="#おまけフォルダ毎にzip圧縮">おまけ:フォルダ毎にzip圧縮</a><ul> <li><a href="#GIFアニメで挙動を確認する">GIFアニメで挙動を確認する</a></li> </ul> </li> <li><a href="#おまけ画像サイズの50化">おまけ:画像サイズの50%化</a><ul> <li><a href="#GIFアニメで挙動を確認する-1">GIFアニメで挙動を確認する</a></li> </ul> </li> <li><a href="#トラブルシューティング">トラブルシューティング</a><ul> <li><a href="#作成したサービスがメニューに出てこないんだけど">作成したサービスがメニューに出てこないんだけど?</a></li> <li><a href="#homebrewのコマンドが実行されてないっぽいんだけど">homebrewのコマンドが実行されてないっぽいんだけど?</a></li> <li><a href="#コマンド実行後通知できない">コマンド実行後通知できない?</a></li> <li><a href="#while-read-file-do-って何だよ">while read file; do って何だよ</a></li> <li><a href="#使わないサービスを非表示にしたいんだけど">使わないサービスを非表示にしたいんだけど?</a></li> <li><a href="#自作したサービスを削除したいんだけど">自作したサービスを削除したいんだけど?</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="モチベーション">モチベーション</h3> <p><span class="feature1">私「このファイルRARか。解凍できん・・・」</span><br /> <span class="feature2">敵「App StoreでThe Unarchiverインストールすれば?」</span><br /> <span class="feature1">私「RARアーカイバなんてインストールしたくない。アンインストール時にゴミが残るの嫌だし挙動をコントロールしたい。っていうか今時どんな理由があってRAR形式なんて使うんだよ・・・」</span><br /> <span class="feature2">敵「うざ」</span><br /> <span class="feature1">私「homebrewでインストールしたp7zipならRAR解凍できる。」</span><br /> <span class="feature2">敵「何それキモ」</span><br /> <span class="feature1">私「Automatorなら確かシェルスクリプト呼べるから、画面からポチポチできそう」</span></p><br /> <p>こんな動機です。他にも、App Storeには無いけど、homebrewには有るコマンドを<span class="feature1">Finder上でトラックパッドでポチポチ実行したい</span>したい場合もです。</p><p>何故ターミナルからコマンドを実行しないかというと、「<span class="feature3">いちいちターミナルでコマンド実行するの大変じゃない?ファイル名に日本語やスペースが混じってると面倒だし</span>」という物凄くつまらない理由になります。</p><p>このつまらない願望をAutomatorなら実現できるので、やってみましょう。</p> </div> <div class="section"> <h3 id="RARの解凍をサービス化してFinderからポチポチできるようにする">RARの解凍をサービス化してFinderからポチポチできるようにする</h3> <div class="section"> <h4 id="homebrewでp7zipをインストールする">homebrewでp7zipをインストールする</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>brew install p7zip </pre> </div> <div class="section"> <h4 id="Automatorからサービスを作成する">Automatorからサービスを作成する</h4> <p>アプリケーションからAutomatorを起動します。<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430031709.png" alt="f:id:treeapps:20190430031709p:plain" title="f:id:treeapps:20190430031709p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>Automatorを起動するといきなり以下のようなダイアログが表示されます。色々表示されていますが、ここは<span class="feature1">新規作成</span>ボタンをクリックします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430055525.png" alt="f:id:treeapps:20190430055525p:plain" title="f:id:treeapps:20190430055525p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>新規作成ボタンをクリックすると以下のようなダイアログが表示されます。書類の種類は<span class="feature1">クイックアクション</span>を選択し、<span class="feature1">選択ボタン</span>をクリックします。少し解りにくいですが、サービスに登録するにはクイックアクションを選択します。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430033408.png" alt="f:id:treeapps:20190430033408p:plain" title="f:id:treeapps:20190430033408p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>選択ボタンをクリックすると以下のような画面が表示されます。いきなり大量の機能が表れて面くらいますが、虫眼鏡アイコンのテキストボックスに<span class="feature1">シェルスクリプト</span>と入力すると、以下のように大量のメニューの中から<span class="feature1">シェルスクリプトを実行</span>が絞り込まれます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430033809.png" alt="f:id:treeapps:20190430033809p:plain" title="f:id:treeapps:20190430033809p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span class="feature1">シェルスクリプトを実行</span>をドラッグし、<span class="feature3">ワークフローを作成するには、ここにアクションまたはファイルをドラッグしてください。</span>というエリアにドロップします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430034118.png" alt="f:id:treeapps:20190430034118p:plain" title="f:id:treeapps:20190430034118p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>ワークフローが受け取る現在の項目を<span class="feature1">ファイルまたはフォルダ</span>に、シェルを<span class="feature1">/bin/bash</span>に変更し、テキストエリアに以下をコピー&ペーストします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">while read file; do</span> <span class="synComment"># ファイルではない場合はスキップ</span> <span class="synStatement">if [</span> <span class="synStatement">!</span> <span class="synStatement">-f</span> <span class="synStatement">&quot;</span><span class="synPreProc">$file</span><span class="synStatement">&quot;</span> <span class="synStatement">]</span>; <span class="synStatement">then</span> <span class="synStatement">continue</span> <span class="synStatement">fi</span> <span class="synComment"># パスが通っていないのでフルパスで解凍する</span> /usr/<span class="synStatement">local</span>/bin/unrar x <span class="synStatement">&quot;</span><span class="synPreProc">$file</span><span class="synStatement">&quot;</span> <span class="synStatement">&quot;</span><span class="synPreProc">$(</span><span class="synSpecial">dirname </span><span class="synStatement">&quot;</span><span class="synPreProc">$file</span><span class="synStatement">&quot;</span><span class="synPreProc">)</span><span class="synStatement">&quot;</span> <span class="synStatement">done</span> <span class="synComment"># 完了を知らせる音を鳴らす</span> afplay /System/Library/Sounds/Glass.aiff </pre><p>※ Automatorはログインシェルを実行してくれないのでunrarをフルパスで記述しています</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430042744.png" alt="f:id:treeapps:20190430042744p:plain" title="f:id:treeapps:20190430042744p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>テキストエリアに上記スクリプトを入力後、<span class="feature1">cmd + s</span>で保存します。ここで保存した名前がそのままサービス名になります。今回は「<span class="feature1">RARを解凍</span>」という名前にしてみました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430034925.png" alt="f:id:treeapps:20190430034925p:plain" title="f:id:treeapps:20190430034925p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>尚、保存したファイルの実態は<span class="feature1">/Users/ユーザ名/Library/Services/RARを解凍.workflow</span>に保存されています。</p><p>これで完成です!</p><p>では早速試してみましょう。</p> </div> <div class="section"> <h4 id="単数複数のrarファイルをサービスで解凍するGIFアニメ">単数・複数のrarファイルをサービスで解凍するGIFアニメ</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430043827.gif" alt="f:id:treeapps:20190430043827g:plain" title="f:id:treeapps:20190430043827g:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="おまけフォルダ毎にzip圧縮">おまけ:フォルダ毎にzip圧縮</h3> <p>例えば以下のように沢山のディレクトリが有るとします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>. ├── sample1 ├── sample2 ・・・略・・・ ├── sample49 └── sample50 </pre><p>このフォルダを1ファイルのzipに圧縮するのではなく、フォルダ毎にzip圧縮をしたい、更にフォルダは削除してzipファイルのみ残したい場合、以下のスクリプトをAutomatorでサービス化するだけです。サービス化手順は前述と全く同じです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">while read f;do</span> <span class="synStatement">if [</span> <span class="synStatement">!</span> <span class="synStatement">-d</span> <span class="synStatement">&quot;</span><span class="synPreProc">$f</span><span class="synStatement">&quot;</span> <span class="synStatement">]</span>; <span class="synStatement">then</span> <span class="synStatement">continue</span> <span class="synStatement">fi</span> <span class="synStatement">cd</span> <span class="synStatement">&quot;</span><span class="synPreProc">${f</span><span class="synStatement">%</span>/*<span class="synPreProc">}</span><span class="synStatement">&quot;</span> zip <span class="synSpecial">-0mr</span> <span class="synStatement">-b</span> /tmp <span class="synStatement">&quot;</span><span class="synPreProc">${f</span><span class="synStatement">##</span>*/<span class="synPreProc">}</span><span class="synStatement">&quot;</span>.zip <span class="synStatement">&quot;</span><span class="synPreProc">${f</span><span class="synStatement">##</span>*/<span class="synPreProc">}</span><span class="synStatement">&quot;</span> <span class="synStatement">-x</span> <span class="synStatement">&quot;</span><span class="synConstant">*/.DS_Store</span><span class="synStatement">&quot;</span> <span class="synStatement">&quot;</span><span class="synConstant">*/Thumbs.db</span><span class="synStatement">&quot;</span> <span class="synStatement">done</span> afplay /System/Library/Sounds/Glass.aiff </pre><p>こちらは、以下の記事を参考にカスタマイズした形になります。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmattintosh.hatenablog.com%2Fentry%2F2013%2F01%2F06%2F145525" title="Finder で選択した複数のディレクトリを個別に ZIP にする - mattintosh note" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://mattintosh.hatenablog.com/entry/2013/01/06/145525">mattintosh.hatenablog.com</a></cite><br /> </p> <div class="section"> <h4 id="GIFアニメで挙動を確認する">GIFアニメで挙動を確認する</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430053019.gif" alt="f:id:treeapps:20190430053019g:plain" title="f:id:treeapps:20190430053019g:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="おまけ画像サイズの50化">おまけ:画像サイズの50%化</h3> <p>なんでこんな機能が必要になるかというと、retinaディスプレイで画面キャプチャを取得すると、解像度の問題か、巨大な画像(2倍のサイズ)になってしまうためです。それを等倍に戻すためにこれが便利だったりします。</p><p>この機能に関しては標準機能のみで行った方が楽なので、<span class="feature1">通常のワークフローのみで作成します</span>。</p><p>ワークフローが受け取る現在の項目を<span class="feature1">イメージファイル</span>に、ワークフローの1件目を<span class="feature1">Finder項目を複製</span>に、ワークフローの2件目を<span class="feature1">イメージをサイズ調整にし、<span class="feature1">比率(パーセント)指定を選択して値に50を指定</span>し、保存すれば完了です。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430052137.png" alt="f:id:treeapps:20190430052137p:plain" title="f:id:treeapps:20190430052137p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>元のファイルを削除せずコピーしてからリサイズするので、操作を誤っても元ファイルへの影響は無いので安心です。</p> <div class="section"> <h4 id="GIFアニメで挙動を確認する-1">GIFアニメで挙動を確認する</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430053258.gif" alt="f:id:treeapps:20190430053258g:plain" title="f:id:treeapps:20190430053258g:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="トラブルシューティング">トラブルシューティング</h3> <div class="section"> <h4 id="作成したサービスがメニューに出てこないんだけど">作成したサービスがメニューに出てこないんだけど?</h4> <p><span class="feature1">ワークフローが受け取る現在の項目</span>の設定値が誤っていると、メニューに表示されない事があります。</p><p>例えば「ワークフローが受け取る現在の項目」を「自動(テキスト)」が選択されている場合、フォルダを右クリックしてもサービスに表れません。選択値の通り、テキストファイル上で右クリックした場合のみ選択できるようになります。従って、対象が画像なのか、フォルダなのか、等を考慮して適切に設定する必要があります。</p> </div> <div class="section"> <h4 id="homebrewのコマンドが実行されてないっぽいんだけど">homebrewのコマンドが実行されてないっぽいんだけど?</h4> <p>Automatorはログインシェルを実行してくれないため、.bashrcや.zshrcを参照しないため、環境変数やパスが未設定の状態で実行されます。</p><p>これに対応するにはフルパスで記述する等が必要になります。</p> </div> <div class="section"> <h4 id="コマンド実行後通知できない">コマンド実行後通知できない?</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># コマンドのインストール</span> brew install terminal-notifier <span class="synComment"># 通知テスト</span> /usr/<span class="synStatement">local</span>/bin/terminal-notifier <span class="synSpecial">-title</span> <span class="synStatement">&quot;</span><span class="synConstant">title</span><span class="synStatement">&quot;</span> <span class="synSpecial">-subtitle</span> <span class="synStatement">&quot;</span><span class="synConstant">subtitle</span><span class="synStatement">&quot;</span> <span class="synSpecial">-message</span> <span class="synStatement">&quot;</span><span class="synConstant">テスト</span><span class="synStatement">&quot;</span> </pre><p>これをスクリプトの最後等に挟むと通知もできます。</p> </div> <div class="section"> <h4 id="while-read-file-do-って何だよ">while read file; do って何だよ</h4> <p>「while read file; do」は標準入力(stdin)から選択された複数のファイル名を受け取っています。</p><p>何故標準入力(stdin)なのかというと、Automatorのワークフローでそう設定したためです。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430062220.png" alt="f:id:treeapps:20190430062220p:plain" title="f:id:treeapps:20190430062220p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>もし「入力の引き渡し方法」を「<span class="feature1">引数として</span>」を選択した場合、スクリプトの1行目を以下のように修正する事で動くようになります。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">while read file; do</span> ↓ <span class="synStatement">for</span> file <span class="synStatement">in</span> <span class="synStatement">&quot;</span><span class="synPreProc">$@</span><span class="synStatement">&quot;</span>; <span class="synStatement">do</span> </pre><p>これは単にシェルスクリプトの処理の問題なだけなので、使い慣れている方で記述すればいいかなと思います。</p> </div> <div class="section"> <h4 id="使わないサービスを非表示にしたいんだけど">使わないサービスを非表示にしたいんだけど?</h4> <p>システム環境設定 -> キーボード -> ショートカットタブを選択 -> リストボックスからサービスを選択 -> 表示したいもののみチェックする</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430061517.png" alt="f:id:treeapps:20190430061517p:plain" title="f:id:treeapps:20190430061517p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>尚、サービスの数が5個以上の場合は<span class="feature1">サービスというサブメニューが表示</span>され、<span class="feature1">5個より少ない場合は以下のようにサブメニューの代わりにコマンド名が直接表示されるようです</span>。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190430/20190430055140.png" alt="f:id:treeapps:20190430055140p:plain" title="f:id:treeapps:20190430055140p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="自作したサービスを削除したいんだけど">自作したサービスを削除したいんだけど?</h4> <p>自作サービスは <span class="feature1">/Users/ユーザ名/Library/Services/</span>に保存されているので、ここにあるファイルを削除するだけで、リアルタイムに削除が反映されるようです。</p><p>恐らく右クリックしてサービスメニューを展開したタイミングで都度このファイル群を参照しているため、リアルタイムに反映されるのだと思われます。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>Windowsでは使えないので汎用性には大変疑問が残りますが、個人用途ではAutomatorは非常に有用だったりします。</p><p>特にシェルスクリプトが実行可能という事は、プログラマの皆様なら何ができるか想像できますね?</p><p>スクリプトを書かなくてもUIだけで構築できるので、実は色々楽だったりします。例えば画像サイズ50%化ですが、macには<span class="feature1">sips</span>という画像操作コマンドがありますが、これは比率を指定できないため、ImageMagick等を別途インストールする必要があります。しかしそれをするためだけに巨大で脆弱性満載なものをインストールしたくありません。そんな時標準ワークフローが利用すると簡単に実現できるので楽です。</p><p>今回のように、App Storeからインストールしたくない系コマンドは沢山あると思うので、サービス化等をしてオペレーションを簡単にしておきたいですね。</p> </div> treeapps MySQLでlimit offset専用一時テーブルを簡単に生成してページネーション処理を高速化する hatenablog://entry/17680117127062452974 2019-04-21T01:03:05+09:00 2019-04-21T23:44:48+09:00 変な挙動が有るので、今回はそこをピックアップします〜 <p>変な挙動が有るので、今回はそこをピックアップします〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418131549.png" alt="f:id:treeapps:20180418131549p:plain" title="f:id:treeapps:20180418131549p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>MySQLのlimit offset問題と言えば、誰もが知る有名な性能劣化問題です。</p><p>今回は<span class="feature1">create table select構文を利用し、primary keyとauto_incrementの強奪現象を利用して、簡単にこの問題を解消</span>してみようと思います。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#MySQLのlimit-offset問題">MySQLのlimit offset問題</a></li> <li><a href="#よくある解決法">よくある解決法</a></li> <li><a href="#create-table-select構文">create table select構文</a><ul> <li><a href="#部分合成">部分合成</a></li> <li><a href="#このfugaテーブルは一体">このfugaテーブルは一体?</a></li> <li><a href="#えなんでfugaテーブルを作る必要が">え?なんでfugaテーブルを作る必要が?</a></li> <li><a href="#んtemporary-table">ん?temporary table?</a></li> <li><a href="#待ってhogeとfugaの両方にPKとauto_incrementあるよね">待って、hogeとfugaの両方にPKとauto_incrementあるよね?</a></li> </ul> </li> <li><a href="#PKとauto_incrementの強奪現象を利用したlimit-offsetの解決">PKとauto_incrementの強奪現象を利用したlimit offsetの解決</a><ul> <li><a href="#limit-offsetしたいテーブル定義とデータの入り方">limit offsetしたいテーブル定義とデータの入り方</a></li> <li><a href="#別途採番テーブルを作成する">別途採番テーブルを作成する</a></li> <li><a href="#hogeとfugaをinner-joinしてbetweenする">hogeとfugaをinner joinしてbetweenする</a></li> <li><a href="#explainしてみる">explainしてみる</a></li> <li><a href="#速度比較をしてみる">速度比較をしてみる</a><ul> <li><a href="#通常のlimit-offset版">通常のlimit offset版</a></li> <li><a href="#between版">between版</a></li> </ul> </li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="環境">環境</h3> <p>検証する前に環境を確認しておきます。</p> <table> <tr> <th>MySQL</th> <td>v5.7(docker上のMySQLです)</td> </tr> <tr> <th>CPU</th> <td>Core i9 9900k</td> </tr> <tr> <th>MEM</th> <td>64G</td> </tr> <tr> <th>ストレージ</th> <td>SSD 1TB</td> </tr> </table> </div> <div class="section"> <h3 id="MySQLのlimit-offset問題">MySQLのlimit offset問題</h3> <p>ざっくり解説しておくと、</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">select</span> * <span class="synSpecial">from</span> big_table limit <span class="synConstant">1000000</span>, <span class="synConstant">1000</span>; </pre><p>こうすると一見1000件しかデータを参照しないように見えますが、実は100万件取得してから999000件を捨てているため、offset値が大きいほど遅くなるというものです。</p> </div> <div class="section"> <h3 id="よくある解決法">よくある解決法</h3> <p>解決方法は大体皆答えが出ていて、その多くは</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">select</span> * <span class="synSpecial">from</span> big_table <span class="synSpecial">where</span> id <span class="synStatement">between</span> <span class="synConstant">0</span> <span class="synStatement">and</span> <span class="synConstant">1000</span>; <span class="synStatement">select</span> * <span class="synSpecial">from</span> big_table <span class="synSpecial">where</span> id <span class="synStatement">between</span> <span class="synConstant">1001</span> <span class="synStatement">and</span> <span class="synConstant">2000</span>; <span class="synStatement">select</span> * <span class="synSpecial">from</span> big_table <span class="synSpecial">where</span> id <span class="synStatement">between</span> <span class="synConstant">2001</span> <span class="synStatement">and</span> <span class="synConstant">3000</span>; </pre><p>という、betweenでインデックスを効かせる形ですね</p><p>ここでありがちな問題があります。<span class="feature1">betweenするには綺麗に歯抜けのないid列的なものが必要だが、勿論そんなものは無いし既存テーブルを変更したくない</span>場合、どうするかです。</p><p>mysqlのcreate tableは実はselectもできてしまうので、今回はこれを利用してさっくりやってみます。</p> </div> <div class="section"> <h3 id="create-table-select構文">create table select構文</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F5.6%2Fja%2Fcreate-table-select.html" title="MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.1.17.1 CREATE TABLE ... SELECT 構文" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/refman/5.6/ja/create-table-select.html">dev.mysql.com</a></cite></p><p>使い方は公式サイト↑参照ですが、最小構成で言うと以下のような事が可能というだけです。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">create</span> <span class="synSpecial">table</span> fuga <span class="synStatement">select</span> * <span class="synSpecial">from</span> hoge; </pre><p>こうすると、hogeの<span class="feature1">構造</span>と<span class="feature1">データ</span>をコピーしたfugaというテーブルを作成する事ができます。(完全コピーではなくbtree index等はコピーされません)</p> <div class="section"> <h4 id="部分合成">部分合成</h4> <p>実はこの構文、意外と柔軟な記述ができて、以下のようにhogeの一部だけを持ってきた部分合成も可能です。</p><p>合成元となるテーブルは以下の定義とします。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">drop</span> <span class="synSpecial">table</span> <span class="synSpecial">if</span> <span class="synStatement">exists</span> hoge; <span class="synStatement">create</span> <span class="synSpecial">table</span> hoge( hoge_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'ID'</span>, hoge_name <span class="synType">varchar</span>(<span class="synConstant">100</span>) <span class="synStatement">not</span> <span class="synSpecial">null</span> <span class="synStatement">comment</span> <span class="synConstant">'名称'</span>, primary key(hoge_id) ) engine=innodb charset=utf8mb4; </pre><p>これからfugaテーブルを作るのですが、hoge_idだけが欲しいので、以下のようにします。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">drop</span> <span class="synSpecial">table</span> <span class="synSpecial">if</span> <span class="synStatement">exists</span> fuga; <span class="synStatement">create</span> temporary <span class="synSpecial">table</span> fuga( fuga_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'fuga ID'</span>, primary key(fuga_id) ) engine=innodb charset=utf8mb4 <span class="synStatement">comment</span> <span class="synConstant">'合成!!'</span> <span class="synStatement">select</span> hoge_id <span class="synSpecial">from</span> hoge <span class="synSpecial">order</span> <span class="synSpecial">by</span> hoge_id <span class="synSpecial">asc</span> ; </pre><p>こうすると、</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; show <span class="synStatement">create</span> <span class="synSpecial">table</span> fuga \G *************************** <span class="synConstant">1</span>. <span class="synSpecial">row</span> *************************** <span class="synSpecial">Table</span>: fuga <span class="synStatement">Create</span> <span class="synSpecial">Table</span>: <span class="synStatement">CREATE</span> TEMPORARY <span class="synSpecial">TABLE</span> `fuga` ( `fuga_id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> AUTO_INCREMENT <span class="synStatement">COMMENT</span> <span class="synConstant">'fuga ID'</span>, `hoge_id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> <span class="synSpecial">DEFAULT</span> <span class="synConstant">'0'</span> <span class="synStatement">COMMENT</span> <span class="synConstant">'ID'</span>, PRIMARY KEY (`fuga_id`) ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 <span class="synStatement">COMMENT</span>=<span class="synConstant">'合成!!'</span> </pre><p>という列を持ったfugaテーブルが作成されます。</p><br /> <p><span class="feature3">待て待て待て!なんか腑に落ちない事があるんだが!?</span></p><p>はい。では順番にその疑問と回答を見ていきましょう。</p> </div> <div class="section"> <h4 id="このfugaテーブルは一体">このfugaテーブルは一体?</h4> <p>これは、limit offset問題をbetweenで解決したいのですが、元のテーブル定義には手を入れたくありません。誰だってそうです。</p><p>なので別途採番テーブルを作成する事で、元の定義を壊さず解決しようとしています。</p> </div> <div class="section"> <h4 id="えなんでfugaテーブルを作る必要が">え?なんでfugaテーブルを作る必要が?</h4> <p>必要無い場合もあります。しかし100%そうとは言い切れません。</p><p>通常auto_incrementの列は連番が採番されます。しかし、<span class="feature3">歯抜けの値が入っている場合</spna>があります。</p><p>可能性としては以下が想定されます。</p> <ul> <li>アプリケーション側でdelete insertしてidが飛び飛びになる。</li> <li>auto_increment_increment 設定を変更し、採番値が奇数になるようにしている。(昔レプリケーションで複数台でIDが重複しないようにサーバ1では偶数、サーバ2では奇数、なんて事をしてる事もありました)</li> <li>データ移行で仕方なくid値を100000から始めている。</li> </ul><p>こんな事が100%無いと言い切れないですし、考えるのも面倒です。なので信頼できる採番値を自分で作ってしまおう、というのがfugaテーブルです。</p> </div> <div class="section"> <h4 id="んtemporary-table">ん?temporary table?</h4> <p>しれっと「create <span class="feature1">temporary</span> table fuga」と記述しました。</p><p>temporaryを付けると、そのセッションのみ閲覧・操作可能な一時テーブルを作成できます。</p> <blockquote cite="https://dev.mysql.com/doc/refman/5.6/ja/create-table.html"> <p>テーブルの作成時に TEMPORARY キーワードを使用できます。TEMPORARY テーブルは現在のセッションにのみ表示され、そのセッションが閉じられると自動的に削除されます。つまり、2 つの異なるセッションが同じ一時テーブル名を使用することができ、互いに、または同じ名前の既存の TEMPORARY 以外のテーブルと競合することはありません。(既存のテーブルは、一時テーブルが削除されるまで非表示になります。)一時テーブルを作成するには、CREATE TEMPORARY TABLES 権限が必要です。</p> <cite><a href="https://dev.mysql.com/doc/refman/5.6/ja/create-table.html">https://dev.mysql.com/doc/refman/5.6/ja/create-table.html</a></cite> </blockquote> <p>create temporary tables権限が必要にはなりますが、他の人に見られずに、更に一時テーブルの削除漏れを無くす事ができるという機能です。</p><p>特に一時テーブルの削除漏れは結構致命的で、後になって「何この変なテーブル?削除していいの?」となり「解らん。怖くて消せないよねそれ・・」みたいな負債が貯まる要因にもなるので、極力付けた方がいいと思われます。</p><p>パフォーマンスも通常のテーブルと比較して特別大きな劣化は見られません。(そもそもID列が2列あるだけのテーブルですし)</p> </div> <div class="section"> <h4 id="待ってhogeとfugaの両方にPKとauto_incrementあるよね">待って、hogeとfugaの両方にPKとauto_incrementあるよね?</h4> <p>そうです。<span class="feature1">これこそが今回の記事の主題だったのです</span>。</p><p>もう一度おさらいしてみましょう。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synStatement">drop</span> <span class="synSpecial">table</span> <span class="synSpecial">if</span> <span class="synStatement">exists</span> hoge; <span class="synStatement">create</span> <span class="synSpecial">table</span> hoge( hoge_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'ID'</span>, hoge_name <span class="synType">varchar</span>(<span class="synConstant">100</span>) <span class="synStatement">not</span> <span class="synSpecial">null</span> <span class="synStatement">comment</span> <span class="synConstant">'名称'</span>, primary key(hoge_id) ) engine=innodb charset=utf8mb4; <span class="synStatement">drop</span> <span class="synSpecial">table</span> <span class="synSpecial">if</span> <span class="synStatement">exists</span> fuga; <span class="synStatement">create</span> temporary <span class="synSpecial">table</span> fuga( fuga_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'fuga ID'</span>, primary key(fuga_id) ) engine=innodb charset=utf8mb4 <span class="synStatement">comment</span> <span class="synConstant">'合成!!'</span> <span class="synStatement">select</span> hoge_id <span class="synSpecial">from</span> hoge <span class="synSpecial">order</span> <span class="synSpecial">by</span> hoge_id <span class="synSpecial">asc</span> ; </pre><p>こうすると、以下ができます。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; show <span class="synStatement">create</span> <span class="synSpecial">table</span> fuga\G *************************** <span class="synConstant">1</span>. <span class="synSpecial">row</span> *************************** <span class="synSpecial">Table</span>: fuga <span class="synStatement">Create</span> <span class="synSpecial">Table</span>: <span class="synStatement">CREATE</span> TEMPORARY <span class="synSpecial">TABLE</span> `fuga` ( `fuga_id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> AUTO_INCREMENT <span class="synStatement">COMMENT</span> <span class="synConstant">'fuga ID'</span>, `hoge_id` bigint(<span class="synConstant">20</span>) unsigned <span class="synStatement">NOT</span> <span class="synSpecial">NULL</span> <span class="synSpecial">DEFAULT</span> <span class="synConstant">'0'</span> <span class="synStatement">COMMENT</span> <span class="synConstant">'ID'</span>, PRIMARY KEY (`fuga_id`) ) ENGINE=InnoDB <span class="synSpecial">DEFAULT</span> CHARSET=utf8mb4 <span class="synStatement">COMMENT</span>=<span class="synConstant">'合成!!'</span> </pre><p>結果だけ見ると、<span class="feature3">後発のfuga_idがhoge_idからauto_incrementとprimary keyを強奪している</span>事になりますね。</p><p>一体何故こんな事が起きるのかは解っていませんが、<span class="feature1">この強奪現象がlimit offset問題に利用できるのです</span>。</p> </div> </div> <div class="section"> <h3 id="PKとauto_incrementの強奪現象を利用したlimit-offsetの解決">PKとauto_incrementの強奪現象を利用したlimit offsetの解決</h3> <p>繰り返し同じ定義を記述しますがご容赦下さい。</p> <div class="section"> <h4 id="limit-offsetしたいテーブル定義とデータの入り方">limit offsetしたいテーブル定義とデータの入り方</h4> <p>以下に対して高速にlimit offsetしたいのですが、残念な事にhoge_idが奇数になってしまっています。しかも一見に綺麗に奇数になっているようで、delete insertによってガタガタの歯抜けになっています。</p> <pre class="code lang-sql" data-lang="sql" data-unlink><span class="synComment">-- テーブルの作成</span> mysql&gt; <span class="synStatement">create</span> <span class="synSpecial">table</span> hoge( -&gt; hoge_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'ID'</span>, -&gt; hoge_name <span class="synType">varchar</span>(<span class="synConstant">100</span>) <span class="synStatement">not</span> <span class="synSpecial">null</span> <span class="synStatement">comment</span> <span class="synConstant">'名称'</span>, -&gt; primary key(hoge_id) -&gt; ) engine=innodb charset=utf8mb4; Query OK, <span class="synConstant">0</span> <span class="synSpecial">rows</span> affected (<span class="synConstant">0</span>.<span class="synConstant">01</span> sec) <span class="synComment">-- 歯抜けのテストデータを1000万件投入</span> mysql&gt; load data local -&gt; infile <span class="synConstant">'/tmp/test.tsv'</span> -&gt; <span class="synSpecial">into</span> <span class="synSpecial">table</span> hoge -&gt; <span class="synType">character</span> <span class="synStatement">set</span> utf8 -&gt; fields -&gt; terminated <span class="synSpecial">by</span> <span class="synConstant">'\t'</span> -&gt; enclosed <span class="synSpecial">by</span> <span class="synConstant">''</span> -&gt; lines -&gt; terminated <span class="synSpecial">by</span> <span class="synConstant">'\n'</span> -&gt; ; Query OK, <span class="synConstant">10000000</span> <span class="synSpecial">rows</span> affected (<span class="synConstant">25</span>.<span class="synConstant">99</span> sec) Records: <span class="synConstant">10000000</span> Deleted: <span class="synConstant">0</span> Skipped: <span class="synConstant">0</span> Warnings: <span class="synConstant">0</span> <span class="synComment">-- テストデータの確認</span> mysql&gt; <span class="synStatement">select</span> * <span class="synSpecial">from</span> hoge limit <span class="synConstant">10</span>; +<span class="synComment">---------+---------------------+</span> | hoge_id | hoge_name | +<span class="synComment">---------+---------------------+</span> | <span class="synConstant">1</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">3</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">5</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">7</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">9</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">11</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">13</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">15</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">17</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">19</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | +<span class="synComment">---------+---------------------+</span> <span class="synConstant">10</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre> </div> <div class="section"> <h4 id="別途採番テーブルを作成する">別途採番テーブルを作成する</h4> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">create</span> temporary <span class="synSpecial">table</span> fuga( -&gt; fuga_id bigint unsigned <span class="synStatement">not</span> <span class="synSpecial">null</span> auto_increment <span class="synStatement">comment</span> <span class="synConstant">'fuga ID'</span>, -&gt; primary key(fuga_id) -&gt; ) engine=innodb charset=utf8mb4 <span class="synStatement">comment</span> <span class="synConstant">'合成!!'</span> -&gt; <span class="synStatement">select</span> -&gt; hoge_id -&gt; <span class="synSpecial">from</span> -&gt; hoge -&gt; <span class="synSpecial">order</span> <span class="synSpecial">by</span> -&gt; hoge_id <span class="synSpecial">asc</span> -&gt; ; Query OK, <span class="synConstant">10000000</span> <span class="synSpecial">rows</span> affected (<span class="synConstant">16</span>.<span class="synConstant">75</span> sec) Records: <span class="synConstant">10000000</span> Duplicates: <span class="synConstant">0</span> Warnings: <span class="synConstant">0</span> <span class="synComment">-- 登録されたデータを確認</span> mysql&gt; <span class="synStatement">select</span> * <span class="synSpecial">from</span> fuga limit <span class="synConstant">10</span>; +<span class="synComment">---------+---------+</span> | fuga_id | hoge_id | +<span class="synComment">---------+---------+</span> | <span class="synConstant">1</span> | <span class="synConstant">1</span> | | <span class="synConstant">2</span> | <span class="synConstant">3</span> | | <span class="synConstant">3</span> | <span class="synConstant">5</span> | | <span class="synConstant">4</span> | <span class="synConstant">7</span> | | <span class="synConstant">5</span> | <span class="synConstant">9</span> | | <span class="synConstant">6</span> | <span class="synConstant">11</span> | | <span class="synConstant">7</span> | <span class="synConstant">13</span> | | <span class="synConstant">8</span> | <span class="synConstant">15</span> | | <span class="synConstant">9</span> | <span class="synConstant">17</span> | | <span class="synConstant">10</span> | <span class="synConstant">19</span> | +<span class="synComment">---------+---------+</span> <span class="synConstant">10</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre><p>fuga_idはauto_incrementなので、自動的にfuga_idにnullがinsertされて採番値が入ったようですね。ちょっと空気を読み過ぎた挙動で不安な感じはします・・・</p><p>temporaryテーブルの場合は自セッション以外からは閲覧・操作不能なので、このfuga_idが他セッションによって追加・更新・削除される可能性は0になり、絶対に信頼できる歯抜けのない採番値になります。</p> </div> <div class="section"> <h4 id="hogeとfugaをinner-joinしてbetweenする">hogeとfugaをinner joinしてbetweenする</h4> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">select</span> -&gt; h.* -&gt; <span class="synSpecial">from</span> -&gt; fuga f -&gt; inner join hoge h -&gt; <span class="synSpecial">on</span> f.hoge_id = h.hoge_id -&gt; <span class="synSpecial">where</span> -&gt; f.fuga_id <span class="synStatement">between</span> <span class="synConstant">0</span> <span class="synStatement">and</span> <span class="synConstant">10</span> -&gt; ; +<span class="synComment">---------+---------------------+</span> | hoge_id | hoge_name | +<span class="synComment">---------+---------------------+</span> | <span class="synConstant">1</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">3</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">5</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">7</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">9</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">11</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">13</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">15</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">17</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | | <span class="synConstant">19</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">23</span> | +<span class="synComment">---------+---------------------+</span> <span class="synConstant">10</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre><p>こんな感じです。後はbetweenの値を足したり引いたりするだけですね!</p> </div> <div class="section"> <h4 id="explainしてみる">explainしてみる</h4> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">explain</span> -&gt; <span class="synStatement">select</span> -&gt; h.* -&gt; <span class="synSpecial">from</span> -&gt; fuga f -&gt; inner join hoge h -&gt; <span class="synSpecial">on</span> f.hoge_id = h.hoge_id -&gt; <span class="synSpecial">where</span> -&gt; f.fuga_id <span class="synStatement">between</span> <span class="synConstant">0</span> <span class="synStatement">and</span> <span class="synConstant">10</span> -&gt; ; +<span class="synComment">----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+</span> | id | select_type | <span class="synSpecial">table</span> | partitions | <span class="synSpecial">type</span> | possible_keys | key | key_len | ref | <span class="synSpecial">rows</span> | filtered | Extra | +<span class="synComment">----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+</span> | <span class="synConstant">1</span> | SIMPLE | f | <span class="synSpecial">NULL</span> | range | PRIMARY | PRIMARY | <span class="synConstant">8</span> | <span class="synSpecial">NULL</span> | <span class="synConstant">10</span> | <span class="synConstant">100</span>.<span class="synConstant">00</span> | <span class="synSpecial">Using</span> <span class="synSpecial">where</span> | | <span class="synConstant">1</span> | SIMPLE | h | <span class="synSpecial">NULL</span> | eq_ref | PRIMARY | PRIMARY | <span class="synConstant">8</span> | kyoritsu_db.f.hoge_id | <span class="synConstant">1</span> | <span class="synConstant">100</span>.<span class="synConstant">00</span> | <span class="synSpecial">NULL</span> | +<span class="synComment">----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+</span> </pre><p>keyがPRIMARYのみの最高の形になりましたね。</p><p>現実ではここからjoinして別テーブルのカラムを取得したりフラグ値の考慮等で複雑化しますが、limit offset問題は解決しています。</p> </div> <div class="section"> <h4 id="速度比較をしてみる">速度比較をしてみる</h4> <div class="section"> <h5 id="通常のlimit-offset版">通常のlimit offset版</h5> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">select</span> * <span class="synSpecial">from</span> hoge limit <span class="synConstant">9999000</span>, <span class="synConstant">1000</span>; +<span class="synComment">----------+---------------------+</span> | hoge_id | hoge_name | +<span class="synComment">----------+---------------------+</span> | <span class="synConstant">19998001</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | | <span class="synConstant">19998003</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | ・・・略・・・ | <span class="synConstant">19999997</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | | <span class="synConstant">19999999</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | +<span class="synComment">----------+---------------------+</span> <span class="synConstant">1000</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">1</span>.<span class="synConstant">65</span> sec) </pre><p>CPUで無理やりぶん回しているので速く見えますが、それでもたった1000件の取得に <span class="feature1">1.65sec</span> もかかっています。</p> </div> <div class="section"> <h5 id="between版">between版</h5> <pre class="code lang-sql" data-lang="sql" data-unlink>mysql&gt; <span class="synStatement">select</span> -&gt; h.* -&gt; <span class="synSpecial">from</span> -&gt; fuga f -&gt; inner join hoge h -&gt; <span class="synSpecial">on</span> f.hoge_id = h.hoge_id -&gt; <span class="synSpecial">where</span> -&gt; f.fuga_id <span class="synStatement">between</span> <span class="synConstant">9999001</span> <span class="synStatement">and</span> <span class="synConstant">10000000</span> -&gt; ; +<span class="synComment">----------+---------------------+</span> | hoge_id | hoge_name | +<span class="synComment">----------+---------------------+</span> | <span class="synConstant">19998001</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | | <span class="synConstant">19998003</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | ・・・略・・・ | <span class="synConstant">19999997</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | | <span class="synConstant">19999999</span> | <span class="synConstant">2019-04-21</span> <span class="synConstant">00</span>:<span class="synConstant">15</span>:<span class="synConstant">29</span> | +<span class="synComment">----------+---------------------+</span> <span class="synConstant">1000</span> <span class="synSpecial">rows</span> <span class="synStatement">in</span> <span class="synStatement">set</span> (<span class="synConstant">0</span>.<span class="synConstant">00</span> sec) </pre><p>速過ぎて <span class="feature1">0.00sec</span> とか出てしまいました。</p><br /> <p>fugaをtemporaryテーブルにしていますが、この場合トランザクションを張りっぱなしにする必要がある(トランザクション終了時にtemporaryは消失する)ので、短いトランザクションにしたい場合は通常のcreate tableにし、必ずテーブル削除に「drop table if exists fuge;」をし、安全に削除するようにします。</p> </div> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>記述していて思いましたが、「<span class="feature3">hoge</span>」「<span class="feature3">fuga</span>」だとどっちがどっちなのか解らなくなりますね・・・・失敗です。</p><p>今回の謎現象である<span class="feature3">PKとauto_incrementの強奪現象</span>について公式リファレンスを流し見したのですが、書いてないっぽい???ので、実に謎な挙動です。</p><p>もしかしたら将来的にこの強奪仕様がしれっと無くなる可能性があるので、そこは注意していきたいですね。</p> </div> treeapps Spring BootでMySQLのconnectorjのreplication schemeで参照クエリをreaderに向ける hatenablog://entry/98012380864085722 2019-02-10T23:37:12+09:00 2019-02-10T23:45:45+09:00 リードレプリカを活用しろ警察に怒られる前に、connectorjのマルチポスト機能を学ばなくては・・・!! <p>リードレプリカを活用しろ警察に怒られる前に、connectorjのマルチポスト機能を学ばなくては・・・!!</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180802/20180802010416.png" alt="f:id:treeapps:20180802010416p:plain" title="f:id:treeapps:20180802010416p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>知らないのは私だけだと思いますが、実はconnectorjにはマルチホストのコネクションを管理する機能が内蔵されています。</p> <ul class="table-of-contents"> <li><a href="#マルチホストコネクションの種類">マルチホストコネクションの種類</a></li> <li><a href="#MasterSlave-Replication-with-ConnectorJってどんな機能">Master/Slave Replication with Connector/Jってどんな機能?</a></li> <li><a href="#何に使うの">何に使うの?</a></li> <li><a href="#Spring-bootで簡単に参照クエリのみを振り分ける">Spring bootで簡単に参照クエリのみを振り分ける</a><ul> <li><a href="#1個目の設定applicationymlのdatasouce設定">1個目の設定:application.ymlのdatasouce設定</a></li> <li><a href="#2個目の設定TransactionalreadOnly--true">2個目の設定:@Transactional(readOnly = true)</a></li> <li><a href="#springdatasourceurl">spring.datasource.url</a></li> <li><a href="#TransactionalreadOnly--true">@Transactional(readOnly = true)</a></li> <li><a href="#レプリケーションの遅延について">レプリケーションの遅延について</a></li> </ul> </li> <li><a href="#GIFアニメで動きを見てみる">GIFアニメで動きを見てみる</a><ul> <li><a href="#readerに参照クエリを投げる例">readerに参照クエリを投げる例</a></li> <li><a href="#writerに参照クエリを投げる例">writerに参照クエリを投げる例</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="マルチホストコネクションの種類">マルチホストコネクションの種類</h3> <p>connectorjには実は以下が標準機能として存在しています。</p> <ul> <li>Server Failover</li> <li>Client-Side Failover when using the X Protocol</li> <li>Load Balancing with Connector/J</li> <li>Master/Slave Replication with Connector/J</li> </ul><p>これらの詳細については以下の公式ドキュメントをご覧下さい。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Fconnector-j%2F8.0%2Fen%2Fconnector-j-multi-host-connections.html" title="MySQL :: MySQL Connector/J 8.0 Developer Guide :: 9 Multi-Host Connections" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-multi-host-connections.html">dev.mysql.com</a></cite></p><p>この中で今回「<span class="feature1">Master/Slave Replication with Connector/J</span>」をピックアップします。</p> </div> <div class="section"> <h3 id="MasterSlave-Replication-with-ConnectorJってどんな機能">Master/Slave Replication with Connector/Jってどんな機能?</h3> <p>名前でネタバレしてますが、<span class="feature1">マスターの場合とスレーブの場合で接続先を変更する</span>事ができる設定方法です。</p><p>実際のユースケースとしては以下のようになります。</p><p><span class="feature1">マスター(更新クエリ)の場合はホストAに接続</span><br /> <span class="feature1">スレーブ(参照クエリ)の場合はホストBに接続</span></p><p>Replicationと書かれていますが、どちらかというとレプリケーション云々よりも、更新クエリと参照クエリを別々の接続先に投げてくれる、と考えるといいと思います。</p> </div> <div class="section"> <h3 id="何に使うの">何に使うの?</h3> <p>データベースを使う場合、特別な事情(どうしてもUDFを使いたい等)が無い限り、マネージドサービス(Amazon RDS等)を使う場合がほとんどです。</p><p>オンプレ時代とは異なり、リードレプリカ機能を使えば、職人技が必要なレプリケーション設定の必要も無く、参照専用インスタンスを画面をポチポチするだけで用意できるようになりました。更に、Amazon Auroraの登場により、writerエンドポイント・readerエンドポイントという便利なものが登場しました。</p><p>しかし、です。いくら参照専用インスタンスが有る、readerエンドポイントが有る、といってもそこに参照クエリを流さないと、完全に宝の持ち腐れになります。そこで今回誰もが使うconnectorjの標準機能を使ってそれを実現します。</p><p>MySQL RouterやMariaDB MaxScale等のミドルウェアを使わずにとりあえずreaderを活用できるので、ミドルウェアのSPOFやメンテを考えなくていいので便利ですね。機能は両者には勝てませんけどね。</p> </div> <div class="section"> <h3 id="Spring-bootで簡単に参照クエリのみを振り分ける">Spring bootで簡単に参照クエリのみを振り分ける</h3> <p>今回はkotlin + Spring bootで参照クエリを参照専用ホストに流してみます。</p><p>設定自体はたった2つで完了します。</p> <div class="section"> <h4 id="1個目の設定applicationymlのdatasouce設定">1個目の設定:application.ymlのdatasouce設定</h4> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">spring</span><span class="synSpecial">:</span> <span class="synIdentifier">datasource</span><span class="synSpecial">:</span> <span class="synIdentifier">url</span><span class="synSpecial">:</span> <span class="synConstant">&quot;jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307,127.0.0.1:3308/work&quot;</span> <span class="synIdentifier">username</span><span class="synSpecial">:</span> worker <span class="synIdentifier">password</span><span class="synSpecial">:</span> worker <span class="synIdentifier">driverClassName</span><span class="synSpecial">:</span> com.mysql.cj.jdbc.Driver </pre> </div> <div class="section"> <h4 id="2個目の設定TransactionalreadOnly--true">2個目の設定:@Transactional(readOnly = true)</h4> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Transactional</span>(readOnly = <span class="synConstant">true</span>) <span class="synType">fun</span> findReaderHost(): HostModel = hostRepository.findOne() </pre><p>以上で参照クエリが参照専用ホストに流れます。たったこれだけです。</p><p>では両者の設定についてもう少し確認してみましょう。</p> </div> <div class="section"> <h4 id="springdatasourceurl">spring.datasource.url</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Fconnector-j%2F8.0%2Fen%2Fconnector-j-master-slave-replication-connection.html" title="MySQL :: MySQL Connector/J 8.0 Developer Guide :: 9.4 Configuring Master/Slave Replication with Connector/J" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-master-slave-replication-connection.html">dev.mysql.com</a></cite></p><p>公式ドキュメントを見れば解りますが、一応説明すると超ざっくり以下のように設定します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">url</span><span class="synSpecial">:</span> <span class="synConstant">&quot;jdbc:mysql:replication://${更新+参照ホスト}:${port},${参照専用ホスト1}:${port},${参照専用ホスト2}:${port}/${DB名}&quot;</span> </pre><p>ホスト+portの組み合わせを半角カンマ区切りで設定します。master(更新+参照が可能なホスト)が1個目固定、2個目移行は全部slave(参照専用ホスト)です。</p><p>RDS+リードレプリカの場合は1個づつ設定、Auroraの場合はwriterエンドポイントを1個目、readerエンドポイントを2個目に設定するだけですね。</p> </div> <div class="section"> <h4 id="TransactionalreadOnly--true">@Transactional(readOnly = true)</h4> <p>AOPで@Serviceを付けたクラスに無条件に@Transactionalを設定するケースもありますが、今回@Transactionalを手動で定義している場合の話になります。</p><p>この設定でポイントは「<span class="feature1">readOnly = true</span>」の部分です。このreadOnlyがtrueなら、自動的にspring.datasource.urlのslaveの方に接続が流れるのです。</p><p><span class="feature1">masterに流れるのかslaveに流れるのかは本当にここだけで決まります</span>。insert・update・deleteだからmasterに接続されるわけではありません。(やっちゃダメですが)更新クエリをslaveにも流せますし、参照クエリをmasterに流す事もできます。</p><p>Spring bootのソースコードは追ってませんが、恐らく@Transactional(readOnly = false)を設定すると、内部で Connection.setReadOnly(true) といった事を自動でしてくれているのだと思われます。なので、他のフレームワークでは異なる指定が必要になるか、そもそもできない可能性があるかもしれません。</p> </div> <div class="section"> <h4 id="レプリケーションの遅延について">レプリケーションの遅延について</h4> <p>Auroraでの話になりますが、ストレージを共有しているAuroraでさえ、レプリケーション遅延、所謂レプリカラグは発生します。</p> <blockquote cite="https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html"> <p>Aurora レプリカは、Aurora DB クラスター内の独立したエンドポイントであり、読み取りオペレーションのスケーリングと可用性の向上に最適です。最大 15 個の Aurora レプリカを、1 つの AWS リージョンの中で DB クラスターが処理するアベイラビリティーゾーン全体に分散できます。DB クラスターボリュームは DB クラスターのデータの複数のコピーで構成されます。ただし、クラスターボリュームのデータは、DB クラスターのプライマリインスタンスおよび Aurora レプリカの 1 つの論理ボリュームとして表されます。</p><p>この結果、すべての Aurora レプリカは、最短のレプリカラグでクエリの結果として同じデータを返します。レプリカラグは、通常はプライマリインスタンスが更新を書き込んだ後、100 ミリ秒未満です。レプリカラグは、データベースの変更レートによって異なります。つまり、データベースに対して大量の書き込みオペレーションが発生している間、レプリカラグが増加することがあります。 </p> <cite><a href="https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html">https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html</a></cite> </blockquote> <p>readerエンドポイント使用時はこのレプリカラグを考慮する必要があります。ここで先程の「<span class="feature1">参照クエリをmasterに流す事もできます</span>」の部分に注目します。</p><p>例えば、「参照クエリ -> 更新クエリ -> 参照クエリ」といった参照と更新が入り乱れ、すぐに更新結果が欲しい場合、readerにクエリを投げてもレプリカラグで最新データが取得できない可能性があります。ここで「<span class="feature1">敢えて参照クエリをmasterに流す</span>」事で、レプリカラグを気にせず最新データを取得できるわけです。</p><p>auroraの100 ミリ秒未満のレプリカラグを許容できるケースは、可能な限りreaderに参照クエリを流せるといいですね。その判断がちょっと難しい可能性はありますが。</p> </div> </div> <div class="section"> <h3 id="GIFアニメで動きを見てみる">GIFアニメで動きを見てみる</h3> <p>実は例によって事前に専用プロジェクトを用意しています。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Fkotlin-spring-boot-replication-driver-example" title="treetips/kotlin-spring-boot-replication-driver-example" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/kotlin-spring-boot-replication-driver-example">github.com</a></cite></p><p>writerを1台、readerを2台用意しており、挙動を解りやすするため敢えてレプリケーションをせず、初期データで異なる値をinsertしておき、サーバ毎に異なる値が返るようにしています。</p> <div class="section"> <h4 id="readerに参照クエリを投げる例">readerに参照クエリを投げる例</h4> <p>docker起動時にhostというテーブルを生成し、writerには「writer1」、reader1には「reader1」、reader2には「reader2」という値をinsertしてあります。</p><p>以下の例は、@Transactional(readOnly = true)を設定し、hostテーブルをselectした結果を連続して取得しています。writerにクエリが投げられず、readerの何れかのサーバに接続される事を確認できます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190210/20190210223855.gif" alt="f:id:treeapps:20190210223855g:plain" title="f:id:treeapps:20190210223855g:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="writerに参照クエリを投げる例">writerに参照クエリを投げる例</h4> <p>以下の例は、@Transactionalを設定し、hostテーブルをselectした結果を連続して取得しています。readerにクエリが投げられず、必ずwriterに接続される事を確認できます。(@TransactionalのreadOnlyは初期値がfalseなのでこの場合readOnly = falseが自動的に設定された事になります)</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190210/20190210224424.gif" alt="f:id:treeapps:20190210224424g:plain" title="f:id:treeapps:20190210224424g:plain" class="hatena-fotolife" itemprop="image"></span></p><br /> <br /> <br /> <p>readerのGIFアニメで気づいたと思いますが、バランシングが<span class="feature3">ラウンドロビンではなくランダム</span>です。</p><p>このバランシング設定をドキュメントから見つける事ができませんでしたが、jdbc:mysql:loadbalanceと同じであれば、デフォルト値はrandomですね。</p> <blockquote cite="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html"> <p>ha.loadBalanceStrategy</p><p> If using a load-balanced connection to connect to SQL nodes in a MySQL Cluster/NDB configuration (by using the URL prefix "jdbc:mysql:loadbalance://"), which load balancing algorithm should the driver use: (1) "random" - the driver will pick a random host for each request. This tends to work better than round-robin, as the randomness will somewhat account for spreading loads where requests vary in response time, while round-robin can sometimes lead to overloaded nodes if there are variations in response times across the workload. (2) "bestResponseTime" - the driver will route the request to the host that had the best response time for the previous transaction. (3) "serverAffinity" - the driver initially attempts to enforce server affinity while still respecting and benefiting from the fault tolerance aspects of the load-balancing implementation. The server affinity ordered list is provided using the property 'serverAffinityOrder'. If none of the servers listed in the affinity list is responsive, the driver then refers to the "random" strategy to proceed with choosing the next server.</p><p>Default: random </p> <cite><a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html">https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html</a></cite> </blockquote> <p>google翻訳すると以下となります。</p> <blockquote> <p>(1) "random" - ドライバはリクエストごとにランダムなホストを選びます。</p><p>これはラウンドロビンよりもうまくいく傾向があります。ランダムさはリクエストの応答時間が異なる負荷を分散させるためですが、ラウンドロビンはワークロード全体の応答時間にばらつきがあるとノードが過負荷になることがあります。</p> </blockquote> <p>という事で、ラウンドロビンよりランダムの方がいい結果になるぞ〜、との事で初期値がランダムなようですね。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>最近会社でAuroraはごくごく普通に使用されるようになりましたが、readerを活用できているプロジェクトが少ないなあと思って調べてみると、connectorjにこんな機能が有る事を知りました。MySQL RouterとMax Scaleは知っているのにこっちを知らないという・・・autoReconnect等のオプション設定は気にするのに、スキーム節は気にした事が無かったです。</p><p>大規模プロジェクトの場合、このクエリの場合はwriterに、あのクエリの場合はreaderに、という判断ができる人とできない人が入り乱れますが、その場合どうしよう?といったルール決めが必要になりそうですね。</p><p>闇雲に使用すると、記事投稿後に何故か記事件数が増えないバグが有る〜、画面操作が速すぎると何故か最新データが取得できないんです〜、みたいな事になるので、それらの対応策を事前に検討した方が良さそうです。</p> </div> treeapps java8から11にアップデートした際にgradle-jooq-pluginで出るエラーの対応 hatenablog://entry/98012380843329083 2019-01-28T10:46:38+09:00 2019-01-28T10:46:38+09:00 依存の修正だけで動きますよ〜 <p>依存の修正だけで動きますよ〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190128/20190128100247.png" alt="f:id:treeapps:20190128100247p:plain" title="f:id:treeapps:20190128100247p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>jOOQに限った話ではないと思いますが、javaを8から11等にアップデートするとjaxbが動かない問題がjOOQでも発生するので、エラーに対応してみます。</p> <ul class="table-of-contents"> <li><a href="#java-v11化した際に起きるエラー">java v11化した際に起きるエラー</a></li> <li><a href="#対応">対応</a><ul> <li><a href="#修正前">修正前</a></li> <li><a href="#修正後">修正後</a></li> </ul> </li> </ul> <div class="section"> <h3 id="java-v11化した際に起きるエラー">java v11化した際に起きるエラー</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>tree:database tree$ ./gradlew <span class="synSpecial">-b</span> jooq.gradle <span class="synSpecial">--stacktrace</span> clean gen <span class="synStatement">&gt;</span> Task :generateExampleJooqSchemaSource FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed <span class="synStatement">for</span> task <span class="synStatement">'</span><span class="synConstant">:generateExampleJooqSchemaSource</span><span class="synStatement">'</span>. <span class="synStatement">&gt;</span> javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath. - with linked exception: <span class="synStatement">[</span>java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory<span class="synStatement">]</span> * Try: Run with <span class="synSpecial">--info</span> or <span class="synSpecial">--debug</span> option to get more log output. Run with <span class="synSpecial">--scan</span> to get full insights. * Exception is: org.gradle.api.tasks.TaskExecutionException: Execution failed <span class="synStatement">for</span> task <span class="synStatement">'</span><span class="synConstant">:generateExampleJooqSchemaSource</span><span class="synStatement">'</span>. at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute<span class="synPreProc">(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">95</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute<span class="synPreProc">(</span>ResolveTaskOutputCachingStateExecuter.java:<span class="synConstant">91</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute<span class="synPreProc">(</span>ValidatingTaskExecuter.java:<span class="synConstant">57</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute<span class="synPreProc">(</span>SkipEmptySourceFilesTaskExecuter.java:<span class="synConstant">119</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ResolvePreviousStateExecuter.execute<span class="synPreProc">(</span>ResolvePreviousStateExecuter.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute<span class="synPreProc">(</span>CleanupStaleOutputsExecuter.java:<span class="synConstant">93</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute<span class="synPreProc">(</span>FinalizePropertiesTaskExecuter.java:<span class="synConstant">45</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute<span class="synPreProc">(</span>ResolveTaskArtifactStateTaskExecuter.java:<span class="synConstant">94</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute<span class="synPreProc">(</span>SkipTaskWithNoActionsExecuter.java:<span class="synConstant">56</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute<span class="synPreProc">(</span>SkipOnlyIfTaskExecuter.java:<span class="synConstant">55</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute<span class="synPreProc">(</span>CatchExceptionTaskExecuter.java:<span class="synConstant">36</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter<span class="synPreProc">$1.executeTask(</span>EventFiringTaskExecuter.java:<span class="synConstant">67</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter<span class="synPreProc">$1.call(</span>EventFiringTaskExecuter.java:<span class="synConstant">52</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter<span class="synPreProc">$1.call(</span>EventFiringTaskExecuter.java:<span class="synConstant">49</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor<span class="synPreProc">$CallableBuildOperationWorker.execute(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">315</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor<span class="synPreProc">$CallableBuildOperationWorker.execute(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">305</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute<span class="synPreProc">(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">175</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor.call<span class="synPreProc">(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">101</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call<span class="synPreProc">(</span>DelegatingBuildOperationExecutor.java:<span class="synConstant">36</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute<span class="synPreProc">(</span>EventFiringTaskExecuter.java:<span class="synConstant">49</span><span class="synPreProc">)</span> at org.gradle.execution.plan.LocalTaskNodeExecutor.execute<span class="synPreProc">(</span>LocalTaskNodeExecutor.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph<span class="synPreProc">$InvokeNodeExecutorsAction.execute(</span>DefaultTaskExecutionGraph.java:<span class="synConstant">355</span><span class="synPreProc">)</span> at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph<span class="synPreProc">$InvokeNodeExecutorsAction.execute(</span>DefaultTaskExecutionGraph.java:<span class="synConstant">343</span><span class="synPreProc">)</span> at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph<span class="synPreProc">$BuildOperationAwareExecutionAction.execute(</span>DefaultTaskExecutionGraph.java:<span class="synConstant">336</span><span class="synPreProc">)</span> at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph<span class="synPreProc">$BuildOperationAwareExecutionAction.execute(</span>DefaultTaskExecutionGraph.java:<span class="synConstant">322</span><span class="synPreProc">)</span> at org.gradle.execution.plan.DefaultPlanExecutor<span class="synPreProc">$ExecutorWorker$1.execute(</span>DefaultPlanExecutor.java:<span class="synConstant">134</span><span class="synPreProc">)</span> at org.gradle.execution.plan.DefaultPlanExecutor<span class="synPreProc">$ExecutorWorker$1.execute(</span>DefaultPlanExecutor.java:<span class="synConstant">129</span><span class="synPreProc">)</span> at org.gradle.execution.plan.DefaultPlanExecutor<span class="synPreProc">$ExecutorWorker.execute(</span>DefaultPlanExecutor.java:<span class="synConstant">202</span><span class="synPreProc">)</span> at org.gradle.execution.plan.DefaultPlanExecutor<span class="synPreProc">$ExecutorWorker.executeNextNode(</span>DefaultPlanExecutor.java:<span class="synConstant">193</span><span class="synPreProc">)</span> at org.gradle.execution.plan.DefaultPlanExecutor<span class="synPreProc">$ExecutorWorker.run(</span>DefaultPlanExecutor.java:<span class="synConstant">129</span><span class="synPreProc">)</span> at org.gradle.internal.concurrent.ExecutorPolicy<span class="synPreProc">$CatchAndRecordFailures.onExecute(</span>ExecutorPolicy.java:<span class="synConstant">63</span><span class="synPreProc">)</span> at org.gradle.internal.concurrent.ManagedExecutorImpl<span class="synPreProc">$1.run(</span>ManagedExecutorImpl.java:<span class="synConstant">46</span><span class="synPreProc">)</span> at org.gradle.internal.concurrent.ThreadFactoryImpl<span class="synPreProc">$ManagedThreadRunnable.run(</span>ThreadFactoryImpl.java:<span class="synConstant">55</span><span class="synPreProc">)</span> Caused by: org.gradle.internal.UncheckedException: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath. - with linked exception: <span class="synStatement">[</span>java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory<span class="synStatement">]</span> at org.gradle.internal.UncheckedException.throwAsUncheckedException<span class="synPreProc">(</span>UncheckedException.java:<span class="synConstant">67</span><span class="synPreProc">)</span> at org.gradle.internal.UncheckedException.throwAsUncheckedException<span class="synPreProc">(</span>UncheckedException.java:<span class="synConstant">41</span><span class="synPreProc">)</span> at org.gradle.internal.reflect.JavaMethod.invoke<span class="synPreProc">(</span>JavaMethod.java:<span class="synConstant">76</span><span class="synPreProc">)</span> at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute<span class="synPreProc">(</span>StandardTaskAction.java:<span class="synConstant">48</span><span class="synPreProc">)</span> at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute<span class="synPreProc">(</span>StandardTaskAction.java:<span class="synConstant">41</span><span class="synPreProc">)</span> at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute<span class="synPreProc">(</span>StandardTaskAction.java:<span class="synConstant">28</span><span class="synPreProc">)</span> at org.gradle.api.internal.AbstractTask<span class="synPreProc">$TaskActionWrapper.execute(</span>AbstractTask.java:<span class="synConstant">704</span><span class="synPreProc">)</span> at org.gradle.api.internal.AbstractTask<span class="synPreProc">$TaskActionWrapper.execute(</span>AbstractTask.java:<span class="synConstant">671</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter<span class="synPreProc">$2.run(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">284</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor<span class="synPreProc">$RunnableBuildOperationWorker.execute(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">301</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor<span class="synPreProc">$RunnableBuildOperationWorker.execute(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">293</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute<span class="synPreProc">(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">175</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DefaultBuildOperationExecutor.run<span class="synPreProc">(</span>DefaultBuildOperationExecutor.java:<span class="synConstant">91</span><span class="synPreProc">)</span> at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run<span class="synPreProc">(</span>DelegatingBuildOperationExecutor.java:<span class="synConstant">31</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction<span class="synPreProc">(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">273</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions<span class="synPreProc">(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">258</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.access<span class="synPreProc">$200(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">67</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter<span class="synPreProc">$TaskExecution.execute(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">145</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.ExecuteStep.execute<span class="synPreProc">(</span>ExecuteStep.java:<span class="synConstant">49</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CancelExecutionStep.execute<span class="synPreProc">(</span>CancelExecutionStep.java:<span class="synConstant">34</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.TimeoutStep.executeWithoutTimeout<span class="synPreProc">(</span>TimeoutStep.java:<span class="synConstant">69</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.TimeoutStep.execute<span class="synPreProc">(</span>TimeoutStep.java:<span class="synConstant">49</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CatchExceptionStep.execute<span class="synPreProc">(</span>CatchExceptionStep.java:<span class="synConstant">33</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CreateOutputsStep.execute<span class="synPreProc">(</span>CreateOutputsStep.java:<span class="synConstant">50</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute<span class="synPreProc">(</span>SnapshotOutputStep.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute<span class="synPreProc">(</span>SnapshotOutputStep.java:<span class="synConstant">29</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CacheStep.executeWithoutCache<span class="synPreProc">(</span>CacheStep.java:<span class="synConstant">134</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CacheStep.lambda<span class="synPreProc">$execute$3(</span>CacheStep.java:<span class="synConstant">83</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CacheStep.execute<span class="synPreProc">(</span>CacheStep.java:<span class="synConstant">82</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.CacheStep.execute<span class="synPreProc">(</span>CacheStep.java:<span class="synConstant">36</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.PrepareCachingStep.execute<span class="synPreProc">(</span>PrepareCachingStep.java:<span class="synConstant">33</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute<span class="synPreProc">(</span>StoreSnapshotsStep.java:<span class="synConstant">38</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute<span class="synPreProc">(</span>StoreSnapshotsStep.java:<span class="synConstant">23</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.executeBecause<span class="synPreProc">(</span>SkipUpToDateStep.java:<span class="synConstant">96</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.lambda<span class="synPreProc">$execute$0(</span>SkipUpToDateStep.java:<span class="synConstant">89</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute<span class="synPreProc">(</span>SkipUpToDateStep.java:<span class="synConstant">52</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute<span class="synPreProc">(</span>SkipUpToDateStep.java:<span class="synConstant">36</span><span class="synPreProc">)</span> at org.gradle.internal.execution.impl.DefaultWorkExecutor.execute<span class="synPreProc">(</span>DefaultWorkExecutor.java:<span class="synConstant">34</span><span class="synPreProc">)</span> at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute<span class="synPreProc">(</span>ExecuteActionsTaskExecuter.java:<span class="synConstant">91</span><span class="synPreProc">)</span> ... <span class="synConstant">32</span> more Caused by: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath. - with linked exception: <span class="synStatement">[</span>java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory<span class="synStatement">]</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0<span class="synPreProc">(</span>Native Method<span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke<span class="synPreProc">(</span>NativeMethodAccessorImpl.java:<span class="synConstant">62</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke<span class="synPreProc">(</span>DelegatingMethodAccessorImpl.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at nu.studer.gradle.jooq.JooqTask<span class="synPreProc">$1.writeConfiguration(</span>JooqTask.groovy:<span class="synConstant">125</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0<span class="synPreProc">(</span>Native Method<span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke<span class="synPreProc">(</span>NativeMethodAccessorImpl.java:<span class="synConstant">62</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke<span class="synPreProc">(</span>DelegatingMethodAccessorImpl.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at nu.studer.gradle.jooq.JooqTask<span class="synPreProc">$1.execute(</span>JooqTask.groovy:<span class="synConstant">118</span><span class="synPreProc">)</span> at nu.studer.gradle.jooq.JooqTask<span class="synPreProc">$1.execute(</span>JooqTask.groovy<span class="synPreProc">)</span> at org.gradle.api.internal.file.DefaultFileOperations.javaexec<span class="synPreProc">(</span>DefaultFileOperations.java:<span class="synConstant">226</span><span class="synPreProc">)</span> at org.gradle.api.internal.project.DefaultProject.javaexec<span class="synPreProc">(</span>DefaultProject.java:<span class="synConstant">1103</span><span class="synPreProc">)</span> at org.gradle.api.internal.ProcessOperations<span class="synPreProc">$javaexec.call(</span>Unknown Source<span class="synPreProc">)</span> at nu.studer.gradle.jooq.JooqTask.executeJooq<span class="synPreProc">(</span>JooqTask.groovy:<span class="synConstant">103</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0<span class="synPreProc">(</span>Native Method<span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke<span class="synPreProc">(</span>NativeMethodAccessorImpl.java:<span class="synConstant">62</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke<span class="synPreProc">(</span>DelegatingMethodAccessorImpl.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at nu.studer.gradle.jooq.JooqTask.generate<span class="synPreProc">(</span>JooqTask.groovy:<span class="synConstant">96</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0<span class="synPreProc">(</span>Native Method<span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke<span class="synPreProc">(</span>NativeMethodAccessorImpl.java:<span class="synConstant">62</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke<span class="synPreProc">(</span>DelegatingMethodAccessorImpl.java:<span class="synConstant">43</span><span class="synPreProc">)</span> at org.gradle.internal.reflect.JavaMethod.invoke<span class="synPreProc">(</span>JavaMethod.java:<span class="synConstant">73</span><span class="synPreProc">)</span> ... <span class="synConstant">68</span> more Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory ... <span class="synConstant">89</span> more * Get more <span class="synStatement">help</span> at https://help.gradle.org BUILD FAILED <span class="synStatement">in</span> 0s <span class="synConstant">2</span> actionable tasks: <span class="synConstant">2</span> executed </pre><p><br /> com.sun.xml.internal.bind.v2.ContextFactory が ClassNotFound とのことです。</p> </div> <div class="section"> <h3 id="対応">対応</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190128/20190128104240.png" alt="f:id:treeapps:20190128104240p:plain" title="f:id:treeapps:20190128104240p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>見にくいので一部抜粋で修正点を確認します。</p> <div class="section"> <h4 id="修正前">修正前</h4> <pre class="code lang-groovy" data-lang="groovy" data-unlink>buildscript { dependencies { classpath <span class="synConstant">&quot;nu.studer:gradle-jooq-plugin:3.0.2&quot;</span> } } jooq { dependencies { jooqRuntime <span class="synConstant">&quot;org.jooq:jooq-codegen:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;org.jooq:jooq-meta:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;org.jooq:jooq:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;mysql:mysql-connector-java:8.0.14&quot;</span> } } </pre> </div> <div class="section"> <h4 id="修正後">修正後</h4> <pre class="code lang-groovy" data-lang="groovy" data-unlink>buildscript { dependencies { classpath <span class="synConstant">&quot;nu.studer:gradle-jooq-plugin:3.0.2&quot;</span> <span class="synComment">// ↓追加</span> classpath <span class="synConstant">&quot;org.glassfish.jaxb:jaxb-core:2.3.0.1&quot;</span> classpath <span class="synConstant">&quot;org.glassfish.jaxb:jaxb-runtime:2.3.2&quot;</span> } } jooq { dependencies { jooqRuntime <span class="synConstant">&quot;org.jooq:jooq-codegen:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;org.jooq:jooq-meta:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;org.jooq:jooq:3.11.9&quot;</span> jooqRuntime <span class="synConstant">&quot;mysql:mysql-connector-java:8.0.14&quot;</span> <span class="synComment">// ↓追加</span> jooqRuntime <span class="synConstant">&quot;javax.xml.bind:jaxb-api:2.3.0&quot;</span> jooqRuntime <span class="synConstant">&quot;javax.activation:javax.activation-api:1.2.0&quot;</span> jooqRuntime <span class="synConstant">&quot;org.glassfish.jaxb:jaxb-core:2.3.0.1&quot;</span> jooqRuntime <span class="synConstant">&quot;org.glassfish.jaxb:jaxb-runtime:2.3.2&quot;</span> } } </pre><p>こんな感じに依存を追加すると、ターミナルから動くようになります。</p> </div> </div> treeapps jOOQ v3.11にアップデートした際にgradle-jooq-pluginで出るエラーの対応 hatenablog://entry/98012380843283097 2019-01-28T10:14:43+09:00 2019-01-28T10:49:58+09:00 jOOQのパッケージ構成が少し変わりますよ〜 <p>jOOQのパッケージ構成が少し変わりますよ〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190128/20190128100247.png" alt="f:id:treeapps:20190128100247p:plain" title="f:id:treeapps:20190128100247p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>先日gradleでjOOQの依存を以下のように更新しました。</p> <table> <tr> <th>依存名</th> <th>アップデート前</th> <th>アップデート後</th> </tr> <tr> <td>jOOQ</td> <td>v3.10.7</td> <td>v3.11.9</td> </tr> <tr> <td>gradle-jooq-plugin</td> <td>v2.0.11</td> <td>v3.0.2</td> </tr> </table><p>gradle-jooq-pluginがメジャーバージョンアップしてたので絶対動かないだろうなあと思ったら、案の定動きませんでした。</p><p>ちょっと修正しただけで動いたので、メモっておきます。</p> <div class="section"> <h3>jOOQをv3.11系にした際に起きるエラー</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>tree:database tree$ ./generate-jooq.sh <span class="synStatement">&gt;</span> Task :generateExampleJooqSchemaSource FAILED <span class="synConstant">1</span> <span class="synConstant">28</span>, <span class="synConstant">2019</span> <span class="synConstant">9</span>:<span class="synConstant">59</span>:<span class="synConstant">52</span> 午前 org.jooq.tools.JooqLogger info 情報: Initialising properties : /Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/database/build/tmp/generateExampleJooqSchemaSource/config.xml <span class="synConstant">1</span> <span class="synConstant">28</span>, <span class="synConstant">2019</span> <span class="synConstant">9</span>:<span class="synConstant">59</span>:<span class="synConstant">53</span> 午前 org.jooq.tools.JooqLogger warn 警告: Type not found : Your configured org.jooq.util <span class="synStatement">type</span> was not found. Do note that <span class="synError">in</span> jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are: - org.jooq.meta - org.jooq.meta.extensions - org.jooq.codegen - org.jooq.codegen.maven See https://github.com/jOOQ/jOOQ/issues/<span class="synConstant">7419</span> <span class="synStatement">for</span> details <span class="synConstant">1</span> <span class="synConstant">28</span>, <span class="synConstant">2019</span> <span class="synConstant">9</span>:<span class="synConstant">59</span>:<span class="synConstant">53</span> 午前 org.jooq.tools.JooqLogger error 重大: Cannot <span class="synStatement">read</span> /Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/database/build/tmp/generateExampleJooqSchemaSource/config.xml. Error : Your configured org.jooq.util <span class="synStatement">type</span> was not found. Do note that <span class="synStatement">in</span> jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are: - org.jooq.meta - org.jooq.meta.extensions - org.jooq.codegen - org.jooq.codegen.maven See https://github.com/jOOQ/jOOQ/issues/<span class="synConstant">7419</span> <span class="synStatement">for</span> details java.lang.ClassNotFoundException: Your configured org.jooq.util <span class="synStatement">type</span> was not found. Do note that <span class="synStatement">in</span> jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are: - org.jooq.meta - org.jooq.meta.extensions - org.jooq.codegen - org.jooq.codegen.maven See https://github.com/jOOQ/jOOQ/issues/<span class="synConstant">7419</span> <span class="synStatement">for</span> details at org.jooq.codegen.GenerationTool.loadClass<span class="synPreProc">(</span>GenerationTool.java:<span class="synConstant">857</span><span class="synPreProc">)</span> at org.jooq.codegen.GenerationTool.run<span class="synPreProc">(</span>GenerationTool.java:<span class="synConstant">331</span><span class="synPreProc">)</span> at org.jooq.codegen.GenerationTool.generate<span class="synPreProc">(</span>GenerationTool.java:<span class="synConstant">222</span><span class="synPreProc">)</span> at org.jooq.codegen.GenerationTool.main<span class="synPreProc">(</span>GenerationTool.java:<span class="synConstant">194</span><span class="synPreProc">)</span> Caused by: java.lang.ClassNotFoundException: org.jooq.util.DefaultGenerator at java.net.URLClassLoader.findClass<span class="synPreProc">(</span>URLClassLoader.java:<span class="synConstant">382</span><span class="synPreProc">)</span> at java.lang.ClassLoader.loadClass<span class="synPreProc">(</span>ClassLoader.java:<span class="synConstant">424</span><span class="synPreProc">)</span> at sun.misc.Launcher<span class="synPreProc">$AppClassLoader.loadClass(</span>Launcher.java:<span class="synConstant">349</span><span class="synPreProc">)</span> at java.lang.ClassLoader.loadClass<span class="synPreProc">(</span>ClassLoader.java:<span class="synConstant">357</span><span class="synPreProc">)</span> at org.jooq.codegen.GenerationTool.loadClass<span class="synPreProc">(</span>GenerationTool.java:<span class="synConstant">821</span><span class="synPreProc">)</span> ... <span class="synConstant">3</span> more </pre><p>エラーに丁寧に何が起きているか書いてますね。これは親切です。</p> </div> <div class="section"> <h3>対応</h3> <table> <tr> <th>変更前パッケージ</th> <th>変更後パッケージ</th> </tr> <tr> <td>org.jooq.util.DefaultGenerator</td> <td>org.jooq.codegen.DefaultGenerator</td> </tr> <tr> <td>org.jooq.util.DefaultGeneratorStrategy</td> <td>org.jooq.codegen.DefaultGeneratorStrategy</td> </tr> <tr> <td>org.jooq.util.mysql.MySQLDatabase</td> <td>org.jooq.meta.mysql.MySQLDatabase</td> </tr> </table><p>上記のようにパッケージがutilからcodegen・metaと細分化されました。org.jooq.meta.mysql.MySQLDatabase は postgres等に適宜変更しましょう。</p><p>この修正だけで基本的に動かくかと思います。</p> </div> treeapps spring-cloud-starter-awsがローカル環境でエラーになる場合の最低限の対応 hatenablog://entry/98012380840718812 2019-01-26T23:26:05+09:00 2019-01-27T15:30:28+09:00 全然解らない。私は雰囲気でspring-cloud-starter-awsを使っている・・・・ <p>全然解らない。私は雰囲気でspring-cloud-starter-awsを使っている・・・・</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029033317.png" alt="f:id:treeapps:20171029033317p:plain" title="f:id:treeapps:20171029033317p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>javaやkotlinで<span class="feature1">org.springframework.cloud:spring-cloud-starter-aws</span>を使う事がまあまああったりします。これを使うとawsのリージョンを自動取得してくれたり、awsフレンドリーな状態でawsのサービスを扱う事ができるようになります。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#エラーを解消した最終的なローカル環境向けのapplicationyml設定">エラーを解消した最終的なローカル環境向けのapplication.yml設定</a></li> <li><a href="#EC2からStack-Nameを自動取得できないよエラー">EC2からStack Nameを自動取得できないよエラー</a><ul> <li><a href="#No-valid-instance-id-defined">No valid instance id defined</a></li> <li><a href="#EC2の情報を自動収集させないようにする">EC2の情報を自動収集させないようにする</a><ul> <li><a href="#applicationymlの設定">application.ymlの設定</a></li> </ul> </li> </ul> </li> <li><a href="#EC2からリージョン名が自動収集できないよエラー">EC2からリージョン名が自動収集できないよエラー</a><ul> <li><a href="#There-is-no-EC2-meta-data-available">There is no EC2 meta data available</a></li> <li><a href="#EC2メタデータを自動収集させないようにする">EC2メタデータを自動収集させないようにする</a><ul> <li><a href="#applicationymlの設定-1">application.ymlの設定</a></li> </ul> </li> </ul> </li> <li><a href="#AmazonSESは東京リージョンに対応してないんだが">AmazonSESは東京リージョンに対応してないんだが</a><ul> <li><a href="#AmazonSESのリージョンをソースコード側で変更する">AmazonSESのリージョンをソースコード側で変更する</a></li> </ul> </li> <li><a href="#おまけローカル開発時にML宛にメールを送信したくないんだが">おまけ:ローカル開発時にML宛にメールを送信したくないんだが!</a><ul> <li><a href="#docker-composeyml">docker-compose.yml</a></li> <li><a href="#applicationyml">application.yml</a></li> <li><a href="#実際にMailCatcherにメールを送信するGIFアニメ">実際にMailCatcherにメールを送信するGIFアニメ</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="環境">環境</h3> <ul> <li>java or kotlin</li> <li>Spring boot(v1系、v2系のどらでも起きます)</li> <li>build.gradleやpom.xmlにorg.springframework.cloud:spring-cloud-starter-awsを設定している</li> <li>application.ymlにはspring-cloud-starter-awsの設定を何も記述していない</li> </ul> </div> <div class="section"> <h3 id="エラーを解消した最終的なローカル環境向けのapplicationyml設定">エラーを解消した最終的なローカル環境向けのapplication.yml設定</h3> <p>いきなり答えを記述すると、EC2以外の環境(ローカル環境等)の場合は、以下になります。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">cloud</span><span class="synSpecial">:</span> <span class="synIdentifier">aws</span><span class="synSpecial">:</span> <span class="synIdentifier">stack</span><span class="synSpecial">:</span> <span class="synComment"> # CloudFormationのstack名を自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">region</span><span class="synSpecial">:</span> <span class="synComment"> # EC2のmetadataを自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">static</span><span class="synSpecial">:</span> ap-northeast-1 </pre><p><br /> では、どんなエラーが起きて、どう解決していくかを見ていきます。</p> </div> <div class="section"> <h3 id="EC2からStack-Nameを自動取得できないよエラー">EC2からStack Nameを自動取得できないよエラー</h3> <p>spring-cloud-starter-awsをapplication.ymlの設定無しにそのまま使うと、ローカル環境で以下のエラーが発生します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synConstant">2019-01-25</span> <span class="synConstant">23</span>:<span class="synConstant">45</span>:<span class="synConstant">29</span>,<span class="synConstant">397</span> ERROR <span class="synStatement">[</span>main<span class="synStatement">]</span> <span class="synStatement">[</span>org.springframework.boot.SpringApplication:<span class="synConstant">858</span><span class="synStatement">]</span> Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">org.springframework.cloud.aws.core.env.ResourceIdResolver.BEAN_NAME</span><span class="synStatement">'</span>: Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">stackResourceRegistryFactoryBean</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class<span class="synStatement">]</span>: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate <span class="synStatement">[</span>org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean<span class="synStatement">]</span>: Factory method <span class="synStatement">'</span><span class="synConstant">stackResourceRegistryFactoryBean</span><span class="synStatement">'</span> threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">1745</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">576</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">498</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.lambda<span class="synPreProc">$doGetBean$0(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">320</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton<span class="synPreProc">(</span><span class="synSpecial">DefaultSingletonBeanRegistry.java:</span><span class="synConstant">222</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean<span class="synPreProc">(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">318</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.getBean<span class="synPreProc">(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">199</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons<span class="synPreProc">(</span><span class="synSpecial">DefaultListableBeanFactory.java:</span><span class="synConstant">846</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization<span class="synPreProc">(</span><span class="synSpecial">AbstractApplicationContext.java:</span><span class="synConstant">863</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.refresh<span class="synPreProc">(</span><span class="synSpecial">AbstractApplicationContext.java:</span><span class="synConstant">546</span><span class="synPreProc">)</span> at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh<span class="synPreProc">(</span><span class="synSpecial">ReactiveWebServerApplicationContext.java:</span><span class="synConstant">67</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refresh<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">775</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refreshContext<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">397</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">316</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">1260</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">1248</span><span class="synPreProc">)</span> at com.example.admin.AdminApplicationKt.main<span class="synPreProc">(</span><span class="synSpecial">AdminApplication.kt:</span><span class="synConstant">21</span><span class="synPreProc">)</span> Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">stackResourceRegistryFactoryBean</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class<span class="synStatement">]</span>: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate <span class="synStatement">[</span>org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean<span class="synStatement">]</span>: Factory method <span class="synStatement">'</span><span class="synConstant">stackResourceRegistryFactoryBean</span><span class="synStatement">'</span> threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined at org.springframework.beans.factory.support.ConstructorResolver.instantiate<span class="synPreProc">(</span><span class="synSpecial">ConstructorResolver.java:</span><span class="synConstant">627</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod<span class="synPreProc">(</span><span class="synSpecial">ConstructorResolver.java:</span><span class="synConstant">607</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">1288</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">1127</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">538</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">498</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.lambda<span class="synPreProc">$doGetBean$0(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">320</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton<span class="synPreProc">(</span><span class="synSpecial">DefaultSingletonBeanRegistry.java:</span><span class="synConstant">222</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean<span class="synPreProc">(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">318</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.getBean<span class="synPreProc">(</span><span class="synSpecial">AbstractBeanFactory.java:</span><span class="synConstant">199</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType<span class="synPreProc">(</span><span class="synSpecial">DefaultListableBeanFactory.java:</span><span class="synConstant">602</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType<span class="synPreProc">(</span><span class="synSpecial">DefaultListableBeanFactory.java:</span><span class="synConstant">590</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.StackResourceRegistryDetectingResourceIdResolver.findSingleOptionalStackResourceRegistry<span class="synPreProc">(</span><span class="synSpecial">StackResourceRegistryDetectingResourceIdResolver.java:</span><span class="synConstant">81</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.StackResourceRegistryDetectingResourceIdResolver.afterPropertiesSet<span class="synPreProc">(</span><span class="synSpecial">StackResourceRegistryDetectingResourceIdResolver.java:</span><span class="synConstant">77</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">1804</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean<span class="synPreProc">(</span><span class="synSpecial">AbstractAutowireCapableBeanFactory.java:</span><span class="synConstant">1741</span><span class="synPreProc">)</span> ... <span class="synConstant">16</span> common frames omitted Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate <span class="synStatement">[</span>org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean<span class="synStatement">]</span>: Factory method <span class="synStatement">'</span><span class="synConstant">stackResourceRegistryFactoryBean</span><span class="synStatement">'</span> threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate<span class="synPreProc">(</span><span class="synSpecial">SimpleInstantiationStrategy.java:</span><span class="synConstant">185</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.instantiate<span class="synPreProc">(</span><span class="synSpecial">ConstructorResolver.java:</span><span class="synConstant">622</span><span class="synPreProc">)</span> ... <span class="synConstant">31</span> common frames omitted Caused by: java.lang.IllegalArgumentException: No valid instance id defined at org.springframework.util.Assert.notNull<span class="synPreProc">(</span><span class="synSpecial">Assert.java:</span><span class="synConstant">198</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.autoDetectStackName<span class="synPreProc">(</span><span class="synSpecial">AutoDetectingStackNameProvider.java:</span><span class="synConstant">75</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.afterPropertiesSet<span class="synPreProc">(</span><span class="synSpecial">AutoDetectingStackNameProvider.java:</span><span class="synConstant">62</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.<span class="synStatement">&lt;</span>init<span class="synStatement">&gt;</span><span class="synPreProc">(</span><span class="synSpecial">AutoDetectingStackNameProvider.java:</span><span class="synConstant">52</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.<span class="synStatement">&lt;</span>init<span class="synStatement">&gt;</span><span class="synPreProc">(</span><span class="synSpecial">AutoDetectingStackNameProvider.java:</span><span class="synConstant">56</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration.stackResourceRegistryFactoryBean<span class="synPreProc">(</span><span class="synSpecial">ContextStackAutoConfiguration.java:</span><span class="synConstant">71</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration<span class="synPreProc">$$</span>EnhancerBySpringCGLIB<span class="synPreProc">$$</span>be0ef810.CGLIB<span class="synPreProc">$stackResourceRegistryFactoryBean$0(</span><span class="synStatement">&lt;</span><span class="synSpecial">generated</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration<span class="synPreProc">$$</span>EnhancerBySpringCGLIB<span class="synPreProc">$$</span>be0ef810<span class="synPreProc">$$</span>FastClassBySpringCGLIB<span class="synPreProc">$$</span>d3106a9b.invoke<span class="synPreProc">(</span><span class="synStatement">&lt;</span><span class="synSpecial">generated</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at org.springframework.cglib.proxy.MethodProxy.invokeSuper<span class="synPreProc">(</span><span class="synSpecial">MethodProxy.java:</span><span class="synConstant">244</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassEnhancer<span class="synPreProc">$BeanMethodInterceptor.intercept(</span><span class="synSpecial">ConfigurationClassEnhancer.java:</span><span class="synConstant">363</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration<span class="synPreProc">$$</span>EnhancerBySpringCGLIB<span class="synPreProc">$$</span>be0ef810.stackResourceRegistryFactoryBean<span class="synPreProc">(</span><span class="synStatement">&lt;</span><span class="synSpecial">generated</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0<span class="synPreProc">(</span><span class="synSpecial">Native Method</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke<span class="synPreProc">(</span><span class="synSpecial">NativeMethodAccessorImpl.java:</span><span class="synConstant">62</span><span class="synPreProc">)</span> at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke<span class="synPreProc">(</span><span class="synSpecial">DelegatingMethodAccessorImpl.java:</span><span class="synConstant">43</span><span class="synPreProc">)</span> at java.base/java.lang.reflect.Method.invoke<span class="synPreProc">(</span><span class="synSpecial">Method.java:</span><span class="synConstant">566</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate<span class="synPreProc">(</span><span class="synSpecial">SimpleInstantiationStrategy.java:</span><span class="synConstant">154</span><span class="synPreProc">)</span> ... <span class="synConstant">32</span> common frames omitted </pre><p>StackTraceの中の「<span class="feature3">No valid instance id defined</span>」「<span class="feature3">stack.config</span>」辺りを見ると何が起きているか解りますね。</p> <div class="section"> <h4 id="No-valid-instance-id-defined">No valid instance id defined</h4> <p>StackTraceからコードを追うと、以下のAssertに引っかかった事が解ります。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synComment">/**</span> <span class="synComment"> *</span><span class="synSpecial"> Represents a stack name provider that automatically detects the current stack name based on the amazon elastic cloud</span> <span class="synComment"> *</span><span class="synSpecial"> environment.</span> <span class="synComment"> */</span> <span class="synType">public</span> <span class="synType">class</span> AutoDetectingStackNameProvider <span class="synType">implements</span> StackNameProvider, InitializingBean { <span class="synType">private</span> <span class="synType">final</span> AmazonCloudFormation amazonCloudFormationClient; <span class="synType">private</span> <span class="synType">final</span> AmazonEC2 amazonEc2Client; <span class="synType">private</span> <span class="synType">final</span> InstanceIdProvider instanceIdProvider; ・・・略・・・ <span class="synType">private</span> String autoDetectStackName(String instanceId) { Assert.notNull(instanceId, <span class="synConstant">&quot;No valid instance id defined&quot;</span>); ・・・略・・・ <span class="synStatement">return</span> <span class="synConstant">null</span>; } </pre><p>順に上から見ていくと、InitializingBeanを継承していて、これはSpring起動時の初期化処理をするもので、更にクラス名が「Auto」と自動で取得をしにいこうとするものであると解ります。<br /> 続いてフィールドに「amazonCloudFormationClient」「amazonEc2Client」「instanceIdProvider」があります。この変数名から連想される事は、CloudFormationのスタックで使うEC2のインスタンスIDを自動で取得しようとするクラスである、と解ります。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.spring.io%2Fspring-cloud-aws%2Fspring-cloud-aws.html%23_automatic_cloudformation_configuration" title="Spring Cloud AWS" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_automatic_cloudformation_configuration">cloud.spring.io</a></cite></p> <blockquote> <p>If the application runs inside a stack (because the underlying EC2 instance has been bootstrapped within the stack), then Spring Cloud AWS will automatically detect the stack and resolve all resources from the stack. Application developers can use all the logical names from the stack template to interact with the services. In the example below, the database resource is configured using a CloudFormation template, defining a logical name for the database instance.</p> </blockquote> <p>いろいろ書いてますが、要は環境がどこであれ、Spring起動時にEC2の情報を自動で収集するからな〜、と言っています。ではここで「<span class="feature3">ローカル環境はEC2じゃないからEC2のインスタンスIDなんて無いんだが</span>」という疑問にぶち当たり、案の定インスタンスIDを取得しようとして100%エラーが発生するわけです。</p><p>ここで重要なのは、Stack、つまり「CloudFormationが」という部分です。</p><p>という事は、<span class="feature1">Spring boot(spring-cloud-starter-aws)のCloudFormationの設定で、EC2情報を自動収集しないようにすれば解決しそう</span>です。</p> </div> <div class="section"> <h4 id="EC2の情報を自動収集させないようにする">EC2の情報を自動収集させないようにする</h4> <p><a href="https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_cloudformation_configuration_in_spring_boot">https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_cloudformation_configuration_in_spring_boot</a></p> <table> <tr> <th>property</th> <th>example</th> <th>description</th> </tr> <tr> <td>cloud.aws.stack.auto</td> <td>true</td> <td>Enables the automatic stack name detection for the application.</td> </tr> </table><p>Spring boot向けの設定に「cloud.aws.stack.auto」が有って初期値は「true」で、自動的にスタック名を収集する設定との事です。つまり<span class="feature1">これをfalseにすれば自動収集が止まる</span>わけです。</p> <div class="section"> <h5 id="applicationymlの設定">application.ymlの設定</h5> <p><figure class="figure-image figure-image-fotolife" title="EC2でない環境でCloudFormationのStack名を収集しないapplication.ymlの設定の仕方"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190126/20190126201739.png" alt="f:id:treeapps:20190126201739p:plain" title="f:id:treeapps:20190126201739p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>EC2でない環境でCloudFormationのStack名を収集しないapplication.ymlの設定の仕方</figcaption></figure></p><p>EC2でない環境のapplication.ymlが↑この設定にすれば自動収集は止まり、エラーにならなくなります。↑の画像では黄色く警告が出ており、コード補完もできないですが、ちゃんと設定自体は存在して有効になるのでご安心下さい。</p><br /> <p><span class="feature3">しかし、これで終わりではありません・・・・</span><br /> </p> </div> </div> </div> <div class="section"> <h3 id="EC2からリージョン名が自動収集できないよエラー">EC2からリージョン名が自動収集できないよエラー</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">cloud</span><span class="synSpecial">:</span> <span class="synIdentifier">aws</span><span class="synSpecial">:</span> <span class="synIdentifier">stack</span><span class="synSpecial">:</span> <span class="synComment"> # CloudFormationのstack名を自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> </pre><p>この設定でSpring bootを起動すると、今度は以下のエラーが起きます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synConstant">2019-01-26</span> <span class="synConstant">20</span>:<span class="synConstant">20</span>:<span class="synConstant">11</span>,<span class="synConstant">455</span> ERROR <span class="synStatement">[</span>main<span class="synStatement">]</span> <span class="synStatement">[</span>org.springframework.boot.SpringApplication:<span class="synConstant">858</span><span class="synStatement">]</span> Application run failed org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">sendMailService</span><span class="synStatement">'</span> defined <span class="synError">in</span> file <span class="synStatement">[</span>/Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/base/out/production/classes/com/example/base/service/SendMailService.class<span class="synStatement">]</span>: Unsatisfied dependency expressed through constructor parameter <span class="synConstant">0</span>; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">javaMailSender</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class<span class="synStatement">]</span>: Unsatisfied dependency expressed through method <span class="synStatement">'</span><span class="synConstant">javaMailSender</span><span class="synStatement">'</span> parameter <span class="synConstant">0</span>; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">amazonSimpleEmailService</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class<span class="synStatement">]</span>: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running <span class="synError">in</span> the EC2 environment. Region detection is only possible <span class="synStatement">if </span>the application is running on a EC2 instance at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">769</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">218</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1308</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1154</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">538</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">498</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.lambda<span class="synPreProc">$doGetBean$0(</span>AbstractBeanFactory.java:<span class="synConstant">320</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton<span class="synPreProc">(</span>DefaultSingletonBeanRegistry.java:<span class="synConstant">222</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">318</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.getBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">199</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons<span class="synPreProc">(</span>DefaultListableBeanFactory.java:<span class="synConstant">846</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization<span class="synPreProc">(</span>AbstractApplicationContext.java:<span class="synConstant">863</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.refresh<span class="synPreProc">(</span>AbstractApplicationContext.java:<span class="synConstant">546</span><span class="synPreProc">)</span> at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh<span class="synPreProc">(</span>ReactiveWebServerApplicationContext.java:<span class="synConstant">67</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refresh<span class="synPreProc">(</span>SpringApplication.java:<span class="synConstant">775</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refreshContext<span class="synPreProc">(</span>SpringApplication.java:<span class="synConstant">397</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span>SpringApplication.java:<span class="synConstant">316</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span>SpringApplication.java:<span class="synConstant">1260</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span>SpringApplication.java:<span class="synConstant">1248</span><span class="synPreProc">)</span> at com.example.admin.AdminApplicationKt.main<span class="synPreProc">(</span>AdminApplication.kt:<span class="synConstant">21</span><span class="synPreProc">)</span> Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">javaMailSender</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class<span class="synStatement">]</span>: Unsatisfied dependency expressed through method <span class="synStatement">'</span><span class="synConstant">javaMailSender</span><span class="synStatement">'</span> parameter <span class="synConstant">0</span>; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">amazonSimpleEmailService</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class<span class="synStatement">]</span>: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running <span class="synError">in</span> the EC2 environment. Region detection is only possible <span class="synStatement">if </span>the application is running on a EC2 instance at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">769</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">509</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1288</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1127</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">538</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">498</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.lambda<span class="synPreProc">$doGetBean$0(</span>AbstractBeanFactory.java:<span class="synConstant">320</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton<span class="synPreProc">(</span>DefaultSingletonBeanRegistry.java:<span class="synConstant">222</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">318</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.getBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">199</span><span class="synPreProc">)</span> at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate<span class="synPreProc">(</span>DependencyDescriptor.java:<span class="synConstant">277</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency<span class="synPreProc">(</span>DefaultListableBeanFactory.java:<span class="synConstant">1244</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency<span class="synPreProc">(</span>DefaultListableBeanFactory.java:<span class="synConstant">1164</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">857</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">760</span><span class="synPreProc">)</span> ... <span class="synConstant">19</span> common frames omitted Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name <span class="synStatement">'</span><span class="synConstant">amazonSimpleEmailService</span><span class="synStatement">'</span> defined <span class="synError">in</span> class path resource <span class="synStatement">[</span>org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class<span class="synStatement">]</span>: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running <span class="synError">in</span> the EC2 environment. Region detection is only possible <span class="synStatement">if </span>the application is running on a EC2 instance at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1745</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">576</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">498</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.lambda<span class="synPreProc">$doGetBean$0(</span>AbstractBeanFactory.java:<span class="synConstant">320</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton<span class="synPreProc">(</span>DefaultSingletonBeanRegistry.java:<span class="synConstant">222</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">318</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractBeanFactory.getBean<span class="synPreProc">(</span>AbstractBeanFactory.java:<span class="synConstant">199</span><span class="synPreProc">)</span> at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate<span class="synPreProc">(</span>DependencyDescriptor.java:<span class="synConstant">277</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency<span class="synPreProc">(</span>DefaultListableBeanFactory.java:<span class="synConstant">1244</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency<span class="synPreProc">(</span>DefaultListableBeanFactory.java:<span class="synConstant">1164</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">857</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray<span class="synPreProc">(</span>ConstructorResolver.java:<span class="synConstant">760</span><span class="synPreProc">)</span> ... <span class="synConstant">33</span> common frames omitted Caused by: java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running <span class="synError">in</span> the EC2 environment. Region detection is only possible <span class="synStatement">if </span>the application is running on a EC2 instance at org.springframework.util.Assert.state<span class="synPreProc">(</span>Assert.java:<span class="synConstant">73</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.region.Ec2MetadataRegionProvider.getRegion<span class="synPreProc">(</span>Ec2MetadataRegionProvider.java:<span class="synConstant">39</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance<span class="synPreProc">(</span>AmazonWebserviceClientFactoryBean.java:<span class="synConstant">92</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance<span class="synPreProc">(</span>AmazonWebserviceClientFactoryBean.java:<span class="synConstant">44</span><span class="synPreProc">)</span> at org.springframework.beans.factory.config.AbstractFactoryBean.afterPropertiesSet<span class="synPreProc">(</span>AbstractFactoryBean.java:<span class="synConstant">142</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1804</span><span class="synPreProc">)</span> at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean<span class="synPreProc">(</span>AbstractAutowireCapableBeanFactory.java:<span class="synConstant">1741</span><span class="synPreProc">)</span> ... <span class="synConstant">44</span> common frames omitted </pre> <div class="section"> <h4 id="There-is-no-EC2-meta-data-available">There is no EC2 meta data available</h4> <p>前述同様、対象コードを見てみます。</p> <pre class="code lang-java" data-lang="java" data-unlink><span class="synComment">/**</span> <span class="synComment"> *</span><span class="synSpecial"> {@link org.springframework.cloud.aws.core.region.RegionProvider} implementation that dynamically retrieves the</span> <span class="synComment"> *</span><span class="synSpecial"> region with the EC2 meta-data.</span><span class="synComment"> This implementation allows application to run against their region without any</span> <span class="synComment"> * further configuration.</span> <span class="synComment"> */</span> <span class="synType">public</span> <span class="synType">class</span> Ec2MetadataRegionProvider <span class="synType">implements</span> RegionProvider { <span class="synPreProc">@Override</span> <span class="synType">public</span> Region getRegion() { Region currentRegion = getCurrentRegion(); Assert.state(currentRegion != <span class="synConstant">null</span>, <span class="synConstant">&quot;There is no EC2 meta data available, because the application is not running &quot;</span> + <span class="synConstant">&quot;in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance&quot;</span>); <span class="synStatement">return</span> currentRegion; } <span class="synType">protected</span> Region getCurrentRegion() { <span class="synStatement">try</span> { InstanceInfo instanceInfo = EC2MetadataUtils.getInstanceInfo(); <span class="synStatement">return</span> instanceInfo != <span class="synConstant">null</span> &amp;&amp; instanceInfo.getRegion() != <span class="synConstant">null</span> ? RegionUtils.getRegion(instanceInfo.getRegion()) : <span class="synConstant">null</span>; } <span class="synStatement">catch</span> (AmazonClientException e) { <span class="synStatement">return</span> <span class="synConstant">null</span>; } } } </pre><p><span class="feature3">EC2のメタデータからインスタンス情報(Region等)を取得しようとする</span>ものですね。当然ローカル環境はEC2でないので、EC2インスタンス情報を取得しようとして100%エラーになるわけです。</p><p>という事は、<span class="feature1">Spring boot(spring-cloud-starter-aws)のリージョン設定で、EC2情報を自動収集しないようにすれば解決しそう</span>です。</p> </div> <div class="section"> <h4 id="EC2メタデータを自動収集させないようにする">EC2メタデータを自動収集させないようにする</h4> <p><a href="https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_region">https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_region</a></p> <table> <tr> <th>property</th> <th>example</th> <th>description</th> </tr> <tr> <td>cloud.aws.region.auto</td> <td>true</td> <td>Enables automatic region detection based on the EC2 meta data service</td> </tr> <tr> <td>cloud.aws.region.static</td> <td>eu-west-1</td> <td>Configures a static region for the application. Possible regions are (currently) us-east-1, us-west-1, us-west-2, eu-west-1, eu-central-1, ap-southeast-1, ap-southeast-1, ap-northeast-1, sa-east-1, cn-north-1 and any custom region configured with own region meta data</td> </tr> </table><p>これです。どうやら初期値は自動収集になっているようです。ではapplication.ymlを以下のように設定して起動します。</p> <div class="section"> <h5 id="applicationymlの設定-1">application.ymlの設定</h5> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">cloud</span><span class="synSpecial">:</span> <span class="synIdentifier">aws</span><span class="synSpecial">:</span> <span class="synIdentifier">stack</span><span class="synSpecial">:</span> <span class="synComment"> # CloudFormationのstack名を自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">region</span><span class="synSpecial">:</span> <span class="synComment"> # EC2のmetadataを自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> </pre><p>この設定に変更して再起動すると、今度は以下のエラーが出ます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synConstant">2019-01-26</span> <span class="synConstant">20</span>:<span class="synConstant">36</span>:<span class="synConstant">46</span>,<span class="synConstant">619</span> ERROR <span class="synStatement">[</span>main<span class="synStatement">]</span> <span class="synStatement">[</span>org.springframework.boot.SpringApplication:<span class="synConstant">858</span><span class="synStatement">]</span> Application run failed java.lang.IllegalArgumentException: Region must be manually configured or autoDetect enabled at org.springframework.cloud.aws.context.config.support.ContextConfigurationUtils.registerRegionProvider<span class="synPreProc">(</span><span class="synSpecial">ContextConfigurationUtils.java:</span><span class="synConstant">65</span><span class="synPreProc">)</span> at org.springframework.cloud.aws.autoconfigure.context.ContextRegionProviderAutoConfiguration<span class="synPreProc">$Registrar.registerBeanDefinitions(</span><span class="synSpecial">ContextRegionProviderAutoConfiguration.java:</span><span class="synConstant">72</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda<span class="synPreProc">$loadBeanDefinitionsFromRegistrars$1(</span><span class="synSpecial">ConfigurationClassBeanDefinitionReader.java:</span><span class="synConstant">364</span><span class="synPreProc">)</span> at java.base/java.util.LinkedHashMap.forEach<span class="synPreProc">(</span><span class="synSpecial">LinkedHashMap.java:</span><span class="synConstant">684</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars<span class="synPreProc">(</span><span class="synSpecial">ConfigurationClassBeanDefinitionReader.java:</span><span class="synConstant">363</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass<span class="synPreProc">(</span><span class="synSpecial">ConfigurationClassBeanDefinitionReader.java:</span><span class="synConstant">145</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions<span class="synPreProc">(</span><span class="synSpecial">ConfigurationClassBeanDefinitionReader.java:</span><span class="synConstant">117</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions<span class="synPreProc">(</span><span class="synSpecial">ConfigurationClassPostProcessor.java:</span><span class="synConstant">327</span><span class="synPreProc">)</span> at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry<span class="synPreProc">(</span><span class="synSpecial">ConfigurationClassPostProcessor.java:</span><span class="synConstant">232</span><span class="synPreProc">)</span> at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors<span class="synPreProc">(</span><span class="synSpecial">PostProcessorRegistrationDelegate.java:</span><span class="synConstant">275</span><span class="synPreProc">)</span> at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors<span class="synPreProc">(</span><span class="synSpecial">PostProcessorRegistrationDelegate.java:</span><span class="synConstant">95</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors<span class="synPreProc">(</span><span class="synSpecial">AbstractApplicationContext.java:</span><span class="synConstant">691</span><span class="synPreProc">)</span> at org.springframework.context.support.AbstractApplicationContext.refresh<span class="synPreProc">(</span><span class="synSpecial">AbstractApplicationContext.java:</span><span class="synConstant">528</span><span class="synPreProc">)</span> at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh<span class="synPreProc">(</span><span class="synSpecial">ReactiveWebServerApplicationContext.java:</span><span class="synConstant">67</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refresh<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">775</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.refreshContext<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">397</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">316</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">1260</span><span class="synPreProc">)</span> at org.springframework.boot.SpringApplication.run<span class="synPreProc">(</span><span class="synSpecial">SpringApplication.java:</span><span class="synConstant">1248</span><span class="synPreProc">)</span> at com.example.admin.AdminApplicationKt.main<span class="synPreProc">(</span><span class="synSpecial">AdminApplication.kt:</span><span class="synConstant">21</span><span class="synPreProc">)</span> </pre><p>Region must be manually configured or autoDetect enabled(cloud.aws.region.staticで設定するか、cloud.aws.region.autoで自動設定しろよな)というエラーです。</p><p>両パラメータには相関があって、自動収集を停止するならば<span class="feature3">cloud.aws.region.staticを設定しないといけない</span>のです。「static」は「<span class="feature1">自動収集しないで静的(決め打ち)で設定する</span>」という意味合いですね。</p><p>ローカル環境なので、以下のようにして、自動収集しないで決め打ちにします。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">cloud</span><span class="synSpecial">:</span> <span class="synIdentifier">aws</span><span class="synSpecial">:</span> <span class="synIdentifier">stack</span><span class="synSpecial">:</span> <span class="synComment"> # CloudFormationのstack名を自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">region</span><span class="synSpecial">:</span> <span class="synComment"> # EC2のmetadataを自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">static</span><span class="synSpecial">:</span> ap-northeast-1 </pre><p><span class="feature1">これで無事起動します!</span></p><p>EC2以外の環境では上記のように自動収集をやめる設定とし、EC2環境の場合は自動収集設定でもよさそうですね。</p> </div> </div> </div> <div class="section"> <h3 id="AmazonSESは東京リージョンに対応してないんだが">AmazonSESは東京リージョンに対応してないんだが</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">cloud</span><span class="synSpecial">:</span> <span class="synIdentifier">aws</span><span class="synSpecial">:</span> <span class="synIdentifier">stack</span><span class="synSpecial">:</span> <span class="synComment"> # CloudFormationのstack名を自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">region</span><span class="synSpecial">:</span> <span class="synComment"> # EC2のmetadataを自動収集しない</span> <span class="synIdentifier">auto</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">static</span><span class="synSpecial">:</span> ap-northeast-1 </pre><p>これだと、東京リージョンに対応していないAmazonSESの場合、エラーになりそうですよね。</p> <blockquote cite="https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_regions"> <p>Amazon SES is not available in all regions of the Amazon Web Services cloud. Therefore an application hosted and operated in a region that does not support the mail service will produce an error while using the mail service. Therefore the region must be overridden for the mail sender configuration. The example below shows a typical combination of a region (EU-CENTRAL-1) that does not provide an SES service where the client is overridden to use a valid region (EU-WEST-1).</p> <cite>[><a href="https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_regions">https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_regions</a>>]</cite> </blockquote> <p>「SESは全リージョンに対応してるわけではないから、cloud.aws.region.staticの設定をしても未対応リージョンだとエラーが出るから、別の方法でリージョンを上書きしてくれよな。」と言っています。サンプルとしてaws-config.xmlを使う例が載っています。↓こういうやつです。<br /> <a href="https://github.com/eugenp/tutorials/blob/master/spring-cloud/spring-cloud-aws/src/main/resources/aws-config.xml">https://github.com/eugenp/tutorials/blob/master/spring-cloud/spring-cloud-aws/src/main/resources/aws-config.xml</a><br /> ここにSESだけ異なるリージョンを書いてもいいぞー、という事だそうです。</p><p>しかし、実際の開発では、local〜staging環境はオレゴンリージョン、production環境はバージニアリージョン、等と利用環境を分けて、負荷分散やバウンスレートの分けをキッチリする事が多いです。するとaws-config.xmlを環境毎に上書きしないといけないビルド設定が必要になるので、これは嫌です。</p><p>折角application.ymlが環境毎に設定を柔軟に変更できる機構があるのですから、それで変更したいですね。</p> <div class="section"> <h4 id="AmazonSESのリージョンをソースコード側で変更する">AmazonSESのリージョンをソースコード側で変更する</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190126/20190126212524.png" alt="f:id:treeapps:20190126212524p:plain" title="f:id:treeapps:20190126212524p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>いきなりDeprecatedの洗礼を浴びます。amazonSimpleEmailService.setRegionは非推奨なので「AwsClientBuilder.setRegion(String)」を使えとのことです。</p><p>AwsClientBuilderはabstractクラスであり、実際はAwsClientBuilderを継承しているAmazonSimpleEmailServiceClientBuilderを使えという事になります。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Faws%2Faws-sdk-java%2Fblob%2Fmaster%2Faws-java-sdk-ses%2Fsrc%2Fmain%2Fjava%2Fcom%2Famazonaws%2Fservices%2Fsimpleemail%2FAmazonSimpleEmailServiceClientBuilder.java" title="aws/aws-sdk-java" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-ses/src/main/java/com/amazonaws/services/simpleemail/AmazonSimpleEmailServiceClientBuilder.java">github.com</a></cite></p><p>で、AmazonSimpleEmailServiceClientBuilderの使い方は公式サイトにズバリそのものがあるので、省略します。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Fses%2Flatest%2FDeveloperGuide%2Fsend-using-sdk-java.html" title="AWS SDK for Java を使用して E メールを送信する - Amazon Simple Email Service" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/send-using-sdk-java.html">docs.aws.amazon.com</a></cite><br /> </p> </div> </div> <div class="section"> <h3 id="おまけローカル開発時にML宛にメールを送信したくないんだが">おまけ:ローカル開発時にML宛にメールを送信したくないんだが!</h3> <p>ローカルで開発していて、例えばhtmlメールを実装中だとします。テンプレートエンジンでif文やらfor文やらをゴリゴリ実装していくわけです。他にもtoやccが正常に分岐できているかもテストしたいですよね。</p><p>するとローカル環境で何度も実際にメール送信してテストしたくなりますね。メール送信先には社内MLが設定される事が多いと思いますが、テストメールが何十通・何百通も送信されると、MLを受信してる人がウザい・全くメールを見なくなる、という事になる可能性があります。</p><p>特にバッチで大量のhtmlメールを送信するテストをしたい時等は困ってしまいます。実装をミスって無限ループでSESにメールを送信してしまい、莫大な金額が請求されたりとか。</p><p>それを回避するため、私はローカル環境の場合はMailCatcher(仮想SMTPサーバ)を利用します。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmailcatcher.me%2F" title="MailCatcher" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://mailcatcher.me/">mailcatcher.me</a></cite></p><p>MailCatcherは仮想メールサーバなのですが、メールホストをMailCatcherに向けて送信すると、MailCatcherの画面上ではメールの受信フォルダに受信されますが、実際のTO・CC・BCCにメールが送信される事はありません。つまり、<span class="feature1">自分のローカル環境に完全に閉じたメールサーバが構築できる</span>という優れものです。勿論テキストメール・htmlメール・マルチパートメールにも対応しています。</p><p>しかしこれをローカル環境に直接インストールしたくありません。そこでdockerです。ではdockerでMailCatcherを起動できるようにし、更にSpring bootからMailCatcherに向けてメールが送信できるようにしてみましょう。</p> <div class="section"> <h4 id="docker-composeyml">docker-compose.yml</h4> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">version</span><span class="synSpecial">:</span> <span class="synConstant">'3'</span> <span class="synIdentifier">services</span><span class="synSpecial">:</span> <span class="synIdentifier">localMailServer</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> schickling/mailcatcher:latest <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span>1080:1080 <span class="synStatement">- </span>25:1025 <span class="synStatement">- </span>465:1025 <span class="synStatement">- </span>587:1025 </pre><p>port=1080はブラウザで表示する画面用のポート、<br /> port=25は一般的なメールサーバのポート、<br /> port=465,587はgmailのポート、<br /> となります。</p> </div> <div class="section"> <h4 id="applicationyml">application.yml</h4> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">mail</span><span class="synSpecial">:</span> <span class="synIdentifier">host</span><span class="synSpecial">:</span> <span class="synConstant">&quot;localhost&quot;</span> <span class="synIdentifier">protocol</span><span class="synSpecial">:</span> <span class="synConstant">&quot;smtp&quot;</span> <span class="synIdentifier">port</span><span class="synSpecial">:</span> <span class="synConstant">25</span> <span class="synIdentifier">default-encoding</span><span class="synSpecial">:</span> UTF-8 <span class="synIdentifier">test-connection</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">properties</span><span class="synSpecial">:</span> <span class="synIdentifier">mail</span><span class="synSpecial">:</span> <span class="synIdentifier">smtp</span><span class="synSpecial">:</span> <span class="synIdentifier">timeout</span><span class="synSpecial">:</span> <span class="synConstant">10000</span> <span class="synIdentifier">connectiontimeout</span><span class="synSpecial">:</span> <span class="synConstant">10000</span> <span class="synIdentifier">writetimeout</span><span class="synSpecial">:</span> <span class="synConstant">10000</span> </pre> </div> <div class="section"> <h4 id="実際にMailCatcherにメールを送信するGIFアニメ">実際にMailCatcherにメールを送信するGIFアニメ</h4> <p>Spring bootでAPIサーバを起動し、<a href="http://localhost:8080/send-multipart-mail/">http://localhost:8080/send-multipart-mail/</a> をGETリクエストするとマルチパートメール(テキストメールとhtmlメールが合体したメール)をMailCatcherに送信し、テキスト・htmlの両方が確認でき、メールのソースも確認できる事をGIFアニメにしてみました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190126/20190126222601.gif" alt="f:id:treeapps:20190126222601g:plain" title="f:id:treeapps:20190126222601g:plain" class="hatena-fotolife" itemprop="image"></span></p><p>勿論TO・CC・BCCに実際にはメールは送信されていません。ここで受信したメールの履歴は、dockerを停止すると全削除されます。(Volume Mountすれば永続化もできると思います)</p><p>ここでは日本語は使っていませんが、ちゃんと日本語も化けずに表示できます。これをローカル環境で用意しておくと、メール送信に関する実装で精神の安定を向上させる事ができます。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>今回書いたやり方で困るのは、<span class="feature3">ローカル環境ではSMTPでDockerのMailCatcherに向けて送信したいが、ローカル以外の環境ではSESに送信したい場合、コードの共通化ができるのか?</span>という点です。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Fses%2Flatest%2FDeveloperGuide%2Fsend-using-sdk-java.html" title="AWS SDK for Java を使用して E メールを送信する - Amazon Simple Email Service" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/send-using-sdk-java.html">docs.aws.amazon.com</a></cite></p><p>↑このやり方はSMTPプロトコルではなく、SESのAPIで送信します。SMTPで送信する場合はJavaMailSender等を使いますよね。しかしAmazonSES APIだとAmazonSimpleEmailServiceClientBuilderを使い、両者で大分コードが変わります。これを共通化するいい方法があるのかがまだ解っていません。</p><p>一応AmazonSESもSMTPプロトコルでメール送信する事ができますが、物凄く遅いです。1回のメール送信で2秒くらいかかる程遅いです。よくあるユースケースとして、メール送信時にユーザーと管理者に異なるメールを同時に送信したい場合があります。その時同期処理で送信すると2秒×2通=4秒もかかります。2通をasync/awaitで非同期送信しても、2秒より速くはなりません。なので、できればSESでSMTPプロトコルは使わず、高速なAPI(AmazonSimpleEmailServiceClientBuilder)を使いたいです。</p><p>もしSMTP(ローカル環境専用)とSES APIを共存しつつソースコードの共通化をするいい方法をご存知の方がいれば是非教えて下さい!</p> </div> treeapps pythonのpipでnpmのnode_modulesのようにローカルインストールして実行する hatenablog://entry/10257846132686315958 2018-12-17T23:26:46+09:00 2018-12-17T23:26:46+09:00 少しトリッキーなので、忘れない内に手順をまとめておきます〜 <p>少しトリッキーなので、忘れない内に手順をまとめておきます〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180424/20180424130724.png" alt="f:id:treeapps:20180424130724p:plain" title="f:id:treeapps:20180424130724p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>サーバ上でpythonのpipでインストールしたモジュールを動かしたいのだけれど、なんとグローバルのpip installが禁止されている・・・!!というかそもそも権限が与えられなくてインストールができない・・・!!!</p><p>等という地獄の環境の場合、npmのように、ローカルインストールできれば簡単に解決するのに、と思いますよね。pythonでも可能なのですが、知ってないと中々できるものではないので、まとめておきます。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#ローカルインストールって">ローカルインストールって?</a></li> <li><a href="#ローカルインストール手順">ローカルインストール手順</a><ul> <li><a href="#pip-installに-tオプションを付けてインストールする">pip installに「-t」オプションを付けてインストールする</a></li> <li><a href="#実行するpyを用意する">実行する.pyを用意する</a></li> <li><a href="#pyを実行するshを用意する">.pyを実行する.shを用意する</a></li> </ul> </li> <li><a href="#最終的なディレクトリ構成">最終的なディレクトリ構成</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="環境">環境</h3> <ul> <li>pythonがインストール済みである。</li> <li>pipがインストール済みである。</li> </ul><p>バージョンを問わず、上記がインストールされている事が前提になります。</p> </div> <div class="section"> <h3 id="ローカルインストールって">ローカルインストールって?</h3> <p>普通に「pip install fabric」などとすると、以下のような全アカウント共通で使用する、所謂グローバルな場所にインストールされてしまいます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>/Library/Python/2.7/site-packages /Library/Python/2.6/site-packages </pre><p>やりたいのは↑ではなく、pythonのディレクトリに依存しない、自由な場所にsite-packagesを配置したいのです。</p><p>という事で早速やっていきましょう。</p> </div> <div class="section"> <h3 id="ローカルインストール手順">ローカルインストール手順</h3> <div class="section"> <h4 id="pip-installに-tオプションを付けてインストールする">pip installに「-t」オプションを付けてインストールする</h4> <p>ここではfabricをインストールしてみます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>pip install <span class="synSpecial">-t</span> site-packages fabric </pre><p><span class="feature1">site-packages</span>というフォルダ名は別の名前でもいいのですが、ここはnpmで言うところのnode_modulesに相当するので、素直にsite-packagesとした方が全員に通じる名称になると思います。</p> </div> <div class="section"> <h4 id="実行するpyを用意する">実行する.pyを用意する</h4> <p>なんでもいいのですが、今回はfabricを例としてい挙げたので、fabricを実行するものを用意してみます。以下を「<span class="feature1">fabfile.py</span>」として保存します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment">#coding:utf-8</span> <span class="synPreProc">from</span> fabric <span class="synPreProc">import</span> task <span class="synPreProc">import</span> shlex, subprocess <span class="synPreProc">@</span><span class="synIdentifier">task</span> <span class="synStatement">def</span> <span class="synIdentifier">task1</span>(c): subprocess.Popen(<span class="synConstant">&quot;hostname&quot;</span>) </pre> </div> <div class="section"> <h4 id="pyを実行するshを用意する">.pyを実行する.shを用意する</h4> <p>pythonを直接実行するのではなく、シェルスクリプトを経由して実行します。</p><p>その際に特定の環境変数と、PATHにバイナリへのパスを追加する事で、pipのコマンドを実行する事が可能になります。以下を「<span class="feature1">run.sh</span>」として保存します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment">#!/bin/sh</span> <span class="synIdentifier">currentDir</span>=<span class="synPreProc">$(</span><span class="synStatement">echo</span><span class="synConstant"> </span><span class="synPreProc">$(</span><span class="synStatement">cd</span><span class="synSpecial"> $</span><span class="synPreProc">(</span>dirname <span class="synPreProc">$0)</span><span class="synSpecial"> &amp;&amp; </span><span class="synStatement">pwd</span><span class="synPreProc">))</span> <span class="synIdentifier">PYTHON_SITE_PACKAGES</span>=<span class="synPreProc">${currentDir}</span>/site-packages <span class="synComment"># site-packagesの位置を一時的に変更する</span> <span class="synStatement">export</span><span class="synIdentifier"> PYTHONPATH=</span><span class="synPreProc">$PYTHONPATH</span>:<span class="synPreProc">${PYTHON_SITE_PACKAGES}</span> <span class="synComment"># pipでローカルインストールしたバイナリへのパスを通す</span> <span class="synStatement">export</span><span class="synIdentifier"> PATH=</span><span class="synPreProc">${PYTHON_SITE_PACKAGES}</span>/bin:<span class="synPreProc">$PATH</span> <span class="synComment"># ↑でパスを通したのでsite-packages/bin配下のバイナリが実行できるようになる</span> fab task1 </pre><p>これでローカルにインストールしたfabricのfabコマンドを実行する事が可能になります。</p> </div> </div> <div class="section"> <h3 id="最終的なディレクトリ構成">最終的なディレクトリ構成</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>tree:<span class="synStatement">test</span> tree$ tree<span class="synStatement"> . </span>-L <span class="synConstant">2</span> . ├── fabfile.py ├── run.sh └── site-packages ├── PyNaCl-1.3.0.dist-info   ・・・略・・・ ├── bin   ・・・略・・・ └── six.pyc </pre><p>バイナリは以下のように配置されます。ここへパスを通せばいつものコマンドが実行できるわけです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>tree:<span class="synStatement">test</span> tree$ ll site-packages/bin/ total <span class="synConstant">24</span> -rwxr-xr-x <span class="synConstant">1</span> tree staff 256B <span class="synConstant">12</span> <span class="synConstant">17</span> <span class="synConstant">22</span>:<span class="synConstant">54</span> fab -rwxr-xr-x <span class="synConstant">1</span> tree staff 256B <span class="synConstant">12</span> <span class="synConstant">17</span> <span class="synConstant">22</span>:<span class="synConstant">54</span> inv -rwxr-xr-x <span class="synConstant">1</span> tree staff 256B <span class="synConstant">12</span> <span class="synConstant">17</span> <span class="synConstant">22</span>:<span class="synConstant">54</span> invoke </pre><p>先程run.shで実行したfabコマンドは↑これです。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>AWS Lambdaでpythonランタイムを選択した際に、lamdaに標準インストールされていないモジュールを追加インストールする場合もローカルインストールする事になるので、この辺は覚えておくと色々と便利そうです。lambdaの件は以前以下の記事を書いたので、合わせてご覧下さい。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F11%2F05%2F145846" title="s3への画像配置でAWS LambdaのPython3.6+Pillow-SIMDで複数サムネイル生成する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.bunkei-programmer.net/entry/2017/11/05/145846">www.bunkei-programmer.net</a></cite></p><p>また、冒頭で述べたようにセキュリティが非常に厳しい環境で、自分のアカウントのオーナー・グループ権限内に閉じて実行する事ができ、不要になった際にフォルダごと削除すれば消えてくれるのは安心感があります。</p><p>更に、グルーバルを汚染せずに済むので色々な人に易しくなりますね。</p> </div> treeapps macのchromeやsafariで真っ白なページが表示される事があるのはESETのWebアクセス保護が原因かも hatenablog://entry/10257846132679793301 2018-12-04T01:26:25+09:00 2018-12-30T20:02:00+09:00 君が犯人だったか〜 <p>君が犯人だったか〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170829/20170829002500.png" alt="f:id:treeapps:20170829002500p:plain" title="f:id:treeapps:20170829002500p:plain" class="hatena-fotolife" itemprop="image"></span></p><br /> <p>何がきっかけなのか解りませんが、最近急にGoogle ChromeやSafariで真っ白なページが表示されるようになりました。</p><p>少し調べてみて原因の特定ができたので、メモしておきます。</p> <div class="section"> <h3>起きている環境</h3> <ul> <li>iMac late 2014</li> <li>macOS mojave</li> <li>ESET ver6.6.300.2</li> <li>Google Chrome v 70.0.3538.110</li> <li>FireFox v64.0</li> <li>Safari v12.0.2</li> </ul> </div> <div class="section"> <h3>起きている現象</h3> <ul> <li>Google Chrome・Safari・Firefoxの3種類のブラウザで真っ白なページが表示される「<span class="feature3">場合</span>」がある。確実に真っ白になるわけではないが、リロードし続けると「<span class="feature3">何故か</span>」表示される事がある。</li> <li>キャッシュを全クリアしても解決しない。</li> <li>OSを再起動しても解決しない。</li> <li>Adblock Plusアドオンを入れている。</li> <li>シークレットモードで表示しても真っ白になるので、どうやらアドオンが原因ではないようだ。</li> </ul><p>こういう状況でした。</p><p>シークレットモードにすると全てのアドオンが使用できなくなるのですが、シークレットモードでも真っ白なので、アドオンが原因である可能性は無くなりました。ちなみにESETはシークレットモードか通常モードかに関係なくWEBアクセス保護が動いているようです。</p><p>もう完全にブラウザのデータがぶっ壊れたかな?と思いましたが、全てのブラウザのデータが一斉に破損する事は少し考えにくく、色々調べてみた結果、どうやら<span class="feature1">ESETセキュリティのWebアクセス保護が原因なのは確定</span>という事がわかりました。</p><p>ではさっそくWebアクセス保護の問題の設定を解消していきましょう。</p> </div> <div class="section"> <h3>Webアクセス保護は無効にしない</h3> <p>Webアクセス保護を無効にすると、以下のように警告が表示されて嫌なので、今回は保護対象を変更する事で対応します。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20181204/20181204011431.png" alt="f:id:treeapps:20181204011431p:plain" title="f:id:treeapps:20181204011431p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3>Webアクセス保護しつつ真っ白ページを解消する</h3> <p>ESETを起動し、左サイドナビの「設定」をクリックし、右ペインの「Webとメール」をクリックします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20181204/20181204011509.png" alt="f:id:treeapps:20181204011509p:plain" title="f:id:treeapps:20181204011509p:plain" class="hatena-fotolife" itemprop="image"></span></p><br /> <p>続いて、Webアクセス保護の「設定」ボタンをクリックします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20181204/20181204011523.png" alt="f:id:treeapps:20181204011523p:plain" title="f:id:treeapps:20181204011523p:plain" class="hatena-fotolife" itemprop="image"></span></p><br /> <p>HTTPプロトコルで使用するポートを「80,8080,3128,443」から「<span class="feature1">8080,3128</span>」に変更します。変更後「すべて表示する」ボタンをクリックします。<br /> 80はhttpの事で、443はhttpsの事です。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20181204/20181204011535.png" alt="f:id:treeapps:20181204011535p:plain" title="f:id:treeapps:20181204011535p:plain" class="hatena-fotolife" itemprop="image"></span></p><br /> <p>すると保存するかどうか聞かれるので、保存します。これで設定完了です!</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20181204/20181204011605.png" alt="f:id:treeapps:20181204011605p:plain" title="f:id:treeapps:20181204011605p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3>雑感</h3> <p>http(80)とhttps(443)をWebアクセス保護から外すという事は、実質Webアクセス保護しないと同じになってしまいますが、真っ白なページが表示されるよりはマシだと思われます。</p><p>真っ白なページが表示されるのは恐らく広告が原因だと想像していますが、はっきりした原因が解らないので、今回はとりあえず保護しない事で対応しました。</p><p>もっといい方法が有る可能性もあるので、もし情報をお持ちの方がいれば是非教えて下さい!</p> </div> treeapps IT業界でプライベートで勉強するかどうかの理想と現実 hatenablog://entry/10257846132623603051 2018-09-08T04:24:12+09:00 2018-09-10T00:23:45+09:00 例のシャッチョさんの第二弾的な記事が賑わっていたので、見てみました〜 <p>例のシャッチョさんの第二弾的な記事が賑わっていたので、見てみました〜</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170817/20170817135400.png" alt="f:id:treeapps:20170817135400p:plain" title="f:id:treeapps:20170817135400p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Faxia.co.jp%2F2018-09-07" title="「プライベートでは一切勉強したくない」と言っていた社員のこと - 株式会社アクシア" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://axia.co.jp/2018-09-07">axia.co.jp</a></cite></p><p>こちらの記事になります。</p> <ul class="table-of-contents"> <li><a href="#雑な概要">雑な概要</a></li> <li><a href="#多かった主張">多かった主張</a></li> <li><a href="#Aさんは必敗する環境に悩む">Aさんは必敗する環境に悩む</a><ul> <li><a href="#Aさんの環境">Aさんの環境</a></li> <li><a href="#周りの人の環境">周りの人の環境</a></li> </ul> </li> <li><a href="#誤解を生んでいる部分について">誤解を生んでいる部分について</a></li> <li><a href="#理想">理想</a><ul> <li><a href="#業務時間内にどんどん勉強しよう">業務時間内にどんどん勉強しよう</a></li> <li><a href="#業務時間外に勉強するという事">業務時間外に勉強するという事</a></li> </ul> </li> <li><a href="#現実">現実</a><ul> <li><a href="#業務時間内に勉強する事への抵抗勢力">業務時間内に勉強する事への抵抗勢力</a></li> <li><a href="#業務時間外に勉強する事">業務時間外に勉強する事</a></li> <li><a href="#会社の利益に繋がらない勉強">会社の利益に繋がらない勉強</a></li> </ul> </li> <li><a href="#エンジニアに向いている向いていない">エンジニアに向いている・向いていない</a></li> <li><a href="#勉強は超辛く継続する事が超難しい">勉強は超辛く継続する事が超難しい</a></li> <li><a href="#給料が上がると勉強できる">給料が上がると勉強できる?</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="雑な概要">雑な概要</h3> <p>シャッチョさんの会社には昔、プライベートでは勉強しないAさんというエンジニアがいて、周りや後輩にどんどんスキル負けする事に悩んでいました。</p><p>その会社ではプライベートの勉強は強制しておらず、自分の人生なんだからプライベートの時間を家族との時間に費やすのも自由だぜ、と主張しています。</p><p>シャッチョさんは悩んでいる彼に対して「プライベートで勉強するしかないんじゃないか?」と言うと、「絶対に勉強したくないでござる」と返ってきましたとさ。めでたしめでたし。</p><br /> <p>んでんで、前回の記事同様に賛否両論が有るらしく、ブコメは光速で1000超えしているわけです。</p> </div> <div class="section"> <h3 id="多かった主張">多かった主張</h3> <p>文章を読んでいない・理解していないブコメ以外を見て、個人的に以下の意見が目に付きました。</p> <ul> <li>業務時間内に勉強させろ。</li> <li>業務時間内に勉強してはいけない。</li> <li>Aさんは御社にマッチしていない。採用ミス。</li> <li>給料上げれば解決。</li> <li>勉強が必要なのはIT業界に限らない。</li> <li>努力しても報われないからやらない。</li> </ul><p>相反する意見も出てました。</p><p>この中で「<span class="feature3">今の日本では努力しても報われない</span>」という意見ですが、最近僕らのKeisuke Honda選手が以下の発言をされています。</p><p><blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">成功したい人にとって休日をもらって休んでるようでは話にならない。中学生の時くらいから休みをもらうと出し抜くチャンスやと思って、もっと休みをくれって思ってた。</p>&mdash; KeisukeHonda(本田圭佑) (@kskgroup2017) <a href="https://twitter.com/kskgroup2017/status/1035507827425439744?ref_src=twsrc%5Etfw">2018年8月31日</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script><br /> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">そういう方へのアドバイスですが<br><br>成功に囚われるな、成長に囚われろ!って願ってます。<br><br>成功は保証はされないが、成長は保証されてる。ってアホほどの経験からの実体験です。</p>&mdash; KeisukeHonda(本田圭佑) (@kskgroup2017) <a href="https://twitter.com/kskgroup2017/status/1035512496407207936?ref_src=twsrc%5Etfw">2018年8月31日</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p><p>「<span class="feature1">成功は保証はされないが、成長は保証されてる</span>」との事です。</p><p>今回の記事に言い換えると「<span class="feature1">プライベートの勉強は給料アップに繋がる保証はないが、Aさんの成長は保証されてる</span>」です。</p> </div> <div class="section"> <h3 id="Aさんは必敗する環境に悩む">Aさんは必敗する環境に悩む</h3> <div class="section"> <h4 id="Aさんの環境">Aさんの環境</h4> <ul> <li>入社時に数ヶ月の研修有り。業務に必要な事なら、業務時間内に研修を受けさせて貰っている。</li> <li><span class="feature3">プライベートに勉強しないでござる</span>。</li> </ul> </div> <div class="section"> <h4 id="周りの人の環境">周りの人の環境</h4> <ul> <li>入社時に数ヶ月の研修有り。業務に必要な事なら、業務時間内に研修を受けさせて貰っている。</li> <li><span class="feature1">プライベートに勉強するでござる</span>。</li> </ul><p><br /> さてさて、Aさんと周りの人達は、業務時間内では全く同じ環境ですが、業務時間外では異なる環境です。</p><p>で、この環境でAさんは「負ける事に悩んでいる」と言いますが、プライベートでは絶対勉強したくありません。</p><p>環境差がプライベートの勉強有無しか無いので、どう考えてもその悩みを解消させる事ができず、シャッチョさんは困ったわけです。</p> </div> </div> <div class="section"> <h3 id="誤解を生んでいる部分について">誤解を生んでいる部分について</h3> <p>シャッチョさんの会社では残業0を掲げているのですが、この「<span class="feature1">残業0</span>」と「<span class="feature1">プライベートで勉強</span>」という<span class="feature3">矛盾する単語が誤解を生んでいる</span>ように見えました。</p><p>この誤解を生むポイントはAさんがプライベートで勉強する人との差に<span class="feature3">悩んでいる</span>という点にあります。</p><p>もし<span class="feature1">悩んでいなければ</span>、この件は「<span class="feature3">Aさんは人生の中で仕事よりプライベートに重きを置いたので、それで差が生まれました</span>」となるので、シャッチョさんはAさんに対して<span class="feature3">エンジニアに向いてないなんて言わず、この記事を投稿する事もしなかったでしょう</span>。</p><p>しかしAさんは<span class="feature1">悩んでいて</span>、Aさんと他の人の環境差がプライベートの勉強有無しか無いので、じゃあプライベートで勉強するしか無いじゃん?となり、無い物ねだりをするAさんに対してエンジニアに向いてないと発言しているのだと推測しています。</p><p>悩むくらいなら勉強しよう、それでも勉強したくないのだから、もうエンジニアに向いてないのでは?という事です。</p><br /> <p><hr /></p><br /> <p>この話はこれ以上話す事が無いのでここまでとします。</p><p>続いて、ブコメの中で「業務時間内外で勉強する」件について意見があったので、私も個人的な理想を書いてみます。</p> </div> <div class="section"> <h3 id="理想">理想</h3> <div class="section"> <h4 id="業務時間内にどんどん勉強しよう">業務時間内にどんどん勉強しよう</h4> <p>タスクが回せるのであれば、余った時間を好きな勉強に費やしましょう。</p><p>タスクが回せない(納期に間に合わない)状況で勉強の時間を割く事をOKとするかは、会社やマネージャーに要相談です。</p><p>また、最初から必ず業務時間内に勉強時間が確保できるスケジュールを顧客が容認できるなら、それがベストです。</p> </div> <div class="section"> <h4 id="業務時間外に勉強するという事">業務時間外に勉強するという事</h4> <p><span class="feature1">プライベートに勉強する事は一切強制しませんし、一切マイナス評価しません</span>。会社の標準的な評価が正当に行われます。</p><p>ただし、プライベートに勉強した人のスキルが向上し、その努力が会社の利益に繋がった場合、<span class="feature1">プラス評価します</span>。</p><p>このプラス評価によって、プライベートに勉強した人と、勉強しなかった人には差が付きます。</p> </div> </div> <div class="section"> <h3 id="現実">現実</h3> <div class="section"> <h4 id="業務時間内に勉強する事への抵抗勢力">業務時間内に勉強する事への抵抗勢力</h4> <p>「<span class="feature3">業務時間内に勉強するとは何事だ!</span>」という意見の方がおり、その論理は「<span class="feature3">勉強は会社の業務内容に含まれていないから</span>」というケースが多いです。</p><p>業務時間内に勉強させたからといって、それが会社への利益に繋がらないのであれば、会社は拒否反応を起こします。</p><p>確かに一理あります。</p><p>この意見について、私は以下のように思っています。</p> <ul> <li>業務時間とプライベートはキッチリ分け、自分の人生(プライベートに何をするか)を自分で決めた方が幸せである。</li> <li>業務時間内に勉強する事で、結果として会社の利益に繋がるのであれば、やらせた方がよい。</li> <li>新技術・新手法の研究等は、必ず良い結果が生まれるわけではないので、仮に何の成果も出なかったとしても許容する。</li> </ul> </div> <div class="section"> <h4 id="業務時間外に勉強する事">業務時間外に勉強する事</h4> <p>「<span class="feature3">勉強辛い。超辛い。</span>」</p><p>はい。</p><p>人間は本当に好きな分野以外を勉強すると、強いストレスを感じ、やる気を失います。</p><p>この業界は近年細分化が進みすぎて、分野の数が増えていってます。すると、特定の言語で特定のプログラムを書く事は好きでも、そこから一歩でも外れるとやりたくなくなります。分野が増えているせいで、好きではない分野も自然に増えていき、やる気を失う機会も増えていきます。</p><p>なので、最初は意欲を持って勉強できますが、自分の好きな分野から外れる事を経験すると、一気に全体的に意欲を失い、いつしかプライベートで勉強する事自体をやめる(諦める)という人が増えていきます。</p><p>気になる分野の書籍を読んでも、好きな部分の章はあっという間に読了するのに、やや苦手・嫌いな部分の章が現れると、途端に拒否反応を示し、そこで読む事を終了してしまう事もあるのではないでしょうか。</p><p>それほど些細な事でモチベーションを喪失してしまう程、勉強というものは難しいと私は考えています。</p> </div> <div class="section"> <h4 id="会社の利益に繋がらない勉強">会社の利益に繋がらない勉強</h4> <p>例えば今の会社ではAIを勉強しても全く意味が無く、たとえプライベートで勉強しても、おちんぎんが増える事はありません。</p><p>しかし、AIを必要としている他の会社に行くと、おちんぎんが増えます。</p><p>これはもう単純に需要と供給の話なので、<span class="feature3">需要が無い場所で「努力したのにおちんぎん増えない!」と憤っても仕方ない</span>のです。今の会社でその需要を生み出す・生み出して貰うか、需要がある会社に移動するとよいと思います。</p><p>ただし、僕らのKeisuke Honda選手の言葉通り、自分自身の成長には繋がるので、今すぐに評価されなくても、結果的に転職がしやすくなったり、新たな概念を理解し覚える事で、今書いているプログラムをより良く書く能力が付く可能性があります。</p><p>「報われないから努力しない!」という姿勢はそれらの機会を逃し、(別の会社等で)プラス評価される機会を損失するので、個人的には勿体無いとは思いますね。</p> </div> </div> <div class="section"> <h3 id="エンジニアに向いている向いていない">エンジニアに向いている・向いていない</h3> <p>エンジニアの向き・不向きについては正直全く解りません。私はまだ若輩なので「<span class="feature1">これができたら向いている</span>」「<span class="feature3">これができないのは向いていない</span>」を定義付ける自信は無いです。</p><p>実際、プライベートで勉強をする人が向いているかというとそうではなく、<span class="feature1">業務時間内の調査・実践だけで実力者になる方</span>も大勢います。逆に、<span class="feature3">プライベートで勉強をしているのに、勉強の仕方が問題でなかなか実にならない方</span>もいます。</p><p>中には最初から勉強好きな人しかこの業界に入ってはならない!等と過激な発言をされる方もいますが、前述のように実践だけ実力をつけてしまう方(恐らく元々素養があるか努力の仕方が上手い人)もいるので、わざわざ門戸を狭くして視野を狭め、色々な可能性を自ら排除してしまうのは勿体ないのではと思いますね。</p><p>また、<span class="feature1">給料据え置きでいいから勉強したくない!というのは全然有り</span>で、その場合は<span class="feature1">技術要素が変化しにくく継続して仕事が有る、所謂長寿の保守案件がオススメ</span>で、実際そういう方は沢山います。新しい事をやりたい人が集まる会社では誰もそういった仕事をやりたがらないので、実は意外と需要が有ります。</p><p>ただ、cobolの例があるとはいえ10年間同じ仕事をし続ける事が可能かは解らないので、5年単位とかで勉強はしておいた方が将来安心はできると思います。</p> </div> <div class="section"> <h3 id="勉強は超辛く継続する事が超難しい">勉強は超辛く継続する事が超難しい</h3> <p>大部分の人間は怠惰であり、勉強が嫌いです。</p><p>かなり極端な例ですが、仮に</p><p>「<span class="feature1">業務時間内に好きなだけ勉強してよいぞ。アーロンチェア・40インチディスプレイ・Threadripper 32コアCPU・メモリ64G・シーケンシャルread 3,400MB/s SSD・勉強用のawsアカウント、無料の食堂、全部用意してやるぞ。</span>」</p><p>と言われたとします。</p><p>で、「<span class="feature3">あなたは本当に勉強できますか?</span>」</p><p>勉強ってそんな簡単にできて、そんな簡単に継続できるものなのか?という疑問がどうしても拭えません。↑のスーパースペックの環境を与えられても尚、勉強をする事ができない人が多いと思ってます。</p> </div> <div class="section"> <h3 id="給料が上がると勉強できる">給料が上がると勉強できる?</h3> <p>「おちんぎんを増やせば解決理論」ですが、「<span class="feature3">今まで業務時間内やプライベートで勉強していない人が、おちんぎんが増えたら勉強するようになるの?</span>」という疑問が湧きます。</p><p><span class="feature3">おちんぎんによって勉強の辛さを克服できる強き人ってそんなに多いでしょうか?</span></p><p>極端な例ですが、仮にお金を持っている企業が「月給100万円やるから、業務時間内に勉強し、それに見合う結果を出せ」みたいな事を言われて、本当に勉強して、本当に結果が出せるでしょうか。一部の元々素養がある人以外は脱落するのではないかと思います。</p><p>そういった事を考えると、おちんぎんを今より増やしたからといって、本当に勉強する事ができる人(勉強するフリをしたりせず知識を増やす)は増えにくいのでは?と思ってしまうわけです。</p><p><span class="feature1">※ おちんぎんを上げる必要は無い、というお話ではありません。</span><br /> </p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>シャッチョさんの記事のブコメを見ていて、もうとにかく</p><p><span class="feature3 font30">勉強はそんな簡単にできないし継続できません!</span></p><p>と感じました。</p><p>そんな簡単に勉強するモチベーションが噴出するなら、みんな勉強しまくって日本の経済が急回復してるよ!、と思ってしまいました。</p><p>大部分の人は、興味を持って勉強をするのは若い間だけで、ある程度年を取ると環境の変化やモチベーションの低下によって勉強をやめてしまいます。ネット上を見ると頻繁に勉強をして継続している方が目立ちますが、実社会では勉強しない人、勉強をやめてしまった人、勉強の頻度が非常に低い人、の方が割合として多いように感じます。</p><p>なので、どちらかというと勉強を始めて且つ継続するにはどうしたらいいか?等を考えていかないといけないと思います。まずその勉強の意欲が無ければ、たとえが業務時間内の勉強が許可されたとしても、エア勉強をしてしまったり、ソリティアしてるだけだったりすると思います。</p><p>今勉強を楽しいと感じていて、それを継続できている方がいれば、それはとても素晴らしい事です。これからもその意欲を失わず、周りの人に勉強の楽しい部分や継続するモチベーションの保ち方等を伝授していって欲しいですね。</p><br /> <p>ちなみに私は、興味のある分野の書籍を購入 → ちょっと読む → 面白いー! → 今日は仕事で疲れたから読まない → 積む、を繰り返してます・・・。更に勉強意欲に非常にムラがあるので、一時的に特定の分野に興味を持って一気に書籍を読み、その実践としてサイトを開発・公開する時もあれば、無気力に何もしない日が続く事もあります。勉強意欲・モチベーションの維持は本当に難しいものです。</p> </div> treeapps macでredis serverをインストールせずにredis-cliのバイナリをビルドしてプロジェクトに組み込む hatenablog://entry/10257846132606763364 2018-08-02T01:54:19+09:00 2018-08-02T02:00:58+09:00 実は単独のバイナリとしてプロジェクトに組み込めます。 <p>実は単独のバイナリとしてプロジェクトに組み込めます。</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180418/20180418114029.png" alt="f:id:treeapps:20180418114029p:plain" title="f:id:treeapps:20180418114029p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>redis-cliですが、macの場合はbrew install redis-cli等と試みてもインストールできません。</p><p>代わりにbrew install redisでredisサーバごとインストールすると、その中の1機能としてredis-cliが付いてきます。しかし、昨今のredis環境はほぼdocker化しており、ローカルにredisサーバ等はインストールしたくありません。</p><p>そこで、できるだけ標準の仕組みでredis-cliのバイナリを生成、プロジェクトに組み込めないか?と思い、実際に試してみたところ、できました。</p> <ul class="table-of-contents"> <li><a href="#環境">環境</a></li> <li><a href="#redis-cliのバイナリ生成手順">redis-cliのバイナリ生成手順</a><ul> <li><a href="#redisのソースコードをダウンロードする">redisのソースコードをダウンロードする</a></li> <li><a href="#makeする">makeする</a></li> <li><a href="#redis-cliバイナリだけ抽出する">redis-cliバイナリだけ抽出する</a></li> </ul> </li> </ul> <div class="section"> <h3 id="環境">環境</h3> <table> <tr> <th>OS</th> <td>macOS high sierra</td> </tr> <tr> <th>redisバージョン</th> <td>v4</td> </tr> </table> </div> <div class="section"> <h3 id="redis-cliのバイナリ生成手順">redis-cliのバイナリ生成手順</h3> <div class="section"> <h4 id="redisのソースコードをダウンロードする">redisのソースコードをダウンロードする</h4> <p>公式ページからダウンロードURLをコピってきます。<br /> <a href="https://redis.io/download">Redis</a><br /> </p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ wget http://download.redis.io/releases/redis-4.0.10.tar.gz $ tar zxf redis-4.0.10.tar.gz </pre> </div> <div class="section"> <h4 id="makeする">makeする</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>$ <span class="synStatement">cd</span> redis-4.0.10 $ make </pre><p>makeが成功すると、srcディレクトリに各種バイナリが生成されます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ll total <span class="synConstant">584</span> -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 158K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> <span class="synConstant">00</span>-RELEASENOTES -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 53B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> BUGS -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 1.8K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> CONTRIBUTING -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 1.5K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> COPYING -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 11B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> INSTALL -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 4.1K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> MANIFESTO -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 151B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> Makefile -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 20K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> README.md drwxr-xr-x <span class="synConstant">12</span> tree wheel 408B <span class="synConstant">8</span> <span class="synConstant">2</span> <span class="synConstant">01</span>:<span class="synConstant">46</span> deps -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 57K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> redis.conf -rwxr-xr-x <span class="synConstant">1</span> tree wheel 271B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> runtest -rwxr-xr-x <span class="synConstant">1</span> tree wheel 280B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> runtest-cluster -rwxr-xr-x <span class="synConstant">1</span> tree wheel 281B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> runtest-sentinel -rw-<span class="synStatement">r</span>--<span class="synStatement">r</span>-- <span class="synConstant">1</span> tree wheel 7.4K <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> sentinel.conf drwxr-xr-x <span class="synConstant">198</span> tree wheel 6.6K <span class="synConstant">8</span> <span class="synConstant">2</span> <span class="synConstant">01</span>:<span class="synConstant">47</span> src ← ここ! drwxr-xr-x <span class="synConstant">12</span> tree wheel 408B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> tests drwxr-xr-x <span class="synConstant">19</span> tree wheel 646B <span class="synConstant">6</span> <span class="synConstant">13</span> <span class="synConstant">20</span>:<span class="synConstant">02</span> utils </pre> </div> <div class="section"> <h4 id="redis-cliバイナリだけ抽出する">redis-cliバイナリだけ抽出する</h4> <p>redis-cliは以下に生成されています。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ll src/redis-cli -rwxr-xr-x <span class="synConstant">1</span> tree wheel 169K <span class="synConstant">8</span> <span class="synConstant">2</span> <span class="synConstant">01</span>:<span class="synConstant">47</span> redis-cli </pre><p>例えばこれを/tmpにコピーします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ cp src/redis-cli /tmp </pre><p>ではこのバイナリを直接実行し、動作確認してみます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ./redis-cli <span class="synSpecial">--version</span> redis-cli 4.0.10 $ ./redis-cli 127.0.0.1:<span class="synConstant">6379</span><span class="synStatement">&gt;</span> keys * <span class="synConstant">1</span><span class="synError">)</span> <span class="synStatement">&quot;</span><span class="synConstant">TEST::com.example.api.repository.database.PrefectureRepository.findAll</span><span class="synStatement">&quot;</span> <span class="synConstant">2</span><span class="synError">)</span> <span class="synStatement">&quot;</span><span class="synConstant">spring:session:sessions:825e37c4-fae1-4b86-affb-8b5b3398e9c5</span><span class="synStatement">&quot;</span> </pre><p>どうやらちゃんと動くようです。これでいちいちローカルにredisサーバをインストールしなくて済みますね。</p><p>しかもこのバイナリをプロジェクトに組み込んでしまえば、gitで全員に同じバージョンのredis-cliを配布する事も可能になります。</p> </div> </div> treeapps GAE/Node.js Standard Environmentでスピンアップからテキストが返るまでの速度をゆる〜く確認する hatenablog://entry/17391345971653833911 2018-06-13T23:29:12+09:00 2018-06-14T09:00:15+09:00 appengineに待望のNode.js standard environmentが正式リリースされたので、早速計測してみました〜 <p>appengineに待望のNode.js standard environmentが正式リリースされたので、早速計測してみました〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170917/20170917230836.png" alt="f:id:treeapps:20170917230836p:plain" title="f:id:treeapps:20170917230836p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> <ul class="table-of-contents"> <li><a href="#前回のあらすじ">前回のあらすじ</a></li> <li><a href="#計測の仕方">計測の仕方</a></li> <li><a href="#環境">環境</a></li> <li><a href="#計測に使用したソースコード">計測に使用したソースコード</a></li> <li><a href="#スピンアップしてフレームワークを初期化してテキストが返るまでの速度">スピンアップしてフレームワークを初期化してテキストが返るまでの速度</a><ul> <li><a href="#計測1回目">計測1回目</a></li> <li><a href="#計測2回目">計測2回目</a></li> <li><a href="#計測3回目">計測3回目</a></li> <li><a href="#計測4回目">計測4回目</a></li> <li><a href="#計測5回目">計測5回目</a></li> </ul> </li> <li><a href="#結果と平均値まとめ">結果と平均値まとめ</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="前回のあらすじ">前回のあらすじ</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F09%2F24%2F220531" title="GAE/Java8+kotlin+Spark Frameworkでスピンアップからjsonが返るまでの速度をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.bunkei-programmer.net/entry/2017/09/24/220531">www.bunkei-programmer.net</a></cite></p><p>GAE/Javaは8になってもやはり初動が遅かったのだ。というかGAE/Java1.7とほとんど違いは無かったのだ。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2016%2F06%2F05%2F130329" title="GAE/go+ginとGAE/java+servletでそれぞれスピンアップの速度差をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.bunkei-programmer.net/entry/2016/06/05/130329">www.bunkei-programmer.net</a></cite></p><p>GAE/Goは(javaと比較すると)くっそ速かったのだ。</p><p>そしてGAE/Node.jsが出たので計測してみたのだ。</p> </div> <div class="section"> <h3 id="計測の仕方">計測の仕方</h3> <p>前回のGAE/Java8の時と同様に、インスタンスを削除して、必ずスピンアップが発生する状態で計測します。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F09%2F24%2F220531" title="GAE/Java8+kotlin+Spark Frameworkでスピンアップからjsonが返るまでの速度をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.bunkei-programmer.net/entry/2017/09/24/220531#%E8%A8%88%E6%B8%AC%E3%81%AE%E4%BB%95%E6%96%B9">www.bunkei-programmer.net</a></cite><br /> </p> </div> <div class="section"> <h3 id="環境">環境</h3> <table> <tr> <th>リージョン</th> <td>asia-northeast1</td> </tr> <tr> <th>Runtime</th> <td>Node.js v8</td> </tr> <tr> <th>Framework</th> <td>Express v4.16.3</td> </tr> </table> </div> <div class="section"> <h3 id="計測に使用したソースコード">計測に使用したソースコード</h3> <p>以下の公式サンプル(最小のhello world)を使用して計測しました。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fnodejs-docs-samples%2Ftree%2Fmaster%2Fappengine%2Fhello-world%2Fstandard" title="GoogleCloudPlatform/nodejs-docs-samples" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/appengine/hello-world/standard">github.com</a></cite></p><p>javascriptは1ファイルで、以下のように非常に短いコードです。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synConstant">'use strict'</span>; <span class="synComment">// [START app]</span> <span class="synStatement">const</span> express = require(<span class="synConstant">'express'</span>); <span class="synStatement">const</span> app = express(); app.get(<span class="synConstant">'/'</span>, (req, res) =&gt; <span class="synIdentifier">{</span> res.<span class="synStatement">status</span>(200).send(<span class="synConstant">'Hello, world!'</span>).end(); <span class="synIdentifier">}</span>); <span class="synComment">// Start the server</span> <span class="synStatement">const</span> PORT = process.env.PORT || 8080; app.listen(PORT, () =&gt; <span class="synIdentifier">{</span> console.log(`App listening on port $<span class="synIdentifier">{</span>PORT<span class="synIdentifier">}</span>`); console.log(<span class="synConstant">'Press Ctrl+C to quit.'</span>); <span class="synIdentifier">}</span>); <span class="synComment">// [END app]</span> </pre> </div> <div class="section"> <h3 id="スピンアップしてフレームワークを初期化してテキストが返るまでの速度">スピンアップしてフレームワークを初期化してテキストが返るまでの速度</h3> <p>前回やったのと同じです。インスタンスが0のスピンダウンした状態からリクエストを受け、実際にフレームワークがテキストを返し終えるまでの時間を計測します。</p> <div class="section"> <h4 id="計測1回目">計測1回目</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180613/20180613230902.png" alt="f:id:treeapps:20180613230902p:plain" title="f:id:treeapps:20180613230902p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="計測2回目">計測2回目</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180613/20180613230915.png" alt="f:id:treeapps:20180613230915p:plain" title="f:id:treeapps:20180613230915p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="計測3回目">計測3回目</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180613/20180613230925.png" alt="f:id:treeapps:20180613230925p:plain" title="f:id:treeapps:20180613230925p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="計測4回目">計測4回目</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180613/20180613230935.png" alt="f:id:treeapps:20180613230935p:plain" title="f:id:treeapps:20180613230935p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="計測5回目">計測5回目</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180613/20180613230946.png" alt="f:id:treeapps:20180613230946p:plain" title="f:id:treeapps:20180613230946p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="結果と平均値まとめ">結果と平均値まとめ</h3> <table> <tr> <th>1回目</th> <th>2回目</th> <th>3回目</th> <th>4回目</th> <th>5回目</th> <th>平均値</th> </tr> <tr> <td>0.809秒</td> <td>0.572秒</td> <td>0.539秒</td> <td>0.755秒</td> <td>0.583秒</td> <td>0.6516秒</td> </tr> </table><p>結果は平均 <span class="feature1">0.6516秒</span> となりました。</p><p>では今まで計測した、GAE/Gonalg + gin、GAE/Java7 + servletと比較してみましょう。</p> <table> <tr> <th>ランタイム</th> <th>1回目</th> <th>2回目</th> <th>3回目</th> <th>4回目</th> <th>5回目</th> <th>平均値</th> </tr> <tr> <th>java8 + spark fw</th> <td>5.16秒</td> <td>5.33秒</td> <td>5.58秒</td> <td>5.07秒</td> <td>5.24秒</td> <td>5.276秒</td> </tr> <tr> <th>java7 + servlet</th> <td>3.65秒</td> <td>3.40秒</td> <td>3.82秒</td> <td>3.78秒</td> <td>3.94秒</td> <td>3.718秒</td> </tr> <tr> <th>go + gin</th> <td>0.506秒</td> <td>0.377秒</td> <td>0.601秒</td> <td>0.501秒</td> <td>0.494秒</td> <td>0.495秒</td> </tr> <tr> <th>node.js + express</th> <td>0.809秒</td> <td>0.572秒</td> <td>0.539秒</td> <td>0.755秒</td> <td>0.583秒</td> <td>0.6516秒</td> </tr> </table><p>golangが最速ではありますが、node.jsはgolangより0.2〜0.3秒程度遅いだけなので、十分なパフォーマンスが出ているように見えますね。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>思ったよりGAE/Node.jsが高速で良かったです!</p><p>GAE/Node.jsは最近Googleが発表された軽量コンテナ環境のgVisorが使われているそうですが、特に遅い事もなく、非常に良い印象です。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.publickey1.jp%2Fblog%2F18%2Fapp_enginenodejsgvisor.html" title="App Engineが軽量コンテナのgVisorを実行環境として採用、スタンダード環境でNode.jsをサポート開始" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.publickey1.jp/blog/18/app_enginenodejsgvisor.html">www.publickey1.jp</a></cite></p><br /> <p>現状私は以下の2サイトをGAEで運用していて、両サイトともGAE/Golangです。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.tree-maps.com%2F" title="地図のWEB TOOLの事ならtree-mapsにお任せ! | tree-maps" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.tree-maps.com/">www.tree-maps.com</a></cite><br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.string-utility.com%2F" title="リアルタイム文字列ユーティリティーの事ならString Utilityにお任せ! | String Utility" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.string-utility.com/">www.string-utility.com</a></cite></p><p>Golangなので、react・angularのSSRは捨てていました。しかし今回待望のNode.jsがリリースされたので、順次Node.jsに移行していこうと思っています。</p><p>平均0.2〜0.3秒程度遅くなるかもしれませんが、SSR可能になる事を考慮すると、その遅延は大した事ないかな?と想像しています。</p><p>いつ移行できるかまだ解りませんが、移行したら何らかの形で告知しようと思います!</p> </div> treeapps angular v6にアップデートした際に起きたエラーの対応 hatenablog://entry/17391345971642735194 2018-05-08T23:11:34+09:00 2018-05-09T00:31:53+09:00 いつものです〜 <p>いつものです〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170918/20170918135756.png" alt="f:id:treeapps:20170918135756p:plain" title="f:id:treeapps:20170918135756p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> <ul class="table-of-contents"> <li><a href="#Local-workspace-file-angularjson-could-not-be-found">Local workspace file ('angular.json') could not be found.</a><ul> <li><a href="#angular-cliのバージョンアップをする">angular-cliのバージョンアップをする</a></li> <li><a href="#angular-clijson-を-angularjson-に変換する">.angular-cli.json を angular.json に変換する</a></li> <li><a href="#angularcore-angularmaterial-rxjsのバージョンアップ">angular/core, angular/material, rxjsのバージョンアップ</a></li> </ul> </li> <li><a href="#import--Observable--from-rxjsObservable-がコンパイルエラー">import { Observable } from 'rxjs/Observable'; がコンパイルエラー</a></li> <li><a href="#TS2322-Type-Storestring-is-not-assignable-to-type-Observablestring">TS2322: Type 'Store&lt;string&gt;' is not assignable to type 'Observable&lt;string&gt;'.</a></li> <li><a href="#Unknown-option---extractCss">Unknown option: '--extractCss'</a></li> <li><a href="#Unknown-option---sourcemaps">Unknown option: '--sourcemaps'</a></li> <li><a href="#Configuration-true-could-not-be-found-in-project-string-utility">Configuration 'true' could not be found in project 'string-utility'.</a></li> <li><a href="#xxxbundlejs-is-not-found">xxx.bundle.js is not found</a></li> <li><a href="#vendorbundlejs-is-not-found">vendor.bundle.js is not found</a></li> <li><a href="#inlinebundlejs-is-not-found">inline.bundle.js is not found</a></li> </ul> <div class="section"> <h3 id="Local-workspace-file-angularjson-could-not-be-found">Local workspace file ('angular.json') could not be found.</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>$ yarn <span class="synStatement">start</span> yarn run v1.3.2 $ ng serve <span class="synSpecial">--extract-css=true</span> <span class="synSpecial">--proxy-config</span> proxy.conf.json Local workspace file <span class="synPreProc">(</span><span class="synStatement">'</span><span class="synConstant">angular.json</span><span class="synStatement">'</span><span class="synPreProc">)</span> could not be found. Error: Local workspacLocal workspace file <span class="synPreProc">(</span><span class="synStatement">'</span><span class="synConstant">angular.json</span><span class="synStatement">'</span><span class="synPreProc">)</span> could not be found.e file <span class="synPreProc">(</span><span class="synStatement">'</span><span class="synConstant">angular.json</span><span class="synStatement">'</span><span class="synPreProc">)</span> could not be found. at WorkspaceLoader._getProjectWorkspaceFilePath <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/workspace-loader.js:</span><span class="synConstant">37</span><span class="synSpecial">:</span><span class="synConstant">19</span><span class="synPreProc">)</span> at WorkspaceLoader.loadWorkspace <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/workspace-loader.js:</span><span class="synConstant">24</span><span class="synSpecial">:</span><span class="synConstant">21</span><span class="synPreProc">)</span> at ServeCommand._loadWorkspaceAndArchitect <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">195</span><span class="synSpecial">:</span><span class="synConstant">32</span><span class="synPreProc">)</span> at ServeCommand.<span class="synStatement">&lt;</span>anonymous<span class="synStatement">&gt;</span> <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">47</span><span class="synSpecial">:</span><span class="synConstant">25</span><span class="synPreProc">)</span> at Generator.next <span class="synPreProc">(</span><span class="synStatement">&lt;</span><span class="synSpecial">anonymous</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at /Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:<span class="synConstant">7</span>:<span class="synConstant">71</span> at new Promise <span class="synPreProc">(</span><span class="synStatement">&lt;</span><span class="synSpecial">anonymous</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at __awaiter <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">3</span><span class="synSpecial">:</span><span class="synConstant">12</span><span class="synPreProc">)</span> at ServeCommand.initialize <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">46</span><span class="synSpecial">:</span><span class="synConstant">16</span><span class="synPreProc">)</span> at Object.<span class="synStatement">&lt;</span>anonymous<span class="synStatement">&gt;</span> <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/</span><span class="synStatement">command</span><span class="synSpecial">-runner.js:</span><span class="synConstant">87</span><span class="synSpecial">:</span><span class="synConstant">23</span><span class="synPreProc">)</span> error Command failed with <span class="synStatement">exit</span> code 1. info Visit https://yarnpkg.com/en/docs/cli/run <span class="synStatement">for</span> documentation about this command. </pre><p>angular v6になり、「.angular-cli.json」というファイルから「angular.json」に変更になり、jsonの構造も変わりました。</p> <div class="section"> <h4 id="angular-cliのバージョンアップをする">angular-cliのバージョンアップをする</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>$ brew upgrade angular-cli <span class="synStatement">==&gt;</span> Upgrading <span class="synConstant">1</span> outdated package, with result: angular-cli 1.7.4 -<span class="synStatement">&gt;</span> 6.0.0 <span class="synStatement">==&gt;</span> Upgrading angular-cli <span class="synStatement">==&gt;</span> Downloading https://homebrew.bintray.com/bottles/angular-cli-6.0.0.high_sierra.bottle.tar.gz <span class="synComment">######################################################################## 100.0%</span> <span class="synStatement">==&gt;</span> Pouring angular-cli-6.0.0.high_sierra.bottle.tar.gz 🍺 /usr/<span class="synStatement">local</span>/Cellar/angular-cli/6.0.0: <span class="synConstant">6</span>,<span class="synConstant">652</span> files, 54.9MB </pre> </div> <div class="section"> <h4 id="angular-clijson-を-angularjson-に変換する">.angular-cli.json を angular.json に変換する</h4> <p>以下のupdateコマンドでマイグレーションしてくれました。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ng update @angular/cli </pre><p>しかし、私の場合sourceRootは「client」としていたのですが、マイグレーション結果は「src」となってしまっていました。これは手動で「client」と修正しました。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">projects</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">string-utility</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">root</span>&quot;: &quot;&quot;, &quot;<span class="synStatement">sourceRoot</span>&quot;: &quot;<span class="synConstant">client</span>&quot;, </pre> </div> <div class="section"> <h4 id="angularcore-angularmaterial-rxjsのバージョンアップ">angular/core, angular/material, rxjsのバージョンアップ</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ng update @angular/core $ ng update @angular/material $ ng update rxjs </pre><p>ng updateコマンドはpackage.jsonを更新するだけなので、別途node_modulesを最新化する必要があります。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ng update rxjs UPDATE package.json <span class="synPreProc">(</span><span class="synConstant">2795</span><span class="synSpecial"> bytes</span><span class="synPreProc">)</span> </pre> </div> </div> <div class="section"> <h3 id="import--Observable--from-rxjsObservable-がコンパイルエラー">import { Observable } from 'rxjs/Observable'; がコンパイルエラー</h3> <p>rxjs v6には破壊的変更が入ったため、以下のようにimportを変更する必要があります。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Observable <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'rxjs/Observable'</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> map <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'rxjs/operators/map'</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synConstant">&quot;rxjs/add/observable/of&quot;</span><span class="synStatement">;</span> ↓ <span class="synStatement">import</span> <span class="synIdentifier">{</span> Observable<span class="synStatement">,</span> <span class="synStatement">of</span> <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'rxjs'</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span> map <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">'rxjs/operators'</span><span class="synStatement">;</span> </pre><p>このimport変更をしたくない方は、「rxjs-compat」を追加する事でimportの変更無しに済ませる事ができます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ yarn add rxjs-compat </pre> </div> <div class="section"> <h3 id="TS2322-Type-Storestring-is-not-assignable-to-type-Observablestring">TS2322: Type 'Store&lt;string&gt;' is not assignable to type 'Observable&lt;string&gt;'.</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>ERROR <span class="synError">in</span> client/app/app.component.ts<span class="synPreProc">(</span><span class="synConstant">71</span><span class="synSpecial">,</span><span class="synConstant">5</span><span class="synPreProc">)</span>: error TS2322: Type <span class="synStatement">'</span><span class="synConstant">Store&lt;boolean&gt;</span><span class="synStatement">'</span> is not assignable to <span class="synStatement">type</span> <span class="synStatement">'</span><span class="synConstant">Observable&lt;boolean&gt;</span><span class="synStatement">'</span>. </pre><p>こんなコンパイルエラーが発生しました。以下のようにrxjs-compatを追加する事でコンパイルエラーが解消されました。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ yarn add rxjs-compat </pre><p>この記事を書いているタイミングでは@ngrx/storeのバージョンは「v5.2.0」で、v6はまだ出ておらず、v6.0.0-beta2が最新でした。</p><p>恐らくこれがv6になれば、rxjs-compatは不要になるのではないかと思われます。</p> </div> <div class="section"> <h3 id="Unknown-option---extractCss">Unknown option: '--extractCss'</h3> </div> <div class="section"> <h3 id="Unknown-option---sourcemaps">Unknown option: '--sourcemaps'</h3> <p>これ系のエラーですが、今までngコマンドのオプションはキャメルケースだったのですが、v6からケバブケースに変更されたようです。</p><p>cliオプションで指定する場合はケバブケースで、angular.jsonで指定する場合はキャメルケースになったようです。</p> <table> <tr> <th>cli</th> <th>angular.json</th> </tr> <tr> <td>--optimization</td> <td>optimization</td> </tr> <tr> <td>--output-hashing</td> <td>outputHashing</td> </tr> <tr> <td>--source-map</td> <td>sourceMap</td> </tr> <tr> <td>--extract-css</td> <td>extractCss</td> </tr> <tr> <td>--named-chunks</td> <td>namedChunks</td> </tr> <tr> <td>--aot</td> <td>aot</td> </tr> <tr> <td>--extract-licenses</td> <td>extractLicenses</td> </tr> <tr> <td>--vendor-chunk</td> <td>vendorChunk</td> </tr> <tr> <td>--build-optimizer</td> <td>buildOptimizer</td> </tr> <tr> <td>--service-worker</td> <td>serviceWorker</td> </tr> <tr> <td>--ngsw-config-path</td> <td>ngswConfigPath</td> </tr> </table><p>何も考えずにマイグレーションするとangular.json側にも設定があるし、package.jsonのngコマンドにも設定があるという重複状態になるので、どちらかに寄せた方が良さそうですね。</p> </div> <div class="section"> <h3 id="Configuration-true-could-not-be-found-in-project-string-utility">Configuration 'true' could not be found in project 'string-utility'.</h3> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ng build <span class="synSpecial">-vc</span><span class="synStatement">=true</span> Configuration <span class="synStatement">'</span><span class="synConstant">true</span><span class="synStatement">'</span> could not be found <span class="synError">in</span> project <span class="synStatement">'</span><span class="synConstant">string-utility</span><span class="synStatement">'</span>. Error: Configuration <span class="synStatement">'</span><span class="synConstant">true</span><span class="synStatement">'</span> could not be found <span class="synError">in</span> project <span class="synStatement">'</span><span class="synConstant">string-utility</span><span class="synStatement">'</span>. at Architect.getBuilderConfiguration <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular-devkit/architect/src/architect.js:</span><span class="synConstant">106</span><span class="synSpecial">:</span><span class="synConstant">23</span><span class="synPreProc">)</span> at runSingleTarget <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">138</span><span class="synSpecial">:</span><span class="synConstant">89</span><span class="synPreProc">)</span> at MergeMapSubscriber.rxjs_2.from.pipe.operators_1.concatMap.project <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:</span><span class="synConstant">143</span><span class="synSpecial">:</span><span class="synConstant">127</span><span class="synPreProc">)</span> at MergeMapSubscriber._tryNext <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:</span><span class="synConstant">122</span><span class="synSpecial">:</span><span class="synConstant">27</span><span class="synPreProc">)</span> at MergeMapSubscriber._next <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:</span><span class="synConstant">112</span><span class="synSpecial">:</span><span class="synConstant">18</span><span class="synPreProc">)</span> at MergeMapSubscriber.Subscriber.next <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Subscriber.js:</span><span class="synConstant">103</span><span class="synSpecial">:</span><span class="synConstant">18</span><span class="synPreProc">)</span> at Observable._subscribe <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/util/subscribeToArray.js:</span><span class="synConstant">9</span><span class="synSpecial">:</span><span class="synConstant">20</span><span class="synPreProc">)</span> at Observable._trySubscribe <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Observable.js:</span><span class="synConstant">177</span><span class="synSpecial">:</span><span class="synConstant">25</span><span class="synPreProc">)</span> at Observable.subscribe <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Observable.js:</span><span class="synConstant">162</span><span class="synSpecial">:</span><span class="synConstant">93</span><span class="synPreProc">)</span> at MergeMapOperator.call <span class="synPreProc">(</span><span class="synSpecial">/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:</span><span class="synConstant">87</span><span class="synSpecial">:</span><span class="synConstant">23</span><span class="synPreProc">)</span> error Command failed with <span class="synStatement">exit</span> code 1. info Visit https://yarnpkg.com/en/docs/cli/run <span class="synStatement">for</span> documentation about this command. </pre><p>これ、ちょっと意味不明なエラーだったのですが、以下のように -vc を削除すると解消しました。(-vcって何のオプションだっけ・・・)</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ ng build <span class="synSpecial">--prod</span> <span class="synSpecial">-vc</span><span class="synStatement">=true</span> ↓ $ ng build <span class="synSpecial">--prod</span> </pre> </div> <div class="section"> <h3 id="xxxbundlejs-is-not-found">xxx.bundle.js is not found</h3> <p>詳細は調べきれていませんが、今まで ng build --prod すると、xxx.bundle.js と出力されていたのが、 xxx.js となったようです。</p> </div> <div class="section"> <h3 id="vendorbundlejs-is-not-found">vendor.bundle.js is not found</h3> <p>ng serveした時はあるのに、ng build --prodすると無くなってました。</p><p>以下のようにproduction設定のvendorChunkをtrueにするとvendor.jsが生成されるようになります。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">projects</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">string-utility</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">architect</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">build</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">configurations</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">production</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">vendorChunk</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> </div> <div class="section"> <h3 id="inlinebundlejs-is-not-found">inline.bundle.js is not found</h3> <p>これは inline.js にすればよいのではなく、ファイル名が「inline.js」から「runtime.js」に変わったようです。</p><p>この辺をまとめると以下となります。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;inline.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;polyfills.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;styles.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;vendor.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;main.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> ↓ <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;runtime.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;polyfills.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;styles.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;vendor.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;main.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre> </div> treeapps create-react-appのESLint or TSLint + Prettier設定とファイル保存時のPrettier+Lint自動実行設定 hatenablog://entry/17391345971636882333 2018-04-20T22:01:56+09:00 2019-06-18T10:19:01+09:00 もう使わない設定がネット上に散見されていたり、両者で微妙な違いがあったりする罠があるので、少しだけまとめてみます。 <p>もう使わない設定がネット上に散見されていたり、両者で微妙な違いがあったりする罠があるので、少しだけまとめてみます。</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20190616/20190616112954.png" alt="f:id:treeapps:20190616112954p:plain" title="f:id:treeapps:20190616112954p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>最近のjsではprettierの設定が一般的になってきているので、create-react-appと組み合わせる方法について、少しだけまとめてみます。</p><p><span class="feature3">※ 本記事はmacを前提としているので、windowsの方は適宜読み替えて下さい。</span><br /> </p> <ul class="table-of-contents"> <li><a href="#BabelかTypescriptか">BabelかTypescriptか</a></li> <li><a href="#Babel版eslint--prettierの設定">Babel版:eslint + prettierの設定</a><ul> <li><a href="#create-react-appをグローバルにインストール">create-react-appをグローバルにインストール</a></li> <li><a href="#babelでプロジェクトを生成">babelでプロジェクトを生成</a></li> <li><a href="#エディタ設定">エディタ設定</a></li> <li><a href="#eslintとprettierのモジュールをインストール">eslintとprettierのモジュールをインストール</a></li> <li><a href="#eslint設定ファイル追加">eslint設定ファイル追加</a></li> <li><a href="#eslintの除外設定ファイル追加">eslintの除外設定ファイル追加</a></li> <li><a href="#prettier設定ファイル追加">prettier設定ファイル追加</a></li> <li><a href="#packagejsonにscript追加">package.jsonにscript追加</a><ul> <li><a href="#lint実行コマンドを追加">lint実行コマンドを追加</a></li> <li><a href="#ESLintとPrettierの競合チェックコマンドを追加">ESLintとPrettierの競合チェックコマンドを追加</a></li> </ul> </li> <li><a href="#lintを実行する">lintを実行する</a></li> </ul> </li> <li><a href="#Typescript版tslint--prettierの設定">Typescript版:tslint + prettierの設定</a><ul> <li><a href="#create-react-appをグローバルにインストール-1">create-react-appをグローバルにインストール</a></li> <li><a href="#Typescriptでプロジェクトを生成">Typescriptでプロジェクトを生成</a></li> <li><a href="#エディタ設定-1">エディタ設定</a></li> <li><a href="#tslintとprettierのモジュールをインストール">tslintとprettierのモジュールをインストール</a></li> <li><a href="#tslintの設定ファイル追加">tslintの設定ファイル追加</a></li> <li><a href="#prettier設定ファイル追加-1">prettier設定ファイル追加</a></li> <li><a href="#packagejsonにscript追加-1">package.jsonにscript追加</a><ul> <li><a href="#lint実行コマンドを追加-1">lint実行コマンドを追加</a></li> <li><a href="#TSLintとPrettierの競合チェックコマンドを追加">TSLintとPrettierの競合チェックコマンドを追加</a></li> </ul> </li> <li><a href="#lintを実行する-1">lintを実行する</a></li> </ul> </li> <li><a href="#ファイル保存時にPrettierとLintを自動実行する">ファイル保存時にPrettierとLintを自動実行する</a><ul> <li><a href="#JetBrans-WebStormIntellij-IDEAやGoLandでも同じの設定">JetBrans WebStorm(Intellij IDEAやGoLandでも同じ)の設定</a></li> <li><a href="#Visual-Studio-Codeの設定">Visual Studio Codeの設定</a><ul> <li><a href="#Typescriptの場合のsettingsjson">Typescriptの場合のsettings.json</a></li> <li><a href="#Babelの場合のsettingsjson">Babelの場合のsettings.json</a></li> </ul> </li> </ul> </li> <li><a href="#Babeleslintのエラー警告集">Babel:eslintのエラー・警告集</a><ul> <li><a href="#Cannot-find-module-eslint-config-standard">Cannot find module 'eslint-config-standard'</a></li> <li><a href="#ESLint-couldnt-find-the-plugin-eslint-plugin-node">ESLint couldn't find the plugin "eslint-plugin-node"</a></li> <li><a href="#ESLint-couldnt-find-the-plugin-eslint-plugin-promise">ESLint couldn't find the plugin "eslint-plugin-promise".</a></li> <li><a href="#ESLint-couldnt-find-the-plugin-eslint-plugin-import">ESLint couldn't find the plugin "eslint-plugin-import".</a></li> <li><a href="#Configuration-for-rule-indent-is-invalid">Configuration for rule "indent" is invalid:</a></li> <li><a href="#prettier-eslintは使わないの">prettier-eslintは使わないの?</a></li> </ul> </li> <li><a href="#Typescripttslintのエラー警告集">Typescript:tslintのエラー・警告集</a><ul> <li><a href="#yarn-lintで大量のrule-requires-type-informationが発生する">yarn lintで大量のrule requires type informationが発生する</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="BabelかTypescriptか">BabelかTypescriptか</h3> <p>create-react-appには、使用されるトランスパイラは標準でBabelとなります。</p><p>しかし、オプションでtypescriptにする事も可能です。(Reactオフィシャルではないようです)</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fwmonk%2Fcreate-react-app-typescript" title="wmonk/create-react-app-typescript" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/wmonk/create-react-app-typescript">github.com</a></cite></p><p>Babelの場合のlintは <span class="feature1">eslint</span> になります。</p><p>Typescriptの場合のlintは <span class="feature1">tslint</span> になります。</p><p>eslintとtslintは目的は同じものですが、設定ファイルの構造や中身が異なります。</p><p>という事で、今回はeslintとtslintの両方のパターンでprettierを適用する方法を書いてみます。</p> </div> <div class="section"> <h3 id="Babel版eslint--prettierの設定">Babel版:eslint + prettierの設定</h3> <div class="section"> <h4 id="create-react-appをグローバルにインストール">create-react-appをグローバルにインストール</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>npm install <span class="synSpecial">-g</span> create-react-app </pre> </div> <div class="section"> <h4 id="babelでプロジェクトを生成">babelでプロジェクトを生成</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>create-react-app eslint-prettier-example </pre> </div> <div class="section"> <h4 id="エディタ設定">エディタ設定</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">cd</span> eslint-prettier-example cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .editorconfig</span> <span class="synConstant"># Editor configuration, see http://editorconfig.org</span> <span class="synConstant">root = true</span> <span class="synConstant">[*]</span> <span class="synConstant">charset = utf-8</span> <span class="synConstant">indent_style = space</span> <span class="synConstant">indent_size = 2</span> <span class="synConstant">insert_final_newline = true</span> <span class="synConstant">trim_trailing_whitespace = true</span> <span class="synConstant">[*.md]</span> <span class="synConstant">max_line_length = off</span> <span class="synConstant">trim_trailing_whitespace = false</span> <span class="synStatement">EOF</span> </pre> </div> <div class="section"> <h4 id="eslintとprettierのモジュールをインストール">eslintとprettierのモジュールをインストール</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint prettier eslint-plugin-prettier eslint-config-prettier </pre><p>eslint-plugin-prettierは、eslintを実行すると、prettier → eslintという順番で実行してくれるモジュールです。(prettierの整形結果をeslintが怒る事があるので)<br /> eslint-config-prettierは、prettierが整形した部分をeslintに無視させるモジュールです。</p><p>create-react-appでeslintするとモジュールが足りないと警告されるので、以下をインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-react eslint-config-react-app eslint-plugin-import eslint-plugin-flowtype eslint-plugin-jsx-a11y </pre><p>js標準コーディング規約(JavaScript Standard Style)をインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-standard eslint-config-standard eslint-plugin-node eslint-plugin-promise </pre><p>eslint-config-standardはJavascript標準スタイル(皆で決めた標準lintルール)のeslint版です。例えば「function hoge(){}」で「hoge」と「()」の間にスペースが無い場合はNGとする、といったルール集です。<br /> <a href="https://standardjs.com/">JavaScript Standard Style</a><br /> 標準スタイルにはgoogle版やairbnb版等があります。任意に変更して下さい。</p> </div> <div class="section"> <h4 id="eslint設定ファイル追加">eslint設定ファイル追加</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .eslintrc.json</span> <span class="synConstant">{</span> <span class="synConstant"> &quot;extends&quot;: [</span> <span class="synConstant"> &quot;standard&quot;,</span> <span class="synConstant"> &quot;plugin:prettier/recommended&quot;</span> <span class="synConstant"> ],</span> <span class="synConstant"> &quot;plugins&quot;: [</span> <span class="synConstant"> &quot;react&quot;,</span> <span class="synConstant"> &quot;prettier&quot;</span> <span class="synConstant"> ],</span> <span class="synConstant"> &quot;parser&quot;: &quot;babel-eslint&quot;,</span> <span class="synConstant"> &quot;parserOptions&quot;: {},</span> <span class="synConstant"> &quot;env&quot;: {</span> <span class="synConstant"> &quot;browser&quot;: true,</span> <span class="synConstant"> &quot;es6&quot;: true</span> <span class="synConstant"> },</span> <span class="synConstant"> &quot;globals&quot;: {</span> <span class="synConstant"> &quot;it&quot;: false</span> <span class="synConstant"> },</span> <span class="synConstant"> &quot;rules&quot;: {</span> <span class="synConstant"> &quot;prettier/prettier&quot;: &quot;error&quot;,</span> <span class="synConstant"> &quot;react/jsx-uses-react&quot;: 1,</span> <span class="synConstant"> &quot;react/jsx-uses-vars&quot;: 1,</span> <span class="synConstant"> &quot;no-console&quot;: &quot;warn&quot;,</span> <span class="synConstant"> // warning Definition for rule 'jsx-a11y/href-no-hash' was not found に対応</span> <span class="synConstant"> &quot;jsx-a11y/href-no-hash&quot;: &quot;off&quot;,</span> <span class="synConstant"> &quot;jsx-a11y/anchor-is-valid&quot;: &quot;off&quot;</span> <span class="synConstant"> }</span> <span class="synConstant">}</span> <span class="synStatement">EOF</span> </pre> </div> <div class="section"> <h4 id="eslintの除外設定ファイル追加">eslintの除外設定ファイル追加</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .eslintignore</span> <span class="synConstant">node_modules</span> <span class="synConstant">**/*.min.js</span> <span class="synConstant">src/registerServiceWorker.js</span> <span class="synStatement">EOF</span> </pre><p>registerServiceWorker.jsを追加しているのは、自動生成されるregisterServiceWorker.jsがJavaScript Standard Styleに準拠しておらず、エラーが出まくるためです。。。</p> </div> <div class="section"> <h4 id="prettier設定ファイル追加">prettier設定ファイル追加</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .prettierrc</span> <span class="synConstant">{</span> <span class="synConstant"> &quot;singleQuote&quot;: false,</span> <span class="synConstant"> &quot;trailingComma&quot;: &quot;es5&quot;,</span> <span class="synConstant"> &quot;semi&quot;: false</span> <span class="synConstant">}</span> <span class="synStatement">EOF</span> </pre><p>prettierに何をさせるかのオプションです。オプション一覧は以下に掲載されています。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprettier.io%2Fdocs%2Fen%2Foptions.html" title="Options · Prettier" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://prettier.io/docs/en/options.html">prettier.io</a></cite><br /> </p> </div> <div class="section"> <h4 id="packagejsonにscript追加">package.jsonにscript追加</h4> <div class="section"> <h5 id="lint実行コマンドを追加">lint実行コマンドを追加</h5> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">lint</span>&quot;: &quot;<span class="synError">eslint</span> --<span class="synError">fix</span> <span class="synError">src/**/</span>*.<span class="synError">js</span>&quot; </pre><p> --fixオプションを付けると、prettierの自動整形+eslintの軽い整形を自動保存してくれるようになります。fixを付けないと、ただlintがエラー・警告を発し、ソースコードに編集は入らない挙動になります。</p> </div> <div class="section"> <h5 id="ESLintとPrettierの競合チェックコマンドを追加">ESLintとPrettierの競合チェックコマンドを追加</h5> <p>eslintの設定とprettierの設定が競合していないかをチェックするコマンドも用意されています。<br /> <a href="https://github.com/alexjoverm/tslint-config-prettier#cli-helper-toolhttps://github.com/prettier/eslint-config-prettier#cli-helper-tool">https://github.com/alexjoverm/tslint-config-prettier#cli-helper-toolhttps://github.com/prettier/eslint-config-prettier#cli-helper-tool</a></p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">eslint-check</span>&quot;: &quot;<span class="synError">eslint</span> --<span class="synError">print</span>-<span class="synError">config</span> .<span class="synError">eslintrc</span>.<span class="synError">js</span> | <span class="synError">eslint</span>-<span class="synError">config</span>-<span class="synError">prettier</span>-<span class="synError">check</span>&quot; </pre> </div> </div> <div class="section"> <h4 id="lintを実行する">lintを実行する</h4> <p>lintを実行すると、prettier -> eslintの順に実行されます。つまり、ソースコードの整形+保存 → lintで書式チェック、という順に処理が行われます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn lint </pre> </div> </div> <div class="section"> <h3 id="Typescript版tslint--prettierの設定">Typescript版:tslint + prettierの設定</h3> <div class="section"> <h4 id="create-react-appをグローバルにインストール-1">create-react-appをグローバルにインストール</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>npm install <span class="synSpecial">-g</span> create-react-app </pre> </div> <div class="section"> <h4 id="Typescriptでプロジェクトを生成">Typescriptでプロジェクトを生成</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>create-react-app <span class="synSpecial">--scripts-version=react-scripts-ts</span> tslint-prettier-example </pre> </div> <div class="section"> <h4 id="エディタ設定-1">エディタ設定</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">cd</span> tslint-prettier-example cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .editorconfig</span> <span class="synConstant"># Editor configuration, see http://editorconfig.org</span> <span class="synConstant">root = true</span> <span class="synConstant">[*]</span> <span class="synConstant">charset = utf-8</span> <span class="synConstant">indent_style = space</span> <span class="synConstant">indent_size = 2</span> <span class="synConstant">insert_final_newline = true</span> <span class="synConstant">trim_trailing_whitespace = true</span> <span class="synConstant">[*.md]</span> <span class="synConstant">max_line_length = off</span> <span class="synConstant">trim_trailing_whitespace = false</span> <span class="synStatement">EOF</span> </pre> </div> <div class="section"> <h4 id="tslintとprettierのモジュールをインストール">tslintとprettierのモジュールをインストール</h4> <p><a href="https://github.com/ikatyang/tslint-plugin-prettier">https://github.com/ikatyang/tslint-plugin-prettier</a><br /> <a href="https://github.com/alexjoverm/tslint-config-prettier">https://github.com/alexjoverm/tslint-config-prettier</a></p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> prettier tslint-plugin-prettier tslint-config-prettier tslint-config-standard </pre><p>tslint-plugin-prettierは、tslintを実行すると、prettier → tslintという順番で実行してくれるモジュールです。<br /> tslint-config-prettierは、prettierが整形した部分をtslintに無視させるモジュールです。(prettierの整形結果をeslintが怒る事があるので)</p> </div> <div class="section"> <h4 id="tslintの設定ファイル追加">tslintの設定ファイル追加</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; tslint.json</span> <span class="synConstant">{</span> <span class="synConstant"> &quot;rulesDirectory&quot;: [</span> <span class="synConstant"> &quot;tslint-plugin-prettier&quot;</span> <span class="synConstant"> ],</span> <span class="synConstant"> &quot;extends&quot;: [</span> <span class="synConstant"> &quot;tslint-config-standard&quot;,</span> <span class="synConstant"> &quot;tslint-config-prettier&quot;</span> <span class="synConstant"> ],</span> <span class="synConstant"> &quot;rules&quot;: {</span> <span class="synConstant"> &quot;prettier&quot;: true</span> <span class="synConstant"> }</span> <span class="synConstant">}</span> <span class="synStatement">EOF</span> </pre><p>tslint-config-standardはJavascript標準スタイル(皆で決めた標準lintルール)のtslint版です。例えば「function hoge(){}」で「hoge」と「()」の間にスペースが無い場合はNGとする、といったルール集です。<br /> <a href="https://standardjs.com/">JavaScript Standard Style</a><br /> 標準スタイルにはgoogle版やairbnb版等があります。任意に変更して下さい。</p> </div> <div class="section"> <h4 id="prettier設定ファイル追加-1">prettier設定ファイル追加</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>cat<span class="synStatement">&lt;&lt;EOF</span><span class="synConstant"> &gt; .prettierrc</span> <span class="synConstant">{</span> <span class="synConstant"> &quot;singleQuote&quot;: false,</span> <span class="synConstant"> &quot;trailingComma&quot;: &quot;es5&quot;,</span> <span class="synConstant"> &quot;semi&quot;: false</span> <span class="synConstant">}</span> <span class="synStatement">EOF</span> </pre><p>prettierに何をさせるかのオプションです。オプション一覧は以下に掲載されています。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprettier.io%2Fdocs%2Fen%2Foptions.html" title="Options · Prettier" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://prettier.io/docs/en/options.html">prettier.io</a></cite><br /> </p> </div> <div class="section"> <h4 id="packagejsonにscript追加-1">package.jsonにscript追加</h4> <div class="section"> <h5 id="lint実行コマンドを追加-1">lint実行コマンドを追加</h5> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">lint</span>&quot;: &quot;<span class="synConstant">tslint -c ./tslint.json --exclude **/*.d.ts --exclude ./node_modules --project . --fix **/*.tsx</span>&quot;, </pre> <ul> <li> <ul> <li>fixオプションを付けると、prettierの自動整形+tslintの軽い整形を自動保存してくれるようになります。fixを付けないと、ただlintがエラー・警告を発し、ソースコードに編集は入らない挙動になります。</li> </ul></li> </ul> </div> <div class="section"> <h5 id="TSLintとPrettierの競合チェックコマンドを追加">TSLintとPrettierの競合チェックコマンドを追加</h5> <p>tslintの設定とprettierの設定が競合していないかをチェックするコマンドも用意されています。<br /> <a href="https://github.com/alexjoverm/tslint-config-prettier#cli-helper-tool">https://github.com/alexjoverm/tslint-config-prettier#cli-helper-tool</a></p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">tslint-check</span>&quot;: &quot;<span class="synError">tslint</span>-<span class="synError">config</span>-<span class="synError">prettier</span>-<span class="synError">check</span> ./<span class="synError">tslint</span>.<span class="synError">json</span>&quot; </pre> </div> </div> <div class="section"> <h4 id="lintを実行する-1">lintを実行する</h4> <p>lintを実行すると、prettier -> tslintの順に実行されます。つまり、ソースコードの整形+保存 → lintで書式チェック、という順に処理が行われます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn lint </pre> </div> </div> <div class="section"> <h3 id="ファイル保存時にPrettierとLintを自動実行する">ファイル保存時にPrettierとLintを自動実行する</h3> <div class="section"> <h4 id="JetBrans-WebStormIntellij-IDEAやGoLandでも同じの設定">JetBrans WebStorm(Intellij IDEAやGoLandでも同じ)の設定</h4> <p>WebStormの設定はPrettier公式が設定例を提示してくれています。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fprettier.io%2Fdocs%2Fen%2Fwebstorm.html" title="WebStorm Setup · Prettier" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://prettier.io/docs/en/webstorm.html">prettier.io</a></cite></p><p>中でも「Auto-save edited files to trigger the watcher」のチェックを外す事を忘れないようにしましょう。これのチェックがついていると、新規関数を追加しようと改行した瞬間Watcherに介入され、絶改行できなくなります。(自分の改行が即座にPrettierに消される)</p><p>File typeを指定できるので、jsxやtsx等の特定の拡張子に限定してWather + Prettierを実行させる事ができます。複数の拡張子を指定した場合は、File Watchersの設定を拡張子毎に複数作成しましょう。</p><p>Prettierのオプション設定ですが、<span class="feature3">Argumentsの実行時引数でオプションをツラツラ書いてしまうとプロジェクト内の.prettierrcと内容が食い違う可能性がある</span>ので、Arguments(Prettierには以下のようにプロジェクト内に配置したPrettier設定ファイルを指定した方が安心できそうです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>--config <span class="synPreProc">$ProjectFileDir</span>$/.prettierrc <span class="synSpecial">--write</span> <span class="synPreProc">$FilePathRelativeToProjectRoot</span>$ </pre><p>「$ProjectFileDir$」にはプロジェクトのルートディレクトリが設定されるようなので、プロジェクトに配置した「.prettierrc」を指定すれば、プロジェクト固有のPrettier整形をWebStormに行わせる事が可能になります。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180420/20180420212313.png" alt="f:id:treeapps:20180420212313p:plain" title="f:id:treeapps:20180420212313p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>File Watchersについては公式の日本語解説に詳細が書かれているので、是非ウォッチしておくとよいと思います。</p><p><a href="https://pleiades.io/help/idea/using-file-watchers.html">&#x30D5;&#x30A1;&#x30A4;&#x30EB;&#x76E3;&#x8996;&#x6A5F;&#x80FD;&#x306E;&#x4F7F;&#x7528; - &#x30D8;&#x30EB;&#x30D7; | IntelliJ IDEA</a></p><br /> <p>一見これで設定が完了したように見えますが、実はこれでは「<span class="feature3">未使用importの整理</span>」が実現できていません。</p><p>これについてはマクロ機能を利用すると簡単に実現できます。既存のcommand + sは「Save all」ですが、「Optimize imports + Save all」にショートカットを書き換えてしまえばよいのです。</p> <ol> <li>File -> Edit -> Macros -> Start Macro Recordingをクリックして録画開始。</li> <li>Code -> Optimize importsをクリック</li> <li>File -> Save Allをクリック</li> <li>File -> Edit -> Macros -> Stop Macro Recordingをクリックして録画終了。マクロ名入力を求められるので「Save All with Optimize Imports」等と命名して保存します。</li> <li>File -> Preferences -> Keymap -> Macros -> Save All with Optimize Imports(自分で付けたマクロ名)をダブルクリック -> Add Keyboard Shortcut -> cmmand + sを指定。既にcommand + sには「Save All」が割り当てられていると警告されますが、removeして上書き保存します。</li> </ol><p>これでファイル保存のcommand + sで、Optimize imports+Save Allが実行され、ついでにFile WatchersによってPrettierも実行されるようになります。</p><p>もしArgumentsの部分に指定ミスがあった場合、File WatchersのPrettier設定時に「Show console」で「On error」を設定している場合(初期はerror)、コンソールに例えば以下のように表示されるので、どう間違えたかを確認し、修正します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>/Users/tree/go/src/tree-maps-go/node_modules/prettier/bin-prettier.js <span class="synSpecial">--find-config-path</span> /Users/tree/go/src/tree-maps-go/.prettierrc <span class="synSpecial">--write</span> client/containers/Test.js <span class="synStatement">[</span>error<span class="synStatement">]</span> Cannot use <span class="synSpecial">--find-config-path</span> with multiple files Process finished with <span class="synStatement">exit</span> code <span class="synConstant">1</span> </pre><p>これで完成です!この設定をすると、以下のような事が自動で行われるようになります。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180420/20180420215512.gif" alt="f:id:treeapps:20180420215512g:plain" title="f:id:treeapps:20180420215512g:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h4 id="Visual-Studio-Codeの設定">Visual Studio Codeの設定</h4> <p>Prettierの拡張機能をインストールします。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180420/20180420223159.png" alt="f:id:treeapps:20180420223159p:plain" title="f:id:treeapps:20180420223159p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>再起動後にprettierの自動実行設定をしますが、今回はワークスペースのみに設定していきます。(共通の設定にすると全jsが自動整形されてしまうので)</p><p>まず、プロジェクト直下に .vscode ディレクトリを作成します。続いて直下にsettings.jsonを作成します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>root  └ .vscode    └ settings.json </pre><p>settings.jsonに以下を記述します。</p> <div class="section"> <h5 id="Typescriptの場合のsettingsjson">Typescriptの場合のsettings.json</h5> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">files.insertFinalNewline</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">javascript.format.enable</span>&quot;: <span class="synConstant">false</span>, &quot;<span class="synStatement">prettier.tslintIntegration</span>&quot;: <span class="synConstant">true</span>, <span class="synError">// for .ts</span> &quot;<span class="synStatement">[typescript]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.codeActionsOnSave</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">source.organizeImports</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synError">// for .tsx</span> &quot;<span class="synStatement">[typescriptreact]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.codeActionsOnSave</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">source.organizeImports</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synError">// for .json with comment</span> &quot;<span class="synStatement">[jsonc]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> </div> <div class="section"> <h5 id="Babelの場合のsettingsjson">Babelの場合のsettings.json</h5> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">files.insertFinalNewline</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">javascript.format.enable</span>&quot;: <span class="synConstant">false</span>, &quot;<span class="synStatement">prettier.eslintIntegration</span>&quot;: <span class="synConstant">true</span>, <span class="synError">// for .js</span> &quot;<span class="synStatement">[javascript]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.codeActionsOnSave</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">source.organizeImports</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synError">// for .jsx</span> &quot;<span class="synStatement">[javascriptreact]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.codeActionsOnSave</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">source.organizeImports</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synError">// for .json with comment</span> &quot;<span class="synStatement">[jsonc]</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">editor.formatOnSave</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">editor.formatOnPaste</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180420/20180420224549.png" alt="f:id:treeapps:20180420224549p:plain" title="f:id:treeapps:20180420224549p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>この設定でTypescriptまたはBabelで、ファイル保存時・ペースト時に自動整形され、且つimportの最適化(未使用importの削除+並び替え)が行われるようになります。ワークスペース設定なので、他のプロジェクトの整形に影響を与える事なく、自動整形が設定されます。.vscode/settings.jsonはgitで管理するといいと思います。</p><p>後はcommand + sをすると、Prettier+ESLint(TSLint)の整形処理が実行されます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180420/20180420224249.gif" alt="f:id:treeapps:20180420224249g:plain" title="f:id:treeapps:20180420224249g:plain" class="hatena-fotolife" itemprop="image"></span></p><p>Prettierの設定は「.prettierrc」を参照してくれているようです。</p><p>未使用importの整理についてはできないっぽい?ようです。(ご存知の方いたら教えてください!)</p> </div> </div> </div> <div class="section"> <h3 id="Babeleslintのエラー警告集">Babel:eslintのエラー・警告集</h3> <div class="section"> <h4 id="Cannot-find-module-eslint-config-standard">Cannot find module 'eslint-config-standard'</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>Cannot <span class="synStatement">find</span> module <span class="synStatement">'</span><span class="synConstant">eslint-config-standard</span><span class="synStatement">'</span> Referenced from: /private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/.eslintrc.json Error: Cannot <span class="synStatement">find</span> module <span class="synStatement">'</span><span class="synConstant">eslint-config-standard</span><span class="synStatement">'</span> Referenced from: /private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/.eslintrc.json at ModuleResolver.resolve <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/util/module-resolver.js:74:19</span><span class="synPreProc">)</span> at resolve <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:515:25</span><span class="synPreProc">)</span> at load <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:584:26</span><span class="synPreProc">)</span> at configExtends.reduceRight <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:421:36</span><span class="synPreProc">)</span> at Array.reduceRight <span class="synPreProc">(</span><span class="synStatement">&lt;</span><span class="synSpecial">anonymous</span><span class="synStatement">&gt;</span><span class="synPreProc">)</span> at applyExtends <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:403:28</span><span class="synPreProc">)</span> at loadFromDisk <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:556:22</span><span class="synPreProc">)</span> at Object.load <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:592:20</span><span class="synPreProc">)</span> at Config.getLocalConfigHierarchy <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config.js:226:44</span><span class="synPreProc">)</span> at Config.getConfigHierarchy <span class="synPreProc">(</span><span class="synSpecial">/private/tmp/</span><span class="synStatement">test</span><span class="synSpecial">/eslint-prettier-example/node_modules/eslint/lib/config.js:180:43</span><span class="synPreProc">)</span> </pre><p>eslint-plugin-standardはeslint-config-standardに依存しているので、eslint-config-standardも一緒にインストールしましょう。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-standard eslint-config-standard </pre> </div> <div class="section"> <h4 id="ESLint-couldnt-find-the-plugin-eslint-plugin-node">ESLint couldn't find the plugin "eslint-plugin-node"</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>Oops! Something went wrong! :<span class="synPreProc">(</span> <span class="synSpecial">ESLint: </span><span class="synConstant">4</span><span class="synSpecial">.</span><span class="synConstant">10</span><span class="synSpecial">.</span><span class="synConstant">0</span><span class="synSpecial">.</span> <span class="synSpecial">ESLint couldn</span><span class="synStatement">'</span><span class="synConstant">t find the plugin &quot;eslint-plugin-node&quot;. This can happen for a couple different reasons:</span> <span class="synConstant">1. If ESLint is installed globally, then make sure eslint-plugin-node is also installed globally. A globally-installed ESLint cannot find a locally-installed plugin.</span> <span class="synConstant">2. If ESLint is installed locally, then it</span><span class="synStatement">'</span><span class="synSpecial">s likely that the plugin isn</span><span class="synStatement">'</span><span class="synConstant">t installed correctly. Try reinstalling by running the following:</span> <span class="synConstant"> npm i eslint-plugin-node@latest --save-dev</span> <span class="synConstant">If you still can</span><span class="synStatement">'</span><span class="synSpecial">t figure out the problem, please </span><span class="synStatement">stop</span><span class="synSpecial"> by https://gitter.im/eslint/eslint to chat with the team.</span> </pre><p>メッセージ通り、eslint-plugin-nodeをインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-node </pre> </div> <div class="section"> <h4 id="ESLint-couldnt-find-the-plugin-eslint-plugin-promise">ESLint couldn't find the plugin "eslint-plugin-promise".</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>Oops! Something went wrong! :<span class="synPreProc">(</span> <span class="synSpecial">ESLint: </span><span class="synConstant">4</span><span class="synSpecial">.</span><span class="synConstant">10</span><span class="synSpecial">.</span><span class="synConstant">0</span><span class="synSpecial">.</span> <span class="synSpecial">ESLint couldn</span><span class="synStatement">'</span><span class="synConstant">t find the plugin &quot;eslint-plugin-promise&quot;. This can happen for a couple different reasons:</span> <span class="synConstant">1. If ESLint is installed globally, then make sure eslint-plugin-promise is also installed globally. A globally-installed ESLint cannot find a locally-installed plugin.</span> <span class="synConstant">2. If ESLint is installed locally, then it</span><span class="synStatement">'</span><span class="synSpecial">s likely that the plugin isn</span><span class="synStatement">'</span><span class="synConstant">t installed correctly. Try reinstalling by running the following:</span> <span class="synConstant"> npm i eslint-plugin-promise@latest --save-dev</span> <span class="synConstant">If you still can</span><span class="synStatement">'</span><span class="synSpecial">t figure out the problem, please </span><span class="synStatement">stop</span><span class="synSpecial"> by https://gitter.im/eslint/eslint to chat with the team.</span> </pre><p>メッセージ通り、eslint-plugin-nodeをインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-promise </pre> </div> <div class="section"> <h4 id="ESLint-couldnt-find-the-plugin-eslint-plugin-import">ESLint couldn't find the plugin "eslint-plugin-import".</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>Oops! Something went wrong! :<span class="synPreProc">(</span> <span class="synSpecial">ESLint: </span><span class="synConstant">4</span><span class="synSpecial">.</span><span class="synConstant">19</span><span class="synSpecial">.</span><span class="synConstant">1</span><span class="synSpecial">.</span> <span class="synSpecial">ESLint couldn</span><span class="synStatement">'</span><span class="synConstant">t find the plugin &quot;eslint-plugin-import&quot;. This can happen for a couple different reasons:</span> <span class="synConstant">1. If ESLint is installed globally, then make sure eslint-plugin-import is also installed globally. A globally-installed ESLint cannot find a locally-installed plugin.</span> <span class="synConstant">2. If ESLint is installed locally, then it</span><span class="synStatement">'</span><span class="synSpecial">s likely that the plugin isn</span><span class="synStatement">'</span><span class="synConstant">t installed correctly. Try reinstalling by running the following:</span> <span class="synConstant"> npm i eslint-plugin-import@latest --save-dev</span> <span class="synConstant">If you still can</span><span class="synStatement">'</span><span class="synSpecial">t figure out the problem, please </span><span class="synStatement">stop</span><span class="synSpecial"> by https://gitter.im/eslint/eslint to chat with the team.</span> </pre><p>メッセージ通り、eslint-plugin-importをインストールします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint-plugin-import </pre> </div> <div class="section"> <h4 id="Configuration-for-rule-indent-is-invalid">Configuration for rule "indent" is invalid:</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint-config-standard/index.js: Configuration <span class="synStatement">for</span> rule <span class="synStatement">&quot;</span><span class="synConstant">indent</span><span class="synStatement">&quot;</span> is invalid: Value <span class="synStatement">&quot;</span><span class="synConstant">[object Object]</span><span class="synStatement">&quot;</span> should NOT have additional properties. Referenced from: /private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/.eslintrc.json Error: /private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint-config-standard/index.js: Configuration <span class="synStatement">for</span> rule <span class="synStatement">&quot;</span><span class="synConstant">indent</span><span class="synStatement">&quot;</span> is invalid: Value <span class="synStatement">&quot;</span><span class="synConstant">[object Object]</span><span class="synStatement">&quot;</span> should NOT have additional properties. Referenced from: /private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/.eslintrc.json at validateRuleOptions <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-validator.js:113:15<span class="synPreProc">)</span> at Object.keys.forEach.id <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-validator.js:153:9<span class="synPreProc">)</span> at Array.forEach <span class="synPreProc">(</span><span class="synStatement">&lt;</span>anonymous<span class="synStatement">&gt;</span><span class="synPreProc">)</span> at validateRules <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-validator.js:152:30<span class="synPreProc">)</span> at Object.validate <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-validator.js:230:5<span class="synPreProc">)</span> at loadFromDisk <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:549:19<span class="synPreProc">)</span> at load <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:592:20<span class="synPreProc">)</span> at configExtends.reduceRight <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:421:36<span class="synPreProc">)</span> at Array.reduceRight <span class="synPreProc">(</span><span class="synStatement">&lt;</span>anonymous<span class="synStatement">&gt;</span><span class="synPreProc">)</span> at applyExtends <span class="synPreProc">(</span>/private/tmp/<span class="synStatement">test</span>/eslint-prettier-example/node_modules/eslint/lib/config/config-file.js:403:28<span class="synPreProc">)</span> </pre><p>これは、create-react-appに内蔵されたeslintのバージョンでは不足していると起きるようです。</p><p>create-react-app自体のバージョンと、内蔵eslintのバージョンはそれぞれ以下の通りです。この組み合わせではエラーが起きました。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ create-react-app <span class="synSpecial">--version</span> <span class="synConstant">1</span>.<span class="synConstant">5</span>.<span class="synConstant">2</span> $ eslint <span class="synSpecial">--version</span> v4.<span class="synConstant">10</span>.<span class="synConstant">0</span> </pre><p>なので、ちょっと嫌ですが別途eslintの最新バージョンをインストールしてしまいます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>yarn add <span class="synSpecial">-D</span> eslint </pre><p>現状では「v4.19.1」インストールされました。このバージョンであればエラーは出ませんでした。</p> </div> <div class="section"> <h4 id="prettier-eslintは使わないの">prettier-eslintは使わないの?</h4> <p><blockquote class="twitter-tweet" data-lang="HASH(0x55fa461fb038)"><p lang="en" dir="ltr">PSA: I&#39;m no longer using prettier-eslint. I use raw prettier and disable all eslint style rules.<br><br>My life has been better ever since...</p>&mdash; Kent C. Dodds (@kentcdodds) <a href="https://twitter.com/kentcdodds/status/913760103118991361?ref_src=twsrc%5Etfw">September 29, 2017</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p><p>ネット上でprettier-eslintを使った例が見られますが、prettier-eslintの作者が↑のようにもう使ってないよ、と言っているので、使わなくて大丈夫です。</p><p>今後はeslint-plugin-prettier と eslint-config-prettierの組み合わせを使えば特に問題ないかと思われます。</p> </div> </div> <div class="section"> <h3 id="Typescripttslintのエラー警告集">Typescript:tslintのエラー・警告集</h3> <div class="section"> <h4 id="yarn-lintで大量のrule-requires-type-informationが発生する">yarn lintで大量のrule requires type informationが発生する</h4> <pre class="code lang-sh" data-lang="sh" data-unlink>Warning: The <span class="synStatement">'</span><span class="synConstant">await-promise</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">no-unused-variable</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">no-use-before-declare</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">return-undefined</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">no-floating-promises</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">no-unnecessary-qualifier</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">no-unnecessary-type-assertion</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. Warning: The <span class="synStatement">'</span><span class="synConstant">strict-type-predicates</span><span class="synStatement">'</span> rule requires <span class="synStatement">type</span> information. </pre><p>projectを指定すると直ります。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&quot;</span><span class="synConstant">lint</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">tslint -c tslint.json --exclude **/*.d.ts --exclude node_modules --fix **/*.tsx</span><span class="synStatement">&quot;</span> </pre><p>↓</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">&quot;</span><span class="synConstant">lint</span><span class="synStatement">&quot;</span>: <span class="synStatement">&quot;</span><span class="synConstant">tslint -c tslint.json --exclude **/*.d.ts --exclude node_modules --project . --fix **/*.tsx</span><span class="synStatement">&quot;</span> </pre> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>これをまとめるの、実はかなり苦戦しました。(単に私が最近のフロントエンド事情・歴史を知らないからですが)もう使われていないモジュールの例に惑わされたり、バージョン違いによる謎エラーなどなど。</p><p>こうして記事にまとめておかないと、1週間後に全部忘れて「eslint + prettier動かねー!「tslint+prettier動かねー!」と発狂して全てを諦めてしまいそうです。</p><p>lintは最初は調査や設定に手間がかかりますが、lint + prettierの効果は絶大で、<span class="feature3">チームの内で横行するオレオレコードフォーマットルール</span>を撲滅したり、<span class="feature3">しょうもない凡ミスをlintで気づく</span>事を自動化する事ができます。</p><p>また、本来コードレビューでは設計やアーキテクチャが問題ないかを見て欲しいのに、<br /> 「<span class="feature3">ここにスペースを追加して下さい</span>」<br /> 「<span class="feature3">ファイル末尾に空行を入れて下さい</span>」<br /> 「<span class="feature3">1行が長すぎるので改行を挟んで下さい</span>」<br /> 「<span class="feature3">行末にセミコロンが抜けています</span>」<br /> 等という指摘に終始されてしまい、本来のレビュー目的が全スルーされる事がよくありますが、これを防ぐ事に繋がります。</p><p>最近私は運悪く実際にそれを経験してしまいましたが、このコードレビューのしょうもない指摘はプロジェクトの品質を下げる事に繋がるだけでなく、退屈な修正作業によってモチベーションが低下する事にも繋がってしまうので、是非設定しておきたいですね!</p> </div> treeapps GAE/Node.js Standard Environmentは現在pre betaのようです hatenablog://entry/17391345971634856454 2018-04-13T23:15:09+09:00 2018-05-02T17:08:20+09:00 !!! <p>!!!</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170917/20170917230836.png" alt="f:id:treeapps:20170917230836p:plain" title="f:id:treeapps:20170917230836p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>GAE/Node.jsですが、既にFlexible Environment版のランタイムは存在しますね。</p><p>ところがです。</p><p>今日何気なく調べていると・・・</p><p><span class="feature1 font20">GAE/Node.js Standard Environmentは現在Early Accessである</span></p><p>ことが解りました。</p> <ul class="table-of-contents"> <li><a href="#GAENodejsのデモサイト">GAE/Node.jsのデモサイト</a></li> <li><a href="#Wappalyzerで確認">Wappalyzerで確認</a></li> <li><a href="#スピンアップフレームワーク初期化画面表示速度は">スピンアップ〜フレームワーク初期化〜画面表示速度は?</a></li> <li><a href="#GAENodejs-Standard-Environmentが正式対応したら">GAE/Node.js Standard Environmentが正式対応したら</a><ul> <li><a href="#SPAサイトのSSR化">SPAサイトのSSR化</a></li> <li><a href="#SSR標準のJavascriptフレームワークが利用可能に">SSR標準のJavascriptフレームワークが利用可能に!</a></li> <li><a href="#サーバサイドとフロントのコード共通化">サーバサイドとフロントのコード共通化</a></li> <li><a href="#SSRは難しい">SSRは難しい</a><ul> <li><a href="#SEOの定数定義の2重管理">SEOの定数定義の2重管理</a></li> <li><a href="#htmlの2重管理">htmlの2重管理</a></li> <li><a href="#サーバとクライアントのhtdocsやURLを合わせる手間">サーバとクライアントのhtdocsやURLを合わせる手間</a></li> <li><a href="#Go-templateのjs読み込みURLをbundlejsにできない問題">Go templateのjs読み込みURLを/bundle.jsにできない問題</a></li> </ul> </li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="GAENodejsのデモサイト">GAE/Node.jsのデモサイト</h3> <p>GAEでnext.js動かす方法あるかな〜?と調べていると、以下のgitリポジトリを発見しました。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fblainegarrett%2Fnode-next-gae-demo" title="blainegarrett/node-next-gae-demo" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/blainegarrett/node-next-gae-demo">github.com</a></cite></p><p>ここをよーく見ると、</p> <blockquote> <p>A working example of running next.js on Google AppEngine's Node Standard Environment Early Access Program.</p> </blockquote> <p>とと書かれているではありませんか!</p><p>更に、こちらのgitリポジトリのリンク先の資料を見ると、GAE/Node.jsはPre-Betaであると書かれています。</p> <blockquote cite="https://docs.google.com/presentation/d/1pUc8VbT4J5ca4qe2zIbqezO6EhLER6E_e5WgsGitDr0/edit#slide=id.g35027a9a40_0_62"> <p>App Engine<br /> Early Access Program<br /> Pre-Beta<br /> Sign up for Google Cloud<br /> Request Invite to EAP <br /> (tinyurl.com/gaenode)</p> <cite><a href="https://docs.google.com/presentation/d/1pUc8VbT4J5ca4qe2zIbqezO6EhLER6E_e5WgsGitDr0/edit#slide=id.g35027a9a40_0_62">https://docs.google.com/presentation/d/1pUc8VbT4J5ca4qe2zIbqezO6EhLER6E_e5WgsGitDr0/edit#slide=id.g35027a9a40_0_62</a></cite> </blockquote> <p>↑のリンク <a href="https://tinyurl.com/gaenode">https://tinyurl.com/gaenode</a> より Early Access Program に申し込むことができるようです!</p> </div> <div class="section"> <h3 id="Wappalyzerで確認">Wappalyzerで確認</h3> <p>更に更にgitリポジトリに<span class="feature1">GAE/Node.js + Next.jsのデモ</span>が公開されているではありませんか!!</p><p>ChromeのアドオンWappalyzerで技術要素をチェックして見ると、バッチリ以下である事が確認できます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180413/20180413214631.png" alt="f:id:treeapps:20180413214631p:plain" title="f:id:treeapps:20180413214631p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3 id="スピンアップフレームワーク初期化画面表示速度は">スピンアップ〜フレームワーク初期化〜画面表示速度は?</h3> <p>GAEといえばスピンアップが真っ先に心配になりますね。</p><p>以前GAE/Golangについてのスピンアップについては以下にまとめたので合わせてご覧下さい。</p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2016%2F06%2F05%2F130329" title="GAE/go+ginとGAE/java+servletでそれぞれスピンアップの速度差をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2016/06/05/130329">www.bunkei-programmer.net</a></cite></p><p>GAE/Java8のスピンアップについては以下をご覧下さい。</p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F09%2F24%2F220531" title="GAE/Java8+kotlin+Spark Frameworkでスピンアップからjsonが返るまでの速度をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2017/09/24/220531">www.bunkei-programmer.net</a></cite></p><p>肝心のGAE/Node.jsのスピンアップ速度ですが、体感ですが Golang > Node.js > Java8 という感じがしました。</p><p>流石にJavaよりは速いが、Golangには負ける、というくらいです。とはいえ結構速いのでは?と思われます。</p><p>GAE/Node.jsはまだpre betaの段階ですが、私がJava8のbeta時にスピンアップ速度を確認し、その後GAになった後にも計測しましたが、違いは見受けられなかったので、今現在のスピンアップ速度がそのままGA後にも適用されると考えていいかもしれません。</p> </div> <div class="section"> <h3 id="GAENodejs-Standard-Environmentが正式対応したら">GAE/Node.js Standard Environmentが正式対応したら</h3> <div class="section"> <h4 id="SPAサイトのSSR化">SPAサイトのSSR化</h4> <p>今までGAE Standard Environment上のSPAサイトのSSR化は鬼門でした。理由は簡単で、ほとんどのフレームワークが実質node.jsが必須になっているためです。</p><p>Angular universalは実は.net coreを正式にSSRサポートしていますが、GAE Standard Environmentのランタイムは.net coreをサポートしていません。</p><p>いくつかStandard EnvironmentでGolangランタイムを使ったSSRを動かしている例があるのですが、通常の方法ではなく、goja等のjavascriptパーサを利用して実現していたり、ちょっと特殊な運用が必要のようでした。</p><p>そもそもこんな事が必要なのは、Standard EnvironmentがNode.jsをサポートしていないためです。</p> </div> <div class="section"> <h4 id="SSR標準のJavascriptフレームワークが利用可能に">SSR標準のJavascriptフレームワークが利用可能に!</h4> <p>ReactならZeitのNext.js、AngularならAngular universal、VueならNuxt.js。</p><p>これらを使う事ができれば、SSR化が格段に楽になりますね!</p> </div> <div class="section"> <h4 id="サーバサイドとフロントのコード共通化">サーバサイドとフロントのコード共通化</h4> <p>所謂Isomophicが可能になり、Golangとjsでそれぞれバリデーションロジックを書かなくてよくなったり、定数の共通化が可能になったり、嬉しいことが一杯ですね。</p> </div> <div class="section"> <h4 id="SSRは難しい">SSRは難しい</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fshibukawa%2Fitems%2F184d3101946ec4fa98c1" title="サーバーサイドレンダリング不要論 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/shibukawa/items/184d3101946ec4fa98c1">qiita.com</a></cite></p><p>サーバ側ではbrowserのwindowオブジェクトが無くてヌルポったり、労力に見合わないし、そもそもいらないんじゃない?という論です。</p><p>しかしですね、ここはGAEです。</p><p>つい先日以下の記事を書きました。</p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2018%2F04%2F01%2F024355" title="SSR無しのReact・Angular製のSPAサイトはGooglebotにどれくらい認識されるのか? - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2018/04/01/024355">www.bunkei-programmer.net</a></cite></p><p>ReactとAngularのSSR無しのSPAサイトがGooglebotにどれくらい認識されるのか(レンダリングできるのか)?という記事です。</p><p>ここではGolang+Reatは認識率高い、Angular+Java8はボロッボロ、という結果に終わりました。</p><p><span class="feature1">後日AngularサイトをGolang化したところ、何とGooglebotの認識率が上がりました</span>。</p><p>Java8の時はサイトマップxmlもほとんどインデキシングしてくれなかったのに、Golang化した途端にインデキシングされはじめました。</p><p>では、GolangとJava8の違いは何でしょうか?考えられる一つの答えは「<span class="feature1">スピンアップからレンダリングが完了するまでの時間</span>」です。Golang化した事で数倍レンダリング完了速度が上がった事で、Googlebotの認識率が上がったようなのです。</p><p>という事は、<span class="feature3">GAEでSSR無しのSPAサイトを運用してGoogleに認識して貰うにはGolangが最適だが、SSRしていないので認識されるかは運次第。しかしGolangの場合はサーバとクライアント側でコードが全く異なるので、コードの共通化は難しい</span>。</p><p>というジレンマが生じます。</p><p>私が開発しているReactサイトの場合、以下の問題を抱えています。</p><p>前提条件として、Reactサイトはtitle・description・ogpのみSSRし、ReactのSSRは行っていません。</p> <div class="section"> <h5 id="SEOの定数定義の2重管理">SEOの定数定義の2重管理</h5> <p>SEO(title, description, ogp)の定数群をGoとReactで別々に定義しています。定数をGo側で定義してSEOのAPIを作ってReactから非同期で参照してもいいのですが、それだと都度HTTPリクエストが発生してしまいます。</p><p>通信が遅延するとjsonが返るのが遅れ、React側のtitleタグの変更処理が遅延し、GAに送信されるtitleタグが1画面古くなったり、ブックマークタイトル名が古くなったりする弊害が発生してしまいます。</p><p>これが嫌なので、現状サーバとクライアント側で違う実装で定数を2重管理しています。サーバ側の定数は画面の初回表示時のみ使用し、初回表示後はReactがjsの定数を使ってtitleタグ等を変更しています。</p> </div> <div class="section"> <h5 id="htmlの2重管理">htmlの2重管理</h5> <p>Go templateの書式 {{ .Hoge }} をReactは解釈できないため、Goが返すhtmlとReactで返すhtmlが共用できず、無駄に2重管理が発生しています。</p><p>React用のhtmlを削除してwebpack-dev-serverにindex.htmlを自動生成させてもいいのですが、それだとviewportやogpタグを出力するコードが別途必要だったりして、結局別の2重管理が発生しているだけで、あまりよろしくありません。</p> </div> <div class="section"> <h5 id="サーバとクライアントのhtdocsやURLを合わせる手間">サーバとクライアントのhtdocsやURLを合わせる手間</h5> <p>webpack-dev-serverとGoのhtdocsのパスをあわせるのが面倒です。webpack-dev-server側のパスの解釈がちょっと癖があって、publicPathを/にするとwatch対象が/になってしまい、全ファイルがライブリロード対象になってしまったり、publicPathを/assets/等とするとトップページが/assets/になってしまったり。</p><p>現状はwebpack-dev-server側でproxyを使用する事でGo側と同様のパスを実現していますが、これは毎回唸る問題なので回避したいです。</p> </div> <div class="section"> <h5 id="Go-templateのjs読み込みURLをbundlejsにできない問題">Go templateのjs読み込みURLを/bundle.jsにできない問題</h5> <p>ローカルでは開発速度を重視したいので、通常はwebpack-dev-serverのport4200で開発(SEO等、SSRできる部分は確認不能)し、SEOを確認する際はGoを通す8080で開発しています。しかしGoを通した開発の場合はwebpack-dev-serverも起動しないとそもそもjsが存在しないので画面が参照できません。</p><p>更に、webpack-dev-serverをオンメモリで開発しようとすると、jsの実体が生成されません(生成すると重い)。すると、Go経由でjsを参照しようとしてもjsの実体が存在しないので、<a href="http://localhost:4200/bundle.js">http://localhost:4200/bundle.js</a> のような&lt;script&gt;を書かないといけません。するとローカル環境の場合は <a href="http://localhost:4200/bundle.js">http://localhost:4200/bundle.js</a> を、productionの場合は/bundle.jsに、といった分岐処理が無駄に発生します。</p><p>分岐を記述するのはGo template側になるので、この時点でGoのhtmlとwebpack-dev-serverのhtmlを共用できません。</p><br /> <p>ざっと考えただけでこれくらいでてきます。一応なんとか全て解決はできていますが、あまり良い状況ではないですね。正直面倒だし2重管理は危険だし、何とかしたいです。</p> </div> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>SSRがまるで銀の弾丸のような書き方をしましたが、勿論そんな事はありません。</p><p>しかし、GAE上での話に限定すると、やる価値は十分にあるのでは?と思います。検索エンジンの事を無視していい場合はSSRなんて不要なのですけどね。</p><p>なんにせよ、現状GAEでSPAサイトを運用すると色々な問題を抱えざるを得ないので、GAE/Node.jsには速く正式リリースを迎えて欲しいですね!</p> </div> treeapps PWAで「ホーム画面に追加」が表示されない時に確認する事 hatenablog://entry/17391345971633563233 2018-04-09T12:50:15+09:00 2018-04-09T13:06:02+09:00 以外としょうもない理由で表示されないんですよね〜 <p>以外としょうもない理由で表示されないんですよね〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170310/20170310234605.png" alt="f:id:treeapps:20170310234605p:plain" title="f:id:treeapps:20170310234605p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>最近PWAが徐々に浸透してきましたね。</p><p>PWAは概念的な言葉なので、いざ具体的にPWAに対応しようとすると、「PWAってそもそも何だ?」となります。</p><p>ネットで調べると、ブラウザでサイトを表示した時に「ホーム画面に追加」ダイアログが表示されるようにする事もPWAの一つである事が解ります。</p><p>しかし、いざ対応してみたけど、何故か「ホーム画面に追加ダイアログ」が一向に表示されません。</p><p>今回はその原因を少しだけまとめてみようと思います。</p> <ul class="table-of-contents"> <li><a href="#manifestjsonがhtmlソース内に定義されていない">manifest.jsonがhtmlソース内に定義されていない</a></li> <li><a href="#ServiceWorkerが登録されていない">ServiceWorkerが登録されていない</a></li> <li><a href="#サイトがTLSに対応していない">サイトがTLSに対応していない</a></li> <li><a href="#サイト訪問回数が2回ではない">サイト訪問回数が2回ではない</a></li> <li><a href="#サイトの訪問間隔が5分以上ではない">サイトの訪問間隔が5分以上ではない</a></li> <li><a href="#manifestjson内のアイコン画像が404である">manifest.json内のアイコン画像が404である</a><ul> <li><a href="#ブラウザやcurlコマンド等でアイコン画像のURLを表示してみる">ブラウザやcurlコマンド等でアイコン画像のURLを表示してみる</a></li> <li><a href="#DevToolsのApplicationタブにあるManifestメニューでmanifestjson情報を確認する">DevToolsのApplicationタブにある、Manifestメニューでmanifest.json情報を確認する</a></li> </ul> </li> <li><a href="#おまけ">おまけ</a><ul> <li><a href="#manifestjsonのshort_nameが12文字を超えたらどうなるか">manifest.jsonのshort_nameが12文字を超えたらどうなるか?</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="manifestjsonがhtmlソース内に定義されていない">manifest.jsonがhtmlソース内に定義されていない</h3> <p>そもそも「ホーム画面に追加」を表示するエントリーポイントは、htmlにmanifest.jsonの定義が存在する事です。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">link</span><span class="synIdentifier"> </span><span class="synType">rel</span><span class="synIdentifier">=</span><span class="synConstant">&quot;manifest&quot;</span><span class="synIdentifier"> </span><span class="synType">href</span><span class="synIdentifier">=</span><span class="synConstant">&quot;/manifest.json&quot;</span><span class="synIdentifier">&gt;</span> </pre><p>もしこの定義が無いページがあれば、全ページに入るように修正しましょう。</p> </div> <div class="section"> <h3 id="ServiceWorkerが登録されていない">ServiceWorkerが登録されていない</h3> <p>よくsw.js等を見かけると思いますが、あれです。</p><p>ServiceWorkerについては、以下の公式リファレンスを参考にして下さい。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdevelopers.google.com%2Fweb%2Ffundamentals%2Fprimers%2Fservice-workers%2F%3Fhl%3Dja" title="Service Worker の紹介  |  Web  |  Google Developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://developers.google.com/web/fundamentals/primers/service-workers/?hl=ja">developers.google.com</a></cite></p><p>手動でServiceWorkerを登録する他に、workbox-swを利用したり、Angularフレームワークを利用しているならangular-service-workerを使う、等によって自動生成する手法もあります。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdevelopers.google.com%2Fweb%2Ftools%2Fworkbox%2F" title="Workbox  |  Google Developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://developers.google.com/web/tools/workbox/">developers.google.com</a></cite></p><p><a href="https://angular.io/guide/service-worker-intro">https://angular.io/guide/service-worker-intro</a><cite class="hatena-citation"><a href="https://angular.io/guide/service-worker-intro">angular.io</a></cite><br /> </p> </div> <div class="section"> <h3 id="サイトがTLSに対応していない">サイトがTLSに対応していない</h3> <blockquote cite="https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja"> <p>HTTPS 経由で配信されている(Service Worker を使用するための要件)。</p> <cite><a href="https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja">https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja</a></cite> </blockquote> <p>もし自サイトがhttpsでアクセスできないなら、そもそもServiceWorkerが動かないので、ホーム画面に追加機能は利用できません。</p> </div> <div class="section"> <h3 id="サイト訪問回数が2回ではない">サイト訪問回数が2回ではない</h3> <p>PWAの現行の仕様では、「<span class="feature1">サイトへの訪問回数が2回以上</span>」なのです。</p><p>もし1回目の訪問の場合、残念ながらダイアログは表示されません。</p><p>これは恐らく「<span class="feature1">2回訪問するという事はこのサイトに興味を持っているのでダイアログを表示する。初回訪問者の場合は、にいきなりダイアログを表示してもただの押し売りのような形になって二度と訪問して貰えなくなる可能性があるので表示しない」というような事を考慮しての仕様なのだと思われます。</p> </div> <div class="section"> <h3 id="サイトの訪問間隔が5分以上ではない">サイトの訪問間隔が5分以上ではない</h3> <p>PWAの現行の仕様では、「<span class="feature1">サイトへの訪問回数が2回以上、更にそれは5分以上の間隔を空けないといけない</span>」なのです。</p> <blockquote cite="https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja"> <p>2 回以上のアクセスがあり、そのアクセスに 5 分以上の間隔がある。</p> <cite><a href="https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja">https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja</a></cite> </blockquote> <p>これも推測ですが、「<span class="feature1">初回訪問者が画面表示後に誤ってリロードする可能性があり、それは興味を持っての事なのか、事故なのか判別できない。5分後の再訪であれば、興味を持っているユーザと判断しても大丈夫だろう</span>」という感じなのだと思われます。</p><p>・・・5分間隔と公式リファレンスに記載されていますが、私が試したところ、5分間隔でなくてもホーム画面に追加は表示されました。今は5分ではないのかもしれませんね。</p> </div> <div class="section"> <h3 id="manifestjson内のアイコン画像が404である">manifest.json内のアイコン画像が404である</h3> <p>これは私が実際に嵌った事なのですが、manifest.jsonのアイコン画像の定義は、以下のようにしますよね。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">icons</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">src</span>&quot;: &quot;<span class="synConstant">/assets/images/logo/icon_64x64.png</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">sizes</span>&quot;: &quot;<span class="synConstant">64x64</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">src</span>&quot;: &quot;<span class="synConstant">/assets/images/logo/icon_128x128.png</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">sizes</span>&quot;: &quot;<span class="synConstant">128x128</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">src</span>&quot;: &quot;<span class="synConstant">/assets/images/logo/icon_144x144.png</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">sizes</span>&quot;: &quot;<span class="synConstant">144x144</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">src</span>&quot;: &quot;<span class="synConstant">/assets/images/logo/icon_256x256.png</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">sizes</span>&quot;: &quot;<span class="synConstant">256x256</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">src</span>&quot;: &quot;<span class="synConstant">/assets/images/logo/icon_512x512.png</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">sizes</span>&quot;: &quot;<span class="synConstant">512x512</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> </pre><p>これ自体はいいのですが、<span class="feature3">もしこの画像に404のものが含まれていたらどうでしょう?</span></p><p>答えは「<span class="feature3">ホーム画面に追加は表示されない</span>」です。</p><p><span class="feature3">この404が厄介なのは、Google ChromeのDevToolsのConsoleタブ・Networkタブ・ApplicationタブのServiceWorkerメニューには404が表示されない点にあります</span>。</p><p>この404を確認するには以下の3つの方法があります。</p> <div class="section"> <h4 id="ブラウザやcurlコマンド等でアイコン画像のURLを表示してみる">ブラウザやcurlコマンド等でアイコン画像のURLを表示してみる</h4> <p>単純に以下のURLをブラウザで確認します。<br /> <a href="https://www.string-utility.com/assets/images/logo/icon_144x144.png">https://www.string-utility.com/assets/images/logo/icon_144x144.png</a></p><p>しかしこれだと複数のアイコン画像の確認に手間がかかってしまいます。</p> </div> <div class="section"> <h4 id="DevToolsのApplicationタブにあるManifestメニューでmanifestjson情報を確認する">DevToolsのApplicationタブにある、Manifestメニューでmanifest.json情報を確認する</h4> <p>Manifestメニューはmanifest.json内の全情報を一覧できるので、こちらの確認方法を事をおすすめします。</p><p>もし404のアイコン画像があると、以下のように表示されます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180409/20180409121824.png" alt="f:id:treeapps:20180409121824p:plain" title="f:id:treeapps:20180409121824p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>404になってしまっているアイコン画像が確認できたので、これを修正しましょう。</p><br /> <p>これらに対応して、ブラウザで自サイトを表示すると、以下のように「ホーム画面に追加」が表示されるようになります!</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180409/20180409124611.jpg" alt="f:id:treeapps:20180409124611j:plain" title="f:id:treeapps:20180409124611j:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> </div> <div class="section"> <h3 id="おまけ">おまけ</h3> <div class="section"> <h4 id="manifestjsonのshort_nameが12文字を超えたらどうなるか">manifest.jsonのshort_nameが12文字を超えたらどうなるか?</h4> <p>manifest.jsonのshort_name属性ですが、公式リファレンスには以下のような警告?が書かれています。</p> <blockquote cite="https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated?hl=ja"> <p>ユーザーがホーム画面にウェブアプリを追加すると、アプリ アイコンの下に short_name プロパティがラベルとして表示されます。 short_name が 12 文字を超えると、ホーム画面上で途中までしか表示されません。</p> <cite><a href="https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated?hl=ja">https://developers.google.com/web/tools/lighthouse/audits/manifest-short_name-is-not-truncated?hl=ja</a></cite> </blockquote> <p>12文字を超えてしまうと、Google ChromeのLightHouseでProgressive Web Appsをテストして見ると、以下のようにエラー表示されます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180409/20180409125407.png" alt="f:id:treeapps:20180409125407p:plain" title="f:id:treeapps:20180409125407p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>ではこの状態で「ホーム画面に追加」は機能するでしょうか?</p><p>答えは「<span class="feature1">正常にホーム画面に追加は機能する</span>」です。(もしかしたら機種によって異なる可能性があります)</p><p>私の管理サイトのshort_nameに「StringUtility」という13文字を指定してLightHouseに怒られ、公式リファレンスで言うところの監査に不合格な筈ですが、いざ試したらホーム画面に追加はちゃんと動きますし、13文字表示できているようです。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180409/20180409130446.png" alt="f:id:treeapps:20180409130446p:plain" title="f:id:treeapps:20180409130446p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>機種やOSやフォントサイズによって恐らく表示領域が異なる場合があるので、short_nameの12文字制限は大目に見てくれているのかもしれませんね。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>アイコン画像が404だったのは、実は数ヶ月気づきませんでした・・・「なーんかホーム画面に追加が表示されないな〜?angular-service-workerが悪いのかな〜?調べるの面倒臭いな〜」という感じで放置していたのですが、解決できて良かったです。</p><p>そもそもmanifest.jsonなんて一回書いたらそうそう編集しないので、確認する機会ってほとんど無いのですよね。</p><p>せめてDevToolsのConsoleかNetworkで404が確認できたら楽なのですけどね。</p> </div> treeapps AngularでElectron起動時にトップページが404になってしまう件に対応する hatenablog://entry/17391345971632390078 2018-04-05T02:48:40+09:00 2018-04-05T02:48:40+09:00 当たり前の結果ですが、意識していないと絶対引っかかってしまうのです〜 <p>当たり前の結果ですが、意識していないと絶対引っかかってしまうのです〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170918/20170918135756.png" alt="f:id:treeapps:20170918135756p:plain" title="f:id:treeapps:20170918135756p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>今回はAngularというか、Electronネタになります。</p> <ul class="table-of-contents"> <li><a href="#Electron起動時に起きる事">Electron起動時に起きる事</a></li> <li><a href="#何が起きたのか">何が起きたのか</a></li> <li><a href="#Angularのルーティングを確認する">Angularのルーティングを確認する</a></li> <li><a href="#ルーティング追加後に再確認">ルーティング追加後に再確認</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="Electron起動時に起きる事">Electron起動時に起きる事</h3> <p>ElectronのloadURLにURLではなくhtmlを指定して起動した後、以下のように404になりました。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180405/20180405022241.png" alt="f:id:treeapps:20180405022241p:plain" title="f:id:treeapps:20180405022241p:plain" class="hatena-fotolife" itemprop="image"></span><br /> </p> </div> <div class="section"> <h3 id="何が起きたのか">何が起きたのか</h3> <p>ElectronはChromiumなので、Chrome DevToolsが起動できます。起動してURLを確認したいので、ElectronでloadURLしたhtmlに以下を追加して起動してみます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>&lt;script&gt; console.log(<span class="synStatement">location</span>); &lt;/script&gt; </pre><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180405/20180405022820.png" alt="f:id:treeapps:20180405022820p:plain" title="f:id:treeapps:20180405022820p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>href部分を見ると、「electron.html」になっている事が確認できます。</p> </div> <div class="section"> <h3 id="Angularのルーティングを確認する">Angularのルーティングを確認する</h3> <p>app-routing.module.tsを開き、URLが「/electron.html」の際にどのコンポーネントが使用されるか、確認してみて下さい。</p><p><span class="feature3">Electronを意識していない場合、まず間違いなくルーティングにヒットせず、404に行ってしまうと思います</span>。</p><p>という事で、以下のようにルーティングを追加します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> routes: Routes = <span class="synIdentifier">[</span> <span class="synIdentifier">{</span>path: <span class="synConstant">'character-count'</span>, component: CharacterCountComponent, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, ・・・略・・・ <span class="synIdentifier">{</span>path: <span class="synConstant">''</span>, component: TopComponent, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>path: <span class="synConstant">'**'</span>, component: NotfoundComponent<span class="synIdentifier">}</span>, <span class="synIdentifier">]</span>; </pre><p>↓</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> routes: Routes = <span class="synIdentifier">[</span> <span class="synIdentifier">{</span>path: <span class="synConstant">'character-count'</span>, component: CharacterCountComponent, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, ・・・略・・・ <span class="synIdentifier">{</span>path: <span class="synConstant">''</span>, component: TopComponent, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>path: <span class="synConstant">'electron.html'</span>, redirectTo: <span class="synConstant">'/'</span>, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, <span class="synComment">// ← これを追加!</span> <span class="synIdentifier">{</span>path: <span class="synConstant">'**'</span>, component: NotfoundComponent<span class="synIdentifier">}</span>, <span class="synIdentifier">]</span>; </pre><p>404の設定の前に、以下を追加し、トップページにリダイレクトしてしまいます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink> <span class="synIdentifier">{</span>path: <span class="synConstant">'electron.html'</span>, redirectTo: <span class="synConstant">'/'</span>, pathMatch: <span class="synConstant">'full'</span><span class="synIdentifier">}</span>, </pre> </div> <div class="section"> <h3 id="ルーティング追加後に再確認">ルーティング追加後に再確認</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180405/20180405023511.png" alt="f:id:treeapps:20180405023511p:plain" title="f:id:treeapps:20180405023511p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f619; 今度は起動後に正常にトップページが表示されるようになりました!!</p><p>location.hrefも / のトップページになっていますね。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>これだとWEBサイト上でelectron.htmlと叩いてもトップページにリダイレクトされるので、念には念を入れる場合は、environmentを利用して、electron起動時のみルーティングを追加する、等を行ってもいいかもしれませんね。通常はそこまでやる必要は無いように思えますが。</p><p>この画面キャプチャから解るように、現在以下の私の個人サイトのElectron化を進めています。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.string-utility.com%2F" title="文字列ユーティリティーの事ならString Utilityにお任せ!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.string-utility.com/">www.string-utility.com</a></cite></p><p>tree-mapsではElectronを既に使用しており、loadURLにURLを指定していましたが、今回はオンラインの必要が無いサイトなのでhtmlを読み込む形式にしました。成果物をGCSにアップロードするか?世代管理どうしよう?等、問題は結構ありますが、準備でき次第告知しようと思います!</p> </div> treeapps SSR無しのReact・Angular製のSPAサイトはGooglebotにどれくらい認識されるのか? hatenablog://entry/17391345971631206181 2018-04-01T02:43:55+09:00 2018-04-05T17:48:23+09:00 実際私が開発している2サイトの実例出しますよ〜 <p>実際私が開発している2サイトの実例出しますよ〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170917/20170917230836.png" alt="f:id:treeapps:20170917230836p:plain" title="f:id:treeapps:20170917230836p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>WEBサイトの開発者に取って常に気になるこの話題について、今回は私が開発している2サイトを実例として、結果をお伝えしようと思います。</p><p><span clas="feature3">※ 最後の「雑感」の後に、Angular製サイトをGolang化してみた結果を追記しました!</span><br /> </p> <ul class="table-of-contents"> <li><a href="#概要">概要</a></li> <li><a href="#GooglebotはどれくらいのJavaScript処理能力があるか">GooglebotはどれくらいのJavaScript処理能力があるか</a></li> <li><a href="#前提">前提</a></li> <li><a href="#React製サイト-tree-maps">React製サイト: tree-maps</a><ul> <li><a href="#サイトの開設日">サイトの開設日</a></li> <li><a href="#PCデバイスでの認識">PCデバイスでの認識</a></li> <li><a href="#モバイルデバイスでの認識">モバイルデバイスでの認識</a></li> <li><a href="#サイトマップxmlとインデックスの登録数">サイトマップxmlとインデックスの登録数</a></li> </ul> </li> <li><a href="#Angular製サイト-StringUtility">Angular製サイト: StringUtility</a><ul> <li><a href="#サイトの開設日-1">サイトの開設日</a></li> <li><a href="#PCデバイスでの認識-1">PCデバイスでの認識</a></li> <li><a href="#モバイルデバイスでの認識-1">モバイルデバイスでの認識</a></li> <li><a href="#本来レンダリングされる筈の画面">本来レンダリングされる筈の画面</a></li> <li><a href="#サイトマップxmlとインデックスの登録数-1">サイトマップxmlとインデックスの登録数</a></li> <li><a href="#botはServiceWorkerのhtmlを取得しているのかどうか">botはServiceWorkerのhtmlを取得しているのかどうか</a></li> <li><a href="#サイト上で確認できるServiceWorker向けのhtml">サイト上で確認できるServiceWorker向けのhtml</a></li> <li><a href="#Googlebotが取得したhtml">Googlebotが取得したhtml</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> <li><a href="#おまけ-20180404追記">おまけ 2018/04/04追記</a><ul> <li><a href="#Golang化して再度Googlebotにクロールさせてみる">Golang化して再度Googlebotにクロールさせてみる</a></li> </ul> </li> </ul> <div class="section"> <h3 id="概要">概要</h3> <p>大分昔からSPA(シングルページアプリケーション)サイトはGoogleに正しく認識されないため、SEO的には最悪だと言われてきました。そのため、SSR(サーバサイドレンダリング)をし、初回画面表示時は本来ReactやAngularが生成するhtmlを、サーバサイドでレンダリングしてクライアント側はそれを表示するだけ、という手法を用います。</p><p>しかしSSRをするためにはほぼNode.jsが必須になるのが現状です。私が愛用しているGoogleAppEngineではまだNode.jsのStandard Environment(無料のやつ)が無いため、Node.jsを無料で使用する事ができません。</p><p>わざわざお金をかけてFlexible EnvironmentでNode.jsを使いたくないなと思い、今日までSSR無しのSPAサイトをGoogleAppEngineで運用してきました。</p><p>では、SSR無しのSPAサイトは、どれくらいGoogleに認識されるのか?今回はそれを簡単に紹介します。</p> </div> <div class="section"> <h3 id="GooglebotはどれくらいのJavaScript処理能力があるか">GooglebotはどれくらいのJavaScript処理能力があるか</h3> <p>以下は2017年の有名SEOサイトの記事ですが、GooglebotはChrome41相当のレンダリング能力を持っているとのことです。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.suzukikenichi.com%2Fblog%2Fgooglebot-uses-a-web-rendering-service-that-is-based-on-chrome-41%2F%3Futm_source%3Dtwitter%26utm_medium%3Devergreen_post_tweeter%26utm_campaign%3Dwebsite" title="Googlebotはレンダリング機能としてChrome41相当の性能を持つ | 海外SEO情報ブログ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.suzukikenichi.com/blog/googlebot-uses-a-web-rendering-service-that-is-based-on-chrome-41/?utm_source=twitter&utm_medium=evergreen_post_tweeter&utm_campaign=website">www.suzukikenichi.com</a></cite><br /> </p> </div> <div class="section"> <h3 id="前提">前提</h3> <p>※ 2018/04/04 追記</p><p>SSR無しといっても、title・description・各種ogpについてはサーバサイドレンダリングしています。(ここだけ頑張ってます)</p><p>SSR無しといっっているのは、React等が生成する筈のjsをサーバサイドで生成しない、という事です。</p> </div> <div class="section"> <h3 id="React製サイト-tree-maps">React製サイト: tree-maps</h3> <p>ここから実例を紹介していきます。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.tree-maps.com%2F" title="tree-maps: 地図のWEB TOOLの事ならtree-mapsにお任せ!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.tree-maps.com/">www.tree-maps.com</a></cite></p><p>以下で開発・運用しています。</p> <table> <tr> <th>インフラ</th> <td>Google Appengine Standar Environment</td> </tr> <tr> <th>ランタイム</th> <td>Golang</td> </tr> <tr> <th>サーバサイド:フレームワーク</th> <td>Echo</td> </tr> <tr> <th>サーバサイド:ビルド</th> <td>goapp</td> </tr> <tr> <th>フロントエンド:フレームワーク</th> <td>React v16, Redux, react-router</td> </tr> <tr> <th>フロントエンド:ビルド</th> <td>webpack v4</td> </tr> <tr> <th>UIフレームワーク</th> <td>material-ui</td> </tr> <tr> <th>CSS</th> <td>PostCSS, Sass(sugarss)</td> </tr> <tr> <th>その他</th> <td>ServiceWorker, Electron</td> </tr> </table> <div class="section"> <h4 id="サイトの開設日">サイトの開設日</h4> <p>tree-mapsは一度フルリニューアルしており、リニューアル前はGAE/Javaで非SPAサイトで、リニューアル後はSPAサイトになっています。</p><p>2017/02/10にリニューアルサイトを開設しており、サイトの開設から1年以上経過しています。</p> </div> <div class="section"> <h4 id="PCデバイスでの認識">PCデバイスでの認識</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401014756.png" alt="f:id:treeapps:20180401014756p:plain" title="f:id:treeapps:20180401014756p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401014809.png" alt="f:id:treeapps:20180401014809p:plain" title="f:id:treeapps:20180401014809p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f601; トップページもプロットページもGooglebotはヘッダーが大きく崩れ、多少CSSのレイアウトがおかしいものの、まあまあ正しく画面を描画できているようです。</p> </div> <div class="section"> <h4 id="モバイルデバイスでの認識">モバイルデバイスでの認識</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401014933.png" alt="f:id:treeapps:20180401014933p:plain" title="f:id:treeapps:20180401014933p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401014945.png" alt="f:id:treeapps:20180401014945p:plain" title="f:id:treeapps:20180401014945p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>モバイル表示の実装はmaterial-uiにお任せ状態です。</p><p>&#x1f601; どうやらモバイルの場合はGooglebotは相当な再現ができているようですね。これは嬉しいです。</p> </div> <div class="section"> <h4 id="サイトマップxmlとインデックスの登録数">サイトマップxmlとインデックスの登録数</h4> <table> <tr> <th>サイトマップxml</th> <td><a href="https://www.tree-maps.com/assets/sitemap.xml">https://www.tree-maps.com/assets/sitemap.xml</a></td> </tr> </table><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401015516.png" alt="f:id:treeapps:20180401015516p:plain" title="f:id:treeapps:20180401015516p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f601; 17URL中16URLは認識して貰えました。上出来ですね。</p> </div> </div> <div class="section"> <h3 id="Angular製サイト-StringUtility">Angular製サイト: StringUtility</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.string-utility.com%2F" title="文字列ユーティリティーの事ならString Utilityにお任せ!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.string-utility.com/">www.string-utility.com</a></cite></p><p>以下で開発・運用しています。</p> <table> <tr> <th>インフラ</th> <td>Google Appengine Standar Environment</td> </tr> <tr> <th>ランタイム</th> <td>Java v1.8 (Kotlin v1.2)</td> </tr> <tr> <th>サーバサイド:フレームワーク</th> <td>Spark framework</td> </tr> <tr> <th>サーバサイド:ビルド</th> <td>Gradle v4.2</td> </tr> <tr> <th>フロントエンド:フレームワーク</th> <td>Angular v5.2</td> </tr> <tr> <th>フロントエンド:ビルド</th> <td>angular-cli v1.7</td> </tr> <tr> <th>UIフレームワーク</th> <td>angular-material</td> </tr> <tr> <th>CSS</th> <td>Sass</td> </tr> <tr> <th>その他</th> <td>ServiceWorker</td> </tr> </table> <div class="section"> <h4 id="サイトの開設日-1">サイトの開設日</h4> <p>StringUtilityは最初からSPAサイトです。</p><p>2017/10/07に開設しており、開設してから5ヶ月程度経過しています。</p> </div> <div class="section"> <h4 id="PCデバイスでの認識-1">PCデバイスでの認識</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401020953.png" alt="f:id:treeapps:20180401020953p:plain" title="f:id:treeapps:20180401020953p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401021010.png" alt="f:id:treeapps:20180401021010p:plain" title="f:id:treeapps:20180401021010p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f631; ・・・うーん、これは・・・</p> </div> <div class="section"> <h4 id="モバイルデバイスでの認識-1">モバイルデバイスでの認識</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401021025.png" alt="f:id:treeapps:20180401021025p:plain" title="f:id:treeapps:20180401021025p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401021042.png" alt="f:id:treeapps:20180401021042p:plain" title="f:id:treeapps:20180401021042p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f631; ・・・やっぱり駄目なのか</p> </div> <div class="section"> <h4 id="本来レンダリングされる筈の画面">本来レンダリングされる筈の画面</h4> <p>このままだと本来の画面はどうな感じなのか解らないと思うので、一応本来のサイトのスクリーンショットも載せておきます。</p><p>トップページはこうです。<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401025726.png" alt="f:id:treeapps:20180401025726p:plain" title="f:id:treeapps:20180401025726p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>CSV・TSV相互変換ページはこうです。<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401025751.png" alt="f:id:treeapps:20180401025751p:plain" title="f:id:treeapps:20180401025751p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>こうして見比べると、Googlebotが全然認識してくれていない事が明確に解りますよね・・・</p> </div> <div class="section"> <h4 id="サイトマップxmlとインデックスの登録数-1">サイトマップxmlとインデックスの登録数</h4> <table> <tr> <th>サイトマップxml</th> <td><a href="https://www.string-utility.com/data/sitemap.xml">https://www.string-utility.com/data/sitemap.xml</a></td> </tr> </table><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401021423.png" alt="f:id:treeapps:20180401021423p:plain" title="f:id:treeapps:20180401021423p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f631; ・・・10URL中2URLしか認識されず</p> </div> <div class="section"> <h4 id="botはServiceWorkerのhtmlを取得しているのかどうか">botはServiceWorkerのhtmlを取得しているのかどうか</h4> <p>React製のtree-mapsでもServiceWorkerを使用しており、サーバサイドorReactが生成するhtmlとServiceWorkerが配信するhtmlは同じですが、Angular製のStringUtilityの方はサーバサイドorAngularが生成するhtmlとServiceWorkerが配信するhtmlが異なっています。これはangular-service-workerが勝手にhtmlを作ってくれるので、そうなっています。</p><p>ではbotはServiceWorker側のhtmlを取得したのでしょうか?見てましょう。</p> </div> <div class="section"> <h4 id="サイト上で確認できるServiceWorker向けのhtml">サイト上で確認できるServiceWorker向けのhtml</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401022131.png" alt="f:id:treeapps:20180401022131p:plain" title="f:id:treeapps:20180401022131p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>こんな感じにUglifyされ、$マークの変数が展開されていないものが配信されます。Googlebotが↑のhtmlを取得していたら、GooglebotはServiceWorkerのhtmlを参照していると言えます。</p> </div> <div class="section"> <h4 id="Googlebotが取得したhtml">Googlebotが取得したhtml</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180401/20180401022229.png" alt="f:id:treeapps:20180401022229p:plain" title="f:id:treeapps:20180401022229p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>どうやら<span class="feature1">GooglebotはServiceWorkerのhtmlではなく、通常のサーバサイド、もしくはAngularが生成したhtmlを取得しているようです</span>。<br /> (サーバサイドのhtmlもAngularのhtmlも、titleタグ等を動的に変更しているので両者の見分けがつきません)</p><p>まあ流石にServiceWorkerのhtmlを取得してはいないだろうとは思っていましたが、ちゃんと検証できるとホッとしますね。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>React製サイトはいい感じにGooglebotに認識されているのに、Angular製サイトは酷い有様でした。</p><p><span class="feature1">AngularはGoogle製のフレームワーク</span>なのでGooglebotと親和性高いかも!?とか思っていたらこれですよ。UIフレームワークもGoogle製のangular-materialなので、Googleフレンドリーなサイトの筈なのですけどね・・・なんという無慈悲な結果。</p><p>両者の違いはほとんど無い筈ですが、サイトの速度が異なります。</p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2016%2F06%2F05%2F130329" title="GAE/go+ginとGAE/java+servletでそれぞれスピンアップの速度差をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2016/06/05/130329">www.bunkei-programmer.net</a></cite><br /> <iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F09%2F24%2F220531" title="GAE/Java8+kotlin+Spark Frameworkでスピンアップからjsonが返るまでの速度をゆる〜く確認する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2017/09/24/220531">www.bunkei-programmer.net</a></cite></p><p>以前↑の記事を書きましたが、ランタイムが<span class="feature1">Golangの場合はスピンアップもフレーワーク初期化も無茶苦茶速い</span>のですが、<span class="feature3">Javaの場合はスピンアップもフレームワーク初期化もかなり遅い</span>のです。</p><p>もしかしたら、この初回起動の速度も影響しているのかもしれませんね。Angular製サイトのランタイムをGolangにしたら、React製のサイトと同様にレンダリングされたら・・・・それこそ無慈悲ですね。</p><p>という事で、<span class="feature1">React製サイトはSSR無しでもGooglebotにいい感じに認識され、Angular製サイトは惨敗でほとんどGooglebotに認識されない</span>、という結果となりました!</p><p>Googlebotの挙動はGoogleの中の人にしか解りませんので、この記事で推測したものは全て外れている可能性がありますので、あくまで一例として参考にできれば、と思います!</p> </div> <div class="section"> <h3 id="おまけ-20180404追記">おまけ 2018/04/04追記</h3> <p>Angular製サイトの結果が良くなかった点について、初回画面表示速度が影響している可能性(そもそもhtmlが返るのが遅くてタイムアウトしかけているか、クロールを諦めた等の可能性)があると思い、思い切ってGoogle Appengineのランタイムを、現在のJava8からGolang1.9に変更してみました!</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.string-utility.com%2F" title="文字列ユーティリティーの事ならString Utilityにお任せ!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.string-utility.com/">www.string-utility.com</a></cite><br /> </p> <table> <tr> <th>種別</th> <th>変更前</th> <th>変更後</th> </tr> <tr> <td>GoogleAppengineランタイム</td> <td>GAE/Java8(Kotlin v1.2.30)</td> <td>GAE/Golang v1.9</td> </tr> <tr> <td>サーバサイド:フレームワーク</td> <td>Spark Framework</td> <td>Echo Framework</td> </tr> <tr> <td>サーバサイド:ビルド</td> <td>Gradle v4.6</td> <td>goapp deploy</td> </tr> <tr> <td>フロントエンド:パッケージマネージャ</td> <td>Yarn</td> <td>Yarn</td> </tr> <tr> <td>フロントエンド:トランスパイラ</td> <td>Typescript</td> <td>Typescript</td> </tr> <tr> <td>フロントエンド:フレームワーク</td> <td>Angular v5.2</td> <td>Angular v5.2</td> </tr> <tr> <td>フロントエンド:ビルド</td> <td>angular-cli</td> <td>angular-cli</td> </tr> <tr> <td>フロントエンド:UIフレームワーク</td> <td>angular-material</td> <td>angular-material</td> </tr> <tr> <td>フロントエンド:スタイル</td> <td>sass</td> <td>sass</td> </tr> </table><p>元々tree-mapsでGolangを使っていたので、String UtilityのGolang化は2日程でサックリできました。</p><p>ランタイムが変わった事で、サーバサイドフレームワークとビルドが代わりましたが、フロントエンド側は全く同じです。</p><p>では、この状態でGooglebotにレンダリングさせたらどうなるでしょうか?高速化した事で何か変化はあったでしょうか?</p> <div class="section"> <h4 id="Golang化して再度Googlebotにクロールさせてみる">Golang化して再度Googlebotにクロールさせてみる</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180404/20180404171403.png" alt="f:id:treeapps:20180404171403p:plain" title="f:id:treeapps:20180404171403p:plain" class="hatena-fotolife" itemprop="image"></span><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180404/20180404171414.png" alt="f:id:treeapps:20180404171414p:plain" title="f:id:treeapps:20180404171414p:plain" class="hatena-fotolife" itemprop="image"></span><br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20180405/20180405174735.png" alt="f:id:treeapps:20180405174735p:plain" title="f:id:treeapps:20180405174735p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>&#x1f605; <span class="feature1">結果が変わってる!!</span>前回はタイトルの文字以外全くレンダリングできてなかったのに、今回は崩れているもののヘッダーも表示できているようです。</p><p>サーバサイド側の作りが悪かったのか、それともGolang化してスピンアップ+フレームワーク初期化の高速化によって、クロール速度アップが影響したのか。何かしらの影響はあったっぽいですね。ただし、<span class="feature3">これでGooglebotがクロールできるようになったかは不明</span>です。改善はしたとは思います。</p><br /> <p>最近会社でJavaではなくサーバサイドKotlin + SpringBoot v2で開発していたりして、Kotlinが凄く良いと思っているのですが、やはりGolangの速度も捨てがたいですね。</p><p>GoogleAppengineやAWSLambda等のコンテナ系の場合、コールドスタート時に遅いjavaは少し辛いので(私のサイトのようにトラフィックが少なすぎてコールドスタート頻発サイトは特にそうです)、Golangはそういう部分では良いですね。書いていて楽しくて楽なのは断然Kotlinではあるのですが。。。</p> </div> </div> treeapps webpack v3からv4へアップデートした時に発生したエラーと対応 hatenablog://entry/17391345971628375438 2018-03-22T23:11:51+09:00 2018-03-22T23:11:51+09:00 いつものやっていきますよ〜 <p>いつものやっていきますよ〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170310/20170310234605.png" alt="f:id:treeapps:20170310234605p:plain" title="f:id:treeapps:20170310234605p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>先日webpack4が正式リリースされましたね。</p><p>create-react-appやangular-cliを使っている場合はバージョンアップしやすいですが、それらを使わずwebpack.configをゴリゴリしている方は、例のごとくバージョンアップに伴うエラーと格闘しているかと思います。</p><p>という事で、私がwebpack v3からv4にアップデートする際に発生したエラーと、その対応をまとめておきます。</p> <ul class="table-of-contents"> <li><a href="#yarn-start時に起きたエラー">yarn start時に起きたエラー</a><ul> <li><a href="#The-mode-option-has-not-been-set">The 'mode' option has not been set</a><ul> <li><a href="#エラーメッセージ">エラーメッセージ</a></li> <li><a href="#対応">対応</a></li> </ul> </li> <li><a href="#Cannot-read-property-eslint-of-undefined">Cannot read property 'eslint' of undefined</a><ul> <li><a href="#エラーメッセージ-1">エラーメッセージ</a></li> <li><a href="#対応-1">対応</a></li> </ul> </li> <li><a href="#loader_compilationapplyPluginsWaterfall-is-not-a-function">loader._compilation.applyPluginsWaterfall is not a function</a><ul> <li><a href="#エラーメッセージ-2">エラーメッセージ</a></li> <li><a href="#対応-2">対応</a></li> </ul> </li> </ul> </li> <li><a href="#実行時エラー">実行時エラー</a><ul> <li><a href="#Uncaught-TypeError-configureStore_default-is-not-a-function">Uncaught TypeError: configureStore_default(...) is not a function</a><ul> <li><a href="#エラーメッセージ-3">エラーメッセージ</a></li> <li><a href="#対応-3">対応</a></li> </ul> </li> </ul> </li> <li><a href="#production-build">production build</a><ul> <li><a href="#webpackoptimizeUglifyJsPlugin-has-been-removed">webpack.optimize.UglifyJsPlugin has been removed</a><ul> <li><a href="#エラーメッセージ-4">エラーメッセージ</a></li> <li><a href="#対応-4">対応</a></li> </ul> </li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="yarn-start時に起きたエラー">yarn start時に起きたエラー</h3> <div class="section"> <h4 id="The-mode-option-has-not-been-set">The 'mode' option has not been set</h4> <div class="section"> <h5 id="エラーメッセージ">エラーメッセージ</h5> <pre class="code" data-lang="" data-unlink>WARNING in configuration The &#39;mode&#39; option has not been set. Set &#39;mode&#39; option to &#39;development&#39; or &#39;production&#39; to enable defaults for this environment. </pre><p>webpack v4からwebpackコマンド実行時にmodeオプションの指定が必須になりました。</p> </div> <div class="section"> <h5 id="対応">対応</h5> <p>公式サイトに詳細が書かれているので、ご覧下さい。<br /> <a href="https://webpack.js.org/concepts/mode/#src/components/Sidebar/Sidebar.jsx">https://webpack.js.org/concepts/mode/#src/components/Sidebar/Sidebar.jsx</a></p><p>一例ですが、以下のように指定します。</p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">watch</span>&quot;: &quot;<span class="synConstant">webpack --mode development --watch --progress --color</span>&quot;, &quot;<span class="synStatement">build</span>&quot;: &quot;<span class="synConstant">webpack --mode production --progress --color</span>&quot;, </pre> </div> </div> <div class="section"> <h4 id="Cannot-read-property-eslint-of-undefined">Cannot read property 'eslint' of undefined</h4> <div class="section"> <h5 id="エラーメッセージ-1">エラーメッセージ</h5> <pre class="code" data-lang="" data-unlink>Module build failed: TypeError: Cannot read property &#39;eslint&#39; of undefined at Object.module.exports (/Users/tree/go/src/tree-maps-go/node_modules/eslint-loader/index.js:148:18)</pre> </div> <div class="section"> <h5 id="対応-1">対応</h5> <p>エラーの原因はよく解っていませんが、以下の対応で直るようです。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fwebpack%2Fwebpack%2Fissues%2F6556%23issuecomment-367895075" title="Webpack 4 issue with eslint · Issue #6556 · webpack/webpack" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/webpack/webpack/issues/6556#issuecomment-367895075">github.com</a></cite><br /> </p> <pre class="code" data-lang="" data-unlink>new webpack.LoaderOptionsPlugin({ options: {} })</pre> </div> </div> <div class="section"> <h4 id="loader_compilationapplyPluginsWaterfall-is-not-a-function">loader._compilation.applyPluginsWaterfall is not a function</h4> <div class="section"> <h5 id="エラーメッセージ-2">エラーメッセージ</h5> <pre class="code" data-lang="" data-unlink>ERROR in ./node_modules/css-loader?importLoaders=1!./node_modules/postcss-loader??ref--6-2!./client/src/assets/css/app.sass Module build failed: TypeError: loader._compilation.applyPluginsWaterfall is not a function at /Users/tree/go/src/tree-maps-go/node_modules/postcss-loader/index.js:122:43 at &lt;anonymous&gt;</pre> </div> <div class="section"> <h5 id="対応-2">対応</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fpostcss%2Fpostcss-loader%2Fissues%2F319%23issuecomment-349450983" title="Webpack 4 support · Issue #319 · postcss/postcss-loader" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/postcss/postcss-loader/issues/319#issuecomment-349450983">github.com</a></cite></p><p>私の場合は以下のようにバージョンアップする事で直りました。</p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">postcss-loader</span>&quot;: &quot;<span class="synConstant">^1.3.0</span>&quot;, ↓ &quot;<span class="synStatement">postcss-loader</span>&quot;: &quot;<span class="synConstant">^2.1.3</span>&quot;, </pre> </div> </div> </div> <div class="section"> <h3 id="実行時エラー">実行時エラー</h3> <div class="section"> <h4 id="Uncaught-TypeError-configureStore_default-is-not-a-function">Uncaught TypeError: configureStore_default(...) is not a function</h4> <div class="section"> <h5 id="エラーメッセージ-3">エラーメッセージ</h5> <pre class="code" data-lang="" data-unlink>Main.js:24 Uncaught TypeError: configureStore_default(...) is not a function at Object../client/src/assets/js/Main.js (Main.js:24) at __webpack_require__ (bootstrap:19) at bootstrap:68 at bootstrap:68</pre> </div> <div class="section"> <h5 id="対応-3">対応</h5> <p>これはwebpack v4関係ないかもしれません。</p><p>単にexport defaultを使ってしまっているコードがあったので、そこでエラーになっていただけでした。</p><p>以下のように修正する事で直ります。</p> <pre class="code" data-lang="" data-unlink>import configureStore from &#39;./store/configureStore&#39; export default function configureStore(initialState) { return createStoreWithMiddleware(rootReducer, initialState); } ↓ import {configureStore} from &#39;./store/configureStore&#39; export function configureStore(initialState) { return createStoreWithMiddleware(rootReducer, initialState); }</pre> </div> </div> </div> <div class="section"> <h3 id="production-build">production build</h3> <div class="section"> <h4 id="webpackoptimizeUglifyJsPlugin-has-been-removed">webpack.optimize.UglifyJsPlugin has been removed</h4> <div class="section"> <h5 id="エラーメッセージ-4">エラーメッセージ</h5> <pre class="code" data-lang="" data-unlink>Error: webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead.</pre> </div> <div class="section"> <h5 id="対応-4">対応</h5> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fshisama%2Fitems%2F730e7a2e6a3090a63a38%23%25E8%25A8%25AD%25E5%25AE%259A%25E3%2583%2595%25E3%2582%25A1%25E3%2582%25A4%25E3%2583%25AB%25E3%2582%2592%25E4%25BD%25BF%25E3%2581%25A3%25E3%2581%259F%25E3%2583%2593%25E3%2583%25AB%25E3%2583%2589" title="設定ファイル不要!webpack 4 でReactをビルドしてみた - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/shisama/items/730e7a2e6a3090a63a38#%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%83%93%E3%83%AB%E3%83%89">qiita.com</a></cite></p><p>webpack v4からUglifyJsPluginが無くなり、代わりにmode=productionにすると、自動的にuglifyされるようになります。</p> </div> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>で、webpack v4でコンパイル速くなった?と聞かれると、正直「う〜〜〜〜〜〜ん・・・」という感じです。</p><p>効果の程は体感としては全く差を感じませんが、ビルドに関わるものは常に最新バージョンを使っておきたいですよね。Gradleなんかもそうです。</p><p>v3からv4対応は、v2からv3への対応の時より遥かに易しいので、「また地獄見たくねーよ!」という方は、多分あの時より簡単にバージョンアップできると思いますので、サクッとバージョンアップしちゃいましょう!!</p> </div> treeapps GAE/Java+Angular5+@angular/service-workerでSPA・PWA・SEOに対応する hatenablog://entry/8599973812337930222 2018-01-16T00:23:23+09:00 2018-01-16T00:23:23+09:00 解決しそうで微妙に解決しない、それがGAEの悲しいところ・・・ <p>解決しそうで微妙に解決しない、それがGAEの悲しいところ・・・</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170918/20170918135756.png" alt="f:id:treeapps:20170918135756p:plain" title="f:id:treeapps:20170918135756p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>私は個人でGoogleAppEngine上でサービスを現状2つ開発していて、そのうちの1つをAngular5で構築しています。以下のサイトです。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.string-utility.com" title="文字列ユーティリティーの事ならString Utilityにお任せ!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.string-utility.com">www.string-utility.com</a></cite></p><p>GAE+Angularで開発するにあたり、ちょっとしんどい事と、どうやってそれを乗り切っているのか(ごまかしているのか)を書いてみます。</p> <ul class="table-of-contents"> <li><a href="#GAESPAの辛さ">GAE+SPAの辛さ</a></li> <li><a href="#ビューのテンプレートエンジンの不一致の何が困るのか">ビューのテンプレートエンジンの不一致の何が困るのか</a></li> <li><a href="#画面初期表示時はどっちのhtmlを出力する">画面初期表示時はどっちのhtmlを出力する?</a></li> <li><a href="#ng-serveのつらみ">ng serveのつらみ</a><ul> <li><a href="#解決策1Thymeleaf側でhttplocalhost4200を参照する">解決策1:Thymeleaf側でhttp://localhost:4200を参照する</a><ul> <li><a href="#ローカルのAPサーバ経由の場合のレンダリング結果">ローカルのAPサーバ経由の場合のレンダリング結果</a></li> <li><a href="#本番環境GAE上の場合のレンダリング結果">本番環境(GAE上)の場合のレンダリング結果</a></li> <li><a href="#ローカルのng-serve経由の場合のレンダリング結果">ローカルのng serve経由の場合のレンダリング結果</a></li> </ul> </li> <li><a href="#解決策2ng-serveではなくng-build---watchする">解決策2:ng serveではなくng build --watchする</a></li> <li><a href="#結局どうするのが良さそうか">結局どうするのが良さそうか</a></li> </ul> </li> <li><a href="#ServiceWorkerと検索エンジンのクローラ">ServiceWorkerと検索エンジンのクローラ</a><ul> <li><a href="#ngswjsonの自動生成例">ngsw.jsonの自動生成例</a></li> <li><a href="#ngsw-workerjsの自動生成例">ngsw-worker.jsの自動生成例</a></li> <li><a href="#angular-cliでappsのrootより外のディレクトリにあるファイルをindexに指定すると何故かminifyされてしまう">.angular-cliでappsのrootより外のディレクトリにあるファイルをindexに指定すると何故かminifyされてしまう</a></li> <li><a href="#ngsw-workerjsって誰がどこで動いてるの">ngsw-worker.jsって誰がどこで動いてるの?</a></li> <li><a href="#人間がサイトを表示した場合とクローラがサイトを表示した場合の違い">人間がサイトを表示した場合とクローラがサイトを表示した場合の違い</a></li> </ul> </li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="GAESPAの辛さ">GAE+SPAの辛さ</h3> <p>辛いというか、</p><p><span class="feature1 font30">GAEのStandard EnvironmentmにNode.jsがあれば万事解決</span></p><p>なのです。。。</p><p>しかし現実は厳しく、2018/01現在ではnode.jsはFlexible Environmentmを使わないと実現できません。</p><p>これはつまり、サーバサイドがnode.jsではないので、サーバ側がjavaでフロント側がjavascript等と言語が異なってしまうし、何より<span class="feature3">ビューのテンプレートエンジンの不一致</span>が起きてしまいます。</p><p>最も辛いのは、このテンプレートエンジンの不一致です。テンプレートエンジンが不一致になると、SEO(画面表示時のtitleの書き換え等)が非常に厄介になるのです。</p> </div> <div class="section"> <h3 id="ビューのテンプレートエンジンの不一致の何が困るのか">ビューのテンプレートエンジンの不一致の何が困るのか</h3> <p>その前に、サーバサイドとフロントの状況を比較してみます。</p> <table> <tr> <th></th> <th>サーバサイド</th> <th>フロント</th> </tr> <tr> <th>言語</th> <td>Java(kotlin)</td> <td>Javascript(Typescript)</td> </tr> <tr> <th>フレームワーク</th> <td>SparkFramework</td> <td>Angular5</td> </tr> <tr> <th>viewテンプレートエンジン</th> <td>Thymeleaf</td> <td>Angular標準</td> </tr> </table><p>こんな感じです。</p><p>サーバとフロントでテンプレートエンジンが異なってしまうと、書き方が異なってしまいますよね。そうなると、同じhtmlをサーバ側とフロント側で共有する事ができなくなってしまいます。</p><p>共有できないという事は、サーバ用とフロント用のビューを2種類必要になり、しょうもない2重管理になってしまいます。</p><p>しかし、今回選んだThymeleafは優れていて、APサーバを通した場合は変数が展開され、APサーバを通さない場合は変数が展開されない、という事が可能になっています。</p><p>例えばThymeleafでは以下のようにtitleタグを書きます。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">title</span><span class="synIdentifier"> th:</span><span class="synType">text</span><span class="synIdentifier">=</span><span class="synConstant">&quot;${seo.title}&quot;</span><span class="synIdentifier">&gt;</span>StringUtility<span class="synIdentifier">&lt;/</span><span class="synStatement">title</span><span class="synIdentifier">&gt;</span> </pre><p>APサーバ、例えばJettyやTomcatを通してこれを表示すると、seo.titleの値でStringUtilityという文字列を置換される、つまり変数が展開され、ついでにth:text属性は除去され、以下のようにレンダリングされます。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">title</span><span class="synIdentifier">&gt;</span>ほげほげ<span class="synIdentifier">&lt;/</span><span class="synStatement">title</span><span class="synIdentifier">&gt;</span> </pre><p>APサーバを通さない場合は、このタグがそのまま表示され、titleタグは「StringUtility」となり、「th:title」の部分はhtmlの属性と判断されるので、無害(あっても何も起きない)になり、以下のようにレンダリングされます。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">title</span><span class="synIdentifier"> th:</span><span class="synType">text</span><span class="synIdentifier">=</span><span class="synConstant">&quot;${seo.title}&quot;</span><span class="synIdentifier">&gt;</span>StringUtility<span class="synIdentifier">&lt;/</span><span class="synStatement">title</span><span class="synIdentifier">&gt;</span> </pre><p>このように、Thymeleafを使う事で、サーバ側とフロント側で同じhtmlを共有する事ができるのでした〜、これにて一件落着!!</p><br /> <p>・・・・とはいきません。これではSEO問題が解決しないのです。</p> </div> <div class="section"> <h3 id="画面初期表示時はどっちのhtmlを出力する">画面初期表示時はどっちのhtmlを出力する?</h3> <p>一度画面を表示してからは、SPAなので画面遷移が発生しないので、Angular側でtitle・description等を置換します。これは問題ありません。</p><p>では、初回の画面表示時は、サーバ側でレンダリングしたビューを表示しますか?それとも静的なhtmlを表示してAngularにSEO情報を書き換えさせますか?</p><p>SEO的にも安全なのは前者で、極力サーバサイド側で描画済みのものを表示した方が、クローラに認識されやすいです。</p><p>2017/08時点では、googleのクローラはGoogleChromeのv41相当だそうで、かなりのJavaScriptを解釈できるようです。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.suzukikenichi.com%2Fblog%2Fgooglebot-uses-a-web-rendering-service-that-is-based-on-chrome-41%2F%3Futm_source%3Dtwitter%26utm_medium%3Devergreen_post_tweeter%26utm_campaign%3Dwebsite" title="Googlebotはレンダリング機能としてChrome41相当の性能を持つ | 海外SEO情報ブログ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.suzukikenichi.com/blog/googlebot-uses-a-web-rendering-service-that-is-based-on-chrome-41/?utm_source=twitter&utm_medium=evergreen_post_tweeter&utm_campaign=website">www.suzukikenichi.com</a></cite><br /> しかしSPAを完全に解釈できるかどうかは、googleのみぞ知る、というのが現状です。という事で、やはりサーバサイド側で変数を展開済みのビューを初期画面に表示するのが良さそうです。</p><p>「<span class="feature1">初期表示時はサーバサイド側のビューを使う</span>」でこの話は終了?</p><p>いえ、少し厄介な問題が残ります。</p><p>SPA、しかも今のところAPIもほぼ無いので、ソースコードの割合はサーバサイド1割、フロント9割、という比率のソースコードになっています。</p><p>という事はフロントエンド側の方が頑張る量が多いので、フロントエンド側をサクサク開発できる環境が望ましいですね。</p><p>ここでちょっと問題が起きます。</p> </div> <div class="section"> <h3 id="ng-serveのつらみ">ng serveのつらみ</h3> <p>フロントはangular-cliをフル活用するのですが、通常は「ng serve」で開発します。これは簡易サーバを起動し、angular側のコードの変更を検知し、差分ビルド+ブラウザの自動リロードが行われ、非常に快適にフロントの開発を行う事が可能になります。</p><p>ここでポイントなのは<span class="feature3">ng serve</span>の挙動にあります。</p><p>標準設定でng serveすると、トップページのURLは <a href="http://localhost:4200">http://localhost:4200</a> になります。ここまではいいのですが、問題なのは<span class="feature3">ビルドされたファイルは実体としてファイル出力されず、メモリ上で展開される</span>点です。</p><p>この状態で、ng serveした簡易サーバではなく、APサーバ経由でサイトを確認したい場合(GAEにデプロイした状態と同じ)、どうなでしょう。</p><p>答えは簡単で、<span class="feature3">angularがビルドしたjsファイルが見つからない</span>という事になります。実体がメモリ上にしかないので当然ですね。</p><p>この挙動は、以下に影響を与える厄介なものです。</p> <ul> <li>ローカルのAPサーバ経由のサイトの挙動</li> <li>ローカルのng serve経由の挙動</li> <li>本番環境(GAEにデプロイ)した状態での挙動</li> </ul><p><span class="feature1">これに対する解決策は2つあります</span>。</p> <div class="section"> <h4 id="解決策1Thymeleaf側でhttplocalhost4200を参照する">解決策1:Thymeleaf側で<a href="http://localhost:4200">http://localhost:4200</a>を参照する</h4> <p>まず、APサーバ(port=4567)と、ng serve(port=4200)を同時に起動した状態にします。</p><p>そして、htmlには以下のようにしてjavascriptを読み込むようにします。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'inline.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'polyfills.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'styles.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'vendor.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'main.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre><p>ローカル環境の場合はassetsHost変数に「<a href="http://localhost:4200/">http://localhost:4200/</a>」をセットし、本番環境の場合は空文字をセットするようにします。</p><p>ローカル環境の場合は、メモリ上にしかjsが存在しないならば、4200ポートを見てしまえば良い、本番環境の場合jsは標準設定だとdistに配置されるので、「src="inline.bundle.js"」とすれば良いのです。</p> <div class="section"> <h5 id="ローカルのAPサーバ経由の場合のレンダリング結果">ローカルのAPサーバ経由の場合のレンダリング結果</h5> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;http://localhost:4200/inline.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;http://localhost:4200/polyfills.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;http://localhost:4200/styles.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;http://localhost:4200/vendor.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;http://localhost:4200/main.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre><p>これでng serve中のjsを参照できますが、これは中々な酷いですね。。。</p> </div> <div class="section"> <h5 id="本番環境GAE上の場合のレンダリング結果">本番環境(GAE上)の場合のレンダリング結果</h5> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;inline.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;polyfills.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;styles.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;vendor.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;main.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre><p>これでng buildした実体のあるファイルを参照できます。</p><p>では、ローカルでng serveした場合はどうでしょう。</p> </div> <div class="section"> <h5 id="ローカルのng-serve経由の場合のレンダリング結果">ローカルのng serve経由の場合のレンダリング結果</h5> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'inline.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'polyfills.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'styles.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'vendor.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;@{${assetsHost} + 'main.bundle.js'}&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;inline.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;polyfills.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;styles.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;vendor.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;main.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre><p>一瞬???と思うかもしれませんが、ng serveは、jsの読み込み用のscriptタグをindex.htmlに自動生成するので、元々のThymeleaf側のタグ(上側)とは別に、ng serveによって自動生成されたscriptタグ(下側)があるのです。</p><p>よく見ると解りますが、上部のThymeleaf側のscriptにはsrc属性がありません。もしThymeleafのテンプレートにsrcを書いてしまうと、ng serveした時にThymeleaf側に書いておいたsrcとng serveが自動生成するsrcが2重に出力され、2回jsが読み込まれてしまいます。</p><p>これを回避するため、トリッキーですが、<span class="feature1">Thymeleaf側には敢えてsrc属性を記述しない</span>状態にしています。こうする事で、ng serve時は上側のscriptタグにはsrc属性が無いので無視され、下側の自分自身(ng serve)したsrcが適用されるのです。</p><p>一応はこれで何とかなるのですが、以下の問題が残ります。</p> <ul> <li><span class="feature3">サーバサイド側で環境によってassetsHost変数を書き換える処理が必要</span></li> <li><span class="feature3">angular側のコード変更時にSparkが変更を検知しないので都度Sparkの再起動が必要になる</span></li> </ul> </div> </div> <div class="section"> <h4 id="解決策2ng-serveではなくng-build---watchする">解決策2:ng serveではなくng build --watchする</h4> <p>ng serveの時は、「jsの実体がなくメモリ上にしか展開されない」おかげで、jsを参照する際は<a href="http://localhost:4200/inline.bundle.js">http://localhost:4200/inline.bundle.js</a> 等という不格好な事をしないといけませんでした。</p><p>では、簡易サーバの起動をやめつつ、ビルドしたファイルを実体化させてしまえばいいのです。それがng build --watchです。</p><p>ng serveとの違いは、ng serveは簡易サーバが起動するので単独でサイト表示が可能なのに対して、ng build --watchはng serveのように簡易サーバは起動せず、他はng serveと同様リアルタイムに差分ビルドしてくれます。</p><p>jsの実体がファイル出力される事で「src="<a href="http://localhost:4200/inline.bundle.js">http://localhost:4200/inline.bundle.js</a>"」ではなく「src="inline.bundle.js"」で参照できるようになりましたが、ng buildもやはりscriptタグを自動出力するので、結局は2重にscriptタグが出力されないよう以下のようにする必要があります。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;inline.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;polyfills.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;styles.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;vendor.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">defer</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier"> th:</span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;main.bundle.js&quot;</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre><p>このやり方でも以下の問題は残ります。</p> <ul> <li><span class="feature3">簡易サーバ無くなったのでAPサーバ経由でサイトの動作確認をする事になり、サクサク開発しにくくなる。</span></li> <li><span class="feature3">angular側のコード変更時にSparkが変更を検知しないので都度Sparkの再起動が必要になる</span></li> </ul> </div> <div class="section"> <h4 id="結局どうするのが良さそうか">結局どうするのが良さそうか</h4> <p>APサーバ経由でのサイトの動作確認は、どうやっても何かしらの問題がある事が解りました。</p><p>色々悩みましたが、私は「<span class="feature1">サーバサイドの確認の時のみSpark経由でサイトを確認し、通常はng serveでサイトをライブリロードしながらサクサク開発していく</span>」のがいいのではないかと思いました。</p><p>サーバサイドはほぼSEO以外の処理は今はやっていないので、この際APサーバ経由での確認は必要な時にしかしなくていいのでは?と思ったわけです。</p><p>APサーバ経由での確認と、ng serve経由での確認をバッサリ分けた事で、前述したAPサーバ経由の面倒臭さ(リソース変更時のSpark再起動)は無くせました。これにて一件落着!!</p><br /> <p>・・・・とはいきません(2回目)。これではServiceWorkerのオフライン対応問題が解決しないのです。</p> </div> </div> <div class="section"> <h3 id="ServiceWorkerと検索エンジンのクローラ">ServiceWorkerと検索エンジンのクローラ</h3> <p>ここでようやく記事タイトルのServiceWorkerの話です。</p><p>SPA + PWAでネイティブアプリのようにウェイウェイするのが最近の流行りなので(単純に高速化するしオフラインで動くし)、ビッグウェーブに乗ってみました。</p><p>angularの場合はServiceWorkerの導入は目茶苦茶楽です。</p><p>新規プロジェクト生成時なら「ng new hoge --service-worker」だけで初期準備が整います。</p><p>既にプロジェクトが存在する場合は、以下のようにします。</p> <ul> <li>yarn add @angular/service-worker する。</li> <li>.angular-cli.jsonのappsの下に「"serviceWorker": true」を追加する。</li> <li>app root(main.tsがある場所)にngsw-config.jsonを作成する。</li> <li>app-module.tsのimportsに「ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production})」を追加する。</li> <li>ng build --prod --output-hashing=none する。(--output-hashing=noneを付けないとSpark側でjsファイル名を特定できない)</li> </ul><p>こうすると、ビルド時にngsw-config.jsonと同じディレクトリに「ngsw.json」と、それを読み込む「ngsw-worker.js」が生成されます。</p> <div class="section"> <h4 id="ngswjsonの自動生成例">ngsw.jsonの自動生成例</h4> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">configVersion</span>&quot;: <span class="synConstant">1</span>, &quot;<span class="synStatement">index</span>&quot;: &quot;<span class="synConstant">/index.html</span>&quot;, &quot;<span class="synStatement">assetGroups</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">app</span>&quot;, &quot;<span class="synStatement">installMode</span>&quot;: &quot;<span class="synConstant">prefetch</span>&quot;, &quot;<span class="synStatement">updateMode</span>&quot;: &quot;<span class="synConstant">prefetch</span>&quot;, &quot;<span class="synStatement">urls</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">/favicon.ico</span>&quot;, &quot;<span class="synConstant">/index.html</span>&quot;, &quot;<span class="synConstant">/inline.bundle.js</span>&quot;, &quot;<span class="synConstant">/main.bundle.js</span>&quot;, &quot;<span class="synConstant">/polyfills.bundle.js</span>&quot;, &quot;<span class="synConstant">/styles.bundle.js</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">patterns</span>&quot;: <span class="synSpecial">[]</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">assets</span>&quot;, &quot;<span class="synStatement">installMode</span>&quot;: &quot;<span class="synConstant">lazy</span>&quot;, &quot;<span class="synStatement">updateMode</span>&quot;: &quot;<span class="synConstant">prefetch</span>&quot;, &quot;<span class="synStatement">urls</span>&quot;: <span class="synSpecial">[]</span>, &quot;<span class="synStatement">patterns</span>&quot;: <span class="synSpecial">[]</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">dataGroups</span>&quot;: <span class="synSpecial">[]</span>, &quot;<span class="synStatement">hashTable</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">/inline.bundle.js</span>&quot;: &quot;<span class="synConstant">a6b91d159ec5c404f79af56e64a01abfed9d57fd</span>&quot;, &quot;<span class="synStatement">/main.bundle.js</span>&quot;: &quot;<span class="synConstant">c904c7921af95a71ef615433c852318a2adff6ba</span>&quot;, &quot;<span class="synStatement">/polyfills.bundle.js</span>&quot;: &quot;<span class="synConstant">6212ebcbc3bc91535ae0d106eda09fb0e9d7c995</span>&quot;, &quot;<span class="synStatement">/styles.bundle.js</span>&quot;: &quot;<span class="synConstant">c03268b506cdfad006567550dc250cf52456bfed</span>&quot;, &quot;<span class="synStatement">/favicon.ico</span>&quot;: &quot;<span class="synConstant">42603f88078e9111bf705e154f57233b61f85a32</span>&quot;, &quot;<span class="synStatement">/index.html</span>&quot;: &quot;<span class="synConstant">e309120eb78eaafb239abec9d15d09668c38382e</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre><p>ngsw-config.jsonを元に、ngsw.jsonが生成されます。</p><p>inline.bundle.js等、ngsw-config.jsonに設定したリソースをハッシュ値を取得し、登録しています。ハッシュ値を登録する理由は、ServiceWorkerのキャッシュ更新のためです。</p><p>ファイルの中身が変わるとハッシュ値が変わり、ブラウザのCache Storageにハッシュ値と共にオフライン利用できるようキャッシュされます。次回ビルド時にファイルの中身に変更があればこのハッシュ値が変わり、ServiceWorkerによってCache Storage側のハッシュ値と比較され、ハッシュ値が異なっていればFetchする、という挙動のようです。賢いですね。</p> </div> <div class="section"> <h4 id="ngsw-workerjsの自動生成例">ngsw-worker.jsの自動生成例</h4> <pre class="code lang-javascript" data-lang="javascript" data-unlink>(<span class="synIdentifier">function</span> () <span class="synIdentifier">{</span> <span class="synConstant">'use strict'</span>; <span class="synComment">/**</span> <span class="synComment"> * @license</span> <span class="synComment"> * Copyright Google Inc. All Rights Reserved.</span> <span class="synComment"> *</span> <span class="synComment"> * Use of this source code is governed by an MIT-style license that can be</span> <span class="synComment"> * found in the LICENSE file at https://angular.io/license</span> <span class="synComment"> */</span> <span class="synComment">/**</span> <span class="synComment"> * Adapts the service worker to its runtime environment.</span> <span class="synComment"> *</span> <span class="synComment"> * Mostly, this is used to mock out identifiers which are otherwise read</span> <span class="synComment"> * from the global scope.</span> <span class="synComment"> */</span> <span class="synStatement">class</span> Adapter <span class="synIdentifier">{</span> <span class="synComment">/**</span> <span class="synComment"> * Wrapper around the `Request` constructor.</span> <span class="synComment"> */</span> newRequest(input, init) <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">new</span> Request(input, init); <span class="synIdentifier">}</span> ・・・・略・・・・ </pre><p>一部抜粋ですが、以下のような事をしているようです。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> res = await <span class="synIdentifier">this</span>.safeFetch(<span class="synIdentifier">this</span>.adapter.newRequest(<span class="synConstant">'ngsw.json?ngsw-cache-bust='</span> + Math.random())); </pre><p>ngsw-worker.jsによってServiceWorkerの登録やキャッシュの更新が行われているようです。</p><br /> <p>ngsw-config.jsonにはオフライン時に表示するためのindexページ指定をするのですが、これはどこを指定しましょう。Thymeleaf側のindex.htmlでしょうか?</p><p>「はい」と言いたいところですが、angular-cliには謎の挙動があります。</p> </div> <div class="section"> <h4 id="angular-cliでappsのrootより外のディレクトリにあるファイルをindexに指定すると何故かminifyされてしまう">.angular-cliでappsのrootより外のディレクトリにあるファイルをindexに指定すると何故かminifyされてしまう</h4> <p>これはバグなのか、そういうオプションなのか不明なのですが、Thymeleaf側のindex.htmlを.angular-cliで以下のように指定してng build --prodすると、何故かminifyされてしまうのです。dist側のindex.htmlではなく、src側のindex.htmlが、です。</p><p>例えば以下のように指定してしまうと、ng build時に src/main/resources/templates/test.htmlは何故かminifyされます。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">$schema</span>&quot;: &quot;<span class="synConstant">./node_modules/@angular/cli/lib/config/schema.json</span>&quot;, &quot;<span class="synStatement">project</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">string-utility</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">apps</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">root</span>&quot;: &quot;<span class="synConstant">src/main/resources/client</span>&quot;, &quot;<span class="synStatement">outDir</span>&quot;: &quot;<span class="synConstant">src/main/resources/public</span>&quot;, &quot;<span class="synStatement">assets</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">images</span>&quot;, &quot;<span class="synConstant">data</span>&quot;, &quot;<span class="synConstant">favicon.ico</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">index</span>&quot;: &quot;<span class="synConstant">../templates/test.html</span>&quot;, ・・・略・・・ </pre><p>この挙動がちょっと意味不明なので、力技で修正してしまいます。</p><p>ng serve・ng build --prod時に、Thymeleaf側のindex.htmlをangular側のapp rootにコピーしてしまいます。例えばpackage.jsonで以下のように感じですコピーを挟んでしまいます。</p> <pre class="code lang-json" data-lang="json" data-unlink>&quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">start</span>&quot;: &quot;<span class="synConstant">npm run copy-index-html &amp;&amp; ng serve</span>&quot;, &quot;<span class="synStatement">build</span>&quot;: &quot;<span class="synConstant">npm run copy-index-html &amp;&amp; ng build --prod</span>&quot;, &quot;<span class="synStatement">copy-index-html</span>&quot;: &quot;<span class="synConstant">cp -f ./src/main/resources/templates/index.html ./src/main/resources/client/sw-index.html</span>&quot; <span class="synSpecial">}</span>, </pre><p>index.htmlのままだとトップページ表示時に静的htmlが優先されてしまう等の面倒が起きそうなので、sw-index.htmlと変えてます。</p><p>これで、ngsw-config.jsonにはsw-index.htmlを指定し、無事Thymeleaf側のindex.htmlに集約できるようになりました。。。長く、しょうもない試行錯誤でした。</p> </div> <div class="section"> <h4 id="ngsw-workerjsって誰がどこで動いてるの">ngsw-worker.jsって誰がどこで動いてるの?</h4> <p>ネット上でServiceWoekerについて調べると、登録処理でよく以下のようなコードを見かけます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">if</span>(<span class="synConstant">&quot;serviceWorker&quot;</span> <span class="synStatement">in</span> navigator) <span class="synIdentifier">{</span> navigator.serviceWorker.register(<span class="synConstant">&quot;/sw.js&quot;</span>); <span class="synIdentifier">}</span>; </pre><p>これに当たるコードを今回書いてないのですが、一体どこで登録されているのでしょうか。</p><p>答えは以下です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>ServiceWorkerModule.register(<span class="synConstant">'/ngsw-worker.js'</span>, <span class="synIdentifier">{</span>enabled: environment.production<span class="synIdentifier">}</span>) </pre><p>これはビルドするとmain.bundle.jsに格納されます。つまり、main.bundle.jsがロードされるとngsw-worker.jsが読み込まれ、ServiceWorkerの登録・更新処理等が実行されます。楽でいいですね。</p> </div> <div class="section"> <h4 id="人間がサイトを表示した場合とクローラがサイトを表示した場合の違い">人間がサイトを表示した場合とクローラがサイトを表示した場合の違い</h4> <p>人間がServiceWorkerに対応したブラウザ(現在だとChromeとFirefox、IE・Safariはこれから対応される)を使っていると、ServiceWorkerが登録され、リロードすると次回からはServiceWorkerのキャッシュからファイルが読み出されるようになります。つまり動的ではないので、title・description等はサーバサイドでレンダリングされず、angularによって書き換えられます。</p><p>一方検索エンジンのクローラですが、まだServiceWorkerに対応していないので、クローラがサイトにアクセスすると、必ず初回画面表示時はAPサーバ経由の動的表示になるので、title・descriptionはサーバサイドレンダリングされるので、SEO的な問題は発生しなくなります。</p><p>ユーザはServiceWorkerでウェイウェイし、クローラはサーバサイド側でレンダリングされたtitle・descriptionを確実に参照できるわけです。</p><p>今後クローラ側にServiceWorkerが搭載!なんて言われたらこの手法は使えなくなるわけですが、それは無さそうな予感がします。</p> </div> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>正直綺麗には解決していませんね。。。Spark側のホットデプロイに対応してないし、ビューのテンプレートの話はこれでいいものなのか。node.js以外でSPA+SEO対応する時のベストプラクティスが知りたいです。</p><p>もしnode.jsが利用可能なら、angular+angular/service-worker+angular/universalで綺麗に統一されたコードが書けただろうなあ・・・と、遠い目になってしまいますね。</p><p>ReactもVueもですが、やはりフロントはnode.jsとセットで使えると絶対楽で、GAEで無理にSPAでSEOに対応しようとすると、結構しんどい目に合うのでご注意下さい。</p><p>というか、クローラがSPAを高い精度で認識できるようになれば、サーバサイドでのtitle・descriptionの書き換えが不要になるのですが、絶対無理でしょうね。こればっかりはずっと付き纏う問題なのかもしれません。</p> </div> treeapps Python Pillow-SIMDで並列画像変換するDockerイメージ用意したよ〜 hatenablog://entry/8599973812314663892 2017-11-05T16:33:05+09:00 2017-11-05T16:33:05+09:00 ちょっと微妙かもしれない・・・ <p>ちょっと微妙かもしれない・・・</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20170501/20170501215041.png" alt="f:id:treeapps:20170501215041p:plain" title="f:id:treeapps:20170501215041p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>最近画像変換周りを触っています。</p><p>実は既にImageMagickで並列画像変換するDockerイメージを用意してたりするのですが、ImageMagickをそろそろ卒業しようと思いまして、今回PythonのPillow-SIMDで全部書き直す事にしました。</p> <ul class="table-of-contents"> <li><a href="#Simple-fast-image-converter">Simple fast image converter</a><ul> <li><a href="#いいからリポジトリ見せろよ">いいからリポジトリ見せろよ</a></li> <li><a href="#何ができるの">何ができるの?</a></li> <li><a href="#技術要素">技術要素</a></li> <li><a href="#どんな時に使うの">どんな時に使うの?</a></li> <li><a href="#ログ">ログ</a></li> </ul> </li> <li><a href="#Pillowとの速度比較">Pillowとの速度比較</a><ul> <li><a href="#Pillow">Pillow</a></li> <li><a href="#Pillow-SIMD">Pillow-SIMD</a></li> </ul> </li> <li><a href="#TIPS">TIPS</a><ul> <li><a href="#OSError-Errno-24-No-file-descriptors-available">OSError: [Errno 24] No file descriptors available</a></li> </ul> </li> <li><a href="#気づいた事">気づいた事</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="Simple-fast-image-converter">Simple fast image converter</h3> <div class="section"> <h4 id="いいからリポジトリ見せろよ">いいからリポジトリ見せろよ</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftreetips%2Fsimple-fast-image-converter" title="treetips/simple-fast-image-converter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/treetips/simple-fast-image-converter">github.com</a></cite></p><p><a href="https://hub.docker.com/r/treetips/simple-fast-image-converter/">https://hub.docker.com/r/treetips/simple-fast-image-converter/</a></p><p>こちらです。</p> </div> <div class="section"> <h4 id="何ができるの">何ができるの?</h4> <p>環境変数で画像パスを指定してあげて、docker-compose upすると、再帰的に画像フォルダを検索して、拡張子毎に異なる圧縮品質で、並列に画像変換(圧縮)を行う事ができます。</p><p>可逆圧縮はそもそも圧縮がほとんど効かずコスパが悪いので、jpg,jpeg辺りの非可逆圧縮のみを対象とすると、非常に高速に圧縮できます。</p><p>ちなみに<span class="feature1">リサイズは行わず、あくまで圧縮のみを行うものです</span>。リサイズしてサムネイルも作成したい場合は、リポジトリ内の scripts/converter.py のimage.save周辺で作成しちゃって下さい。</p> </div> <div class="section"> <h4 id="技術要素">技術要素</h4> <ul> <li>Docker v17</li> <li>docker-compose v1.16</li> <li>Alpine Linux v3.6</li> <li>Python v3.6.3</li> <li>Pillow-SIMD v4.3.0</li> </ul><p>Alpine linuxベースのpythonイメージに、頑張ってPillow-SIMDをインストールしてイメージを生成してます。依存ライブラリが多いので、228MBというデブデブなイメージになってしまいました。。。</p><p>並列処理についてはPython側でmultiprocessingのProcessで、マルチプロセスで行っています。Dockerに割り当てられているCPU個数を取得して、コア数分自動で並列処理されます。</p><p>もしdocker for macやdocker for windowsをご利用の方は、DockerへのCPU割当数を増やして実行すると、CPUをフルに使い切って高速に変換可能になります。</p> </div> <div class="section"> <h4 id="どんな時に使うの">どんな時に使うの?</h4> <p>主にCIサーバで以下のフローで使う事を想定しています。</p> <ol> <li>Jenkinsでgit clone。</li> <li>cloneしたファイル群に対して、画像変換をかけて圧縮。</li> <li>ビルド。</li> <li>デプロイ。</li> <li>やったね。</li> </ol><p>Jenkinsの場合はWORKSPACEという環境変数に自動でパスが設定されますよね。本dockerイメージもWORKSPACEという環境変数を設定するようにしたので、Jenkinsフレンドリーだと思います。(多分)</p><p>もし対象の画像パスを変えたい場合は、以下のように任意のパスに書き換える事ができます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">export</span><span class="synIdentifier"> WORKSPACE=</span>/tmp/images </pre><p>拡張子毎に圧縮品質を調整したい場合は、settings.txtを編集して下さい。</p> <pre class="code" data-lang="" data-unlink>SUPPORT_EXTENSIONS=.jpg,.jpeg DEFAULT_IMAGE_QUALITY=80 JPEG=70 GIF=70</pre><p>↑のJPEG・GIFといった部分が、フォーマット毎の圧縮品質値で、これはPillow-SIMDに完全に依存しています。詳細は <a href="http://pillow.readthedocs.io/en/3.4.x/handbook/image-file-formats.html">http://pillow.readthedocs.io/en/3.4.x/handbook/image-file-formats.html</a> をご覧下さい。</p><p>ちなみに<span class="feature3">画像変換されたファイルは上書きされます</span>のでご注意下さい。元々CIで使う事を想定しているので、上書きにしちゃってます。</p> </div> <div class="section"> <h4 id="ログ">ログ</h4> <p>↓こんな感じに標準出力されます。システム情報と、対象ファイルの「拡張子」「(本当の)画像フォーマット」「圧縮品質」「圧縮時間」「画像パス」を出力します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">378</span> INFO <span class="synStatement">===</span> SYSTEM INFO <span class="synStatement">=========================================</span> 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">385</span> INFO System : Linux 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Release : 4.9.49-moby 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Version : <span class="synComment">#1 SMP Wed Sep 27 23:17:17 UTC 2017</span> 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Machine : x86_64 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Processor : 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Python version : 3.6.3 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">386</span> INFO Compiler : GCC 6.3.0 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">387</span> INFO Docker cpu core : <span class="synConstant">4</span> 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">387</span> INFO <span class="synStatement">=========================================================</span> 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">463</span> INFO <span class="synIdentifier">ext</span>=.jpg, <span class="synIdentifier">format</span>=JPEG, <span class="synIdentifier">quality</span>=<span class="synConstant">70</span>, <span class="synIdentifier">time</span>=0.06s, <span class="synIdentifier">path</span>=/images/1124191045763.jpg 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">493</span> INFO <span class="synIdentifier">ext</span>=.jpg, <span class="synIdentifier">format</span>=JPEG, <span class="synIdentifier">quality</span>=<span class="synConstant">70</span>, <span class="synIdentifier">time</span>=0.03s, <span class="synIdentifier">path</span>=/images/background.jpg 8c40001ff7da_simple-fast-image-converter | <span class="synConstant">2017-11-05</span> <span class="synConstant">07</span>:<span class="synConstant">19</span>:<span class="synConstant">10</span>,<span class="synConstant">499</span> INFO elapsed_time <span class="synStatement">=</span> 0.11s </pre> </div> </div> <div class="section"> <h3 id="Pillowとの速度比較">Pillowとの速度比較</h3> <p>jpg, png混在で、21362ファイル、合計268.2MByte の画像が存在するフォルダに対して、画像変換をかけてみました。</p><p>さてさて、PillowとPillow-SIMDはどう違いが出るでしょうか。</p><p>PillowとPillow-SIMDは、Dockerイメージでpip installする際にpillowとするか、pillow-simdとするか、で切り分けています。</p><p>ということで、同じ条件でそれぞれ5回づつ変換してかかった時間を計測してみました。</p> <div class="section"> <h4 id="Pillow">Pillow</h4> <table> <tr> <th>1回目</th> <td>25.66s</td> </tr> <tr> <th>2回目</th> <td>29.70s</td> </tr> <tr> <th>3回目</th> <td>27.16s</td> </tr> <tr> <th>4回目</th> <td>28.62s</td> </tr> <tr> <th>5回目</th> <td>28.30s</td> </tr> </table> </div> <div class="section"> <h4 id="Pillow-SIMD">Pillow-SIMD</h4> <table> <tr> <th>1回目</th> <td>27.79s</td> </tr> <tr> <th>2回目</th> <td>27.79s</td> </tr> <tr> <th>3回目</th> <td>27.70s</td> </tr> <tr> <th>4回目</th> <td>27.85s</td> </tr> <tr> <th>5回目</th> <td>30.54s</td> </tr> </table><p>んんん?対して変わらない???AVX2に対応していないCPUだからですかね。うーむ。。。</p> </div> </div> <div class="section"> <h3 id="TIPS">TIPS</h3> <div class="section"> <h4 id="OSError-Errno-24-No-file-descriptors-available">OSError: [Errno 24] No file descriptors available</h4> <p>手持ちのiMacのDocker for macで画像変換を試しても、全くエラーが起きなかったのですが、Linuxサーバ上のDockerで試すと、以下のエラーが発生しました。</p> <pre class="code lang-python" data-lang="python" data-unlink>Traceback (most recent call last): File <span class="synConstant">&quot;/tmp/scripts/converter.py&quot;</span>, line <span class="synConstant">118</span>, <span class="synStatement">in</span> &lt;module&gt; convert_parallel(src_file_path_units) File <span class="synConstant">&quot;/tmp/scripts/converter.py&quot;</span>, line <span class="synConstant">106</span>, <span class="synStatement">in</span> convert_parallel job.start() File <span class="synConstant">&quot;/usr/local/lib/python3.6/multiprocessing/process.py&quot;</span>, line <span class="synConstant">105</span>, <span class="synStatement">in</span> start self._popen = self._Popen(self) File <span class="synConstant">&quot;/usr/local/lib/python3.6/multiprocessing/context.py&quot;</span>, line <span class="synConstant">223</span>, <span class="synStatement">in</span> _Popen <span class="synStatement">return</span> _default_context.get_context().Process._Popen(process_obj) File <span class="synConstant">&quot;/usr/local/lib/python3.6/multiprocessing/context.py&quot;</span>, line <span class="synConstant">277</span>, <span class="synStatement">in</span> _Popen <span class="synStatement">return</span> Popen(process_obj) File <span class="synConstant">&quot;/usr/local/lib/python3.6/multiprocessing/popen_fork.py&quot;</span>, line <span class="synConstant">20</span>, <span class="synStatement">in</span> __init__ self._launch(process_obj) File <span class="synConstant">&quot;/usr/local/lib/python3.6/multiprocessing/popen_fork.py&quot;</span>, line <span class="synConstant">66</span>, <span class="synStatement">in</span> _launch parent_r, child_w = os.pipe() <span class="synType">OSError</span>: [Errno <span class="synConstant">24</span>] No <span class="synIdentifier">file</span> descriptors available </pre><p>なんかファイルディスクリプタが足りないとの事です。ホストOSはAmazonLinuxで、ファイルディスクリプタは65536にしてあります。</p><p>となると足りていないのはDockerのコンテナ側ですね。という事で以下のように設定すると、コンテナ内のファイルディスクリプタを増やす事ができ、無事、ファイルディスクリプタ不足が起きなくなりました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">ulimits</span><span class="synSpecial">:</span> <span class="synIdentifier">nproc</span><span class="synSpecial">:</span> <span class="synConstant">65535</span> <span class="synIdentifier">nofile</span><span class="synSpecial">:</span> <span class="synIdentifier">soft</span><span class="synSpecial">:</span> <span class="synConstant">20000</span> <span class="synIdentifier">hard</span><span class="synSpecial">:</span> <span class="synConstant">40000</span> </pre><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.docker.com%2Fcompose%2Fcompose-file%2F%23ulimits" title="Compose file version 3 reference" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.docker.com/compose/compose-file/#ulimits">docs.docker.com</a></cite><br /> </p> </div> </div> <div class="section"> <h3 id="気づいた事">気づいた事</h3> <p>この画像変換イメージを作り、自分で使っていて気づいたのですが・・・</p><p><span class="feature3 font30">拡張子偽装が結構見つかる</span></p><p>という点です。</p><p>例えば、ファイル名が「xxx.png」だったとします。しかし、Pillowで画像フォーマットを確認してみると「JPEG」と表示されます。拡張子というものはいくらでも偽装して嘘を付けるのですが、以外なほどポロポロ見つかります。</p><p>一番困るのがPNGが.png以外の拡張子に偽装されているケースです。pngは可逆圧縮ですが、圧縮をかけてもほとんどファイルサイズは変わりません。ほとんど圧縮できないのに、圧縮にはJPEGよりも数倍時間を要します。可逆圧縮はコストパフォーマンスは最悪なので、可能な限り可逆圧縮の圧縮は避けるべきです。</p><p>可逆圧縮フォーマットを、非可逆圧縮の拡張子に偽装しているケースは非常にいやらしいですね。今の実装だと偽装拡張子を無視する形にしていますが、pngの圧縮が混入してしまうと、圧縮速度が段違いに遅くなってしまうので、偽装されている場合は処理をスキップしようかな〜?なんて考えてます。</p><p>なんせjpgは0.1〜0.3秒程度で変換できるのに、pngだと1〜10秒かかる事もあるので、少しの混入で相当遅くなってしまうのです。会社の業務プロジェクトのアセット内ですら偽装拡張子がポロポロでてくるので、ネットから拾った壁紙のような画像群に対して変換を書けてしまうと、偽装pngのせいで処理速度が大幅劣化しそうです。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>私はPythonはfabricで少し書いた程度の知識しか無いのですが、今のPython v3.6.3は、型アノテーションというものがあるのですね。知りませんでした。微妙にkotlinと記法が似てて、v2系の頃とは大分様変わりしていて、ちょっとビックリしました。</p><p>昔は型が無い事を売りにしていたと思うのですが、今となっては真逆ですね。どの言語も後付で型を導入したり、最初から型有りきで登場したり。後付で型を導入するケースの場合、やはりIDEとの連携が弱い(最初から型があるものと比較して)ので、強力な連携をしてくれるIDEを使わないと、逆に書きにくくなってしまうジレンマがあるような、無いような。</p><p>今回Pythonをちょっと触ったわけですが、競合である?Rubyは私はさっぱりです。Ruby系のツール、例えばCapistrano・Chef等は全部敬遠しており、唯一使っているのがVagrantでVagrantfileをいじる時くらいでしょうか。</p><p>完全に好き嫌いの話ですが、私はrubyって好きではないし、好きになれないです。私は型や文法がカッチリしている方が好きなので、「色々な書き方ができる!」「ワンライナーでこれだけ書ける!」といったものは避けたく、一定のルールで一定の記述ができる方を好みます。</p><p>かといってよく触るJavaだと堅苦しいのは間違い無いので、最近は専らkotlinかtypescript辺りを好んで使っています。この辺は型がありつつ推論で緩く記述する事もできて、ちょうどいい感じです。<div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4774193836/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51Q6TDdmpHL._SL160_.jpg" class="hatena-asin-detail-image" alt="IntelliJ IDEAハンズオン――基本操作からプロジェクト管理までマスター" title="IntelliJ IDEAハンズオン――基本操作からプロジェクト管理までマスター"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4774193836/treeapps5-22/">IntelliJ IDEAハンズオン――基本操作からプロジェクト管理までマスター</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> 山本裕介,今井勝信</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> 技術評論社</li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/11/08</li><li><span class="hatena-asin-detail-label">メディア:</span> 大型本</li><li><a href="http://d.hatena.ne.jp/asin/4774193836/treeapps5-22" target="_blank">この商品を含むブログを見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4839961743/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51YVxOeBctL._SL160_.jpg" class="hatena-asin-detail-image" alt="Kotlinイン・アクション" title="Kotlinイン・アクション"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4839961743/treeapps5-22/">Kotlinイン・アクション</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> マイナビ出版</li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/10/31</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><a href="http://d.hatena.ne.jp/asin/4839961743/treeapps5-22" target="_blank">この商品を含むブログを見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4865940391/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51HzwN0d1iL._SL160_.jpg" class="hatena-asin-detail-image" alt="Kotlinスタートブック -新しいAndroidプログラミング" title="Kotlinスタートブック -新しいAndroidプログラミング"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4865940391/treeapps5-22/">Kotlinスタートブック -新しいAndroidプログラミング</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> 長澤太郎</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> リックテレコム</li><li><span class="hatena-asin-detail-label">発売日:</span> 2016/07/13</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><a href="http://d.hatena.ne.jp/asin/4865940391/treeapps5-22" target="_blank">この商品を含むブログ (1件) を見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div><div class="hatena-asin-detail"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4865940669/treeapps5-22/"><img src="https://images-fe.ssl-images-amazon.com/images/I/51tA8TFUOZL._SL160_.jpg" class="hatena-asin-detail-image" alt="Kotlin Webアプリケーション 新しいサーバサイドプログラミング" title="Kotlin Webアプリケーション 新しいサーバサイドプログラミング"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazon.co.jp/exec/obidos/ASIN/4865940669/treeapps5-22/">Kotlin Webアプリケーション 新しいサーバサイドプログラミング</a></p><ul><li><span class="hatena-asin-detail-label">作者:</span> 長澤太郎</li><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> リックテレコム</li><li><span class="hatena-asin-detail-label">発売日:</span> 2017/10/06</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li><li><a href="http://d.hatena.ne.jp/asin/4865940669/treeapps5-22" target="_blank">この商品を含むブログを見る</a></li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> </div> treeapps s3への画像配置でAWS LambdaのPython3.6+Pillow-SIMDで複数サムネイル生成する hatenablog://entry/8599973812314639461 2017-11-05T14:58:46+09:00 2017-11-05T14:58:46+09:00 前回からちょっとだけ進化します〜 <p>前回からちょっとだけ進化します〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029033317.png" alt="f:id:treeapps:20171029033317p:plain" title="f:id:treeapps:20171029033317p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F10%2F29%2F034923" title="s3への画像配置でAWS LambdaのPython3.6+Pillowで複数サムネイル生成する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2017/10/29/034923">www.bunkei-programmer.net</a></cite></p><p>前回は、Python Pillowでサムネイルを作成しましたね。</p><p>しかしですね、実はPillowより更に一歩進んだ<span class="feature1">Pillow-SIMD</span>というものが存在するのです。</p> <div class="section"> <h3>Pithon Pillow-SIMD</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fuploadcare%2Fpillow-simd" title="uploadcare/pillow-simd" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/uploadcare/pillow-simd">github.com</a></cite><br /> </p> <div class="section"> <h4>Pillowと100%互換がある</h4> <blockquote> <p>Pillow-SIMD is "following" Pillow. Pillow-SIMD versions are 100% compatible drop-in replacements for Pillow of the same version. </p> </blockquote> <p>このように、Pillow-SIMDは、Pillowと100%互換を持ちます。</p> </div> <div class="section"> <h4>超高速</h4> <blockquote> <p>The results show that for resizing Pillow is always faster than ImageMagick, Pillow-SIMD, in turn, is even faster than the original Pillow by the factor of 4-6. In general, Pillow-SIMD with AVX2 is always 16 to 40 times faster than ImageMagick and outperforms Skia, the high-speed graphics library used in Chromium.</p> </blockquote> <p>更に、Pillowと比較して大体4〜6倍高速、ImageMagickと比較して大体16〜40倍、高速のようです。</p> <blockquote> <p>Pillow-SIMD project is production-ready. The project is supported by Uploadcare, a SAAS for cloud-based image storing and processing.</p> </blockquote> <p>そして、production-readyという事で、production環境でもイケるようです。</p> </div> <div class="section"> <h4>合計ファイルサイズ</h4> <div class="section"> <h5>Pillowの合計ファイルサイズ</h5> <pre class="code lang-sh" data-lang="sh" data-unlink>$ <span class="synStatement">du</span> <span class="synSpecial">-ms</span> ./Pillow <span class="synConstant">19</span> ./Pillow </pre> </div> <div class="section"> <h5>Pillow-SIMDの合計ファイルサイズ</h5> <pre class="code lang-sh" data-lang="sh" data-unlink>$ <span class="synStatement">du</span> <span class="synSpecial">-ms</span> Pillow-SIMD <span class="synConstant">4</span> Pillow-SIMD </pre><p>Pillowが19MByteに対して、Pillo-SIMDはなんと4MByteです。</p> </div> </div> </div> <div class="section"> <h3>AWS Lambda + Pillow-SIMDで画像変換</h3> <p>まず、前回はPillowで画像変換を行いました。</p><p>これを、Pillow-SIMDに置き換えます。正直凄く簡単に置き換えられます。</p><p><a href="https://github.com/uploadcare/pillow-simd#installation">https://github.com/uploadcare/pillow-simd#installation</a></p> <pre class="code lang-sh" data-lang="sh" data-unlink>pip uninstall pillow <span class="synIdentifier">CC</span>=<span class="synStatement">&quot;</span><span class="synConstant">cc -mavx2</span><span class="synStatement">&quot;</span> pip install <span class="synSpecial">-U</span> <span class="synSpecial">--force-reinstall</span> pillow-simd </pre><p>1行目は、既にPillowがインストールされている場合はアンインストールしてね、という事ですが、AWS Lambdaで使用する場合はpipにインストールしないので、これは飛ばして大丈夫です。</p><p>↑は<span class="feature1">これはpipにインストールする形式なので、Lambdaにアップロードできるようにローカルにモジュールをインストールさせます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synIdentifier">CC</span>=<span class="synStatement">&quot;</span><span class="synConstant">cc -mavx2</span><span class="synStatement">&quot;</span> pip install <span class="synSpecial">-U</span> <span class="synSpecial">--force-reinstall</span> pillow-simd <span class="synSpecial">-t</span> . </pre><p>これだけです。「-t .」を追加しただけです。後はPillowと全く同じ手順でlambdaにアップロードすれば、Pillowと全く同じように使用できます。</p><p>一応手順を書いておくと、以下になります。</p> <div class="section"> <h4>setup.cfg</h4> <p>AWS LambdaにPillow-SIMDを反映するため、以下を用意します。</p> <pre class="code" data-lang="" data-unlink>[install] install-purelib=$base/lib64/python</pre><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Flambda%2Flatest%2Fdg%2Flambda-python-how-to-create-deployment-package.html" title="デプロイパッケージの作成 (Python) - AWS Lambda" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html">docs.aws.amazon.com</a></cite><br /> </p> </div> <div class="section"> <h4>Pillow-SIMDをインストール</h4> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synIdentifier">CC</span>=<span class="synStatement">&quot;</span><span class="synConstant">cc -mavx2</span><span class="synStatement">&quot;</span> pip install <span class="synSpecial">-U</span> <span class="synSpecial">--force-reinstall</span> pillow-simd <span class="synSpecial">-t</span> . </pre><p>後は、前回同様にzip圧縮してlambdaにアップロードするだけです。</p> </div> <div class="section"> <h4>画像変換ソースコード</h4> <p>PillowとAPIが同一なので、Pillowで動くコードは、Pillow-SIMDでも動きます。私が検証した感じだと、1行も変えずに動きました。</p><p>なので、前回の記事の以下のコードがそのまま使えます。<br /> <iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.bunkei-programmer.net%2Fentry%2F2017%2F10%2F29%2F034923" title="s3への画像配置でAWS LambdaのPython3.6+Pillowで複数サムネイル生成する - 文系プログラマによるTIPSブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.bunkei-programmer.net/entry/2017/10/29/034923#サムネイル生成ファイル">www.bunkei-programmer.net</a></cite><br /> </p> </div> </div> <div class="section"> <h3>雑感</h3> <p>ImageMagickはそろそろ嫌だなー、という事でPillowを始め、そして更にPillow-SIMDをやってみました。</p><p>Pillowと比較してPillow-SIMDは4〜6倍速い(自称)そうですが、そこまで体感はできませんね。こればかりはちゃんと計測してみないと解りませんが、少なくともImageMagickよりはかなり速いように思えます。画質も申し分無く、機能も中々多いです。</p><p>もしPythonが嫌いでなければ、Pillow-SIMDで画像変換を行うと、幸せになれるかもしれません。</p> </div> treeapps s3への画像配置でAWS LambdaのPython3.6+Pillowで複数サムネイル生成する hatenablog://entry/8599973812312290835 2017-10-29T03:49:23+09:00 2017-10-29T11:07:03+09:00 しょうもないメモ書きみたいな記事です〜 <p>しょうもないメモ書きみたいな記事です〜</p><br /> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029033317.png" alt="f:id:treeapps:20171029033317p:plain" title="f:id:treeapps:20171029033317p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>私は業務で結構長く続く負債満載プロジェクトを担当していたのですが、ようやくAWS移行する事になり、絶賛デスマ中なのです。</p><p><span class="feature3">※ 私はインフラエンジニアではなく、片手間でインフラもやるサーバサイド・フロントエンド?エンジニアです。</span><br /> </p> <ul class="table-of-contents"> <li><a href="#AWS-LambdaとPillowで画像変換する">AWS LambdaとPillowで画像変換する</a><ul> <li><a href="#要件">要件</a></li> <li><a href="#環境整備">環境整備</a></li> <li><a href="#labmdaにアップロードするzipを生成する">labmdaにアップロードするzipを生成する</a><ul> <li><a href="#setupcfg">setup.cfg</a></li> <li><a href="#Pillowをローカルに展開する">Pillowをローカルに展開する</a></li> <li><a href="#サムネイル生成ファイル">サムネイル生成ファイル</a></li> <li><a href="#フォルダ構造">フォルダ構造</a></li> <li><a href="#成果物をzip化する">成果物をzip化する</a></li> </ul> </li> <li><a href="#lambdaの設定">lambdaの設定</a></li> <li><a href="#TIPS">TIPS</a><ul> <li><a href="#出力先フォルダは作成しておかなくてもよい">出力先フォルダは作成しておかなくてもよい</a></li> <li><a href="#ソース画像パスを維持したまま別バケットに出力したい">ソース画像パスを維持したまま別バケットに出力したい</a></li> <li><a href="#lambdaのメモリ割り当て量">lambdaのメモリ割り当て量</a></li> </ul> </li> </ul> </li> <li><a href="#おまけ1私と画像変換の変遷">おまけ1:私と画像変換の変遷</a><ul> <li><a href="#javaのawtによる直列画像変換">javaのawtによる直列画像変換。</a></li> <li><a href="#ImageMagickによる並列画像変換">ImageMagickによる並列画像変換。</a></li> <li><a href="#問題点">問題点</a></li> <li><a href="#そしてAWS環境による画像変換へ">そしてAWS環境による画像変換へ</a></li> <li><a href="#問題点-1">問題点</a><ul> <li><a href="#お客さんは従量課金に慣れていない">お客さんは従量課金に慣れていない</a></li> <li><a href="#クラウドに不慣れなメンバーは一杯いる">クラウドに不慣れなメンバーは一杯いる</a></li> </ul> </li> </ul> </li> <li><a href="#おまけ2s3にアップロードする前に画像変換したい">おまけ2:s3にアップロードする前に画像変換したい</a></li> <li><a href="#雑感">雑感</a></li> </ul> <div class="section"> <h3 id="AWS-LambdaとPillowで画像変換する">AWS LambdaとPillowで画像変換する</h3> <div class="section"> <h4 id="要件">要件</h4> <ul> <li>s3のバケットに画像が配置されたらAWS Lambdaを起動して画像変換する。</li> <li>変換元画像はjpgのみとし、jpgからjpgを生成する。</li> <li>lambdaのランタイムはPython v3.6とする。</li> <li>画像変換にはPython Pillowを使用する。</li> <li>変換元となるオリジナル画像からアスペクト比を保ったサムネイルを2種類生成し、画質を調整して別のバケットに画像をアップロードする。</li> <li>オリジナル画像はサイズは変えず、画質の調整のみを行う。</li> <li>サムネイルの縦横サイズ、画質のクオリティ等をパラメータ化し、環境変数のみで挙動を変える事ができる。</li> <li>ステージング・本番環境などの環境毎にサムネイル生成ロジックを用意せず、環境を変えるだけで済むようにして全環境同一のコードを利用する。</li> </ul><p>現状だと画像変換に関してはnode.jsよりpythonの方が速いようですね。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Ftanakatsu1080%2Fitems%2Fa41bdbdd79760edb07d5" title="AWS Lambdaを使ったサムネール作成でNode.jsとPythonを比較してみた - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/tanakatsu1080/items/a41bdbdd79760edb07d5">qiita.com</a></cite></p><p>そして更なる高速化を図るため、ImageMagickではなくPython Pillowを使います。</p> </div> <div class="section"> <h4 id="環境整備">環境整備</h4> <p>今回はImageMagickではなくPython v3.6でPython Pillowを使って画像変換します。そのためには最低限以下が必要です。</p> <ul> <li>python v3.6のインストール。</li> <li>pipのインストール。</li> </ul><p>lambdaにはImageMagickが最初からインストールされていますが、Pillowはインストールされていません。</p><p>従って、lambdaの登録時にzip圧縮する際に一緒にPillowを内包する必要があります。Pillowを内包するために今回pipを使うので、pipを使えるようにしておきます。</p><p>python自体のインストールはpyenvを使うと、v2.7系とv3.6系を共存できるので、それらで行うといいと思います。</p><p>pyenv install -lでv3.6系が列挙されない場合はpyenv自体のバージョンが古いので、バージョンアップして下さい。</p><p>pipが無い場合は、以下のようにインストールする事ができます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>sudo yum install python-setuptools sudo easy_install pip </pre><p>検証していませんが、mac上でこれらを行うとlinux版と異なるモジュールの場合がある可能性があるため、この作業はlinuxサーバ上で行った方が安全です。</p> </div> <div class="section"> <h4 id="labmdaにアップロードするzipを生成する">labmdaにアップロードするzipを生成する</h4> <p>まずはPillowをローカルに展開する準備です。</p> <div class="section"> <h5 id="setupcfg">setup.cfg</h5> <pre class="code" data-lang="" data-unlink>[install] install-purelib=$base/lib64/python</pre><p>これを配置しておく事で、zipに内包したライブラリを、lambda上で利用できるようになります。</p><p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fdocs.aws.amazon.com%2Fja_jp%2Flambda%2Flatest%2Fdg%2Flambda-python-how-to-create-deployment-package.html" title="デプロイパッケージの作成 (Python) - AWS Lambda" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html">docs.aws.amazon.com</a></cite><br /> </p> </div> <div class="section"> <h5 id="Pillowをローカルに展開する">Pillowをローカルに展開する</h5> <p>システムにインストールするのではなく、カレントディレクトリにファイル・フォルダを展開するイメージです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>pip install Pillow <span class="synSpecial">-t</span> . </pre><p>これでpipでインストールされるPillow関連ファイル群が、カレントディレクトリに展開されます。</p> </div> <div class="section"> <h5 id="サムネイル生成ファイル">サムネイル生成ファイル</h5> <p>以下を「thumbnail_util.py」というファイル名で保存します。ファイル名はlambda設定時に使います。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># -*- coding: utf-8 -*-</span> <span class="synPreProc">from</span> PIL <span class="synPreProc">import</span> Image , JpegImagePlugin <span class="synPreProc">import</span> os, re, boto3 s3 = boto3.client(<span class="synConstant">'s3'</span>) SMALL_IMAGE_SIZE = os.environ[<span class="synConstant">'SMALL_IMAGE_SIZE'</span>] TINY_IMAGE_SIZE = os.environ[<span class="synConstant">'TINY_IMAGE_SIZE'</span>] IMAGE_QUALITY = os.environ[<span class="synConstant">'IMAGE_QUALITY'</span>] <span class="synStatement">def</span> <span class="synIdentifier">convert_image</span>(event, context): srcBucket = event[<span class="synConstant">'Records'</span>][<span class="synConstant">0</span>][<span class="synConstant">'s3'</span>][<span class="synConstant">'bucket'</span>][<span class="synConstant">'name'</span>] destBucket = <span class="synConstant">'dest-image'</span> key = event[<span class="synConstant">'Records'</span>][<span class="synConstant">0</span>][<span class="synConstant">'s3'</span>][<span class="synConstant">'object'</span>][<span class="synConstant">'key'</span>] originalImagePath = <span class="synConstant">'/tmp/'</span> + os.path.basename(key) <span class="synStatement">try</span>: <span class="synComment"># 対象バケットから画像をlambda内にダウンロード</span> s3.download_file(Bucket=srcBucket, Key=key, Filename=originalImagePath) <span class="synComment"># JPEGがMPOと判定されてしまう問題の回避</span> <span class="synComment"># https://qiita.com/csakatoku/items/f5d1d5e91077cf8a3650</span> JpegImagePlugin._getmp = <span class="synStatement">lambda</span> x: <span class="synIdentifier">None</span> originalImage = Image.<span class="synIdentifier">open</span>(originalImagePath, <span class="synConstant">'r'</span>) originalImage.save(originalImagePath, <span class="synConstant">'JPEG'</span>, quality=IMAGE_QUALITY, optimize=<span class="synIdentifier">True</span>) destOriginalImagePath = re.sub(<span class="synConstant">r'^input/'</span>, <span class="synConstant">'original/'</span>, key) s3.upload_file(Filename=originalImagePath, Bucket=destBucket, Key=destOriginalImagePath) image1 = Image.<span class="synIdentifier">open</span>(originalImagePath, <span class="synConstant">'r'</span>) image1.thumbnail((SMALL_IMAGE_SIZE, SMALL_IMAGE_SIZE), Image.LANCZOS) image1.save(originalImagePath, <span class="synConstant">'JPEG'</span>, quality=IMAGE_QUALITY, optimize=<span class="synIdentifier">True</span>) image1DestPath = re.sub(<span class="synConstant">r'^input/'</span>, <span class="synConstant">'small/'</span>, key) s3.upload_file(Filename=originalImagePath, Bucket=destBucket, Key=image1DestPath) image2 = Image.<span class="synIdentifier">open</span>(originalImagePath, <span class="synConstant">'r'</span>) image2.thumbnail((TINY_IMAGE_SIZE, TINY_IMAGE_SIZE), Image.LANCZOS) image2.save(originalImagePath, <span class="synConstant">'JPEG'</span>, quality=IMAGE_QUALITY, optimize=<span class="synIdentifier">True</span>) image2DestPath = re.sub(<span class="synConstant">r'^input/'</span>, <span class="synConstant">'tiny/'</span>, key) s3.upload_file(Filename=originalImagePath, Bucket=destBucket, Key=image2DestPath) <span class="synStatement">return</span> <span class="synStatement">except</span> <span class="synType">Exception</span> <span class="synStatement">as</span> e: <span class="synIdentifier">print</span>(e) <span class="synStatement">raise</span> e </pre><p>os.environの部分が、lambda側で設定する環境変数から値を取得しています。</p><p>lambdaは恐らくdocker的なコンテナで、普通にOSが起動しています。なので、lambdaのコンテナからオリジナル画像があるs3のバケットから目的の画像をダウンロードし、lambda内で画像変換し、変換したものを別のs3にアップロードします。</p><p>s3に画像が配置されるとこれが起動します。つまり1画像につき1 lambdaが起動する事になり、複数画像を配置すれば、その数だけlambda(コンテナ)が並列でガンガン起動していきます。</p> </div> <div class="section"> <h5 id="フォルダ構造">フォルダ構造</h5> <pre class="code lang-sh" data-lang="sh" data-unlink>. ├── □ PIL ├── □ Pillow-4.3.0.dist-info ├── □ __pycache__ ├── □ olefile ├── □ olefile-0.44-py3.6.egg-info ├── OleFileIO_PL.py ├── setup.cfg └── thumbnail_util.py </pre><p>□ はディレクトリです。</p> </div> <div class="section"> <h5 id="成果物をzip化する">成果物をzip化する</h5> <p>zip時の注意点ですが、zipを解凍した際にファイルが展開されないといけません。</p><p>つまり、zipを解凍した際に以下の構造になっている場合はNGです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>lambda ├── □ PIL ├── □ Pillow-4.3.0.dist-info ├── □ __pycache__ ├── □ olefile ├── □ olefile-0.44-py3.6.egg-info ├── OleFileIO_PL.py ├── setup.cfg └── thumbnail_util.py </pre><p>以下のようになっていればOKです。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>□ PIL □ Pillow-4.3.0.dist-info □ __pycache__ □ olefile □ olefile-0.44-py3.6.egg-info OleFileIO_PL.py setup.cfg thumbnail_util.py </pre><p>zipファイル名は何でもOKです。自由に命名して下さい。</p> </div> </div> <div class="section"> <h4 id="lambdaの設定">lambdaの設定</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029021949.png" alt="f:id:treeapps:20171029021949p:plain" title="f:id:treeapps:20171029021949p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022250.png" alt="f:id:treeapps:20171029022250p:plain" title="f:id:treeapps:20171029022250p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022305.png" alt="f:id:treeapps:20171029022305p:plain" title="f:id:treeapps:20171029022305p:plain" class="hatena-fotolife" itemprop="image"></span></p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022332.png" alt="f:id:treeapps:20171029022332p:plain" title="f:id:treeapps:20171029022332p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>「ハンドラ」ですが、どの関数を呼ぶかを記述します。lambdaから見るとどのファイル名のどの関数なのかが解らないので、「ファイル名.関数名」という指定をしてあげる必要があります。</p><p>ここで登場する「環境変数」ですが、ここに指定した値を、関数から「os.environ」で取得する事ができます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022345.png" alt="f:id:treeapps:20171029022345p:plain" title="f:id:treeapps:20171029022345p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>このままだとlambdaは自動起動してくれないので、「何をした時にlambdaを起動するか」という「トリガー」を指定します。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022404.png" alt="f:id:treeapps:20171029022404p:plain" title="f:id:treeapps:20171029022404p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>イベントソースにS3を指定したいのですが、プルダウンの下の方に合って探すのが大変なので、s3とフィルタした方が速いです。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022417.png" alt="f:id:treeapps:20171029022417p:plain" title="f:id:treeapps:20171029022417p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>「イベントタイプ」には、「オブジェクトの作成(全て)」を指定しました。細かく指定できますが、ファイルを新規アップロードした時だけでなく、上書きされた場合等を考慮して全てを選択しました。ここは要件によって任意に変更するといいと思います。</p><p>「プレフィックス」で「バケット内のどのパスに配置されたらイベントを起動するか」を設定します。</p><p>「サフィックス」ではファイルの拡張子等の条件を付ける事ができます。</p><p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/treeapps/20171029/20171029022436.png" alt="f:id:treeapps:20171029022436p:plain" title="f:id:treeapps:20171029022436p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>イベントの設定ができたら、保存ボタンをクリックして完了です!</p><p>後はs3のinputフォルダに画像を配置すると、別のバケットに画像がアップロードされます。</p> </div> <div class="section"> <h4 id="TIPS">TIPS</h4> <div class="section"> <h5 id="出力先フォルダは作成しておかなくてもよい">出力先フォルダは作成しておかなくてもよい</h5> <p>例えば「output」というフォルダに画像を出力するとして、outputフォルダを事前に生成しなくていいのか?と思うかもしれませんが、不要でした。自動で生成されます。</p><p>というか、S3は実はフォルダという概念が無いので、フォルダの生成という事自体が不要なのです。</p><p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdev.classmethod.jp%2Fcloud%2Faws%2Famazon-s3-folders%2F" title="Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする | Developers.IO" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://dev.classmethod.jp/cloud/aws/amazon-s3-folders/">dev.classmethod.jp</a></cite><br /> </p> </div> <div class="section"> <h5 id="ソース画像パスを維持したまま別バケットに出力したい">ソース画像パスを維持したまま別バケットに出力したい</h5> <p>thumbnail_util.pyに以下のコードがあったと思います。</p> <pre class="code lang-python" data-lang="python" data-unlink>key = event[<span class="synConstant">'Records'</span>][<span class="synConstant">0</span>][<span class="synConstant">'s3'</span>][<span class="synConstant">'object'</span>][<span class="synConstant">'key'</span>] </pre><p>これは、アップロードされたバケット内の画像ファイルパスが取得されます。</p><p>これが文字列で取得できるので、後は以下のように普通に文字列操作して出力先を加工できます。(reはregexの略のようです)</p> <pre class="code lang-python" data-lang="python" data-unlink>destOriginalImagePath = re.sub(<span class="synConstant">r'^input/'</span>, <span class="synConstant">'original/'</span>, key) </pre> </div> <div class="section"> <h5 id="lambdaのメモリ割り当て量">lambdaのメモリ割り当て量</h5> <p>今回検証した感じだと、最低の128MByteで十分なようです。</p><p>もし速度が遅かったりOutOfMemoryするようなら任意に増やすといいと思います。</p> </div> </div> </div> <div class="section"> <h3 id="おまけ1私と画像変換の変遷">おまけ1:私と画像変換の変遷</h3> <p>おまけが実は本編的なやつです。</p><p>私が担当している業務での画像変換の変遷です。</p> <div class="section"> <h4 id="javaのawtによる直列画像変換">javaのawtによる直列画像変換。</h4> <p>最初はjavaでやっていたようです。(私は担当してません)</p><p>たまに使われるawtによる画像変換ですが、これがもう<span class="feature3">異常な程遅く、画質も良くはありません</span>。</p><p><span class="feature3">余りにも処理が遅く、なんと次の日の定時バッチ実行までに終わらない事がある程でした</span>。</p> </div> <div class="section"> <h4 id="ImageMagickによる並列画像変換">ImageMagickによる並列画像変換。</h4> <p>そして私は任命されました。</p><p>「<span class="feature3">オマエ、コノジョウキョウ、ナントカシタマエ!</span>」</p><p>と。</p><p>という事で、できるだけ時間をかけず、且つできるだけ簡単に解決する方法を考えました。</p><p>そこで私はjavaのawtの画像変換ではなく、ImageMagicのconvertコマンドによる画像変換を試し、相当速度アップ(詳細に速度比較してません)し、画質も改善した事を確認します。</p><p>最後の問題はトータルの実行時間です。何十時間もかかっている実行時間を何とかしなくてはなりません。簡単な解決策は<span class="feature1">直列実行を並列実行に変える</span>というものでした。</p><p>bashだけで並列数を管理するのが(インフラの都合上)ちょっと難しかったので、javaでスレッド数を管理する事にしました。</p> <ol> <li>cronが起動し、cronで画像変換javaバッチを起動。</li> <li>javaバッチからbashのconvertコマンドを呼び出し、並列8スレッドで画像変換を実行。</li> <li>全てのスレッドが実行完了するのをawait的に待ち、完了したらログ出力+メールで通知。</li> </ol><p>こんな感じに直しました。並列実行の効果は抜群で、awt時代は1秒間に数枚程度しか画像変換できませんでしたが、変更後は1秒間に約60〜70枚画像変換できるようになりました。</p><p><span class="feature1">最終的に画像変換は30分〜1時間で完了するまでに高速化する事ができました</span>。</p><p>その新画像変換プログラムで数年運用し、何とか業務は回っていましたが、問題はありました。</p> </div> <div class="section"> <h4 id="問題点">問題点</h4> <p>画像を変換しているサーバを画像サーバと呼びますが、画像サーバは当然冗長化され、複数台で構成されています。更に、画像サーバといいつつメールサーバも兼任していたり、webサーバがインストールされていたり、もはや役割が曖昧なサーバです。</p> <ul> <li>画像サーバに配置された画像を全サーバに同期させるのにGlusterFSを使ってますが、くっそ重く、CPU使用率が上がりやすい。</li> <li>GlusterFSを管理できる人が全員退職し、もはや誰も触れず、秘伝のタレとすら呼べない、ロストテクノロジー状態です。</li> <li>GlusterFSの同期のせいか、ImageMagicの並列画像変換でCPU使用率100%張り付き+LoadAverageが20〜30で推移する負荷が毎日発生。</li> <li>並列処理というのは難しいものなので、画像変換プログラムを触れる人が私以外いない。</li> <li>画像変換負荷に引きづられて他の処理(webサーバへのアクセスやメール送信処理)が遅い・失敗する事が多々有った。</li> </ul><p>まあそうですよね。色々兼任させつつ並列処理を回し、それをディスク同期してるんですもの。教科書に載せてもいいくらいの悪い例ですね。</p><br /> <p>そしてAWS化する事になり、色々とインフラを変えます。</p> </div> <div class="section"> <h4 id="そしてAWS環境による画像変換へ">そしてAWS環境による画像変換へ</h4> <table> <tr> <th>オンプレ</th> <th>AWS</th> </tr> <tr> <td>画像サーバ</td> <td>撤廃してAmazon S3へ。</td> </tr> <tr> <td>GlusterFSのディスク同期</td> <td>Amazon S3なのでサーバ間のディスク同期要らず。</td> </tr> <tr> <td>ImageMagick</td> <td>AWS LambdaからPython Pillowを実行。</td> </tr> <tr> <td>画像サーバの総合負荷</td> <td>サーバ自体が無くなった</td> </tr> <tr> <td>夜間バッチによる一括画像変換</td> <td>AWS Lambdaによるリアルタイム並列画像変換。</td> </tr> </table><p>こんな感じに変更し、以下の負債を解消する事ができました。</p> <ul> <li>物理サーバが壊れた場合にインフラベンダのディスク交換に1ヶ月を要する、という負債を解消。</li> <li>GlusterFSで問題が起きたら実は誰も修復できません、という負債を解消。</li> <li>画像サーバの負荷による他プログラムへの影響、という負債を解消。</li> <li>並列処理の難しさ、という負債を解消。</li> <li>画像サーバに画像を配置する別開発ベンダが、公開終了したデータの画像を削除してくれず、延々と画像が増え続ける、という負債を解消。</li> </ul><p>サーバは無くなり、負荷は全部S3・lambda側が吸収してくれるようになりました。</p><p>画像を削除してくれない問題も、S3に延々と蓄積していく事になりますが、ライフサイクルを設定できたり、物理サーバと構造が異なる事によって増えてもまあ問題無い(多分)ので、安心感があります。</p> </div> <div class="section"> <h4 id="問題点-1">問題点</h4> <p>やはりAWS移行後も問題点はあると思います。</p> <div class="section"> <h5 id="お客さんは従量課金に慣れていない">お客さんは従量課金に慣れていない</h5> <p>オンプレ環境では無料でできていた画像変換が、AWS移行後は従量課金となります。</p><p>普通に考えれば、オンプレで無料で実現していた件は、裏で膨大な人件費がかかっている事は想像付くと思いますが、お客さんはそれが理解できません。</p><p>なので「<span class="feature3">なんでお金かかるの!?お金かえして!!</span>」みたいな反応をします。</p><p>ここは落ち着いて、無料だと思っていた今までの画像変換は、裏で開発ベンダがせっせと超高額な人件費を消費し、それを開発費として請求していたのでコストが見えて無かっただけですよ〜、的な説明をするしかありません。</p> </div> <div class="section"> <h5 id="クラウドに不慣れなメンバーは一杯いる">クラウドに不慣れなメンバーは一杯いる</h5> <p><span class="feature1">問、今の時代、AWS・GCP・Azureのどれか触った事あるでしょ???</span></p><p><span class="feature3">答、そんな事はありません。パートナー社員はインフラ未経験者は多く、「私はインフラはさっぱりです」という人は本当に多いです。</span></p><p>はい。開発メンバーはプロパー社員が1人(アーキテクト兼インフラ兼PM担当)、パートナー社員が複数人、という構成は世の中に一杯あると思いますが、そういう場合に「今後これチームメンバーに任せられるかな?」という不安は拭い去れません。</p><p>こればっかりはチームメンバーに根気よくレクチャーし、勉強して貰うしかありませんね。</p> </div> </div> </div> <div class="section"> <h3 id="おまけ2s3にアップロードする前に画像変換したい">おまけ2:s3にアップロードする前に画像変換したい</h3> <p>例えば既存プロジェクトで、構成を変えずに画像変換したいぞ!という要望です。</p><p>処理フロー的には以下をイメージします。</p> <ol> <li>CIでgit pull</li> <li>取得したソースコード内から画像を探索し、画像変換。</li> <li>各サーバに配布してデプロイ。</li> </ol><p>こんな感じでpullとデプロイの間に画像変換を挟みます。</p><p>私の担当業務ではこれをdockerで行っています。</p> <ol> <li>alpine linuxベースの画像変換コンテナを起動。</li> <li>コンテナが起動すると、ImageMagickが起動し、コンテナ内で並列に画像変換が行われる。</li> <li>画像ファイルはvolumeマウントされているので、コンテナ内の変換結果はそのままホスト側に配置されている。</li> </ol><p>その処理の一部ですが、以下のような形で行っています。</p><p><a href="https://hub.docker.com/r/treetips/alpine-imagemagick-mogrify-example/">https://hub.docker.com/r/treetips/alpine-imagemagick-mogrify-example/</a></p><p>ImageMagicには、フォルダ内の画像ファイルを一括変換するmogrifyというコマンドがあります。</p><p><a href="https://www.imagemagick.org/script/mogrify.php">Command-line Tools: Mogrify @ ImageMagick</a></p><p>これを利用しつつ、parallelというコマンドを使って、複数フォルダに対してmogrifyを実行しています。並列処理を並列処理している感じです。</p><p>dockerで画像変換する際の注意点として、コンテナ内のowner:groupと、ホスト側のowner:groupのマッピングです。何も考えずにコンテナ側で画像変換しても、owner:groupは恐らくgidの数字が表示されてしまっていると思います。コンテナとホストでowner:groupが共通でないので、gid・uidが揃っていないので、こうなってしまいます。</p><p>なので、コンテナで作業ユーザを追加する際に、ホスト側で予めuid・gidを取得し、そのidを使ってコンテナ内の作業ユーザを作成、その作業ユーザでmogrifyすると、コンテナ内で変換した画像のowner:groupはホスト側と同一になります。</p><p>その作業が面倒で、且つsudo権限をもっていて、且つ簡単にsudoコマンドをスクリプトから叩ける場合は、「sudo chown -R hoge:hoge」しちゃってもいいかなと思います。</p><p>今はImageMagickで行っていますが、もしかしたらこっちもPython Pillowで行ったらもっと速くなるかもしれませんね。</p> </div> <div class="section"> <h3 id="雑感">雑感</h3> <p>案の定おまけが本編化してしまいました。</p><p>lambdaを使って画像変換する例はネット上でよく見かけますが、手段が目的にならないように注意したいですね。「こういう問題があるからs3+lambdaで解決しよう!」はとても良いのですが、「s3+lambdaを使いたい、だから画像変換で使ってみよう」だと、思わぬ部分で要件を満たせなかったりします。</p><p>クラウドでは「あれが使ってみたい!」という気持ちが先行して、手段が目的になってしまい、炎上するケースが後を絶たないので、そこは本当に注意していきたいものです。</p> </div> treeapps