Wowgineer Notes WowTechエンジニアの徒然開発日記 2023-09-12T13:22:47+09:00 wowtech-dev Hatena::Blog hatenablog://blog/98012380858207926 新卒がFlutterで自作アプリ開発をしてみた!〜Flutter を選んだ理由と実際に開発をしてみて〜 hatenablog://entry/820878482957189540 2023-09-12T13:22:47+09:00 2023-09-12T13:22:47+09:00 新卒がFlutter で自作アプリ開発をしてみた! こんにちは、新卒3期生として7月からアプリ開発チームに配属されました、スギヤマです!今回は開発に慣れるためにFlutterで開発してみて学んだことを紹介します。 この記事はFlutterに興味を持ち始めた人や新卒がアプリ開発する際になぜFlutterを選択したのかを知りたい人向けの記事です。 1.なぜFlutterを使用したのか 今回、開発するにあたってFlutterを選定したのかというと、将来性が高いフレームワークと言われているからです。実際にGoogleトレンドで調べてみると、検索率が高いことが以下のグラフでも一目瞭然です。 ReactN… <h1 id="新卒がFlutter-で自作アプリ開発をしてみた">新卒がFlutter で自作アプリ開発をしてみた!</h1> <p>こんにちは、新卒3期生として7月からアプリ開発チームに配属されました、スギヤマです!<br>今回は開発に慣れるためにFlutterで開発してみて学んだことを紹介します。<br> この記事はFlutterに興味を持ち始めた人や新卒がアプリ開発する際になぜFlutterを選択したのかを知りたい人向けの記事です。</p> <h2 id="1なぜFlutterを使用したのか">1.なぜFlutterを使用したのか<br /> </h2> <p>今回、開発するにあたってFlutterを選定したのかというと、将来性が高いフレームワークと言われているからです。実際にGoogleトレンドで調べてみると、検索率が高いことが以下のグラフでも一目瞭然です。 <figure class="figure-image figure-image-fotolife" title="ReactNativeとFlutterとXamarinのトレンド動向"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtsy/20230814/20230814182749.png" width="1200" height="706" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ReactNativeとFlutterとXamarinのトレンド動向</figcaption></figure></p> <p><strong>【1つ目:効率的にアプリを開発できること】</strong><br> Flutterにはアプリケーションを停止したり、再起動したりせずに修正を反映できる「Hot Reload」機能があります。コードをアプリにすぐに反映できるため、開発時間を大幅に削減できるのが特徴です。<br> またFlutterで開発するとAndroidとiOSを同じコードで開発できるため、本来二倍かかるはずだった人件費と時間を削減できます。<br></p> <p><strong>【2つ目:学習する量が少ない】</strong><br> Flutterで使用している言語はDartです。<br> Dartは、JavaScriptに替わる言語だと期待され開発された背景があるため、文法が非常に似ています。これにより、Flutterによる開発をする場合、JavaやJavaScriptを学んだ人にとっては、学習ハードルが低く、比較的理解しやすいのが特徴です。<br><br></p> <h2 id="2Flutter-とReactNativeとXamarinを比較してみた">2.Flutter とReactNativeとXamarinを比較してみた<br /> </h2> <p><figure class="figure-image figure-image-fotolife" title="Flutter とReactNativeとXamarinの比較"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtsy/20230810/20230810184602.png" width="1186" height="274" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Flutter とReactNativeとXamarinの比較</figcaption></figure></p> <table> <thead> <tr> <th> 比較対象 </th> <th> メリット </th> <th> デメリット </th> </tr> </thead> <tbody> <tr> <td>Flutter</td> <td>・ウィジェットが豊富<br>・コミュニティが急速に成長している<br>・公式ドキュメントが非常に充実している<br>・テストが少なくて済む<br>・アプリが速い。ホットリロードでリアルタイムで変更点を逐一把握できる<br>・Webやデスクトップ(Windows、macOSやLinux)に対応したアプリを開発できる</td> <td>・ネイティブ開発と比較すると、スマートフォンの最新機能が使えないなどいくらか劣る<br>・AndroidとiOS両方に対応したアプリケーションを開発できることを想定して開発されているので、アプリの容量が非常に大きい<br>・ネイティブ開発と比較して、ツールやライブラリのセットが限定される</td> </tr> <tr> <td>ReactNative</td> <td>・ホットリロード機能がある<br>・JavaScriptの知識と経験があれば簡単に学習・開発できる<br>・コードを再利用できる<br>・コミュニティ活動が活発である<br>・世界の有名なアプリケーションに使われている<br>・実績があり、Flutterよりも歴史が長くビジネス面で信頼されている</td> <td>・Flutterと同様にネイティブ開発に劣る場面がある<br>・モバイル開発にしか特化していないので、Flutterと比較して選択肢が限られる<br>・パッケージとライブラリの中には、長期間メンテナンスされていないものが少なくない<br>・Flutterと同様に、クロスプラットフォームに対応しているのでアプリケーションの容量がネイティブより大きい</td> </tr> <tr> <td>Xamarin</td> <td>・ネイティブコードにコンパイルされるため、高いパフォーマンスである<br>・C#を使用し、.NETライブラリと統合してクロスプラットフォームアプリケーションを開発可能<br>・ネイティブUIコントロールへのアクセスが容易で、各プラットフォームの特徴を活かせる<br>・共通のコードベースを使用することで、複数プラットフォームの開発を効率的に行える</td> <td>・React NativeやFlutterに比べてコミュニティが小さく、アップデートが遅れることがある<br>・ネイティブコードへのコンパイルが必要で、プラットフォームごとに設定が必要<br>・ウェブプラットフォームへの対応は限定的であり、ウェブアプリケーションの開発には向いていない</td> </tr> </tbody> </table> <p><br>トレンドや発展性から考えるとFlutter を用いることが良いと考えられます。一方で、Flutterの動作環境には違いがあるため、注意が必要です。<br></p> <h2 id="3FlutterでAndroidiOSMacPCWebの開発する際の特徴">3.FlutterでAndroid、iOS、Mac/PC、Webの開発する際の特徴</h2> <p><strong>【Android・iOSの特徴】</strong><br> FlutterはAndroid・iOS両方の開発が一度でできますが、例えば、カメラや通知などのOS固有の機能は、各OSごとに個別に開発する必要があります。さらに、OSアップデートがある場合、OSの新機能などを利用するためにはFlutter のアップデートやサポートされるライブラリーが開発されるのを待つ必要もあるため、場合によっては独自に開発することが求められます。<br><br> <strong>【Mac/PCの特徴】</strong><br> ネイティブのウェブアプリケーションは、HTML、CSS、JavaScriptといったウェブテクノロジーを使用して作成されます。一方で、Flutterのウェブデスクトップアプリケーションは、Flutterの独自のUIフレームワークを使用して構築されます。これにより、ネイティブのウェブテクノロジーとの適合性に制約が生じることがあります。<br><br> <strong>【Webの特徴】</strong><br> ウェブアプリケーションは、異なるブラウザ間での互換性に注意が必要です。一部のブラウザでは正しく表示されない、動作しない、または予期しない結果をもたらすことがあります。一部のライブラリーが対応していない場合があるため注意が必要です。<br><br></p> <h2 id="4実際に自作アプリを作ってみた">4.実際に自作アプリを作ってみた</h2> <p><strong>(1)何を作ったのか</strong><br> 今回作ったアプリは「スーパーお得情報シェアアプリ」です。<br> いろんな店舗に行かずとも、今日の野菜や商品の値段をアプリ上で比較して最安値の店舗を知れるアプリを作りました。<br></p> <p><figure class="figure-image figure-image-fotolife" title="自作アプリ「「スーパーお得情報シェアアプリ」」"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtsy/20230810/20230810184323.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>自作アプリ「スーパーお得情報シェアアプリ」</figcaption></figure></p> <p><strong>(2)取り組んだ背景、目的</strong><br> なぜ、このアプリを作ったのかというと野菜や果物といった食べ物はどこの店が一番安いのかわからず店を行ったり来たりした経験があり、ランキングで安い食べ物を知ることができるアプリが欲しいと考えたからです。<br></p> <p><strong>(3)難しかったところ、課題点とどう対処したか</strong><br> 難しかったところはデータの整合性を保つことが難しかったことです。<br></p> <p>【実現したいこと】<br> ・登録画面で商品を登録するとリストに商品名だけが表示され、その商品名を押した遷移先ではお店の情報と値段の情報がランキングで表示されるようにしたい。<br> ・新たに同じ商品名を登録したときはリストに同じ表品名が並ぶことなくランキング画面で値段の安い順に表示されるようにしたい。<br></p> <p>作成中の問題点は同じ商品名を登録して更新すると同じ名前の商品がリストに並んでしまうので、登録されたデータの整合性を保つ方法について考えることが、私にとっては課題でした。<br><br> 対処として、同じproductNameがあった場合は既存のインスタンスに追加し、新しいproductNameの場合は新たにインスタンスを作るといった、条件を分けてあげることで解決しました。また、1回目の条件if{}の続きに2回目の条件をelse{}で処理を書かないことがキーポイントでした。<br></p> <p>前提として、ShopProductはお店の情報と該当の商品名を保持します。Productは商品の情報を保持します。<br> itemListはProductの配列、newShopProductは渡されてきた新しい情報が入ります。<br></p> <p>例えば、else{}で続きの処理を書いたBAD例<br></p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synStatement">for</span> (<span class="synType">Product</span> p <span class="synStatement">in</span> itemList) {  <span class="synStatement">if</span> (p.productName <span class="synStatement">==</span> newShopProduct.productName) {   p.shopProductList.<span class="synIdentifier">add</span>(newShopProduct); } <span class="synStatement">else</span> {   itemList.<span class="synIdentifier">add</span>(    <span class="synType">Product</span>(     productName<span class="synStatement">:</span> newShopProduct.productName,     shopProductList<span class="synStatement">:</span> [newShopProduct]    )   );  } } <span class="synIdentifier">setState</span>(() {}); </pre> <p><br>この実装をすると、同じ名前が既にあったとしても配列全ての項目を判定する前に次の処理に進んでしまうため、同じ名前がないと判断されてしまいます。<br></p> <p>それを阻止するために以下の実装をします。<br></p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">bool</span> isExist = <span class="synConstant">false</span>; <span class="synStatement">for</span> (<span class="synType">Product</span> p <span class="synStatement">in</span> itemList) {  <span class="synStatement">if</span> (p.productName <span class="synStatement">==</span> newShopProduct.productName) {   isExist = <span class="synConstant">true</span>;   p.shopProductList.<span class="synIdentifier">add</span>(newShopProduct);   <span class="synStatement">break</span>;  } } <span class="synStatement">if</span> (<span class="synStatement">!</span>isExist) {  itemList.<span class="synIdentifier">add</span>(   <span class="synType">Product</span>(    productName<span class="synStatement">:</span> newShopProduct.productName,    shopProductList<span class="synStatement">:</span> [newShopProduct]   )  ); } <span class="synIdentifier">setState</span>(() {}); </pre> <p>このように、実装することで配列全ての項目をチェックした後に次の処理に進むため、同じ名前があったとしても配列に追加されずに理想の形にすることができました。<br><br> <strong>(4)まとめ</strong><br> 初めてのアプリ開発でほぼ、上司と同期に助けていただきながら製作しましたが、新しい発見と学びだらけで目が回りそうな開発期間でした。今回学んだことを忘れずにいち早く貢献できるように次回からも頑張っていきたいです。</p> <h1 id="参考資料">参考資料</h1> <p>・「ionic, React Native, Flutter, Cordova - 調べる」<br> <a href="https://trends.google.co.jp/trends/explore?q=ionic,React%20Native,Flutter,Cordova&geo=JP,JP,JP,JP&date=today%2012-m,today%2012-m,today%2012-m,today%2012-m#GEO_MAP">https://trends.google.co.jp/trends/explore?q=ionic,React%20Native,Flutter,Cordova&geo=JP,JP,JP,JP&date=today%2012-m,today%2012-m,today%2012-m,today%2012-m#GEO_MAP</a> (2023.8.10最終アクセス)<br> ・「React Native vs. Flutter vs. Ionic vs. Xamarin vs. Nativescript (A Detailed Comparison)」<br> <a href="https://inapp-inc.medium.com/react-native-vs-flutter-vs-ionic-vs-xamarin-vs-nativescript-a-detailed-comparison-bfa99d72c4bb">https://inapp-inc.medium.com/react-native-vs-flutter-vs-ionic-vs-xamarin-vs-nativescript-a-detailed-comparison-bfa99d72c4bb</a> (2022.10.31)<br> ・「Flutterの将来性は?メリット・デメリットや特徴も紹介!」<br> <a href="https://tech.hipro-job.jp/column/5874">https://tech.hipro-job.jp/column/5874</a> (2023.06.09)<br> ・「Flutter / フラッターとは?メリット、デメリット、特徴、将来性を解説」<br> <a href="https://genee.jp/contents/tech0003/(2022.07.25)">https://genee.jp/contents/tech0003/(2022.07.25)</a><br> ・「【徹底比較】Flutter VS React Native」<br> <a href="https://zenn.dev/nameless_sn/articles/flutter_vs_reactnative(202.11.21)">https://zenn.dev/nameless_sn/articles/flutter_vs_reactnative(202.11.21)</a><br> ・「クロスプラットフォーム開発手法の比較(Xamarin、Flutter、React Native)」<br> <a href="https://qiita.com/tonionagauzzi/items/9a57bf876b7e956065a4">https://qiita.com/tonionagauzzi/items/9a57bf876b7e956065a4</a> (2019.12.24)<br></p> <p>Kingsoftは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!</p></p> <p><a href="https://recruit.kingsoft.jp/career/" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" style="cursor: pointer;" alt="f:id:wowtech-dev:20190313163146j:plain" /></a></p> wtsy ChatGPTの活用法と新卒社会人が開発した献立提案アプリ hatenablog://entry/820878482957177496 2023-08-10T18:20:14+09:00 2023-09-12T16:49:53+09:00 こんにちは!新卒3期生として7月からアプリ開発チームに配属されました遠藤です。 この記事では、食材管理とChatGPTの連携による献立提案アプリを紹介します。このアプリの概要やシステム構成、技術的課題の克服方法について、さらに初めてのFlutter開発やChatGPT連携を通じての成長体験もまとめています。ChatGPT連携に興味があるけど、具体的な使い方が分からないという人に向けて書いていますので、ぜひご覧ください! 何を作ったか このアプリは、購入した食べ物のリストを元にChatGPTが献立を提案してくれるというアプリです。ユーザーは購入した食材の名前や個数、消費期限を入力し、登録したリス… <p>こんにちは!新卒3期生として7月からアプリ開発チームに配属されました遠藤です。<br></p> <p>この記事では、食材管理とChatGPTの連携による献立提案アプリを紹介します。<br>このアプリの概要やシステム構成、技術的課題の克服方法について、さらに初めてのFlutter開発やChatGPT連携を通じての成長体験もまとめています。ChatGPT連携に興味があるけど、具体的な使い方が分からないという人に向けて書いていますので、ぜひご覧ください!</p> <h1 id="何を作ったか">何を作ったか</h1> <p>このアプリは、購入した食べ物のリストを元にChatGPTが献立を提案してくれるというアプリです。ユーザーは購入した食材の名前や個数、消費期限を入力し、登録したリストから献立を検索できます。ChatGPTとは、ユーザーの質問に対してAIが生成した文章を返してくれるという生成AIの一種です。ChatGPTとのAPI連携により、ユーザーが登録した食べ物を元に最適な献立を提案することができるため、無駄なく食べ物を消費することができます。</p> <p><figure class="figure-image figure-image-fotolife" title="献立提案アプリの使い方"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtce/20230814/20230814154223.png" width="1200" height="654" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>使い方</figcaption></figure></p> <h1 id="取り組んだ背景と目的">取り組んだ背景と目的</h1> <p>今回開発したアプリは私が本配属後初めて取り組んだものです。今後の開発でFlutterを使用していき慣れる必要があり、取り組みました。<br>また、ChatGPT関連の機能を使ってみたいなと思っていたため、このアプリ制作を通じてChatGPTとの連携をやってみようと思いました。<br>  このアプリを作った目的は、今年4月から一人暮らしを始めて、お得な食べ物を購入しても使い切れずに結局は捨ててしまうということが何回かあり、何とかしたいと思っていたことと、食費を節約でき、食品ロスを防ぐことにつながるということは多くの人にとっても有用なのではないかと思い、作ることにしました。</p> <h1 id="FlutterとChatGPTの連携">FlutterとChatGPTの連携</h1> <p>以下が今回私が開発したアプリのシークエンス図です。<br></p> <p><figure class="figure-image figure-image-fotolife" title="献立提案アプリのシークエンス図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtce/20230810/20230810181012.png" width="1200" height="834" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>献立提案アプリのシークエンス図</figcaption></figure></p> <p>製品レベルのプロジェクトでは、アプリ内にAPIキーなどの認証情報を埋め込むことはせず、セキュアなサーバーを利用して通信を仲介するのが一般的ですが、今回は研修用のため、アプリ内に直接ChatGPTのapiキーを持たせて作りました。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">class</span> <span class="synType">MyHttpRequest</span> { <span class="synType">static</span> <span class="synType">const</span> _apiKey = <span class="synConstant">&quot;&quot;</span>; <span class="synType">Future</span><span class="synStatement">&lt;</span><span class="synType">List</span><span class="synStatement">&lt;</span><span class="synType">Recipe</span><span class="synStatement">&gt;&gt;</span> <span class="synIdentifier">getRecipes</span>(<span class="synType">List</span><span class="synStatement">&lt;</span><span class="synType">Food</span><span class="synStatement">&gt;</span> foods, <span class="synType">int</span> requestCount) <span class="synStatement">async</span> { <span class="synComment">//Future&lt;List&lt;Recipe&gt;&gt;</span> <span class="synType">String</span> openApiKey = _apiKey; <span class="synType">String</span> foodList = <span class="synConstant">&quot;&quot;</span>; <span class="synStatement">for</span> (<span class="synType">var</span> food <span class="synStatement">in</span> foods) { foodList <span class="synStatement">+=</span> <span class="synConstant">&quot; </span><span class="synPreProc">${food.name}</span><span class="synConstant"> </span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>; } <span class="synType">String</span> systemPrompt = <span class="synConstant">&quot;&quot;&quot;</span> <span class="synConstant"> # 命令</span> <span class="synConstant"> あなたは主婦の視点を持った料理研究家として、優先度が高い順に並んだ食べ物から作成されたレシピを3品教えてくれるようにプログラムされています。</span> <span class="synConstant"> しかし必ず指示された食べ物全てを使わないといけないという訳ではありません。</span> <span class="synConstant"> また、調味料などは自由に使って構いません。その際は献立で使う材料として追加して下さい。</span> <span class="synConstant"> ## Input Format:</span> <span class="synConstant"> </span><span class="synPreProc">$foodList</span> <span class="synConstant"> &quot;&quot;&quot;</span>; https<span class="synStatement">:</span><span class="synComment">//platform.openai.com/docs/guides/gpt/how-should-i-set-the-temperature-parameter</span> <span class="synType">var</span> url = <span class="synType">Uri</span>.<span class="synIdentifier">parse</span>(<span class="synConstant">'https://api.openai.com/v1/chat/completions'</span>); <span class="synType">var</span> body = <span class="synIdentifier">jsonEncode</span>({ <span class="synConstant">'model'</span><span class="synStatement">:</span> <span class="synConstant">'gpt-3.5-turbo'</span>,<span class="synComment">// or 'model': 'gpt-4',</span> <span class="synConstant">'messages'</span><span class="synStatement">:</span> [ {<span class="synConstant">'role'</span><span class="synStatement">:</span> <span class="synConstant">'user'</span>, <span class="synConstant">'content'</span><span class="synStatement">:</span> foodList}, {<span class="synConstant">'role'</span><span class="synStatement">:</span> <span class="synConstant">'system'</span>, <span class="synConstant">'content'</span><span class="synStatement">:</span> systemPrompt} ], <span class="synConstant">'functions'</span><span class="synStatement">:</span>[ { <span class="synConstant">&quot;name&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;get_recipes&quot;</span>, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;複数の食べ物のリストを基にレシピの提案を3品してください。食べ物リストが少なすぎる場合、素直に'わからない'と答えてください。&quot;</span>, <span class="synConstant">&quot;parameters&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;object&quot;</span>, <span class="synConstant">&quot;properties&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;recipe1&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;object&quot;</span>, <span class="synConstant">&quot;properties&quot;</span><span class="synStatement">:</span>{ <span class="synConstant">&quot;menu&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;string&quot;</span>, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;提案する3つのレシピのうち1品目のレシピの名前。例:'豚肉の生姜焼き'&quot;</span>, }, <span class="synConstant">&quot;ingredients&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;array&quot;</span>, <span class="synConstant">&quot;items&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;object&quot;</span>, <span class="synConstant">&quot;properties&quot;</span><span class="synStatement">:</span>{ <span class="synConstant">&quot;name&quot;</span><span class="synStatement">:</span>{ <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;string&quot;</span>, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;提案する3つのレシピのうち1品目のレシピで使う材料の名前。例1:豚ロース, 例2:玉ねぎ, 例3:薄力粉, 例4:すりおろし生姜, 例5:醤油, 例6:砂糖, 例7:料理酒, 例8:油&quot;</span> }, <span class="synConstant">&quot;quantity&quot;</span><span class="synStatement">:</span>{ <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;string&quot;</span>, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;提案する3つのレシピのうち1品目のレシピで使う材料の量。例1:100g, 例2: 1/4個, 例3:適量, 例4:小さじ1, 例5:大さじ1, 例6:小さじ1, 例7:大さじ1, 例8:小さじ1/2&quot;</span> }, }, <span class="synConstant">&quot;required&quot;</span><span class="synStatement">:</span> [<span class="synConstant">&quot;name&quot;</span>,<span class="synConstant">&quot;quantity&quot;</span>], }, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;提案する3つのレシピのうち1品目のレシピで使う材料の配列。&quot;</span>, }, <span class="synConstant">&quot;process&quot;</span><span class="synStatement">:</span> { <span class="synConstant">&quot;type&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;string&quot;</span>, <span class="synConstant">&quot;description&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;提案する3つのレシピのうち1品目のレシピの作り方。例:'1. 豚ロースに薄力粉をまぶす。</span><span class="synSpecial">\n</span><span class="synConstant">2. ボウルにたれの材料(すりおろし生姜, 醤油, 砂糖, 料理酒)を入れて混ぜ合わせる。</span><span class="synSpecial">\n</span><span class="synConstant">3. 中火で熱したフライパンにごま油をひき、1を入れて焼く。豚ロースに火が通り、三日月型に切った玉ねぎを入れる。</span><span class="synSpecial">\n</span><span class="synConstant">4.豚肉の両面にこんがりと焼き色が付いたら2を入れる。</span><span class="synSpecial">\n</span><span class="synConstant">5.中火で炒め合わせ、全体に味がなじんだら完成。'&quot;</span>, }, }, }, <span class="synConstant">&quot;recipe2&quot;</span><span class="synStatement">:</span>{ <span class="synComment">//以下省略</span> }, <span class="synConstant">&quot;recipe3&quot;</span><span class="synStatement">:</span>{ <span class="synComment">//以下省略</span> } }, <span class="synConstant">&quot;required&quot;</span><span class="synStatement">:</span> [<span class="synConstant">&quot;recipe1&quot;</span>,<span class="synConstant">&quot;recipe2&quot;</span>,<span class="synConstant">&quot;recipe3&quot;</span>], } } ], <span class="synConstant">&quot;function_call&quot;</span><span class="synStatement">:</span> <span class="synConstant">&quot;auto&quot;</span>, <span class="synConstant">&quot;temperature&quot;</span><span class="synStatement">:</span> <span class="synConstant">0.1</span>, }); <span class="synType">var</span> response = <span class="synStatement">await</span> http.<span class="synIdentifier">post</span>( url, headers<span class="synStatement">:</span> { <span class="synConstant">'Content-Type'</span><span class="synStatement">:</span> <span class="synConstant">'application/json'</span>, <span class="synConstant">'Authorization'</span><span class="synStatement">:</span> <span class="synConstant">'Bearer </span><span class="synPreProc">$openApiKey</span><span class="synConstant">'</span>, }, body<span class="synStatement">:</span> body, ); } } </pre> <h1 id="難しかったところ課題点とどう対処したか">難しかったところ、課題点とどう対処したか</h1> <p>まず、初めて使うChatGPTとの連携において、いくつか壁がありました。<br>一つ目は、ChatGPTからのレスポンスのときに、アプリで表示するためのフォーマットである「jsonフォーマット」で返すよう指定したにも関わらず高確率でテキストで返され、正しく情報を取得できないという問題が起きました。しかし、調べていくと、「Function Calling」という方法を見つけ、これを使うことで正しくレスポンスを取得することができました。これについて詳しく説明します。</p> <h1 id="Function-Callingについて">Function Callingについて</h1> <p>Function Callingは、他のサービスの情報を元にChatGPT本来の自然言語で返すために使います。例えば、ChatGPTはリアルタイムの情報がないため今日の天気を聞いても答えることができません。しかし、今日の天気の情報を取得できるプラグインを用いて取得した情報を基にChatGPTに質問することで、自然言語でリアルタイムの天気の情報を送ってくれ、ユーザ側ではChatGPTからリアルタイムの天気の情報が送られることになります。<br> 上記のコードは、Function Callingを用いてpostリクエストしたコードです。postリクエストを送るときにbodyという中身にchatGPTのバージョン情報や受け取りたい情報のフォーマットの指定、その他chatGPTの設定情報などを与えます。functionsというパラメータで「get_recipes」と名付けた関数を作成しています。function_callというパラメータをautoとすることで、実行される関数が自動で選択されます。今回はfunctionsで作成している関数は一つしかないので、autoにしても必然的に一つの関数に決まるのですが、複数関数を作成すると中身の情報を見て自動的に関数を選択し、欲しいフォーマットに沿った情報を返してくれるそうです。</p> <p>今回のアプリでは、本来のFunction Callingの目的の使い方はせず、欲しいフォーマットで返してもらうという目的で使いました。 Function Callingの使い方に関しては、以下の公式ドキュメントを参考に作成しました。</p> <p>Function Callingの使い方 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fplatform.openai.com%2Fdocs%2Fguides%2Fgpt%2Ffunction-calling" title="OpenAI Platform" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://platform.openai.com/docs/guides/gpt/function-calling">platform.openai.com</a></cite></p> <p>次に、レスポンスのフォーマットが正しくなったのはいいものの、今度は一部の情報が空で返ってくるという課題に直面しました。この課題については、調べて試してもなかなか改善できなかったのと、そもそも概念的な理解が難しかったため、日々の業務でChatGPTの開発を行っている陳さんからアドバイスをいただきました。具体的な対処法としては、以下の3つの点を改善しました。</p> <p>方法1:ChatGPTのバージョンをアップデートする<br> 方法2:温度(temperature)プロパティの調整<br> 方法3:functionsで指定するプロパティ「description」の説明を詳細に記述する<br></p> <p>まず、ChatGPTのバージョンをgpt-3.5-turboからgpt-4にアップデートすることで、より高度なレスポンスが得られるようにしました。これを試した時点で、欲しい情報が入った状態で返ってきたため、一応課題は解決できましたが、gpt-3.5-turboに比べると一回のリクエストで約20倍コストがかかるようで、一回一回のリクエストの重みを感じながら使いました(笑)バージョンを落としてでも望ましい回答が得られる方がいいと思い、他の方法も試しました。</p> <p>次は、温度(temperature)プロパティの調整をすると、回答の多様性をコントロールできるというものです。値が低ければ低いほどより確実な答えが返り、高いほどより創造的な回答が返ってきます。</p> <p>実際に試してみました! 今回は使って欲しい食材を「卵、ゴーヤ、キウイ、ほうれん草」にして、chatGPTに献立を提案してもらいました。</p> <p>バージョン3.5 値が低いとき(temperature = 0.1) <figure class="figure-image figure-image-fotolife" title="temperature = 0.1のときのレシピ提案画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtce/20230810/20230810181926.png" width="1200" height="939" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>temperature = 0.1のときのレシピ提案画面</figcaption></figure></p> <p>値が高いとき(temperature = 0.9)</p> <p><figure class="figure-image figure-image-fotolife" title="temperature = 0.9のときのレシピ提案画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wtce/20230810/20230810182653.png" width="1200" height="942" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>temperature = 0.9のときのレシピ提案画面</figcaption></figure></p> <p>値が低いときは味が想像できるレシピを提案してくれましたが、値が高くなると「キウイとゴーヤのサラダ」という何とも言えないレシピを提案してくれました(笑)</p> <p>さらに、descriptionを充実させることで、より細かい情報を得ることができるようにしました。これは3つの中で単純でありながら効果が高い方法だと感じました。</p> <h1 id="さらにやってみたいこと">さらにやってみたいこと</h1> <p>画像生成で、料理の画像を表示してみたいです。 世に出ている料理検索アプリは画像や動画が基本的にはあります。画像や動画があると、視覚的に分かりやすくなり、ユーザーの作るモチベーションに繋がると思うので、画像も表示できたらもっと使いやすくなるかなと思いました。</p> <h1 id="まとめ所感">まとめ(所感)</h1> <p> Flutterでの開発は初めてだったので、APIから受け取ったデータや関数の定義の型などを意識しなければならないのが新鮮でした。また、今までの開発ではクラスやインスタンスを利用したデータの受け渡しや操作を行わず一つのファイルで変数を多用するという開発をしていたため、データの構造が見やすくなり、結果的に実装しやすくなるということが分かりました。  chatGPTは、基本的な知識しかなかったため、概念的な理解を理解するのがそもそも難しいなと感じました。今回動くものは作れましたが、完全に理解できたとは言えないので、これからも少しずつ勉強していきたいです。</p> <h1 id="参考資料">参考資料</h1> <p>Tempertureについて<br> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fplatform.openai.com%2Fdocs%2Fguides%2Fgpt%2Fhow-should-i-set-the-temperature-parameter" title="OpenAI Platform" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://platform.openai.com/docs/guides/gpt/how-should-i-set-the-temperature-parameter">platform.openai.com</a></cite></p> <p>Function Callingについて<br></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Frakuraku-engineer.com%2Fposts%2Fchatgpt-api-function-calling%2F" title="OpenAIの新機能Function Callingを誰でも分かるようイメージを解説 - らくらくエンジニア" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://rakuraku-engineer.com/posts/chatgpt-api-function-calling/">rakuraku-engineer.com</a></cite></p> <p>Kingsoftは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!</p></p> <p><a href="https://recruit.kingsoft.jp/career/" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" style="cursor: pointer;" alt="f:id:wowtech-dev:20190313163146j:plain" /></a></p> wtce iOS版WowTalkアプリへWidgetを追加した時の学びをまとめておく hatenablog://entry/4207112889953815466 2023-01-18T18:53:53+09:00 2023-01-18T18:53:53+09:00 明けましておめでとうございます。ワウテックアプリ開発チームの岡野です。 久しぶりの投稿となりましたが、今回は今度リリース予定のiOS11.9.1で利用可能になったwidget機能について、いくつかでた課題とその対応方法について後学のため整理しておきたいと思います。 先日Androidのwidget機能についてもまとめておりますので、そちらもご確認いただければと思います。 engineer.wowtech.co.jp Widget機能について WowTalkのWidget機能 Widget機能実装で困ったこと 既存のクラスが使えない 問題 対応 Widgetの表示がグレーアウトする 問題 対応 … <p>明けましておめでとうございます。ワウテックアプリ開発チームの岡野です。</p> <p>久しぶりの投稿となりましたが、今回は今度リリース予定のiOS11.9.1で利用可能になったwidget機能について、いくつかでた課題とその対応方法について後学のため整理しておきたいと思います。</p> <p>先日Androidのwidget機能についてもまとめておりますので、そちらもご確認いただければと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fengineer.wowtech.co.jp%2Fentry%2F2022%2F10%2F05%2F121621" title="WowTalkでウィジェット開発する際のポイント - Wowgineer Notes" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://engineer.wowtech.co.jp/entry/2022/10/05/121621">engineer.wowtech.co.jp</a></cite></p> <ul class="table-of-contents"> <li><a href="#Widget機能について">Widget機能について</a><ul> <li><a href="#WowTalkのWidget機能">WowTalkのWidget機能</a></li> </ul> </li> <li><a href="#Widget機能実装で困ったこと">Widget機能実装で困ったこと</a><ul> <li><a href="#既存のクラスが使えない">既存のクラスが使えない</a><ul> <li><a href="#問題">問題</a></li> <li><a href="#対応">対応</a></li> </ul> </li> <li><a href="#Widgetの表示がグレーアウトする">Widgetの表示がグレーアウトする</a><ul> <li><a href="#問題-1">問題</a></li> <li><a href="#対応-1">対応</a></li> </ul> </li> <li><a href="#データの共有方法">データの共有方法</a><ul> <li><a href="#問題-2">問題</a></li> <li><a href="#対応-2">対応</a></li> </ul> </li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考">参考</a></li> </ul> <h1 id="Widget機能について">Widget機能について</h1> <p>Widget機能はiOS14から搭載された機能で、ホーム画面にアプリの特定の機能のショートカットを追加する機能です。お天気アプリやニュースアプリ、株などのWidgetを使ってる方も多いのではないでしょうか。僕自身もカレンダーアプリや天気アプリやバッテリー残量を表示するWidgetは日頃使っています。</p> <h2 id="WowTalkのWidget機能">WowTalkのWidget機能</h2> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113155848.png" width="30%"/> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113155839.png" width="30%"/><br/></p> <p>iOS版WowTalkの11.9.1から、日頃連絡を取るメンバーのチャットルームへ遷移するためのWidgetを実装しました。</p> <p>デフォルトで現在ログインしている自分のアカウントが選ばれた状態になってますが、編集メニューからメンバーリストにいる他のメンバーを選択することも可能です。Widgetは複数登録することもできるので、ホーム画面から簡単にメンバーに連絡をとることができるようになります。</p> <p>また今回はシンプルにこの機能だけですが、今後は通知バッジを表示したり共有、タスク、日報など各種機能の情報を表示するなども検討できればと思います。</p> <h1 id="Widget機能実装で困ったこと">Widget機能実装で困ったこと</h1> <p>ここからは今回WowTalkでWidget機能を実装していく上で生じた課題と対応方法について共有したいと思います。</p> <h2 id="既存のクラスが使えない">既存のクラスが使えない</h2> <h3 id="問題">問題</h3> <p>現在のWowTalkのクラス間の関係は複雑になっており、様々なクラスから参照されるユーティリティクラスが存在します。またこのクラスが逆に他のモデルクラスも参照することから内部的には不要な参照を含むクラス間の関係性が形成されています。</p> <p>これは今後解消を要する課題であると同時に、今回Widgetを実装する上での大きな課題となりました。</p> <p>例えば、WidgetExtensionのTargetでWowTalk内のメンバーを表すBuddyクラスを利用しようとしたとき、XcodeからBuddy.mのTargetMembershipにWidgetExtensionを追加する必要があります。</p> <p>このときTargetMembershipに追加する必要のあるクラスはBuddyを取得するためのDatabase、およびBuddyが参照してるクラスも含む必要があります。しかしながらDatabaseやBuddyから参照してるユーティリティクラス(Utility.h)が今度は別の多くのクラスを参照している状況から、本来は不要なクラスを全部Target Membershipに追加する必要が出てきてしまいました。</p> <p>「だったら全部追加すればいいだろう」と言う人もいるかもしれませんがその数が膨大だったため追加漏れがないかビルドの都度チェックしたり、ARC(自動でメモリを解放する仕組み)を利用していない古いObjective-Cファイルを追加したときに発生したコンパイルフラグの互換性エラーを解消する必要が出てきたりと、過剰な労力が発生したため既存のクラスの利用は避けることにしました。</p> <p><figure class="figure-image figure-image-fotolife" title="複雑に絡み合うカルマ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113160133.png" width="1200" height="781" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>複雑に絡み合うカルマ</figcaption></figure></p> <h3 id="対応">対応</h3> <p>上記の理由からBuddyを利用することは諦め、新しくWidgetで表示するメンバー情報のみを管理するBuddyWrapperクラスを作りました。他のクラスとの参照関係を最低限にすることでWidgetExtensionがビルド時にCompileする必要があるクラスを低減しました。</p> <p>また他の利点として、本来Buddyクラスで保持していたプロパティの中にはWidgetでは使わないものもあり、その分のコードを削除することでクラスとしてもスッキリすることができました。</p> <p>また他にも部門情報を保存するGroupRoomInfoの代わりにDepartmentInfoWrapperクラスを、UserDefaultへの値の設定や取得をするクラスの代わりにGroupDefaultクラスを作成するなど、Widgetに必要な最低限の依存関係で構築されたクラスを作成・利用することでWidgetExtension内部のクラス関係をスッキリさせることができました。</p> <p><figure class="figure-image figure-image-fotolife" title="クラス間の依存関係を必要最低限にする"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113160159.png" width="1200" height="801" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>クラス間の依存関係を必要最低限にする</figcaption></figure></p> <h2 id="Widgetの表示がグレーアウトする">Widgetの表示がグレーアウトする</h2> <h3 id="問題-1">問題</h3> <p><figure class="figure-image figure-image-fotolife" title="Widgetがグレーアウトして閲覧・編集ができない"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113160220.png" width="30%"/><figcaption>Widgetがグレーアウトして閲覧・編集ができない</figcaption></figure></p> <p>Widgetの開発を進めている途中で、ビルドしたWidgetがグレーアウトする現象が発生するようになりました。この問題は自分で開発をしている時には気づかなかったのですが、テスターから指摘を受けWowTalkアプリケーションの再インストールを行ったり検証で使っていた端末以外の端末などで再現することがわかり、何が原因なのかがなかなかわからない問題の1つでした。</p> <h3 id="対応-1">対応</h3> <p>特定のcommitから問題が起きていることはわかったので、一度問題が起きていない時点のcommitからブランチを作り、調査を進めたところ、Widgetに表示する文言の多言語対応(localize)を行った際にIntentdefinitionファイルをBaseではなくEnglishにしてしまっていたことが原因でした。</p> <p><figure class="figure-image figure-image-fotolife" title="正しい状態"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113160328.png" width="1200" height="770" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>正しい状態。右側のLocalizationを見るとintentdefinitionの設定がBaseになっており、サポートする言語にチェックが入る。</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="間違った状態"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20230113/20230113160325.png" width="514" height="336" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>間違った状態。Baseにチェックがない。</figcaption></figure></p> <p>Base(もしくはBase .lproj)とは、StoryboardやXibのファイルを多言語対応させる際に生成されるもので、大昔の書き方ではStoryboard内のテキストを多言語対応する際はlocalizableの設定ファイル(上のプロジェクト画像左のproject ツリー上でオレンジのアイコンで表されるもの)ではなくStoryBoardやXibファイルごとに設定ファイルを定義する必要がありました。</p> <p>Widget Extensionとは異なり、iOSプロジェクトではProject > info > Use Base Internationaliztionを有効にすることでStoryBoardやXibファイルと言語設定ファイルを切り離すことができるようになりましたが、intentdefinitionなどにはこの設定がないため、従来通りBaseにチェックを入れて多言語対応をする必要があります。(参考リンク参照)</p> <blockquote><p><strong>Note</strong></p> <p>Xcode adds the <code>Base</code> and the development language to the localization table by default. Use the Base localization for resources that support string substitution at runtime, such as storyboard, XIB, and Siri intent definition files.</p></blockquote> <h2 id="データの共有方法">データの共有方法</h2> <h3 id="問題-2">問題</h3> <p>WowTalkアプリケーションではアプリ内で利用するユーザのデータはローカルのDBに保存されていますが、Widget ExtensionからはこのDBファイルに直接アクセスすることはできず、Widgetの設定画面で利用したいユーザの情報を本体アプリのターゲットから直接共有できませんでした。</p> <h3 id="対応-2">対応</h3> <p>iOSのapp Groupsの仕組みを利用し、アプリケーションがバックグランドになるタイミングで、DBファイルをapp Groups用のディレクトリにコピーし、Widget ExtensionからコピーされたDBファイルを参照、データを取得することで情報の共有を行いました。</p> <p>この手法の利用シーンは幅広く、NotificationServiceなどの Extensionでも本体アプリのデータを利用したい時などに利用できます。</p> <p>またDB以外にもWidgetに表示するユーザのアイコンなども同様の仕組みを利用して本体と同期できるようにしています。</p> <pre class="code lang-c" data-lang="c" data-unlink>@implementation <span class="synIdentifier">Database</span>(AppGroup) <span class="synComment">//アプリがバックグランドに切り替わる時に呼ばれる処理</span> +(<span class="synType">void</span>)copyDBFile { <span class="synComment">//copyされるDBファイルはユーザごとの切り分けれるようユーザ固有のIDにする</span> NSString* wowtalkid = [[NSUserDefaults standardUserDefaults] objectForKey:@<span class="synConstant">&quot;wowtalk_id&quot;</span>]; NSArray *documentDirectories = <span class="synIdentifier">NSSearchPathForDirectoriesInDomains</span>(NSDocumentDirectory, NSUserDomainMask, YES); NSString *directory = [documentDirectories objectAtIndex:<span class="synConstant">0</span>]; NSString * dbPath = [directory stringByAppendingPathComponent:[NSString stringWithFormat:@<span class="synConstant">&quot;.%@&quot;</span>,wowtalkid]]; NSFileManager *manager = [NSFileManager defaultManager]; <span class="synStatement">if</span> ([manager fileExistsAtPath:dbPath]) { <span class="synComment">//同一のAppGroupsに設定されたProject, Extensionで共有可能なディレクトリパスを取得</span> NSURL *url = [manager containerURLForSecurityApplicationGroupIdentifier:@<span class="synConstant">&quot;//app groupsのIDを設定&quot;</span>]; NSData *db = [manager contentsAtPath:dbPath]; NSString *path = [url.path stringByAppendingPathComponent:[NSString stringWithFormat:@<span class="synConstant">&quot;.%@&quot;</span>, wowtalkid]]; [db writeToFile:path atomically:YES]; NSDictionary *attr = [manager attributesOfItemAtPath:url.path error:nil]; <span class="synIdentifier">NSLog</span>(@<span class="synConstant">&quot;copied db size :</span><span class="synSpecial">%llu</span><span class="synConstant">&quot;</span>,attr.fileSize); } } @end </pre> <h1 id="まとめ">まとめ</h1> <p>いかがだったでしょうか。</p> <p>1つ目の「既存クラスが使えない」という問題は当社特有の問題といえばそうですが、どこのサービスでも起こりうる問題だと思います。特に歴史が長いサービスなどで構成の見直しや技術負債の精算が行われていない状況では起きやすいのかなとも思いますので回避するテクニックの1つとして参考になれば幸いです。</p> <p>また2つ目の「Widgetがグレーアウトする問題」というのも、「iOS14の初期で起きてる問題だからOSをアップデートしろ」や「端末を再起動してみろ」といった回答は見かけるのですが具体的な原因となりうるものについて言及した記事は少ないので同じ問題を抱えてる方の一助になれば幸いです。</p> <p>今回のWidget開発を通じて改めてiOS開発の奥深さを感じましたが、今後はユーザの声を反映してWidgetをより便利にしていければと思います。</p> <h1 id="参考">参考</h1> <p><strong>Adding support for languages and regions</strong><br/> <a href="https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions">https://developer.apple.com/documentation/xcode/adding-support-for-languages-and-regions</a></p> <p><strong>Internationalizing the User Interface</strong> <a href="https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html#//apple_ref/doc/uid/10000171i-CH3-SW2">https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html#//apple_ref/doc/uid/10000171i-CH3-SW2</a></p> <p><strong>iOSアプリの多言語対応について</strong><br/> <a href="https://techblog.zozo.com/entry/ios-localization">https://techblog.zozo.com/entry/ios-localization</a></p> <p>WowTechは随時新メンバーを募集中ですので、一緒に働ける方をお待ちしております!</p></p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" style="cursor: pointer;" alt="f:id:wowtech-dev:20190313163146j:plain" /></a></p> okano4413 初めてFlutter開発に挑戦してわかったこと・困ったこと hatenablog://entry/4207112889946618529 2022-12-21T19:09:21+09:00 2022-12-21T19:09:21+09:00 こんにちは!アプリ開発チームでAndroidの開発を担当しているラジタです。 今回はFlutterフレームワークを使って、WowTalkのモバイル版のサンクス機能の開発を行いました。最近Flutterでの開発アプリや機能が多いので、この記事では、Flutterの開発について自分が困った点や、気に入った点を紹介したいと思います。 Flutterの説明 Dart言語説明 Flutterを使うメリット Flutterを使うデメリット よく使うクラス StatefulBuilderとStatelessWidget ページ移動 (Navigator) 大変だった点 アラートボックスの種類が多かった AP… <p>こんにちは!アプリ開発チームでAndroidの開発を担当しているラジタです。</p> <p>今回はFlutterフレームワークを使って、WowTalkのモバイル版のサンクス機能の開発を行いました。最近Flutterでの開発アプリや機能が多いので、この記事では、Flutterの開発について自分が困った点や、気に入った点を紹介したいと思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221219/20221219111852.png" width="953" height="272" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul class="table-of-contents"> <li><a href="#Flutterの説明">Flutterの説明</a></li> <li><a href="#Dart言語説明">Dart言語説明</a></li> <li><a href="#Flutterを使うメリット">Flutterを使うメリット</a></li> <li><a href="#Flutterを使うデメリット">Flutterを使うデメリット</a></li> <li><a href="#よく使うクラス">よく使うクラス</a><ul> <li><a href="#StatefulBuilderとStatelessWidget">StatefulBuilderとStatelessWidget</a></li> <li><a href="#ページ移動-Navigator">ページ移動 (Navigator)</a></li> </ul> </li> <li><a href="#大変だった点">大変だった点</a><ul> <li><a href="#アラートボックスの種類が多かった">アラートボックスの種類が多かった</a></li> <li><a href="#APIで取得したデータをViewに反映させるまでの流れがNativeと異なっていた">APIで取得したデータをViewに反映させるまでの流れがNativeと異なっていた</a></li> <li><a href="#リンクをブラウザで開く方法">リンクをブラウザで開く方法</a></li> <li><a href="#Debugについて">Debugについて</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考">参考</a></li> </ul> <h1 id="Flutterの説明">Flutterの説明</h1> <p>Flutterとは、2018年にGoogleが発表したマルチプラットフォームアプリ用のオープンソース(ソースコードが一般公開されている)型のフレームワークです。この枠組みの「Hot Reload」という、書いたコードを即座にアプリの挙動に反映できる機能を使うことで、より簡単かつ迅速に開発を進めることができるようになります。Flutterのフレームワークには、Googleが2011年に発表したDartと呼ばれるアプリ開発言語が使われています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201195723.png" width="1200" height="563" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="Dart言語説明">Dart言語説明</h1> <p>Dartとは、サーバー、モバイル、デスクトップ、ウェブアプリケーションを構築できる高水準のプログラミング言語です。Dartと他のプログラミング言語の違いは、Pubと呼ばれる独自のパッケージマネージャーが使われていることです。開発者はこれらのパッケージを使用して、DartおよびFlutterアプリを構築できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221219/20221219112246.png" width="800" height="300" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="Flutterを使うメリット">Flutterを使うメリット</h1> <ol> <li>開発速度の向上と開発コストの削減<br/> Native開発との大きな違いは、クロスプラットフォーム開発がある点です。これにより1つのソースコードでiOSとAndroidという複数のWowTalkアプリの機能の開発ができるようになるため、開発の速度が上がり、プラットフォーム間で実装しないといけないコードの総量を削減することができます。</li> <li>保守と運用コストの削減<br/> WowTalkのサンクス以外の機能に関して、Androidアプリは、Androidアプリエンジニア(自分)が、iOSアプリはiOSアプリエンジニアが保守・運用を担当します。稼働が増える分、それに合わせて、保守と運用コストも増えます。クロスプラットフォーム(Flutter)開発の場合は、一つのソースコードでAndroidとIOS両方モバイルアプリを動かしますので、対応稼働が減り、必然的にコストも減ります。</li> <li>仕様変更に強く、試験にかかる工数を抑えることができる<br/> WowTalk Native開発では、ソースコードの変更後は、変更点適応のためにモバイルアプリの再起動(再ビルド)をする必要があります。しかし、Flutterにはホットリロード(Hot Reload)と言う機能があり、開発中にcmd+sを押すと、変更点をすぐにリロード(更新)することができます。</li> </ol> <h1 id="Flutterを使うデメリット">Flutterを使うデメリット</h1> <ol> <li>端末独自のOSの影響を受けてしまう<br/> Flutterの大きなメリットは、一つのコードでWowTalk IOSとAndroidモバイルアプリ開発ができることを挙げましたが、OSごとに搭載されている機能(カメラ機能、位置情報機能など)に関しては、個別でソースコードの更新をする必要があります。また、iOSやAndroidのOSアップデートで追加された新しい機能をFlutterがサポートするまでに時間がかかる場合があります。</li> <li>ネイティブ開発と比べるとライブラリが少ない<br/> ネイティブ開発で用いられるアプリ開発言語は、(iOSアプリの場合はSwift、Androidアプリの場合はJavaなど)と比較すると少ないですが、徐々に増えてきています。</li> <li>ネイティブ開発の方が動作面で有利に働く場合がある<br/> Flutterは、マルチプラットフォームに対応していることから、保守性の面でも動作の改善は早いですが、UI側でのレンダリング、ドロップダウンスピード、テキストのフォーカスなどの面でNativeアプリと比べると、動きが良くないと感じる場合があります。</li> </ol> <h1 id="よく使うクラス">よく使うクラス</h1> <h3 id="StatefulBuilderとStatelessWidget">StatefulBuilderとStatelessWidget</h3> <p>Stateとは「状態」や「状況」という意味です。変数の値、端末内に保存しているデータやAPIで取得したデータ、入力フォーム等のUIの状態や入力内容、端末の状態などがこれにあたります。このウィジェットタイプは自分の変数を変更することで自身を再ビルドすることができ、表示内容を自動的に変更できます。</p> <p>StatefulBuilderとは、StatelessWidgetをStatefulWidgetとして扱うためのウィジェットです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201195830.png" width="412" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>今回この機能のモバイル版の開発にあたり、交換時に表示するダイアログは、StatefullBuilderを使って作成しました。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synStatement">return</span> <span class="synType">AlertDialog</span>( title<span class="synStatement">:</span> <span class="synType">Text</span>(<span class="synType">Localized</span>.<span class="synIdentifier">of</span>(context).redeem_point_exchange_title), <span class="synConstant">01</span> content<span class="synStatement">:</span> <span class="synType">StatefulBuilder</span>( builder<span class="synStatement">:</span> (<span class="synType">BuildContext</span> context, <span class="synType">StateSetter</span> setState) { <span class="synStatement">return</span> <span class="synType">Column</span>(mainAxisSize<span class="synStatement">:</span> <span class="synType">MainAxisSize</span>.min, children<span class="synStatement">:</span> <span class="synStatement">&lt;</span><span class="synType">Widget</span><span class="synStatement">&gt;</span>[ <span class="synType">Padding</span>( padding<span class="synStatement">:</span> <span class="synType">const</span> <span class="synType">EdgeInsets</span>.<span class="synIdentifier">all</span>(<span class="synConstant">10</span>), child<span class="synStatement">:</span> <span class="synType">Text</span>( <span class="synType">Localized</span>.<span class="synIdentifier">of</span>(context).redeem_point_exchange_sub_title, style<span class="synStatement">:</span> <span class="synType">const</span> <span class="synType">TextStyle</span>(color<span class="synStatement">:</span> <span class="synType">Colors</span>.grey, fontSize<span class="synStatement">:</span> <span class="synConstant">12</span>), )), <span class="synType">Padding</span>( padding<span class="synStatement">:</span> <span class="synType">const</span> <span class="synType">EdgeInsets</span>.<span class="synIdentifier">all</span>(<span class="synConstant">0</span>), child<span class="synStatement">:</span> <span class="synType">DropdownButtonHideUnderline</span>( child<span class="synStatement">:</span> <span class="synType">DropdownButton2</span>( isExpanded<span class="synStatement">:</span> <span class="synConstant">true</span>, ...... value<span class="synStatement">:</span> _selectedPoint, onChanged<span class="synStatement">:</span> (value) { _selectedPoint = value <span class="synStatement">as</span> <span class="synType">String</span>; _selectedItem = <span class="synType">int</span>.<span class="synIdentifier">parse</span>(_selectedPoint<span class="synStatement">!</span>); exRate = (_selectedItem<span class="synStatement">/</span>exchangeRate).<span class="synIdentifier">toString</span>(); <span class="synConstant">02</span> <span class="synIdentifier">setState</span>((){ requiredPoint = <span class="synType">MessageUtil</span>.<span class="synIdentifier">format</span>(<span class="synType">Localized</span>.<span class="synIdentifier">of</span>(context).redeem_point_exchange_required_point, exRate); requiredPointDescription = <span class="synType">MessageUtil</span>.<span class="synIdentifier">highlightText</span>(requiredPoint,exRate, <span class="synType">const</span> <span class="synType">TextStyle</span>(color<span class="synStatement">:</span> <span class="synType">Colors</span>.grey, fontSize<span class="synStatement">:</span> <span class="synConstant">12</span>), <span class="synType">const</span> <span class="synType">TextStyle</span>(color<span class="synStatement">:</span> myColors.point_highlight)); }); }, ), ), <span class="synType">Padding</span>( padding<span class="synStatement">:</span> <span class="synType">const</span> <span class="synType">EdgeInsets</span>.<span class="synIdentifier">all</span>(<span class="synConstant">10</span>), <span class="synConstant">03</span> child<span class="synStatement">:</span> requiredPointDescription), <span class="synType">Column</span>( crossAxisAlignment<span class="synStatement">:</span> <span class="synType">CrossAxisAlignment</span>.start, children<span class="synStatement">:</span> <span class="synStatement">&lt;</span><span class="synType">Widget</span><span class="synStatement">&gt;</span>[ ...... </pre> <ol> <li>StatefulBuilderウィジェットを追加しています。</li> <li>DropDownのonChangeを行う際は、必要なポイントを計算し、setStateを使ってUI更新依頼を行います。</li> <li>UIに設定されているTextは更新されます。</li> </ol> <p>以下はUIが更新される画面の例です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201195943.gif" width="410" height="728" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ページウィジェット (StatelessWidget)とは、Stateの状態を持たない静的な(static)ウィジェットのことです。ウィジェットで使用する変数は親ウィジェットから渡された値を使用することしかできません。値の変更はウィジェット内でできません。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201200033.png" width="361" height="179" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>WowTalkのサンクス機能では、ユーザが獲得しているポイントを表示するヘッダー部分をStatelessWidgetで実装しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201200057.png" width="408" height="730" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このヘッダー部分のポイント値は、APIから返ってくる値を表示する設定です。サンクステンプレートやギフトを交換した際に、APIから返ってきた値は更新されます。</p> <h3 id="ページ移動-Navigator">ページ移動 (Navigator)</h3> <p>Flutterでの宣言的なUI構築で画面遷移の方法にNavigatorを使います。今回の実装でNavigator.pushとNavigator.popを使いました。</p> <pre class="code lang-dart" data-lang="dart" data-unlink>child<span class="synStatement">:</span> <span class="synType">GestureDetector</span>( onTap<span class="synStatement">:</span> () { <span class="synType">Navigator</span>.<span class="synIdentifier">push</span><span class="synStatement">&lt;</span><span class="synType">void</span><span class="synStatement">&gt;</span>(context, <span class="synType">MaterialPageRoute</span>(builder<span class="synStatement">:</span> (context) <span class="synStatement">=&gt;</span> <span class="synType">GiftRecordView</span>())); }, </pre> <p>Navigator.pushを使うと、指定した画面(widget!)へ遷移します。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">void</span> <span class="synIdentifier">pressOk</span>(<span class="synType">BuildContext</span> context, <span class="synType">String</span> _selectedItem) { <span class="synStatement">for</span> (<span class="synType">var</span> gift <span class="synStatement">in</span> feePointGiftList) { <span class="synStatement">if</span> (gift.requiredPoint.<span class="synIdentifier">toString</span>() <span class="synStatement">==</span> _selectedItem) { <span class="synIdentifier">exchangePointHandler</span>(gift.giftId.<span class="synIdentifier">toString</span>()); <span class="synType">Navigator</span>.<span class="synIdentifier">pop</span>(context); } } } </pre> <p>Navigator.popの場合、元の画面に戻ります。</p> <h1 id="大変だった点">大変だった点</h1> <h3 id="アラートボックスの種類が多かった">アラートボックスの種類が多かった</h3> <p>ポイント交換ダイアログ画面のDropDownを表示する必要があり、調べてみると、複数の開発元から作られたライブラリーが見つかりました。最初に<strong>awesome_dropdown</strong>ライブラリーを使って開発を行いました。動作確認した際に、ドロップダウンを開いてキャンセルで戻った場合、UIが残ってしまうという不具合が発生しました。また、IOSでタップしても反応しない不具合も発生しました。Googleで解決方法を調べても見つからなかったので今回は<strong>dropdown_button2</strong>ライブラリーを使っています。</p> <p>大変だった実装</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221201/20221201200133.gif" width="410" height="728" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="APIで取得したデータをViewに反映させるまでの流れがNativeと異なっていた">APIで取得したデータをViewに反映させるまでの流れがNativeと異なっていた</h3> <p>今回のプロジェクトでAPIからのデータ取得は、index.dartファイルをentry pointにしてViewに反映させるまでの流れになっていました。</p> <p>AndroidのNative開発では、直接対象の画面からAPIを呼んでデータ取得することになっているので流れに関して悩みました。</p> <p>WowTalkのNative側の実装では、画面に表示したいデータがある場合、ActivityからAPIへリクエストするためのクラス(WebIf)へリクエストを送り、返ってきたレスポンスをViewに渡して表示していますが、Flutterの実装では画面のルートViewにあたるindex.dartで子ビューで利用されるProviderを登録し、Providerが保持するBlocを使ってAPIへのリクエストと親ウィジェットから子ウィジェットへのデータの受け渡しを実現しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20221219/20221219114345.png" width="606" height="372" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="リンクをブラウザで開く方法">リンクをブラウザで開く方法</h3> <p>今回の対応で交換履歴からリンクを開く必要があったので、リンクをブラウザで開くコードを調べました。</p> <p>まず①のコードでは上手くいきませんでした。</p> <p>①上手くいかなかったコード</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">Future</span><span class="synStatement">&lt;</span><span class="synType">void</span><span class="synStatement">&gt;</span> <span class="synIdentifier">_launchURL</span>(<span class="synType">String</span> strUrl) <span class="synStatement">async</span> { <span class="synType">final</span> url = <span class="synType">Uri</span>.<span class="synIdentifier">parse</span>(strUrl); <span class="synStatement">if</span> (<span class="synStatement">await</span> <span class="synIdentifier">canLaunchUrl</span>(url)) { <span class="synStatement">await</span> <span class="synIdentifier">launchUrl</span>(url); } } </pre> <p>②上手くできたコード</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">Future</span><span class="synStatement">&lt;</span><span class="synType">void</span><span class="synStatement">&gt;</span> <span class="synIdentifier">_launchURL</span>(<span class="synType">String</span> strUrl) <span class="synStatement">async</span> { <span class="synStatement">if</span> (<span class="synStatement">await</span> <span class="synIdentifier">canLaunch</span>(strUrl)) { <span class="synStatement">await</span> <span class="synIdentifier">launch</span>(strUrl); } } </pre> <p>原因を調べてみると、<strong>「launchUrl」メソッドとURLが有効か否かチェックする「canLaunchUrl」が、バージョン6.1.0から</strong>変更されたメソッドになっていました。今回使ってるのは、6.0.5バージョンなので②の書き方が正しかったです。</p> <p>今回使ったライブラリーは「<a href="https://pub.dev/packages/url_launcher">url_launcher | Flutter Package</a>」です。</p> <h3 id="Debugについて">Debugについて</h3> <p>Flutterの開発中にDebugで実装確認する必要があり、BreakPointを設定し、手元の端末でDebugモードで動かしてみました。するとBreakポイントに止まりませんでした。</p> <p>チーム内で相談したり、Googleで調べてみると、Emulatorだと上手くいくということが分かったので、Emulatorで試してみると、Debugが上手くできました。</p> <p>改善する必要がある点としては、今回サンクス機能以外はNativeで開発しているので、WowTalkアプリと一緒にDebugができなくなっています。不具合対応やバージョンアップをするとまたFlutterのサンクスモジュールで別に対応してNative(wowtalk)アプリに混ぜる必要があります。</p> <h2 id="まとめ">まとめ</h2> <p>Flutterを使って初めて開発しましたが、Flutterはすごく便利なフレームワークだと思いました。これからもFlutterを使って新しい機能開発をしたいと思います。</p> <p>ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。<br />随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" style="cursor: pointer;" alt="f:id:wowtech-dev:20190313163146j:plain" /></a></p> <h2 id="参考">参考</h2> <ol> <li>URLを開くために使うライブラリー <br/> <a href="https://pub.dev/packages/url_launcher">url_launcher</a></li> <li>カスタマイズができるドロップダウンライブラリー <br/> <a href="https://pub.dev/packages/dropdown_button2">dropdown_button2</a> , <a href="https://pub.dev/packages/awesome_dropdown">awesome_dropdown</a></li> </ol> rajithamendis サンクス新機能の開発を通して新しく勉強になったこと hatenablog://entry/4207112889929217221 2022-10-28T17:57:19+09:00 2022-10-28T17:57:19+09:00 こんにちは!新卒2期生として7月からWeb開発チームに配属されました。河原です(^^) 今回の記事が初投稿です!!入社前に読んでいたブログをまさか自分が執筆する側になるとは思っていなかったので、今すごく嬉しいです。よろしくお願いいたします! 今回WowTalkのサンクス新機能の開発に携わらせていただいたので、そこで勉強になったことを初学者の視点で、まとめていきたいと思います。 はじめに WowTalkのサンクス機能について サンクス機能開発を通して勉強になったこと フロントエンド部分での学び 【Redux・Redux-toolkitについて】 【ReduxとRedux-toolkitの違い】 … <p>こんにちは!新卒2期生として7月からWeb開発チームに配属されました。河原です(^^)<br /> 今回の記事が初投稿です!!入社前に読んでいたブログをまさか自分が執筆する側になるとは思っていなかったので、今すごく嬉しいです。よろしくお願いいたします!</p> <p>今回WowTalkのサンクス新機能の開発に携わらせていただいたので、そこで勉強になったことを初学者の視点で、まとめていきたいと思います。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#WowTalkのサンクス機能について">WowTalkのサンクス機能について</a></li> <li><a href="#サンクス機能開発を通して勉強になったこと">サンクス機能開発を通して勉強になったこと</a><ul> <li><a href="#フロントエンド部分での学び">フロントエンド部分での学び</a><ul> <li><a href="#ReduxRedux-toolkitについて">【Redux・Redux-toolkitについて】</a></li> <li><a href="#ReduxとRedux-toolkitの違い">【ReduxとRedux-toolkitの違い】</a></li> <li><a href="#useStateを用いたデータ操作のイメージ">【useStateを用いたデータ操作のイメージ】</a></li> <li><a href="#propsを用いたデータの受け渡しのイメージ">【propsを用いたデータの受け渡しのイメージ】</a></li> <li><a href="#Reduxを用いたデータの受け渡しのイメージ">【Reduxを用いたデータの受け渡しのイメージ】</a></li> </ul> </li> <li><a href="#API実行部分開発での学び">API実行部分開発での学び</a><ul> <li><a href="#Promiseについて">【Promiseについて】</a></li> <li><a href="#そもそも非同期処理とは何か">【そもそも非同期処理とは何か?】</a></li> <li><a href="#Promiseの引数について">【Promiseの引数について】</a></li> <li><a href="#thencatchについて">【then(),catch()について】</a></li> <li><a href="#Fetch-APIについて">【Fetch APIについて】</a></li> </ul> </li> <li><a href="#バックエンド部分での学び">バックエンド部分での学び</a><ul> <li><a href="#エラー処理について">【エラー処理について】</a></li> <li><a href="#アクセス途中でのエラー">【アクセス途中でのエラー】</a></li> <li><a href="#予期していないデータが入る可能性">【予期していないデータが入る可能性】</a></li> <li><a href="#そもそもデータがない可能性">【そもそもデータがない可能性】</a><ul> <li><a href="#Itemdataそのものが空の場合">Itemdataそのものが空の場合</a></li> <li><a href="#指定されたitemIdのデータがない場合">指定されたitemIdのデータがない場合</a></li> <li><a href="#コードを修正">コードを修正!</a></li> </ul> </li> </ul> </li> <li><a href="#終わりに">終わりに</a></li> </ul> </li> </ul> <h3 id="はじめに">はじめに</h3> <p>開発に携わったサンクス新機能ですが、実はどんな機能なのかまだ発表できません...!ですので、今回は代わりにWowTalkのサンクス機能についてご紹介します!</p> <h3 id="WowTalkのサンクス機能について">WowTalkのサンクス機能について</h3> <p>WowTalkのサンクス機能は主に3つの機能から成り立っています。</p> <ol> <li><p><strong>レター機能</strong><br /> 日頃のちょっとした場面や大事な場面で仲間にありがとうを送る仕組みです。レターを読むと何だかほっこりします。</p></li> <li><p><strong>サンクススコア機能</strong> <br /> WowTalk内でありがとうの気持ちがどれほど伝えられているか、自分を中心としてサンクススコアとして可視化されます。</p></li> <li><p><strong>ポイント機能</strong><br /> 自分がありがとうを送った分だけポイントが貯まります。現在貯めたポイントは新しいレターのデザインと交換できます。すごく可愛いデザインのレターがたくさんあり、どれと交換しようか迷ってしまいます。</p></li> </ol> <p><em>「ありがとう」で幸せな働き方の実現をサポートする</em><br /> 心あたたまる機能がこのサンクス機能です!</p> <h2 id="サンクス機能開発を通して勉強になったこと">サンクス機能開発を通して勉強になったこと</h2> <p>それでは、今回サンクス機能の開発で勉強になったことを、自身の復習も兼ねて<strong>フロントエンド、API実行部分、バックエンド開発</strong>に分けて、まとめていきたいと思います!(勉強になったことがたくさんありすぎて、どれを取り上げて記事にしようか非常に迷いました(笑))</p> <h3 id="フロントエンド部分での学び">フロントエンド部分での学び</h3> <h4 id="ReduxRedux-toolkitについて">【Redux・Redux-toolkitについて】</h4> <p>学生時代に本当に少しだけ<code>React</code>をかじっただけの私。<code>state</code>の管理といえば、<code>useState</code>!<code>useState</code>はとっても便利!という感覚でしたが、いざ既存コードを見ると、<code>Redux-toolkit</code>というものが。なんだこれは。いきなりパニック。</p> <p><code>useState</code>でもこんなに便利なものがあるのか〜と感動していましたが、<code>Redux-toolkit</code>は更にデータを管理するのに便利な仕組みが盛りだくさんでした!</p> <h4 id="ReduxとRedux-toolkitの違い">【ReduxとRedux-toolkitの違い】</h4> <p>弊社のサンクス機能の開発で使われているのは<code>Redux-toolkit</code>です。これは<code>Redux</code>をより簡単に扱えるようにしたものです。</p> <h4 id="useStateを用いたデータ操作のイメージ">【useStateを用いたデータ操作のイメージ】</h4> <p><code>useState</code>は<code>state</code>を簡単に管理することができる機能です。<code>state</code>は<code>props</code>と異なり、自由にデータを書き換えることができます。<code>useState</code>の状態管理はそのコンポーネント内で完結するという特徴があります。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>graph TB id1((ItemModal&lt;br/&gt;&lt;br/&gt;itemModalVisible&lt;br/&gt;true/false)) id2((MyMenu&lt;br/&gt;&lt;br/&gt;LoginCount&lt;br/&gt;1,2...))</pre> <h4 id="propsを用いたデータの受け渡しのイメージ">【propsを用いたデータの受け渡しのイメージ】</h4> <p><code>useState</code>は同じコンポーネント内のみでデータを保持するため、コンポーネント間でのデータの受け渡しでは<code>props</code>を用います。<code>props</code>の特徴として、親コンポーネントから子コンポーネントに渡される値であり、子コンポーネントから親コンポーネントの値を直接操作することはできないといった制約があります。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>graph TB A(&#34;App&#34;)--props--&gt;B(&#34;ShopMenu&#34;)--props--&gt;D(&#34;ItemModal&#34;) A(&#34;App&#34;)--props--&gt;C(&#34;MyMenu&#34;)--props--&gt;E(&#34;PointModal&#34;)</pre> <p>この図の場合、<code>MyMenu</code>から<code>ItemModal</code>へのデータの受け渡しや、<code>ItemModal</code>から<code>PointModal</code>へといった、親子関係のないデータ同士の受け渡しはできません。</p> <p>また<code>props</code>は親コンポーネントから値を受け取っているだけなので、子コンポーネントで値を変更して、親コンポーネントに値を返す...といったことはできません。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>graph BT D(&#34;ItemModal&#34;)--X NG X--&gt;B(&#34;ShopMenu&#34;) E(&#34;PointModal&#34;)--X NG X--&gt;C(&#34;MyMenu&#34;)</pre> <h4 id="Reduxを用いたデータの受け渡しのイメージ">【Reduxを用いたデータの受け渡しのイメージ】</h4> <p>しかし、<code>Redux</code>はコンポーネントの外で状態管理をするため、どのコンポーネントからでもデータがアクセスでき、簡単にデータを管理することができます!!コンポーネント間の関係性を考えなくて良いのはとても便利でした。</p> <p>【Reduxを用いたデータの受け渡しのイメージ】</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>graph TB A(&#34;Redux&#34;)--useSelecter--&gt;B(&#34;ShopMenu&#34;)--&gt;A(&#34;Redux&#34;) A(&#34;Redux&#34;)--&gt;C(&#34;MyMenu&#34;)--&gt;A(&#34;Redux&#34;) A(&#34;Redux&#34;)--&gt;D(&#34;ItemModal&#34;)--&gt;A(&#34;Redux&#34;) A(&#34;Redux&#34;)--&gt;E(&#34;PointModal&#34;)--useDispatch--&gt;A(&#34;Redux&#34;)</pre> <p>今回のサンクス新機能での開発では、フロントに表示させるデータを親子関係のないコンポーネント間で変更させるという作業が多かったのですが、<code>Redux-toolkit</code>を利用したことで、コンポーネントの繋がりを気にすることなく、どのコンポーネントからでも<code>useSelecter()</code>や<code>useDispatch()</code>を使ってアクセスできました!これは自分の中で革命だったと思います。</p> <p><code>Redux</code>を組み合わせることで、<code>React</code>がまたさらに使いやすくなるのですね...!!すごいです!</p> <p>★Redux・Redux-toolkitの使い方を執筆するにあたり以下の記事を参考にさせていただきました。ありがとうございました。</p> <ul> <li><a href="https://ramble.impl.co.jp/903/" target="_blank">【React】StateとPropsの明確な違い</a></li> <li><a href="https://reffect.co.jp/react/react-redux-for-beginner" target="_blank">React初心者でも読めば必ずわかるReactのRedux講座</a></li> </ul> <h3 id="API実行部分開発での学び">API実行部分開発での学び</h3> <p>フロント部分の開発を進め、新しい画面ができていくことに喜んでいた私ですが、その後先輩に作成していただいた<code>API</code>を使って、サーバーからデータを取得することを先輩に勧められました。</p> <p><code>Fetch API</code>?<code>非同期処理</code>?<code>Promise</code>?なんだそれは???わからない言葉だらけで、またまたパニックになりました(笑)色々と試行錯誤を繰り返し、先輩に作っていただいた<code>API</code>から初めてデータを取ることができた、あの何とも言えない感動はすごかったです...!</p> <p>ですのでこの項では<code>Fetch API</code>の使い方についてまとめ直し、得た知識を改めて確認しようと思います。</p> <h4 id="Promiseについて">【Promiseについて】</h4> <p>ではここで、<code>Fetch API</code>の説明で出てくる<code>Promise</code>について復習しておきます。<code>Promise</code>は、<strong>非同期処理</strong>の最終的な完了、もしくは失敗を表すオブジェクトです!</p> <h4 id="そもそも非同期処理とは何か">【そもそも非同期処理とは何か?】</h4> <p>非同期処理とは、<strong>処理結果を待たずして次の処理に進める処理</strong>のことです。<br /> もし仮にコードの途中に非常に処理に時間のかかるコードがあったとします。順番通りに行う同期処理では、その処理が終わるのを待たなくてはいけないため時間がかかります。</p> <p>しかし、非同期処理はその処理が終わる前に次の処理を実行できるため、時間短縮につながりユーザビリティ向上が期待できます。</p> <h4 id="Promiseの引数について">【Promiseの引数について】</h4> <p><code>Promise</code>は<code>resolve</code>と、<code>reject</code>の2つの関数を引数に取ります。 - resolve:処理が成功した時のメッセージを表す - reject:処理が失敗した時のメッセージを表す</p> <p>その後、<code>then</code>を使ってコールバック処理を実行します。</p> <h4 id="thencatchについて">【then(),catch()について】</h4> <ul> <li>then():処理が成功した場合(<code>resolve</code>)の処理を実行する。</li> <li>catch():処理が失敗した場合(<code>reject</code>)の場合の処理を実行する。何だか、<code>try...catch文</code>みたいです!</li> </ul> <p>ここまでの流れを図にしてみると...</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>graph LR A(&#34;Promise&#34;)--&#34;成功&#34;--&gt;B(&#34;resolve(data)&#34;)--&#34;dataを渡せる&#34;--&gt;D(&#34;.then(data)&#34;) A(&#34;Promise&#34;)--&#34;失敗&#34;--&gt;C(&#34;reject(error)&#34;)--&#34;errorを渡せる&#34;--&gt;E(&#34;.catch(error)&#34;)</pre> <p>ここまでの流れをコードで見てみると...</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">let</span> getItemsInfo = <span class="synStatement">new</span> Promise((resolve, reject) =&gt; <span class="synIdentifier">{</span> <span class="synComment">//このコードは3000ミリ秒後、つまり3秒後に実行される</span> setTimeout(<span class="synIdentifier">function</span>()<span class="synIdentifier">{</span> <span class="synStatement">const</span> data = <span class="synIdentifier">[</span> <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;炎の剣&quot;</span>, price:500<span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;氷の剣&quot;</span>, price:500<span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;雷の剣&quot;</span>, price:500<span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;鉄の鎧&quot;</span>, price:1000<span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;薬草&quot;</span>, price:50<span class="synIdentifier">}</span>, <span class="synIdentifier">{</span>name: <span class="synConstant">&quot;すごい薬草&quot;</span>, price:100<span class="synIdentifier">}</span>, <span class="synIdentifier">]</span> <span class="synComment">//コメントを外すとエラー処理が実行される</span> <span class="synComment">// if(data.length &gt; 5){</span> <span class="synComment">// //今回手動でエラーを投げているため、rejectを使わなくても自動的に</span> <span class="synComment">// // catchに飛ぶのでこのような表記になっている。基本的にはreject()を記載すること</span> <span class="synComment">// throw &quot;アイテムの数が多すぎるよ!持ちきれない!&quot;;</span> <span class="synComment">// }</span> resolve(data); <span class="synIdentifier">}</span>, 3000); <span class="synIdentifier">}</span>).then((data) =&gt; <span class="synIdentifier">{</span> console.log(data); <span class="synComment">//【成功したら】dataの中身が出力</span> <span class="synIdentifier">}</span>).<span class="synStatement">catch</span>((err)=&gt; <span class="synIdentifier">{</span> console.log(err); <span class="synComment">//【失敗したら】Uncaught アイテムの数が多すぎるよ!持ちきれない!と出力</span> <span class="synIdentifier">}</span>); <span class="synComment">//</span> console.log(<span class="synConstant">&quot;先に出力されるよ&quot;</span>); </pre> <p>★Promiseの使い方を執筆するにあたり<a href="https://techplay.jp/column/581" target="_blank">こちら</a>の記事を参考にさせていただきました。ありがとうございました。</p> <h4 id="Fetch-APIについて">【Fetch APIについて】</h4> <p><code>Fetch</code>の意味を和訳すると、読み込む、とってくるという意味がヒットします。<code>Fetch API</code>はその意味の通り、外部の<code>API</code>にリクエストを送信し、データを取得することができるインターフェイスです。</p> <p>では早速、<code>API</code>からデータを取得してみましょう! まずはリクエストを発行します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">//先輩に作っていただいたAPIからデータをとるぞー!</span> fetch(<span class="synConstant">&quot;https://〇〇〇.sample.php&quot;</span>); </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kawahara22/20221024/20221024104128.jpg" width="476" height="146" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>み...見えない...!!(´·ω·`)ショボーン</p> <p><code>Promise</code>が結果で出力されているため、中身が見えません。 ですので後ろに<code>then</code>を繋げて必要な処理を書いていきましょう! エラー処理もされるように<code>catch</code>も書いて...と。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>fetch(<span class="synConstant">&quot;https://〇〇〇.sample.php&quot;</span>) <span class="synComment">// Response オブジェクトから JSON の本文の内容を抽出する</span> .then(response =&gt; <span class="synIdentifier">{</span> <span class="synStatement">return</span> response.json(); <span class="synIdentifier">}</span>) <span class="synComment">// Promiseのデータの中身を取り出す</span> .then(data =&gt; <span class="synIdentifier">{</span> console.log(data); <span class="synComment">//おまけ</span> <span class="synStatement">for</span>(<span class="synStatement">const</span> element of data)<span class="synIdentifier">{</span> console.log(<span class="synConstant">`</span><span class="synSpecial">${element.name}</span><span class="synConstant">は</span><span class="synSpecial">${element.age}</span><span class="synConstant">歳です!`</span>); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>) <span class="synComment">// エラーを検出したら、catchに処理が入る</span> .<span class="synStatement">catch</span>(error =&gt; <span class="synIdentifier">{</span> console.log(error); <span class="synIdentifier">}</span>); </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kawahara22/20221024/20221024104157.jpg" width="536" height="382" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>先輩の<code>API</code>から無事<code>data</code>が取れました!わーい!!(^○^) これが<code>Fetch API</code>でのデータ取得の流れです!</p> <p>★Fetch APIについて執筆するにあたり<a href="https://breezegroup.co.jp/202004/javascript-fetch/" target="_blank">こちら</a>の記事を参考にさせていただきました。ありがとうございました。</p> <h3 id="バックエンド部分での学び">バックエンド部分での学び</h3> <p>当初の目標としてはフロントエンドのみの開発だったのですが、せっかく<code>API</code>からデータを取得できたので、今度は<code>API</code>の開発にもチャレンジしてみないかと先輩からご提案をいただき、なんと今回、バックエンドの開発にも少しだけ携わらせていただきました!(うれしい!)</p> <p>フロント以外の部分はほとんど触れたことがなかったので、わからないことだらけでしたが、こうしてアウトプットする機会をいただけたので、少しでも多く学びとして残せるよう、頑張って執筆してみようと思います!チャレンジです!</p> <p>バックエンドは<code>node.js</code>で<code>AWS Lambda関数</code>を書きました!</p> <h4 id="エラー処理について">【エラー処理について】</h4> <p>念願の技術開発部での開発の初仕事ということもあり、舞い上がっていた私。とにかく何としてもまずは動くようにということにばかり意識を取られてしまい、最初先輩にコードレビューをいただいた際にエラー処理に不十分な部分がたくさんあるとご指摘いただきました。まさしく私の性格を体現したような猪突猛進コードだったと思います...(笑)</p> <p>ここからは開発途中に特に躓いた部分を取り上げます。</p> <h4 id="アクセス途中でのエラー">【アクセス途中でのエラー】</h4> <p>例えばデータベースに接続して<code>Itemdata</code>を取得するとします。</p> <p>【<code>Itemdata</code>のイメージ】</p> <table> <thead> <tr> <th> itemId </th> <th> name </th> <th> price </th> </tr> </thead> <tbody> <tr> <td> 1 </td> <td> 炎の剣 </td> <td> 500 </td> </tr> <tr> <td> 2 </td> <td> 氷の剣 </td> <td> 500 </td> </tr> <tr> <td> 3 </td> <td> 雷の剣 </td> <td> 500 </td> </tr> <tr> <td> 4 </td> <td>鉄の鎧 </td> <td> 1000 </td> </tr> <tr> <td> 5 </td> <td>薬草 </td> <td> 50 </td> </tr> <tr> <td> 6 </td> <td>すごい薬草 </td> <td> 100 </td> </tr> </tbody> </table> <p>このとき、データベースにアクセスする際に、エラーが起きてしまい正しくデータが取れないかもしれません。 もしこの時にエラーメッセージを出さないと、なぜアプリが動かないのかがわからないため、お客様が混乱してしまいます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> getItemdataHandler = () =&gt; <span class="synIdentifier">{</span> <span class="synComment">// getItemdata()でItemdataを取得できるとする</span> <span class="synStatement">const</span> res = await getItemdata(); <span class="synStatement">if</span> (res !== <span class="synConstant">false</span>) <span class="synIdentifier">{</span> <span class="synStatement">return</span> createResponse(res); <span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span> <span class="synComment">//resの中にきちんと値が入っていない場合はエラーを出す</span> <span class="synStatement">return</span> createErrorResponse(<span class="synConstant">&quot;getItemdataHandler err&quot;</span>); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>; </pre> <p>これでもし通信が失敗しても、エラーメッセージが表示されるようになりました!</p> <h4 id="予期していないデータが入る可能性">【予期していないデータが入る可能性】</h4> <p>ここで先ほどの<code>Itemdata</code>ですが、このように当初の予定とは異なったデータが入ってしまう可能性もあります。</p> <p>【予期しない値が入った<code>Itemdata</code>のイメージ】</p> <table> <thead> <tr> <th> itemId </th> <th> name </th> <th> price </th> </tr> </thead> <tbody> <tr> <td> 1 </td> <td> 炎の剣 </td> <td> 500 </td> </tr> <tr> <td> 2 </td> <td> 氷の剣 </td> <td> 500 </td> </tr> <tr> <td> 3 </td> <td> 雷の剣 </td> <td> 500 </td> </tr> <tr> <td> 4 </td> <td> 鉄の鎧 </td> <td> 1000 </td> </tr> <tr> <td> 5 </td> <td> 薬草 </td> <td> undefined </td> </tr> <tr> <td> 6 </td> <td> すごい薬草 </td> <td> 100 </td> </tr> </tbody> </table> <p>薬草の<code>price</code>が<code>undefined</code>になってしまっています。 この状態でもし<code>item</code>を購入しようとしてしまうと...</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">let</span> yourMoney = 1000; <span class="synStatement">const</span> buy = (id) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> item = Itemdata.find(item =&gt; item.itemId === id); console.log(<span class="synConstant">`</span><span class="synSpecial">${item.name}</span><span class="synConstant">を購入した!`</span>); yourMoney = yourMoney - item.price console.log(<span class="synConstant">`今の所持金は</span><span class="synSpecial">${yourMoney}</span><span class="synConstant">円です`</span>); <span class="synComment">//薬草を購入した!</span> <span class="synComment">//今の所持金はNaN円です と出力</span> <span class="synIdentifier">}</span> buy(5); </pre> <p>本来数値が入って欲しい部分に<code>NaN</code>が入ってしまいました...!!(T T) このまま処理を進めてしまうと、さまざまなエラーが起きてしまいます。</p> <p>また、今回は<code>undefined</code>でしたが、もし<code>null</code>が入った状態でこのコードを走らせてしまうと</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>薬草を購入した! 今の所持金は1000円です </pre> <p>と表示されてしまいます。今度はタダで買えてしまう...!これはまずいです(^^;</p> <p>ですので、適切な値が入っているかチェックするコードを追加します。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">let</span> yourMoney = 1000; <span class="synStatement">const</span> buy = (id) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> item = Itemdata.find(item =&gt; item.itemId === id); <span class="synStatement">if</span>(item.price === <span class="synStatement">undefined</span> || item.price === <span class="synStatement">null</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;priceの値が不適切です!&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> console.log(<span class="synConstant">`</span><span class="synSpecial">${item.name}</span><span class="synConstant">を購入した!`</span>); yourMoney = yourMoney - item.price console.log(<span class="synConstant">`今の所持金は</span><span class="synSpecial">${yourMoney}</span><span class="synConstant">円です`</span>); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> buy(5); <span class="synComment">// priceの値が不適切です!と出力</span> </pre> <h4 id="そもそもデータがない可能性">【そもそもデータがない可能性】</h4> <h5 id="Itemdataそのものが空の場合">Itemdataそのものが空の場合</h5> <p>今<code>Itemdata</code>の中にデータがあるものだと仮定してコードを書いていましたが、<code>Itemdata</code>に、もしもデータが入っていなかった場合にもエラーが起きてしまいます。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synIdentifier">let</span> yourMoney = 1000; <span class="synStatement">const</span> buy = (id) =&gt; <span class="synIdentifier">{</span> <span class="synComment">// lengthで空かどうかを判定、エラー処理を追加。</span> <span class="synStatement">if</span>(Itemdata.length === 0)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;Itemデータがありません&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> <span class="synStatement">const</span> item = Itemdata.find(item =&gt; item.itemId === id); <span class="synStatement">if</span>(item.price === <span class="synStatement">undefined</span> || item.price === <span class="synStatement">null</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;priceの値が不適切です!&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> console.log(<span class="synConstant">`</span><span class="synSpecial">${item.name}</span><span class="synConstant">を購入した!`</span>); yourMoney = yourMoney - item.price; console.log(<span class="synConstant">`今の所持金は</span><span class="synSpecial">${yourMoney}</span><span class="synConstant">円です`</span>); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> buy(3); </pre> <p>これで万が一<code>Itemdata</code>の中身が空でも処理は止まりませんね!よかった、よかった。ε-(´∀`*)ホッ</p> <h5 id="指定されたitemIdのデータがない場合">指定された<code>itemId</code>のデータがない場合</h5> <p><code>Itemdata</code>のデータは6つ、つまり<code>itemId</code>は6までしかありませんが、もしここで存在しない<code>itemId</code>を指定した時にもエラーが起きてしまいます!(こちら先輩にご指摘いただき、気がつきました。気をつけていてもやはり見落としがありますね!勉強になります(^^)!) それでは早速エラー処理を書いていきましょう!</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> buy = (id) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">if</span>(Itemdata.length === 0)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;Itemデータがありません&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> <span class="synStatement">const</span> item = Itemdata.find(item =&gt; item.itemId === id); <span class="synComment">//指定されていないidを指定した場合、itemの中身はundefinedになるのでエラー処理を追加</span> <span class="synStatement">if</span>(item === <span class="synStatement">undefined</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;指定したデータがありません&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> <span class="synStatement">if</span>(item.price === <span class="synStatement">undefined</span> || item.price === <span class="synStatement">null</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;priceの値が不適切です!&quot;</span>); <span class="synIdentifier">}</span><span class="synStatement">else</span><span class="synIdentifier">{</span> console.log(<span class="synConstant">`</span><span class="synSpecial">${item.name}</span><span class="synConstant">を購入した!`</span>); yourMoney = yourMoney - item.price; console.log(<span class="synConstant">`今の所持金は</span><span class="synSpecial">${yourMoney}</span><span class="synConstant">円です`</span>); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> buy(7); <span class="synComment">// 範囲外のitemIdを指定した場合は指定したデータがありませんと表示。</span> </pre> <p>できました!と思いきやあれ...?なんだかコードが読みにくくなってきたような...?(゜∀゜;)コンナハズデハ<br /> それもそのはず。ネストが深くなってしまったのです...!<strong>ガード節</strong>を使い修正します。</p> <h5 id="コードを修正">コードを修正!</h5> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> buy = (id) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">if</span>(Itemdata.length === 0)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;Itemデータがありません&quot;</span>); <span class="synStatement">return</span>; <span class="synIdentifier">}</span> <span class="synStatement">const</span> item = Itemdata.find(item =&gt; item.itemId === id); <span class="synStatement">if</span>(item === <span class="synStatement">undefined</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;指定したデータがありません&quot;</span>); <span class="synStatement">return</span>; <span class="synIdentifier">}</span> <span class="synStatement">if</span>(item.price === <span class="synStatement">undefined</span> || item.price === <span class="synStatement">null</span>)<span class="synIdentifier">{</span> console.log(<span class="synConstant">&quot;priceの値が不適切です!&quot;</span>); <span class="synStatement">return</span>; <span class="synIdentifier">}</span> console.log(<span class="synConstant">`</span><span class="synSpecial">${item.name}</span><span class="synConstant">を購入した!`</span>); yourMoney = yourMoney - item.price; console.log(<span class="synConstant">`今の所持金は</span><span class="synSpecial">${yourMoney}</span><span class="synConstant">円です`</span>); <span class="synIdentifier">}</span> buy(4); </pre> <p>先ほどのコードよりもすっきりしたかなと思います!よかったです!<br /> お客様にご利用していただくサービスだからこそ、処理の止まらない、エラーが起きた際はエラーが発生したのだとわかるようなコードを書くことの大切さを学びました。</p> <h3 id="終わりに">終わりに</h3> <p>最後までお読みいただきありがとうございました! たくさん挑戦できる環境をいただけ、すごく嬉しかったです! いつもたくさんご指導いただきありがとうございます!もっともっと知識を吸収して、早く一人前のエンジニアになるのが目標です。これからも頑張ります!(^^)</p> <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> <script>mermaid.initialize({startOnLoad: true});</script> kawahara22 プログラミング未経験だった新卒が語る!これまでとこれから hatenablog://entry/4207112889916535219 2022-10-13T14:36:39+09:00 2022-10-13T14:36:39+09:00 こんにちは!新卒2期生として7月からアプリ開発チームに配属されました、M.I.です! 本日は、入社からこれまでに体験したことや、プログラミング未経験の身でどのように開発へ携わっているのかをお話したいと思います。この記事が、ワウテックに興味のある皆さんにとって働くイメージを持つ一助になると嬉しいです✨ 学生時代のお話 新卒研修期間について 本配属後について(自作アプリ編) 学んだこと 楽しかったこと&よかったこと 大変だったこと 本配属後について(WowDesk編) 今後の目標 学生時代のお話 前述の通り、私は現在技術開発部のアプリ開発チームに所属しています。が!学生時代からプログラミングに慣れ… <p>こんにちは!新卒2期生として7月からアプリ開発チームに配属されました、M.I.です!</p> <p>本日は、入社からこれまでに体験したことや、プログラミング未経験の身でどのように開発へ携わっているのかをお話したいと思います。<br />この記事が、ワウテックに興味のある皆さんにとって働くイメージを持つ一助になると嬉しいです✨</p> <ul class="table-of-contents"> <li><a href="#学生時代のお話">学生時代のお話</a></li> <li><a href="#新卒研修期間について">新卒研修期間について</a></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="#本配属後についてWowDesk編">本配属後について(WowDesk編)</a></li> <li><a href="#今後の目標">今後の目標</a></li> </ul> <p> </p> <h4 id="学生時代のお話">学生時代のお話</h4> <p>前述の通り、私は現在技術開発部のアプリ開発チームに所属しています。が!学生時代からプログラミングに慣れ親しんでいたかというと、そうではありませんでした。</p> <p>元々文系の学部に所属していて、プログラミング自体もProgateという学習アプリに触れたことがある程度でした。<br />そんな状態ながらも、研修期間を経て技術開発部に決まったこれまでをお話したいと思います!</p> <p> </p> <h4 id="新卒研修期間について">新卒研修期間について</h4> <p>ワウテックでは、新卒向けにジョブローテーションという研修制度があり、私たちは約2ヶ月かけて全部署を回りました。</p> <p>私は一週間技術開発部にアサインされ、製品のテストや初歩的なUI設計、サイトの構築などを行いました。</p> <p>わからないことばかりでしたが、どんな疑問でも皆さん丁寧に答えてくださり、開発の面白さとチームの温かさを知る貴重な機会になりました。</p> <p> </p> <h4 id="本配属後について自作アプリ編">本配属後について(自作アプリ編)</h4> <p>7月から本配属となり、私がまず取り組んだのは「自作アプリの開発」でした。</p> <p>とにもかくにも知識がまっさらな状態なので、ひとまずアプリの作成を通して開発の基礎知識を身につけよう!💪と始めた取り組みでした。</p> <p> </p> <p>概要をざっくりご説明すると、「映画の聖地といわれている場所の情報をまとめたアプリ」です!<br />筆者はいわゆる「聖地巡礼」が好きなんですが、既存のアプリだと自身のニーズを満たせていませんでした。</p> <p>ないなら作ってしまおう!ということで実際の完成画面がこちら↓(著作権の都合上画像はぼかしています)</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/iimuraWow/20220914/20220914114242.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <p>また、アプリを作る中で学んだことや楽しかったこと、大変だったことをピックアップしてみました!</p> <h5 id="学んだこと">学んだこと</h5> <p>✔️ テーブルビューの使い方<br /> →データをリスト状に表示する方法のことです。</p> <p>✔️ ユーザーデフォルトの使い方<br /> →ユーザーが入力した情報を永続的に保持できます。右側の画像にあるメモ欄で利用しています。</p> <p>✔️ データベース、APIの基礎知識<br /> →表示するデータはサーバーからAPIを利用して取得し、データベースに保存する仕組みです。</p> <p> </p> <h5 id="楽しかったことよかったこと">楽しかったこと&よかったこと</h5> <p>▶︎ 自分のほしいと思った機能や見た目を自分の手で作れたこと(初めてアプリを動かした時の感動は忘れられません…!)</p> <p>▶︎ 作業に詰まった時も、自社製品の”WowTalk”を使ってすぐ質問できるので、テレワークでも安心して開発に取り組めたこと</p> <p> </p> <h5 id="大変だったこと">大変だったこと</h5> <p>▶︎ 覚えることが多く内容整理に追われていたこと</p> <p>▶︎ 疑問点が出てきた時の調べ方がわからなかったこと(個人的に調べ物をするスキルはエンジニアにとってすごく大切なものだと思ってます)</p> <p> </p> <p>このような紆余曲折を経て、約1ヶ月かけ自作アプリを完成させることができました!</p> <p> </p> <h4 id="本配属後についてWowDesk編">本配属後について(WowDesk編)</h4> <p>自作アプリ作成がひと段落し、次はワウテックの自社製品である”WowDesk”の機能追加に取り組みました。</p> <p>WowDeskについてざっくりご説明すると、タブレットでオフィスへの来訪者の受付対応ができるシステムのことです。このシステムをスマートスピーカーと接続すれば、来訪の通知を音声で受け取ることもできます。(詳しくはこちら↓)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wowdesk.jp%2F" title="メインページ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.wowdesk.jp/">www.wowdesk.jp</a></cite></p> <p> </p> <p>その中で私が取り組んだのは、「来訪者の音声通知をどのスマートスピーカーで鳴らすか利用者がカスタムできる」という機能です(近日リリース予定となっております!)。</p> <p>利用できるスマートスピーカーが複数ある場合、来訪者の属性によってスピーカーの鳴らし分けができる、というイメージです。</p> <p>実際に作った画面がこちら↓</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/i/iimuraWow/20220914/20220914104607.png" width="1200" height="838" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p>初めて見る自社製品の既存コードに、最初は??状態でしたが、自作アプリで得た基礎知識や上司の方の助力によりなんとか完成させることができました!本当に感謝です…!✨</p> <p> </p> <h4 id="今後の目標">今後の目標</h4> <p>いかがでしたでしょうか?以上がプログラミング未経験の新卒社員が、入社してから現在に至るまで体験したことです!<br />本配属から約2ヶ月、自社製品に携わる機会をいただけているものの、まだまだ自分の知識不足を感じる日々です笑<br />今はとにかくどんどん知識を吸収して、早く一人前のエンジニアになるのが目標です!</p> <p> </p> <p>ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。<br />随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /></a></p> iimuraWow WowTalkでウィジェット開発する際のポイント hatenablog://entry/4207112889915555153 2022-10-05T12:16:21+09:00 2022-10-07T18:20:06+09:00 今回初めて本ブログの執筆を担当することになりました、ラジタです。 今年の初めに入社し、現在アプリ開発チームでAndroidの開発を担当しております。近年IOSやAndroidにおいて、ウィジェット機能が注目されています。ワウトークはチャットでやり取りするユーザが多いため、ホーム画面からすぐにチャットに入れる様、Androidワウトークアプリ開発の新機能として、お気に入り連絡先ウィジェットをリリースする予定です。 今回はこの開発を通じて得た知見をもとに、ウィジェット機能を実装する際のポイントについて簡単に紹介していきます。 目次 ✓ ウィジェットとは ✓ 基本情報 ✓ Android Studi… <p><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">今回初めて本ブログの執筆を担当することになりました、ラジタです。</span></p> <p><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">今年の初めに入社し、現在アプリ開発チームでAndroidの開発を担当しております。近年IOSやAndroidにおいて、ウィジェット機能が注目されています。ワウトークはチャットでやり取りするユーザが多いため、ホーム画面からすぐにチャットに入れる様、Androidワウトークアプリ開発の新機能として、お気に入り連絡先ウィジェットをリリースする予定です。</span></p> <p>今回はこの開発を通じて得た知見をもとに、ウィジェット機能を実装する際のポイントについて簡単に紹介していきます。</p> <p> </p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220912/20220912134426.jpg" width="1200" height="851" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <h2 id="目次"><strong>目次</strong></h2> <p><strong>✓</strong> ウィジェットとは</p> <p><strong>✓</strong> 基本情報</p> <p><strong>✓</strong> Android Studio から簡単にウィジェット作りましょう</p> <p><strong>✓</strong> 自動に追加されるウィジェット設定確認しましょう</p> <p><strong>✓</strong> ウィジェット追加する場合のクラスの役割</p> <p><strong>✓</strong> 困った点</p> <p><strong>✓</strong> まとめ</p> <p> </p> <h3 id="ウィジェットとは">ウィジェットとは</h3> <p>ウィジェットは、ホーム画面上に常に表示されるアプリのショートカット機能のことです。近年IOSでもAndroidでも流行してきているホーム画面のカスタマイズに不可欠な要素です。アプリの特に重要なデータや機能を「ひと目で」確認できるようにし、そうしたデータや機能にユーザーのホーム画面から直接アクセスできるようにするもの、ということができます。ユーザーはウィジェットをホーム画面のパネルからパネルへ移動したり、(サポートされていれば)好みに合わせてサイズを変更してウィジェットに表示される情報量を調整したりできます。Androidスマホにおける利便性の要となる機能といっても過言ではないでしょう。</p> <h3 id="基本情報">基本情報</h3> <p>Androidウィジェットを作成するには、以下のものが必要です。</p> <ul> <li>AppWidgetProviderInfo オブジェクト</li> <li>AppWidgetProvider クラスの実装</li> <li>ビューのレイアウト</li> </ul> <p>詳細参照:<a href="https://developer.android.com/guide/topics/appwidgets?hl=ja">https://developer.android.com/guide/topics/appwidgets?hl=ja</a></p> <h3 id="Android-Studio-から簡単にウィジェット作りましょう"><span class="notion-enable-hover" data-reactroot="" data-token-index="0" style="font-weight: 600;">Android Studio から簡単にウィジェット作りましょう!!</span></h3> <p>New &gt; Widget &gt; AppWidget からウィジェット追加画面を開けます。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220930/20220930160131.png" width="1200" height="897" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <table> <thead> <tr> <th>項目名</th> <th>設定</th> </tr> </thead> <tbody> <tr> <td>Class Name</td> <td>ウィジェットの名前</td> </tr> <tr> <td>Placement</td> <td>[Homescreen, Keyguard,Both] 中から選んでください。</td> </tr> <tr> <td>Resizable</td> <td>[Both , Horizontal , Vertical , none] ウィジェットのサイズ変更できる方法の設定</td> </tr> <tr> <td>Minimum Width</td> <td>ウィジェットの最小幅はセルカウントで追加してください</td> </tr> <tr> <td>Minimum Height</td> <td>ウィジェットの最小<span style="caret-color: #4d5156; color: #4d5156; font-family: arial, sans-serif; font-size: 14px; font-style: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: auto; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration: none; display: inline !important; float: none;">高さ</span>はセルカウントで追加してください</td> </tr> <tr> <td>Configuration Screen</td> <td>ウィジェット設定画面追加</td> </tr> <tr> <td>Source Language</td> <td>ウィジェットに使用する言語</td> </tr> </tbody> </table> <h3 id="自動に追加されるウィジェット設定確認しましょう"><span class="notion-enable-hover" data-reactroot="" data-token-index="0" style="font-weight: 600;">自動に追加されるウィジェット設定確認しましょう</span></h3> <p>ウィジェットを作成した時にウィジェット設定ファイルが追加されます。</p> <p>例:project/res/xml/favorite_contact_widget_info.xml</p> <pre><code class="language-xml">&lt;?xml version="1.0" encoding="utf-8"?&gt; &lt;appwidget-provider xmlns:android="&lt;http://schemas.android.com/apk/res/android&gt;" android:description="@string/app_widget_description" android:initialKeyguardLayout="@layout/test" android:initialLayout="@layout/test" android:minWidth="110dp" android:minHeight="40dp" android:previewImage="@drawable/example_appwidget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen" /&gt; </code></pre> <table> <thead> <tr> <th>項目名</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>minWidth、minHeight</td> <td>サイズ。セルをdpで変換して指定</td> </tr> <tr> <td>updatePeriodMillis</td> <td>更新間隔をミリ秒で指定。AlarmManagerなどで自力で更新かける場合は省略可</td> </tr> <tr> <td>previewImage</td> <td>ウィジェット選択一覧画面に表示する画像を指定。省略した場合はアプリアイコンが使用される</td> </tr> <tr> <td>initialLayout</td> <td>表示するレイアウトxmlを指定</td> </tr> <tr> <td>configure</td> <td>配置直後に起動されるActivityを指定。ウィジェット設定画面がない場合は省略可</td> </tr> <tr> <td>resizeMode</td> <td>ホーム画面に配置されたウィジェットをロングタップすることでサイズ変更する場合に指定</td> </tr> <tr> <td>widgetCategory</td> <td>Android 5未満ではロック画面にも配置できたが現在はホーム画面のみの指定</td> </tr> </tbody> </table> <p><strong>ウィジェット追加する場合の<span class="notion-enable-hover" data-token-index="1" data-reactroot="">クラスの役割</span></strong></p> <p>ユーザーからウィジェット追加する際のフローになります。<span class="notion-enable-hover" style="font-family: 'SFMono-Regular', Menlo, Consolas, 'PT Mono', 'Liberation Mono', Courier, monospace; line-height: normal; background: rgba(135,131,120,0.15); color: #eb5757; border-radius: 3px; font-size: 85%; padding: 0.2em 0.4em;" spellcheck="false" data-token-index="1" data-reactroot="">WidgetConfigureActivity</span>でユーザからの追加設定を<span class="notion-enable-hover" style="font-family: 'SFMono-Regular', Menlo, Consolas, 'PT Mono', 'Liberation Mono', Courier, monospace; line-height: normal; background: rgba(135,131,120,0.15); color: #eb5757; border-radius: 3px; font-size: 85%; padding: 0.2em 0.4em;" spellcheck="false" data-token-index="1" data-reactroot="">SharedPreferences</span>に保存し<span class="notion-enable-hover" style="font-family: 'SFMono-Regular', Menlo, Consolas, 'PT Mono', 'Liberation Mono', Courier, monospace; line-height: normal; background: rgba(135,131,120,0.15); color: #eb5757; border-radius: 3px; font-size: 85%; padding: 0.2em 0.4em;" spellcheck="false" data-token-index="1" data-reactroot="">FavoriteContactWidget</span>クラスを呼びます。<span class="notion-enable-hover" style="font-family: 'SFMono-Regular', Menlo, Consolas, 'PT Mono', 'Liberation Mono', Courier, monospace; line-height: normal; background: rgba(135,131,120,0.15); color: #eb5757; border-radius: 3px; font-size: 85%; padding: 0.2em 0.4em;" spellcheck="false" data-token-index="1" data-reactroot="">FavoriteContactWidget</span>で追加情報取得して、<span class="notion-enable-hover" style="font-family: 'SFMono-Regular', Menlo, Consolas, 'PT Mono', 'Liberation Mono', Courier, monospace; line-height: normal; background: rgba(135,131,120,0.15); color: #eb5757; border-radius: 3px; font-size: 85%; padding: 0.2em 0.4em;" spellcheck="false" data-token-index="1" data-reactroot="">AppWidgetManager</span>へウィジェットの更新を依頼します</p> <p> </p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220930/20220930170352.png" width="529" height="410" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <table> <thead> <tr> <th> </th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>WidgetConfigureActivity.kt</td> <td>Widgetの設定管理Activity</td> </tr> <tr> <td>getPendingIntent()</td> <td>別のActivityから情報取得</td> </tr> <tr> <td>updateWidget()</td> <td>AppWidgetProviderに情報渡す</td> </tr> <tr> <td>Widget.kt</td> <td>AppWidgetProviderクラス</td> </tr> <tr> <td>onUpdate()</td> <td>appWidgetManagerからWidgetId取得</td> </tr> <tr> <td>updateAppWidget()</td> <td>RemoteViewを更新(WidgetUIの更新)</td> </tr> </tbody> </table> <p> </p> <p><strong>別のActivityからウィジェットアップデートを呼ぶ方法</strong></p> <p>intentにsetActionとAppWidgetManager.ACTION_APPWIDGET_UPDATEを設定し、AppWidgetManagerからidsを取得してブロードキャストでWidgetクラスに渡します。</p> <p>FavoriteContactWidgetクラスのonUpdate関数でidsを受け取って各ウィジェットの新しい情報を取得しウィジェットUI更新依頼をAppWidgetManagerに渡します。</p> <pre><code class="language-java">public void updateWidgets(){ Intent intent = new Intent(this, FavoriteContactWidget.class); intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); int[] ids = AppWidgetManager.getInstance(this).getAppWidgetIds(new ComponentName(this, FavoriteContactWidget.class)); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); sendBroadcast(intent); }</code></pre> <h2 id="実装時に困った点">実装時に困った点</h2> <p><strong><span style="text-decoration: underline;">①ウィジェット自動更新はどのタイミングで行うのか</span></strong></p> <p>今回のワウトークに追加したウィジェットの情報(連絡先ユーザのプロフィル写真、名前)は更新が必要でしたので、どのタイミングで自動更新されるか困ったので詳しく調べてみました。</p> <p>AppWidgetProviderInfo.xmlの中の<code>android:updatePeriodMillis="86400000"</code>でミリ秒設定しても上手く行きませんでした。どのタイミングからカウントされるのか分からず、自動更新されるタイミングがバラバラだったので、詳しく調べてみました。</p> <p>調査した結果としては”実際の更新が正確に行われるとは限りません”とオフィシャルAndroid仕様書に書いてありました。</p> <p>自分の中で実装を確認したところ、ウィジェット追加したタイミングから開始するケースが多かったです。</p> <p>参照:<a href="https://developer.android.com/develop/ui/views/appwidgets#other-attributes">https://developer.android.com/develop/ui/views/appwidgets#other-attributes</a></p> <p><span style="text-decoration: underline;"><strong>② ウィジェット情報はどこに保存すれば良いのか</strong></span></p> <p>複数お気に入りウィジェット追加した場合にそのウィジェットに関しての情報どこで保存した方が良いか悩んでました。</p> <p>時計などのウィジェットの場合は、別に情報を保存する必要がありません。しかし、今回開発したワウトークのお気に入りの連絡先ウィジェットでは、ユーザの名前と画像を表示することと、タップした瞬間にお気に入りの連絡先のルームを開くことができる様にするための実装だったので、ユーザーのIDとWidgetIDを保存する必要がありました。</p> <p>ウィジェット情報の保存方法は二通りあります。</p> <p>⑴ ローカルデータベースに保存する(詳しく複数の情報を保存する)</p> <p>⑵ SharedPreferencesに保存する(IDや少ない値を保存する)</p> <p>ID2つほどでしたら、データベースに入れるよりも、SharedPreferencesに保存することが推奨されています。</p> <p><span style="text-decoration: underline;"><strong>③ ウィジェット設定からMainアプリの別のActivityを開く際、MainActivityを開けたりしまうこと</strong></span></p> <p>今回はウィジェット設定の画面で別のContactActivityを開いてからお気に入りコンタクトの情報を選択する方法であり、ウィジェット設定開く時はアプリ全体(MainActivity)を開けてしまう問題が発生した上で下の方向で解決できました。</p> <p>ウィジェット設定から呼び出すActivityのAndroidManifest中のLaunchMode設定は<code>android:launchMode="singleTask"</code>にする必要がありました。</p> <p>修正前               修正後     </p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220930/20220930183952.gif" width="181" height="426" loading="lazy" title="" class="hatena-fotolife" itemprop="image" />     <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220930/20220930184158.gif" width="182" height="428" loading="lazy" title="" class="hatena-fotolife" itemprop="image" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;" />          </p> <p>参照:<a href="https://developer.android.com/guide/topics/manifest/activity-element">https://developer.android.com/guide/topics/manifest/activity-element</a></p> <p><span style="text-decoration: underline;"><strong>④ウィジェット追加リスト画面のサイズ</strong></span></p> <p>ウィジェット追加リスト画面でウィジェットの各サイズのプレビュー画像で表示する必要がありました。画像を作成する際にどのサイズで作成する知らなかったので調べてみました。</p> <p>選択一覧でホーム画面に配置できるウィジェットにはサイズが記載されており、以下の画像の2x4や4x6などがそれに当たります。 これらのサイズはセルという単位で、1x1でホーム画面のアプリアイコン1個分のサイズです。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220906/20220906165957.png" width="209" height="487" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p>ウィジェット追加する時はウィジェットのサイズセルで設定します。ウィジェットに表示する画像のサイズ確認するために下の計算が使います。</p> <p>実装時はdpで指定するため次の式でdpへ変換します。</p> <p>70 x n - 30</p> <p>例えば1x1のウィジェットを作成する場合はn=1で計算して40dpx40dpで指定することとなります。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/rajithamendis/20220906/20220906170036.png" width="207" height="481" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <h3 id="まとめ">まとめ</h3> <p>Androidアプリ開発してたんですが、ウィジェット開発は初めてで問題なく開発段階完了できたのは嬉しいです。これからもユーザ体験高くなる新しいウィジェット開発したいと思います。</p> <p>ワウテックには、多くの学びを得る機会と挑戦させてもらえる環境があります。<br />随時新メンバーを大募集中ですので、一緒に働ける方をお待ちしております!</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank" rel="noopener"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" style="cursor: pointer;" alt="f:id:wowtech-dev:20190313163146j:plain" /></a></p> rajithamendis Redux Toolkit を導入してみたら、とても良かった!!! hatenablog://entry/26006613711341030 2021-05-18T14:30:26+09:00 2022-10-05T12:14:50+09:00 Web開発チームの山本です。 Reactで作成した既存のアプリケーションにRedux Toolkitを導入してみたのですが、想像以上によかったので、今回はRedux Toolkitについて紹介します。 Redux Toolkit とは Reduxを効率的に開発するために作られた公式のツールセットです。 Reduxの公式ドキュメントにもRedux Toolkitについて書かれています。Reduxの公式ドキュメントでも、Redux Toolkitを使うことを推奨しており、Reduxをこれから始める人やシンプルにRedux書きたい人にも良さそうです。Redux Toolkitには、Immer、Res… <p>Web開発チームの山本です。</p> <p>Reactで作成した既存のアプリケーションに<a href="https://redux-toolkit.js.org/" target="_blank">Redux Toolkit</a>を導入してみたのですが、想像以上によかったので、今回はRedux Toolkitについて紹介します。</p> <p> </p> <h3 id="Redux-Toolkit-とは">Redux Toolkit とは</h3> <p>Reduxを効率的に開発するために作られた公式のツールセットです。 <a href="https://redux.js.org/introduction/getting-started" target="_blank">Reduxの公式ドキュメント</a>にもRedux Toolkitについて書かれています。Reduxの公式ドキュメントでも、Redux Toolkitを使うことを推奨しており、Reduxをこれから始める人やシンプルにRedux書きたい人にも良さそうです。Redux Toolkitには、Immer、Reselect、Redux-Thunkも含まれているので、自分でインストールする必要がないので、助かります。</p> <p> </p> <h3 id="インストール">インストール</h3> <p>既存のアプリケーションにReduxを導入するときは、Redux Toolkitと<a href="https://react-redux.js.org/" target="_blank">React Redux</a>をインストールしましょう。React Reduxは、Redux用の公式React UIバインディングレイヤーです。Redux自体はどんなフレームワークとも一緒に使用することができるので、ReduxとReactを一緒に使う場合は、React Reduxも使ってこの2つのライブラリを結びつける必要があります。詳しくは<a href="https://react-redux.js.org/introduction/why-use-react-redux" target="_blank">こちら</a>。</p> <p class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">npm install @reduxjs/toolkit react-redux</p> <p> </p> <p>Redux Toolkitを使う前は、Middlewareとしてredux-loggerを入れてStoreの更新のログをコンソールに表示していたのですが、Chormeのデベロッパーツールに<a href="https://github.com/zalmoxisus/redux-devtools-extension" target="_blank">Reduxの拡張機能</a>が使えるようになったので、redux-loggerは入れなくてもいいと思います。Reduxの拡張機能でStoreのデータを確認できたり、アクションの呼び出しも追跡できます。</p> <p> </p> <h3 id="ReducerとActionを一緒に書ける">ReducerとActionを一緒に書ける</h3> <p>下記はRedux Toolkitで使用されているAPIです。これまでは、ActionとReducerを別のファイルで書いたのが、Redux Toolkitではシンプルにかけるようになったので、同じファイル内で管理できるようになりました。</p> <p> </p> <p>これまでReduxでアクションを定義する時は、アクションタイプの定数と、そのタイプのアクションを構築するためのアクションクリエーター関数を別々に宣言していました。Reduxのreducerは、switch文を使って実装されることが多く、扱われるアクションタイプごとに1つのケースが必要です。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Action</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink=""><br />const RECEIVE_All_TEMPLATE_DATA = 'RECEIVE_All_TEMPLATE_DATA' function set_all_template_data(data) { return { type: RECEIVE_All_TEMPLATE_DATA, payload:data, } } </pre> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Reducer</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const initialState = {templates: [] } function counterReducer(state = initialState, action) { switch (action.type) { case 'RECEIVE_All_TEMPLATE_DATA': return { ...state, templates: [ ...action.data]} default: return state } } </pre> <p>Redux ToolkitのcreateActionとcreateReducerを使うと、上記のコードをシンプルに書くことができます。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { createAction, createReducer } from '@reduxjs/toolkit'; <br />const setAllTemplates = createAction("RECEIVE_All_TEMPLATE_DATA"); const initialState = { templateArray:[] } const templateReducer = createReducer(initialState, (builder) =&gt; { builder .addCase(setAllTemplates, (state, action) =&gt; { state.templateArray = action.payload }) });</pre> <p> </p> <p>またcreateSliceを使うと、Actionと Reducerも一緒に書くことができます。アクションタイプの定数やswitch文がなくなり、かなりコード自体も見やすくなり、コード量も少なくなったので、嬉しいです。actionのpayloadの値をカスタマイズしたい時は、createSliceでは<a href="https://redux-toolkit.js.org/api/createSlice" target="_blank">prepare</a>を使えばできるので、私は基本的にcreateSliceを使っています。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { createSlice } from '@reduxjs/toolkit';<br /><br />const initialState = { <br /> templateArray:[] <br />}<br /><br />const templateSlice = createSlice({ name: 'templates', initialState: initialState, reducers: { setAllTemplates: (state, action) =&gt; { state.templateArray = action.payload } } })</pre> <p> </p> <h3 id="TypeScriptに対応している"> TypeScriptに対応している</h3> <p>Redux Toolkitのドキュメントに、<a href="https://redux-toolkit.js.org/usage/usage-with-typescript" target="_blank">TypeScriptでの使い方</a>も書かれているので、TSを使用している既存サービスにRedux Toolkitを導入してもサンプルコードも豊富なため、特に大きなハードルはありませんでした。</p> <p> </p> <p>Storeの設定やHooksの設定もサクッとできました。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">app/store.ts</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { configureStore } from '@reduxjs/toolkit' import reducer from '../reducers' export const store = configureStore({ reducer }) export type RootState = ReturnType&lt;typeof store.getState&gt; export type AppDispatch = typeof store.dispatch</pre> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">app/hooks.ts</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import type { RootState, AppDispatch } from './store' // 元からあるuseDispatchやuseSelectorの代わりに、アプリ全体で使用します export const useAppDispatch = () =&gt; useDispatch&lt;AppDispatch&gt;() export const useAppSelector: TypedUseSelectorHook&lt;RootState&gt; = useSelector</pre> <p> </p> <p>ただ、createAsyncThunkで結構ハマってしまいました。3つのパラメーターの型定義が、複雑で最初は混乱しました。<a href="https://times.hrbrain.co.jp/entry/2020/12/08/redux-toolkit-async-thunk" target="_blank">この方のブログ記事</a>に詳しく書かれており、とても助かりました。ありがとうございます。<a href="https://redux-toolkit.js.org/api/createAsyncThunk#promise-lifecycle-actions" target="_blank">createAsyncThunk</a>は、非同期リクエストにおける各ライフサイクル毎(非同期処理中、非同期処理の成功時、非同期処理の失敗時)に処理をフックすることができるので、便利です。</p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>以前は、複数のファイルをまたがってReduxの設定をしていたのですが、Redux Toolkitによって、かなりシンプルに書けるようになったので嬉しいです。拡張機能もまだ触りきれていない部分もあるので、引き続き色々と試していきたいです。</p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /> </a></p> yamamoto5555 入社後半年経過して社内ツールを開発した話 hatenablog://entry/26006613711360919 2021-04-03T03:37:08+09:00 2022-10-05T12:15:03+09:00 SREチームの三浦です。 入社して半年が経過しました。SREの取組みとして開発チームであった課題を解決するために、社内ツールを作成しましたので紹介していきます。 課題 採用技術 システム構成図 開発した感じたこと 課題 当社のサービスWowTalkのサーバーはいくつか環境があり、バージョン管理の手法が担当者に委ねられていた問題がありました。 Ex) A環境 API: v2.5.0 Web: v1.4.0 / B環境 API: v2.6.0 Web: v1.4.0 → APIのバージョンの違いを確認することをわかりやすくしたい なので、今回はバージョン管理を一元化し、チーム全員が一覧で確認できる… <p>SREチームの三浦です。</p> <p>入社して半年が経過しました。SREの取組みとして開発チームであった課題を解決するために、社内ツールを作成しましたので紹介していきます。</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> </ul> <h5 id="課題">課題</h5> <p>当社のサービスWowTalkのサーバーはいくつか環境があり、バージョン管理の手法が担当者に委ねられていた問題がありました。</p> <p>Ex) A環境 API: v2.5.0  Web: v1.4.0 / B環境 API: v2.6.0  Web: v1.4.0 </p> <p> → APIのバージョンの違いを確認することをわかりやすくしたい</p> <p>なので、今回はバージョン管理を一元化し、チーム全員が一覧で確認できるツールを開発しました。</p> <p>追加で、GitLabのリリースノートにも反映してほしいと要望がありましたので、連携できる機能を開発しました。</p> <p> </p> <h5 id="採用技術">採用技術</h5> <p><バックエンド></p> <p><strong>・Express</strong></p> <p>別の社内ツール用で構築されていたので、併用して使う事にしました。</p> <p>主にAPIとして使用します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fexpressjs.com%2Fja%2F" title="Express - Node.js Web アプリケーション・フレームワーク" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://expressjs.com/ja/">expressjs.com</a></cite></p> <p><strong>・Firestore Database </strong></p> <p>FirebaseのNoSQLデータベースサービスです。当社のサービスで一部使用しているので、採用しました。</p> <p> </p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Ffirestore%3Fhl%3Dja" title="Cloud Firestore  |  Firebase" 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://firebase.google.com/docs/firestore?hl=ja">firebase.google.com</a></cite></p> <p> </p> <p><フロントエンド></p> <p><strong>・Next.js</strong></p> <p>Reactのフレームワークです。コンポーネントが充実していたり、SSRやSSGにも対応しています。2020年のバージョンアップでかなり機能が充実し、勢いがあるフレームワークです。</p> <p>下記の記事よりも、Next.jsの人気が高くなっていることがわかります。オススメのフレームワークです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.publickey1.jp%2Fblog%2F21%2Fstate_of_javascript_2020reactexpressjest24000.html" title="State of JavaScript 2020:いちばん利用率の高いJSフレームワーク、フロントエンドがReact、バックエンドはExpress、テストにはJest。2万4000人の調査結果" 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/21/state_of_javascript_2020reactexpressjest24000.html">www.publickey1.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnextjs-ja-translation-docs.vercel.app%2F" title="Next.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://nextjs-ja-translation-docs.vercel.app/">nextjs-ja-translation-docs.vercel.app</a></cite></p> <p> ・<strong>Ant Design</strong></p> <p>ReactのUIフレームワークです。こちらも当社サービスで一部採用していたので、使用しました。標準で便利なコンポーネントが準備されているので、自作のコンポーネントは作成せずにAnt Designのコンポーネントを利用しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fant.design%2F" title="Ant Design - The world's second most popular React UI 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://ant.design/">ant.design</a></cite></p> <p> </p> <h5 id="システム構成図">システム構成図</h5> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/miura1213/20210402/20210402155321.png" alt="f:id:miura1213:20210402155321p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p>①社内環境の都合上、Next.jsで静的ファイルを作成してサーバーに置いています。</p> <p>②Next.jsで作られた静的ファイル上で、Expressで作成したAPIをfetchして、RESTAPIを実現してます。</p> <p>③GitLab APIを利用して、タグに紐づくリリースノートを更新するAPIもExpress<br />で開発をしています。</p> <p> </p> <p><UI></p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/miura1213/20210402/20210402164013.png" alt="f:id:miura1213:20210402164013p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p>・全体をAntDesign感で統一しています。</p> <p>・上記ツールに登録されたバージョン情報はGitLabにも反映されます。</p> <p> </p> <h5 id="開発した感じたこと">開発した感じたこと</h5> <p><よかったこと></p> <p>・普段は当社サービスの機能追加や改修を行うことが多いが、ゼロから設計してツールを作成したことは良い経験になった。</p> <p>(通常はフロントメインの業務も多いが、今回はREST APIを学ぶ機会があってよかった。)</p> <p>・自分の興味のある技術を採用して、楽しく開発できたこと(一番重要)。</p> <p> </p> <p><反省点></p> <p>・学習時間が長くて、期間ギリギリで終わったこと。</p> <p>・運用面を考慮できずに、機能の修正を何回もしたこと。</p> <p>・ファイル名やDBの構成を何度も変更して時間がなくなったこと。</p> <p> </p> <p>これから運用していきますが、課題→改善の繰り返しが重要なので日々改善していこうと思います。</p> <p>当社ではこの様に様々なことにチャレンジできる環境があります</p> <p> </p> <p>当社ではこの様に挑戦できる環境があります。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /></a></p> <p> </p> miura1213 技術開発部のテレワーク事情が気になるから調査してみた hatenablog://entry/26006613654777338 2021-03-08T10:49:37+09:00 2022-10-05T12:15:20+09:00 Web開発チームの山本です。 ワウテックではコロナ以前からテレワークの制度は導入されていたのですが、2020年の緊急事態宣言からテレワークに切り替わりました。現在も会社からはテレワークを推奨されているため、基本テレワークで業務を行なっています。そんなテレワーク中心になってから1年近く経ち部内のみんなが、どのようにテレワークをしているか気になるので、調査してみました。 1. シゴト環境 モニターや椅子などを揃えて、工夫してる人が多い モニターや椅子など、皆さんテレワークでも仕事できる環境を整えている印象でした。やはり長時間の座り仕事なので、アーロンチェアを買っている人もいました(すごく羨ましい!… <p>Web開発チームの山本です。</p> <p>ワウテックではコロナ以前からテレワークの制度は導入されていたのですが、2020年の緊急事態宣言からテレワークに切り替わりました。現在も会社からはテレワークを推奨されているため、基本テレワークで業務を行なっています。そんなテレワーク中心になってから1年近く経ち部内のみんなが、どのようにテレワークをしているか気になるので、調査してみました。</p> <p> </p> <h3 id="1-シゴト環境">1. シゴト環境</h3> <h5 id="モニターや椅子などを揃えて工夫してる人が多い">モニターや椅子などを揃えて、工夫してる人が多い</h5> <p>モニターや椅子など、皆さんテレワークでも仕事できる環境を整えている印象でした。やはり長時間の座り仕事なので、アーロンチェアを買っている人もいました(すごく羨ましい!)。お値段張りますが、雲の上にも座っているみたいと例えられるのが納得できるほどの価値はあるようです(本人談)。</p> <div class="freezed"> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B01MUZOWBH/hatena-blog-22/" target="_blank"><img src="https://m.media-amazon.com/images/I/410b4QZltjL.jpg" class="hatena-asin-detail-image" alt="【正規品】 Herman Miller (ハーマンミラー) アーロンチェア オフィスチェア B(ミディアム)サイズ グラファイト(ブラック) BBキャスター 12年保証 AER1B23DWALPG1G1G1BBBK23103" title="【正規品】 Herman Miller (ハーマンミラー) アーロンチェア オフィスチェア B(ミディアム)サイズ グラファイト(ブラック) BBキャスター 12年保証 AER1B23DWALPG1G1G1BBBK23103" /></a> <div class="hatena-asin-detail-info"> <p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B01MUZOWBH/hatena-blog-22/" target="_blank">【正規品】 Herman Miller (ハーマンミラー) アーロンチェア オフィスチェア B(ミディアム)サイズ グラファイト(ブラック) BBキャスター 12年保証 AER1B23DWALPG1G1G1BBBK23103</a></p> <ul> <li><span class="hatena-asin-detail-label">メディア:</span> ホーム&amp;キッチン</li> </ul> </div> <div class="hatena-asin-detail-foot"> </div> </div> </div> <p> </p> <p>モニターを使っている人も多く、 Philipsの4Kモニターを2台同時利用するのをオススメしている人もいました。PCだけでなく、モニターがあると開発が捗りそうです。 </p> <div class="freezed"> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07ZQNRSG6/hatena-blog-22/" target="_blank"><img src="https://m.media-amazon.com/images/I/41M1CcfUnaL.jpg" class="hatena-asin-detail-image" alt="PHILIPS ディスプレイ 278E1A/11 (27インチ/4K/IPS/5年保証/HDMI/DisplayPort/PIP&amp;PBP)" title="PHILIPS ディスプレイ 278E1A/11 (27インチ/4K/IPS/5年保証/HDMI/DisplayPort/PIP&amp;PBP)" /></a> <div class="hatena-asin-detail-info"> <p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B07ZQNRSG6/hatena-blog-22/" target="_blank">PHILIPS ディスプレイ 278E1A/11 (27インチ/4K/IPS/5年保証/HDMI/DisplayPort/PIP&amp;PBP)</a></p> <ul> <li><span class="hatena-asin-detail-label">発売日:</span> 2019/11/29</li> <li><span class="hatena-asin-detail-label">メディア:</span> Personal Computers</li> </ul> </div> <div class="hatena-asin-detail-foot"> </div> </div> </div> <p> </p> <h5 id="仕事のお供にコーヒー必須">仕事のお供にコーヒー必須</h5> <p>テレワーク中の必須アイテムとして、コーヒーと答える人が多かったです。コーヒーメーカーで決まった回数飲む人もいれば、冷めないサーモスのステンレスマグカップがいいなど、各々自分に合うコーヒーのスタイルがありそうです。コーヒー党の一方で、はちみつ紅茶を飲んでいる人もいるので、技術部は全員がコーヒーを飲むわけではないですよ(念のため)。</p> <p> </p> <p>コーヒーを飲む人の中には、デロンギでコーヒーライフを充実されている人もいました。全自動なのが、いいですね。お手頃なコーヒーメーカーだとシロカも使っている方がいるようです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbarista.delonghi.co.jp%2Fproducts%2Fetam36365mb.html" title="デロンギ 全自動コーヒーマシン | プリマドンナXS | ETAM36365MB" 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://barista.delonghi.co.jp/products/etam36365mb.html" target="_blank">barista.delonghi.co.jp</a></cite></p> <p> </p> <div class="freezed"> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00OAZO6OM/hatena-blog-22/" target="_blank"><img src="https://m.media-amazon.com/images/I/41iU3jirG6L.jpg" class="hatena-asin-detail-image" alt="シロカ ドリップ式コーヒーメーカー SCM-401[メッシュフィルター/ドリップ方式]" title="シロカ ドリップ式コーヒーメーカー SCM-401[メッシュフィルター/ドリップ方式]" /></a> <div class="hatena-asin-detail-info"> <p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B00OAZO6OM/hatena-blog-22/" target="_blank">シロカ ドリップ式コーヒーメーカー SCM-401[メッシュフィルター/ドリップ方式]</a></p> <ul> <li><span class="hatena-asin-detail-label">発売日:</span> 2014/11/28</li> <li><span class="hatena-asin-detail-label">メディア:</span> ホーム&amp;キッチン</li> </ul> </div> <div class="hatena-asin-detail-foot"> </div> </div> </div> <p> </p> <p>また行き詰まった時のお散歩や音楽聴いたりと、家にいる時間が長いので、それぞれちょっとした息抜きもしているようです。仕事柄座っている時間が多いので、テレワークになってから運動不足や肩が以前よりもコルというのを聞くようになりました。そんな時に、ストレッチで肩こり解消する本もあるようなので、これで体調も整えたいです。</p> <div class="freezed"> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B088GWBFHQ/hatena-blog-22/" target="_blank"><img src="https://m.media-amazon.com/images/I/51hEXnbaJuL.jpg" class="hatena-asin-detail-image" alt="座り仕事の疲れがぜんぶとれるコリほぐしストレッチ――首・肩・腰が軽くなる!" title="座り仕事の疲れがぜんぶとれるコリほぐしストレッチ――首・肩・腰が軽くなる!" /></a> <div class="hatena-asin-detail-info"> <p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B088GWBFHQ/hatena-blog-22/" target="_blank">座り仕事の疲れがぜんぶとれるコリほぐしストレッチ――首・肩・腰が軽くなる!</a></p> <ul> <li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%A4%CA%A4%A1%A4%B5%A4%F3" target="_blank" class="keyword">なぁさん</a></li> <li><span class="hatena-asin-detail-label">発売日:</span> 2020/06/04</li> <li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li> </ul> </div> <div class="hatena-asin-detail-foot"> </div> </div> </div> <p> </p> <h5 id="開発ツールではGitLab便利">開発ツールではGitLab便利</h5> <p>開発ツールとしてGitLabを使ってプロジェクト管理などをしているので、テレワークでも問題なく仕事を進めることができています。スケジュール管理などもGitLabで行おうとする動きも最近あるので、それに関しては、主軸となっているメンバーが別のブログで書いてくれたらいいなと思っています(お願いします!!!)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fabout.gitlab.com%2F" title="DevOps Platform Delivered as a Single Application" 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://about.gitlab.com/">about.gitlab.com</a></cite></p> <p> </p> <h3 id="2-働き方">2. 働き方</h3> <h5 id="テレワークの課題">テレワークの課題</h5> <p>基本的に開発部はテレワークで作業しているので、テレワークになってコミュニケーションが取りづらくなったと感じているか?とメンバーに聞いてみました。そしたら、ほとんどのメンバーが、何かしらのコミュニケーション面での課題を感じたことがあるという結果になりました。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="テレワークになってコミュニケーションが取りづらくなったと感じているか?"> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210226/20210226113017.png" alt="f:id:yamamoto5555:20210226113017p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <figcaption class="mceEditable">テレワークになってコミュニケーションが取りづらくなったと感じているか?</figcaption> </figure> <p> </p> <p>具体的には、小さな疑問なども気軽に聞くことが難しくなったという声が多かったです。今までは、確認したいことも隣にいたので、簡単に聞くことができていたのが、テレワークによって質問するハードルが少し上がってしまったということでした。</p> <p> </p> <p>一方で、進歩確認などは課題は少ないという声もありました。チームによっては毎日1on1をすることで、業務に対する疑問を解消するようにしているところもあります。</p> <p> </p> <h5 id="チャットだけでなくテレビ電話も一緒に使う">チャットだけでなくテレビ電話も一緒に使う</h5> <p>テレワーク時にコミュニケーションで気をつけていることや工夫していることがあるのかを聞いてみると、文章だけでは伝わりにくいことは、通話やテレビ電話などでサクッと聞くという声がありました。文章だと複雑になってしまうこともあるのですが、意外と電話すると直ぐに解決できることも実体験としてはあります。</p> <p> </p> <p>また、文章にする際は主語や述語などの表現に気をつけながら、誤解が起きにくいように気をつけていたり、文章だけでは淡白になってしまいがちなので、スタンプを使うようにするという工夫をしているようです。近くにいないからこそ、少し丁寧なコミュニケーションを心掛ける必要があるのかもしれません。</p> <p> </p> <h3 id="3-今後の課題">3. 今後の課題</h3> <p> 顔を合わせることが少なくなったことで、雑談の機会は減ったように感じます。特に他の部署と話す機会は少なくなってしまいました。開発部では、ちょっとした雑談もできるチャットルームがあり 、日頃の情報共有などを行なっていますが、隣にいるような雑談とは少し違います。まだまだ課題は多いですが、テレワークで仕事する状態は続きそうなので、これからも改善できる部分には取り組んでいきたいです。</p> <p> </p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /> </a></p> yamamoto5555 初めてVue + VuexでNewsAPIを使ったアプリ作ってみた hatenablog://entry/26006613668904379 2021-02-03T10:52:56+09:00 2022-10-05T12:15:42+09:00 Web開発チームの山本です。 普段はReactで開発をしているのですが、Vueには触れたことがなかったので、簡単なアプリを作成してみました。今回は、ニュース記事を表示できるアプリについて紹介します。 アプリ概要 開発環境 準備 Vue CLIをインストール プロジェクトの作成 Vue用のUIライブラリ News APIのアカウント登録 Vueでコンポーネント作成 Vuexの作成 Vuexとは Vuexの導入 stateの作成 NewsAPIでデータ取得 actionの作成 mutationの作成 機能の実装 ニュースの表示 カテゴリー選択 ニュースのブックマーク まとめ アプリ概要 今回は、N… <p>Web開発チームの山本です。</p> <p>普段はReactで開発をしているのですが、Vueには触れたことがなかったので、簡単なアプリを作成してみました。今回は、ニュース記事を表示できるアプリについて紹介します。</p> <ul class="table-of-contents"> <li><a href="#アプリ概要">アプリ概要</a></li> <li><a href="#開発環境">開発環境</a></li> <li><a href="#準備">準備</a><ul> <li><a href="#Vue-CLIをインストール">Vue CLIをインストール</a></li> <li><a href="#プロジェクトの作成">プロジェクトの作成</a></li> </ul> </li> <li><a href="#Vue用のUIライブラリ">Vue用のUIライブラリ</a></li> <li><a href="#News-APIのアカウント登録">News APIのアカウント登録</a></li> <li><a href="#Vueでコンポーネント作成">Vueでコンポーネント作成</a></li> <li><a href="#Vuexの作成">Vuexの作成</a><ul> <li><a href="#Vuexとは">Vuexとは</a></li> <li><a href="#Vuexの導入">Vuexの導入</a><ul> <li><a href="#stateの作成">stateの作成</a></li> <li><a href="#NewsAPIでデータ取得">NewsAPIでデータ取得</a></li> <li><a href="#actionの作成">actionの作成</a></li> <li><a href="#mutationの作成">mutationの作成</a></li> </ul> </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> </ul> <p> </p> <p> </p> <h3 id="アプリ概要">アプリ概要</h3> <p>今回は、News APIを使ってカテゴリーごとに記事を表示させていきます。ただVueに集中したかったので、フロントのみを実装しています。</p> <p> </p> <p>またVuexも使ってみたかったので、ブックマーク機能を追加しました。ブックマークした記事は、ブックマークのページでまとめて表示することもできます。</p> <p> </p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210129/20210129113252.png" alt="f:id:yamamoto5555:20210129113252p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <h3 id="開発環境">開発環境</h3> <p>私の開発環境は次のようになっています。</p> <ul> <li>MacBookPro</li> <li style="box-sizing: inherit; margin: 0px; padding: 0px;">Node.js v12.16.2</li> <li style="box-sizing: inherit; margin: 0px; padding: 0px;">npm 6.14.4</li> </ul> <h3 id="準備">準備</h3> <h6 id="Vue-CLIをインストール">Vue CLIをインストール</h6> <p><a href="https://cli.vuejs.org/guide/installation.html" target="_blank">Vue CLI</a>は、Vue.jsを迅速に開発するためのフルシステムです。ReactでいうCreate React Appに近いかなと思います。Vue CLIは、基本的に必要なことをやってくれるので、とても助かります。ありがたや。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">$npm install -g @vue/cli $vue --version @vue/cli 4.5.8</pre> <p> </p> <h6 id="プロジェクトの作成">プロジェクトの作成</h6> <p>news-appというプロジェクトを作成します。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">$vue create news-app $cd news-app<br />//サーバーを起動 $npm run serve</pre> <p> ブラウザでhttp://localhost:8081/ を開くと、サンプルが表示さます。サンプル表示させただけですが、表示できると嬉しいですね!</p> <p> </p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210108/20210108111443.png" alt="f:id:yamamoto5555:20210108111443p:plain" title="" class="hatena-fotolife" itemprop="image" width="598" /> </p> <h3 id="Vue用のUIライブラリ">Vue用のUIライブラリ</h3> <p>今回UIライブラリに<a href="https://vuematerial.io/" target="_blank">Vue Material</a>を使用します。雰囲気Google Newsぽくなるかなと思ったからです(なって欲しい)。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">$npm install vue-material --save</pre> <p> </p> <p>公式ドキュメントで必要なコンポーネントだけをimportすることが推奨されていたので、main.jsで使いたいコンポーネントだけをimportしていきます。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import Vue from 'vue' import App from './App.vue' import { MdButton, MdCard, MdLayout, MdApp, MdList, MdToolbar, MdDrawer, MdIcon, MdContent } from 'vue-material/dist/components' import 'vue-material/dist/vue-material.min.css' import 'vue-material/dist/theme/default.css' Vue.config.productionTip = false Vue.use(Vuex) Vue.use(MdButton) Vue.use(MdCard) Vue.use(MdLayout) Vue.use(MdApp) Vue.use(MdList) Vue.use(MdToolbar) Vue.use(MdDrawer) Vue.use(MdIcon) Vue.use(MdContent)</pre> <h3 id="News-APIのアカウント登録">News APIのアカウント登録</h3> <p>表示させるニュースは、<a href="https://newsapi.org/s/google-news-api">NewsAPI</a>を使っていきます。アカウントを作成すると、API keyを取得できます。NewsAPIは、カテゴリーごとや言語ごとにニュースを取得できるので便利でした。</p> <h3 id="Vueでコンポーネント作成">Vueでコンポーネント作成</h3> <p>Vue Materialを使用して、Headerを作成しましょう。Vue MaterialのComponentsの中の<a href="https://vuematerial.io/components/app/" target="_blank">App</a>を使いました。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/App.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div id="app"&gt; &lt;div class="page-container"&gt; &lt;md-app md-mode="reveal"&gt; &lt;md-app-toolbar class="md-primary"&gt; &lt;md-button class="md-icon-button"&gt; &lt;md-icon&gt;menu&lt;/md-icon&gt; &lt;/md-button&gt; &lt;span class="md-title"&gt;News App&lt;/span&gt; &lt;/md-app-toolbar&gt; &lt;/md-app&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt;<br /> &lt;script&gt; export default { name: 'App', } &lt;/script&gt;<br /> &lt;style&gt; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } &lt;/style&gt;</pre> <p>上記のコードをかくと、http://localhost:8081/に下のような画像のようにHeaderが表示されます。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210120/20210120110856.png" alt="f:id:yamamoto5555:20210120110856p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p>同じように下記のコンポーネントを作成していきます。</p> <ul> <li>NewsItem.vue:ニュースを表示するカードのコンポーネント</li> <li>Content.vue:複数のカードを表示されるコンポーネント</li> </ul> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/NewsItem.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;md-card&gt; &lt;md-card-area&gt; &lt;md-card-media&gt; &lt;/md-card-media&gt; &lt;md-card-header&gt; &lt;div class="md-title"&gt;Article Title&lt;/div&gt; &lt;/md-card-header&gt; &lt;md-card-content&gt; Content......... &lt;/md-card-content&gt; &lt;/md-card-area&gt; &lt;md-card-actions md-alignment="space-between"&gt; &lt;md-button class="md-icon-button" &gt; &lt;md-icon&gt;bookmark&lt;/md-icon&gt; &lt;/md-button&gt; &lt;md-button class="md-primary"&gt;Read More&lt;/md-button&gt; &lt;/md-card-actions&gt; &lt;/md-card&gt; &lt;/template&gt;</pre> <p> Vue MaterialにCardというコンポーネントがあるので、それを使用してカードを作成します。</p> <p> </p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/Content.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div class="md-layout md-gutter md-alignment-center"&gt; &lt;div v-for="n in 8" class="md-layout-item" :key="n"&gt;<br /> &lt;NewsItem :article="n"/&gt;<br /> &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; import NewsItem from "./NewsItem.vue" export default { components: { NewsItem }, } &lt;/script&gt; <br /><br /><br /><br /></pre> <p> NewsItem.vueをContent.vueで呼び出し複数のカードを表示していきます。</p> <p>作成した Content.vueをApp.vueで読み込んでみましょう。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/App.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div id="app"&gt; &lt;div class="page-container"&gt; &lt;md-app md-mode="reveal"&gt; &lt;md-app-toolbar class="md-primary"&gt; &lt;md-button class="md-icon-button"&gt; &lt;md-icon&gt;menu&lt;/md-icon&gt; &lt;/md-button&gt; &lt;span class="md-title"&gt;News App&lt;/span&gt; &lt;/md-app-toolbar&gt; &lt;md-app-content&gt; &lt;Content /&gt; &lt;/md-app-content&gt; &lt;/md-app&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; export default { import Content from './components/Content.vue' name: 'App', components: { Content }, } &lt;/script&gt;</pre> <p> </p> <p>そうすると、カードを複数表示するコンポーネントが完成します。UIライブラリを使用すれば、そんなに時間がかからず作成できるはずです。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210120/20210120114053.png" alt="f:id:yamamoto5555:20210120114053p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p><br /><br /> </p> <h3 id="Vuexの作成">Vuexの作成</h3> <p> 次に、登録したNewsAPIを使用して、ニュースのデータを取得し表示します。さらに、次の3つのことを実現させたいです。</p> <ul> <li>カテゴリーごとに記事を表示させる</li> <li>記事をブックマークできる</li> <li>ブックマークされた記事は一覧で表示できるようにする</li> </ul> <p>上記のことを実現するために考えなくてはいけないのは、記事とブックマークされた記事のデータ管理です。ただこれをVueで行うと、データの受け渡しをpropsで行うことになり、複雑化してしまいます。。。今回は、Vuexを使用してデータ一元化を行ないます。</p> <p> </p> <h5 id="Vuexとは">Vuexとは</h5> <p>ReactでいうReduxのように、Vue.js アプリケーションのための状態管理のライブラリです。詳しいことは、<a href="https://vuex.vuejs.org/ja/" target="_blank">公式ドキュメント</a>に書かれています。</p> <p> </p> <h5 id="Vuexの導入">Vuexの導入</h5> <p>Vuexをインストールしましょう。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">$npm install vuex --save</pre> <p>インストールしたら、main.jsを編集しVuexの設定をします。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import Vue from 'vue' import Vuex from 'vuex' import App from './App.vue' import store from './store' Vue.config.productionTip = false Vue.use(Vuex) new Vue({ render: h =&gt; h(App), store, }).$mount('#app')</pre> <p>「import store from './store'」と記載していますが、storeのディレクトリは作成していなかったので、作っていきます。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/index.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import Vue from 'vue' import Vuex from 'vuex' import news from './modules/news' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ modules: { news, }, strict: debug, })</pre> <p> <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/modules/news.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">// initial state const state = () =&gt; ({ }) // getters const getters = {} // actions const actions = { } // mutations const mutations = { } export default { namespaced: true, state, getters, actions, mutations }</pre> <p> </p> <h6 id="stateの作成">stateの作成</h6> <p>stateは、アプリケーション全体のデータを管理するところです。今回のアプリでは、3つのデータを保存していきます。</p> <ul> <li>カテゴリーごとに表示するニュースのデータ</li> <li>選択しているカテゴリー状態</li> <li>ブックマークしているデータ</li> </ul> <p>newsDataはカテゴリーごとにデータを取り出しやすくしたかったので、keyをカテゴリー名にしています。</p> <p> <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/modules/news.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const state = () =&gt; ({ newsData: { "technology":[], "entertainment":[], "business":[], "science":[], }, activeCategory: "business", bookmarkData:[] })</pre> <p> </p> <h6 id="NewsAPIでデータ取得">NewsAPIでデータ取得</h6> <p>保存するニュースのデータをNewsAPIから取得します。指定したカテゴリーのデータを取得したかったので、引数でカテゴリー名を渡しています。APIからデータ取得するときに、axiosを使用しました。詳しい使い方は、<a href="https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html" target="_blank">ドキュメント</a>をご覧ください。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import axios from 'axios' <br /><br />const key = NewsAPIの登録後に渡されるAPI key export const changeCategory = async (category) =&gt; { const res = await axios.get('https://newsapi.org/v2/top-headlines?country=jp&amp;category='+category+'&amp;apiKey='+ key) const articles = res.data.articles; return articles }</pre> <h6 id="actionの作成">actionの作成</h6> <p>actionでAPIの実行をしましょう。初回のみデータ取得したいので、既にカテゴリーのデータがある場合には、更新せずにreturn falseしています。また、本当はサーバー側でidの割り振ると思うのですが、今回はフロントだけで処理を行なっているため、article_id_listというのを使って、idを割り振っています。ニュースデータは固定で20件ずつ取るようにしているので、article_id_listのstart_indexを20ごとに決めています。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { changeCategory } from '../../../api/news.js' <br />const article_id_list = {business: 0, entertainment: 20, technology: 40, science: 60} const actions = { async getArticles({commit, state}){ const category = state.activeCategory if(state.newsData[category].length ){ return false }else{ const new_articles = await changeCategory(category) const article_data = new_articles.map((article, index)=&gt;{ const start_index = article_id_list[category] article.id = index + start_index article.bookmark = false return article }) commit('setArticles', {data: article_data, category: category}) return new_articles } } } </pre> <p>カテゴリーの変更をするactionを追加しましょう。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">import { changeCategory } from '../../../api/news.js' <br />const article_id_list = {business: 0, entertainment: 20, technology: 40, science: 60} const actions = { async getArticles({commit, state}){ const category = state.activeCategory if(state.newsData[category].length ){ return false }else{ const new_articles = await changeCategory(category) const article_data = new_articles.map((article, index)=&gt;{ const start_index = article_id_list[category] article.id = index + start_index article.bookmark = false return article }) console.log("article_data", article_data) commit('setArticles', {data: article_data, category: category}) return new_articles } },<br />  //カテゴリーの変更をするaction追加 updateCategory({commit, state}, category){ if(state.activeCategory !== category){ commit('setActiveCategory', category) } } }</pre> <p> </p> <h6 id="mutationの作成">mutationの作成</h6> <p> mutationでデータの変更をしていきます。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const mutations = {<br /> //取得した記事のデータを保存 setArticles(state, data){ const news_data = data.data const news_category = data.category state.newsData[news_category] = news_data },<br />   //選択中のカテゴリーを保存 setActiveCategory(state, category){ state.activeCategory = category } }</pre> <p> 記事のデータは、全てを渡すのではなく、選択しているカテゴリーのデータだけを渡すようにgettersの中に書いていきます。</p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const getters = { getArticlesByCategory: (state) =&gt; { const category = state.activeCategory return state.newsData[category] } }</pre> <h3 id="機能の実装">機能の実装</h3> <h5 id="ニュースの表示">ニュースの表示</h5> <p>App.vueで記事のデータを取得します。createdの中で、getArticlesのアクションを実行しましょう。mapGettersを使って、Component内でstoreに保存した記事のデータを取り出します。そして、Content Componentにarticlesを渡していきます。 </p> <p><em style="font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; text-align: left;">src/components/App.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div id="app"&gt; &lt;div class="page-container"&gt; &lt;md-app md-mode="reveal"&gt; &lt;md-app-toolbar class="md-primary"&gt; &lt;md-button class="md-icon-button"&gt; &lt;md-icon&gt;menu&lt;/md-icon&gt; &lt;/md-button&gt; &lt;span class="md-title"&gt;News App&lt;/span&gt; &lt;/md-app-toolbar&gt; &lt;md-app-content &gt; &lt;Content :articles="articles"/&gt; &lt;/md-app-content&gt; &lt;/md-app&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; import Content from './components/Content.vue' import { mapState, mapGetters } from 'vuex' export default { name: 'App', components: { Content }, computed: { ...mapState({ activeCategory: state =&gt; state.news.activeCategory, }), ...mapGetters('news', { articles: 'getArticlesByCategory' }) }, created () { this.$store.dispatch('news/getArticles') }, } &lt;/script&gt;</pre> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/Content.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div class="md-layout md-gutter md-alignment-center"&gt; &lt;div v-for="article in articles" class="md-layout-item" :key="article.title"&gt; &lt;NewsItem :article="article"/&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; import NewsItem from "./NewsItem.vue" export default { components: { NewsItem }, props: { articles: Array } } &lt;/script&gt;</pre> <p>  </p> <p>記事によっては、画像データがないのもあったので、画像がない場合は、no_image.jpgを表示するようにしています。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/NewsItem.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;md-card :id="article.id"&gt; &lt;md-card-area&gt; &lt;md-card-media&gt; &lt;img :src="article.urlToImage" :alt="article.title" @error="noImage"&gt; &lt;/md-card-media&gt; &lt;md-card-header&gt; &lt;div class="md-title"&gt;{{article.title}}&lt;/div&gt; &lt;/md-card-header&gt; &lt;md-card-content&gt; {{article.description}} &lt;/md-card-content&gt; &lt;/md-card-area&gt; &lt;md-card-actions md-alignment="space-between"&gt; &lt;md-button v-bind:href="article.url" class="md-primary"&gt;Read More&lt;/md-button&gt; &lt;/md-card-actions&gt; &lt;/md-card&gt; &lt;/template&gt; &lt;script&gt; import AssetsImage from "@/assets/no_image.jpg"; export default { name: 'NewsItem', props: { article: Object }, data(){ return { assetImage: AssetsImage } }, methods: { noImage(element){ element.target.src = this.assetImage } } } &lt;/script&gt;</pre> <p> これで最初の画面に、ニュースを表示できるようになりました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210129/20210129113631.png" alt="f:id:yamamoto5555:20210129113631p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <h5 id="カテゴリー選択"> <br />カテゴリー選択</h5> <p>ニュースデータの取得と表示はできたので、カテゴリーを選択して表示できるようにします。まずは、Drawerの左上のアイコンをクリックした時に表示/非表示できるようにします。これも<a href="https://vuematerial.io/" target="_blank">Vue Material</a>のDrawerのコンポーネントを使っていきます。SideMenu.vueの中に、Drawer内に表示するものを書いていきます。</p> <p> </p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/SideMenu.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div&gt; &lt;md-toolbar class="md-transparent" md-elevation="0"&gt;Category&lt;/md-toolbar&gt; &lt;md-list&gt; &lt;md-list-item class="item-active"&gt; &lt;md-icon&gt;business&lt;/md-icon&gt; &lt;span id="business" class="md-list-item-text" @click="setResource('business')"&gt;Business&lt;/span&gt; &lt;/md-list-item&gt; &lt;md-list-item&gt; &lt;md-icon&gt;music_note&lt;/md-icon&gt; &lt;span id="entertainment" class="md-list-item-text" @click="setResource('entertainment')"&gt;Entertainment&lt;/span&gt; &lt;/md-list-item&gt; &lt;md-list-item&gt; &lt;md-icon&gt;emoji_objects&lt;/md-icon&gt; &lt;span id="technology" class="md-list-item-text" @click="setResource('technology')"&gt;Technology&lt;/span&gt; &lt;/md-list-item&gt; &lt;md-list-item&gt; &lt;md-icon&gt;science&lt;/md-icon&gt; &lt;span id="science" class="md-list-item-text" @click="setResource('science')"&gt;Science&lt;/span&gt; &lt;/md-list-item&gt; &lt;md-list-item&gt; &lt;md-icon&gt;bookmarks&lt;/md-icon&gt; &lt;span id="bookmark" class="md-list-item-text" @click="setResource('bookmark')"&gt;Bookmark&lt;/span&gt; &lt;/md-list-item&gt; &lt;/md-list&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; export default { name: 'SideMenu', props:{ setResource: Function }, } &lt;/script&gt; &lt;style&gt; .md-list-item{ cursor: pointer; } .md-list .item-text-active{ color:#448aff; } &lt;/style&gt;</pre> <p>SideMenu.vueをApp.vueから呼び出します。このDrawerの表示/表示のデータは、storeに入れずに、menuVisibleとしてlocalで管理します。そして、setResourceというメソッドで、カテゴリーを変更していきます。</p> <p> </p> <p> <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/App.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;div id="app"&gt; &lt;div class="page-container"&gt; &lt;md-app md-mode="reveal"&gt; &lt;md-app-toolbar class="md-primary"&gt; &lt;md-button class="md-icon-button" @click="menuVisible = !menuVisible"&gt; &lt;md-icon&gt;menu&lt;/md-icon&gt; &lt;/md-button&gt; &lt;span class="md-title"&gt;News App&lt;/span&gt; &lt;/md-app-toolbar&gt; &lt;md-app-drawer :md-active.sync="menuVisible" &gt; &lt;SideMenu :setResource = "setResource" /&gt; &lt;/md-app-drawer&gt; &lt;md-app-content&gt;<br />      &lt;Content :articles="articles"/&gt;<br />     &lt;/md-app-content&gt; &lt;/md-app&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script&gt; import Content from './components/Content.vue' import SideMenu from './components/SideMenu.vue' import { mapState, mapGetters } from 'vuex' export default { name: 'App', components: { SideMenu, Content }, data() { return { menuVisible: false, } }, ・<br />・<br />・<br />・<br />・<br /> methods: {<br /> setResource(category){<br />   this.$store.dispatch('news/updateCategory', category)<br />   this.menuVisible = false<br /> }<br /> }<br />} &lt;/script&gt; &lt;style&gt; #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } .md-drawer { width: 230px; max-width: calc(100vw - 125px); } .md-content{ height: calc(100vh - 64px) } &lt;/style&gt;</pre> <p> </p> <p>左上のアイコンをクリックすると、下のようにDrawerが表示されるようになりました。各カテゴリーをクリックしても、カテゴリー毎のニュースも表示されるようになっていると思います。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210129/20210129195241.png" alt="f:id:yamamoto5555:20210129195241p:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <h5 id="ニュースのブックマーク">ニュースのブックマーク</h5> <p>actionとmutationの追加からしていきます。</p> <p>  <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/modules/news.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const actions = { ・<br />・<br />・<br />・<br />・<br />  //ブックマークデータの更新のactionを追加 updateBookmark({commit}, value){ commit('updateBookmark', value) } }</pre> <p>  <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/modules/news.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const mutations = {<br />・<br />・<br />・<br />・<br />・<br />  //ブックマークした記事の保存または削除 updateBookmark(state, id){ let category = state.activeCategory<br /> //既にブックマークされているか const bookmark_index = state.bookmarkData.findIndex((item) =&gt; item.id == id)<br />      if(category !== null &amp;&amp; bookmark_index !== -1){ category = state.bookmarkData[bookmark_index].category }<br /> const article_index = state.newsData[category].findIndex((item) =&gt; item.id == id) const bookmark = state.newsData[category][article_index].bookmark<br /> if(article_index != null){<br /> //newsDataのbookmarkをtrue・falseを変更 state.newsData[category][article_index].bookmark = !bookmark //ブックマークされていなかったら、bookmarkDataにデータを追加 if(bookmark_index === -1){ let copy_data = Object.assign(state.newsData[category][article_index]) copy_data.category = category state.bookmarkData.push(copy_data) }else{<br />         //既にある場合は、bookmarkDataから削除 state.bookmarkData.splice(bookmark_index, 1) } } } }</pre> <p> mutationsのupdateBookmarkで、bookmarkDataとnewsDataの更新をしていきます。</p> <p> </p> <p>activeCategoryがbookmarkの時は、bookmarkDataを渡すようにgettersを変更します。</p> <p>  <em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/store/modules/news.js</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">const getters = { getArticlesByCategory: (state) =&gt; { const category = state.activeCategory<br />     let data<br />     if(category === "bookmark"){<br />      data = state.bookmarkData<br />     }else{<br />      data = state.newsData[category]<br />     }<br />      return data; } }</pre> <p> </p> <p>次に、クリックイベントを追加するために、コンポーネントの変更を行います。</p> <p><em style="opacity: 0.6; color: #3d5f6d; font-family: Roboto, 游ゴシック, YuGothic, 'ヒラギノ角ゴ ProN', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, sans-serif; font-size: 14.88px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">src/components/NewsItem.vue</em></p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">&lt;template&gt; &lt;md-card :id="article.id"&gt; &lt;md-card-area&gt; &lt;md-card-media&gt; &lt;img :src="article.urlToImage" :alt="article.title" @error="noImage"&gt; &lt;/md-card-media&gt; &lt;md-card-header&gt; &lt;div class="md-title"&gt;{{article.title}}&lt;/div&gt; &lt;/md-card-header&gt; &lt;md-card-content&gt; {{article.description}} &lt;/md-card-content&gt; &lt;/md-card-area&gt; &lt;md-card-actions md-alignment="space-between"&gt; &lt;md-button class="md-icon-button" @click="onChangeBookmark(article.id)"&gt; &lt;md-icon v-if="article.bookmark"&gt;bookmark&lt;/md-icon&gt; &lt;md-icon v-else&gt;bookmark_border&lt;/md-icon&gt; &lt;/md-button&gt; &lt;md-button v-bind:href="article.url" class="md-primary"&gt;Read More&lt;/md-button&gt; &lt;/md-card-actions&gt; &lt;/md-card&gt; &lt;/template&gt; &lt;script&gt; import AssetsImage from "@/assets/no_image.jpg"; export default { name: 'NewsItem', props: { article: Object }, data(){ return { assetImage: AssetsImage } }, methods: { onChangeBookmark(id){ this.$store.dispatch('news/updateBookmark', id) }, noImage(element){ element.target.src = this.assetImage } } } &lt;/script&gt;</pre> <p>ブックマークのアイコンをクリックすると、onChangeBookmarkの関数から、先ほど追加したupdateBookmarkのアクションが実行されます。</p> <p> </p> <p>これで、ブックマークの機能が完成しました!</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20210201/20210201115440.gif" alt="f:id:yamamoto5555:20210201115440g:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>初めてVueとVuexを使ってアプリを作りましたが、初心者でもReactに比べて時間をかけずに作成することができました。まだまだこのアプリの改善したいところもありますが、短時間で動くアプリを作るのを目標に作ってみました。これから、Vueを使っていく上で、様々な疑問が出てくると思うので、その時はまた別の記事でまとめたいです。</p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /> </a></p> yamamoto5555 業務未経験で入社して3ヶ月経って感じた事 hatenablog://entry/26006613666113518 2020-12-17T11:30:48+09:00 2020-12-17T16:37:30+09:00 こんにちは。WowDesk担当の三浦です。今回が初投稿となります。よろしくお願いいたします。私が当社に入社して3ヶ月が経ちましたので所感を書いていこうと思います。ワウテックでエンジニアになりたいと思う方の参考になればと思います。 簡単な経歴 入社後の業務内容 入社して感じた事 まとめ 参考リンク 簡単な経歴 前職では、自社サービスの保守・運用・導入がメインのIT企業に約4年いました。その後、本格的に開発に携わりたいという気持ちが強くなりエンジニアになる道を目指し、プログラミングの学習を開始しました。そして、当社と縁があり実務未経験として入社することになります。 入社後の業務内容 当社のクラウド… <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/miura1213/20201216/20201216213358.jpg" alt="f:id:miura1213:20201216213358j:plain" title="" class="hatena-fotolife" itemprop="image" /></p> <p>こんにちは。WowDesk担当の三浦です。<br />今回が初投稿となります。よろしくお願いいたします。<br />私が当社に入社して3ヶ月が経ちましたので所感を書いていこうと思います。<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></li> </ul> <h5 id="簡単な経歴">簡単な経歴</h5> <p>前職では、自社サービスの保守・運用・導入がメインのIT企業に約4年いました。<br />その後、本格的に開発に携わりたいという気持ちが強くなりエンジニアになる道を目指し、プログラミングの学習を開始しました。<br />そして、当社と縁があり実務未経験として入社することになります。<br /><cite class="hatena-citation"></cite> </p> <h5 id="入社後の業務内容">入社後の業務内容</h5> <p>当社のクラウド受付サービス「WowDesk」のWeb開発を担当することになりました。<br />最初は簡易的な機能開発や、React-Reduxの導入を実施しました。</p> <p><iframe class="embed-card embed-webcard" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" title="HOME" src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wowdesk.jp%2F" frameborder="0" scrolling="no"></iframe><cite class="hatena-citation"><a href="https://www.wowdesk.jp/">www.wowdesk.jp</a></cite></p> <h5 id="入社して感じた事">入社して感じた事</h5> <p>私が入社して感じた事を、苦戦した事とよかった事の2つの視点で記載していきます。 </p> <p><strong><苦戦した事></strong></p> <ol> <li>既存のコードが読めない<br />これは知識不足で、コードの意味が全くわかりませんでした。実務未経験の最初の壁だと思います。例えば、私は基本JavaScriptを記述しているのですが、非同期処理が未知の世界でした。<br />(今でも記述はしますが、100%分かるかと言われると怪しいです・・・)<br />しかし、この辺りは先輩方が本当に優しくて、しっかりとフォローした頂きなんとか乗り越えていくことができました。<br /><br /></li> <li>開発だけでなく、自社サービスの発展も視野に入れる<br />開発作業だけでなく、チーム内でどういったニーズがある機能を追加するか等を議論していきます。ここの部分に関しては、前職にはなかった思考だったので今でも苦戦しています。<br />単純に機能追加をするだけでなく、お客様の本質的な課題解決につながる機能をしっかりと議論していく必要があります。<br /><br /></li> <li>リモートワークのコミュニケーション<br />私が入社した時には、コロナウイルスの真っ只中でした。ほとんどのメンバーがリモートワークを実施していました。<br />一番苦戦したことは、コードに関する質問が気軽にできないということでした。併せて上記の知識不足の為、全く前に進めませんでした。そこで「Chrome リモートデスクトップ」を利用し、質問等がしやすくなりました。また、先輩のデバッグの仕方など直で確認できるので、取り入れて正解だったなと思います。<br /> <p><iframe class="embed-card embed-webcard" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" title="Chrome リモート デスクトップを使って他のパソコンにアクセスする - パソコン - Google Chrome ヘルプ" src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsupport.google.com%2Fchrome%2Fanswer%2F1649523%3Fco%3DGENIE.Platform%253DDesktop%26hl%3Dja" frameborder="0" scrolling="no"></iframe><cite class="hatena-citation"><a href="https://support.google.com/chrome/answer/1649523?co=GENIE.Platform%3DDesktop&amp;hl=ja">support.google.com</a></cite></p>  </li> </ol> <p><strong><よかった事></strong></p> <ol> <li>自由な働き方<br />まずなんといっても私服!前職では毎日スーツを来ており、最初は私服がなじめませんでした。(田舎出身という事もあり、みんなおしゃれに見える・・・)<br />リモートワークも早めに導入しており、ベンチャー企業らしさを感じる一面です。<br /><br /></li> <li>開発環境が良い<br />開発メンバーの椅子が最高です。個人では買うことがない椅子で業務を行うことできます。椅子だけではなく、開発PCのスペック等、エンジニアが開発する上で良い環境で業務を行うことができます。<br /><br /></li> <li>開発メンバーが優しい ※最重要<br />開発メンバーには本当に恵まれているなと感じています。実務未経験者の私でも丁寧にフォローしていただけています。<br />ただ、何でも教えていただく訳でもなく、自分で考えるべき部分はしっかり指導していただけます。<br />当社に入社や検討される方がいらしたらこの部分は、安心して頂ければと思います。</li> </ol> <h5 id="まとめ">まとめ</h5> <p>ワウテックでエンジニアになりたいという方の参考になればと思っております。<br />また、技術的な投稿も今後は投稿していきますので、よろしくお願いいたします。</p> <p> </p> <h5 id="参考リンク">参考リンク</h5> <ul> <li> <p><a href="http://www.wowdesk.jp">【WowDesk】クラウド型受付システム/ビジネスチャットとも連携!</a></p> </li> <li> <p><a href="https://support.google.com/chrome/answer/1649523?co=GENIE.Platform%3DDesktop&amp;hl=ja">Chrome リモート デスクトップを使って他のパソコンにアクセスする - パソコン - Google Chrome ヘルプ</a></p> </li> </ul> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> <p> </p> <p> </p> miura1213 Reduxのパフォーマンス改善でImmutable Dataの概念にぶち当たった話 hatenablog://entry/26006613637141143 2020-11-02T11:36:03+09:00 2021-05-07T10:15:01+09:00 Web開発チームの山本です。 以前の記事でReduxのStore設計について書いた通り、Reduxの道へと踏み出したのですが、その後パフォーマンス面での課題にぶち当たったので、今回はReduxでパフォーマンス改善に取り組んだことについて紹介します。 背景 課題 Reduxの仕組み ReduxとImmutable Data 改善内容 改善結果 まとめ 補足: 参考リンク 背景 お客様によりWowTalkを使っていただくため社内の浸透をサポートする集計機能を実装しました(詳しくはこちら)。その中の最終アクティブでは、アカウントに所属するユーザーの最終アクティブを表示しています。この最終アクティブの… <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200821/20200821113553.jpg" alt="f:id:yamamoto5555:20200821113553j:plain" title="f:id:yamamoto5555:20200821113553j:plain" class="hatena-fotolife" itemprop="image" /></p> <p>Web開発チームの山本です。</p> <p>以前の記事で<a href="https://engineer.wowtech.co.jp/entry/2020/08/24/100311" target="_blank">ReduxのStore設計</a>について書いた通り、Reduxの道へと踏み出したのですが、その後パフォーマンス面での課題にぶち当たったので、今回はReduxでパフォーマンス改善に取り組んだことについて紹介します。</p> <ul class="table-of-contents"> <li><a href="#背景">背景</a></li> <li><a href="#課題">課題</a></li> <li><a href="#Reduxの仕組み">Reduxの仕組み</a><ul> <li><a href="#ReduxとImmutable-Data">ReduxとImmutable Data</a></li> </ul> </li> <li><a href="#改善内容">改善内容</a></li> <li><a href="#改善結果">改善結果</a></li> <li><a href="#まとめ">まとめ</a><ul> <li><a href="#補足">補足:</a></li> <li><a href="#参考リンク">参考リンク </a></li> </ul> </li> </ul> <h3 id="背景">背景</h3> <p>お客様によりWowTalkを使っていただくため社内の浸透をサポートする集計機能を実装しました(詳しくは<a href="https://www.wowtalk.jp/info/active_userdata.html" target="_blank">こちら</a>)。その中の最終アクティブでは、アカウントに所属するユーザーの最終アクティブを表示しています。この最終アクティブの画面の表示が、ユーザーの多いアカウントでは遅いという問題がありました。最終アクティブ画面以外にも影響が出ていたため、早急に改善する必要がありました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20201006/20201006105315.png" alt="f:id:yamamoto5555:20201006105315p:plain" title="f:id:yamamoto5555:20201006105315p:plain" class="hatena-fotolife" itemprop="image" width="563" /></p> <p> </p> <h3 id="課題">課題</h3> <p>今回は、4000人以上のアカウントで最終アクティブのページの読み込みが遅いのが、顕著に現れたので、Google Dev Toolsを使用し計測してみました。計測方法としては、MacBook Proを使用し、4000人ほどのアカウントで試しました。最終的には、数万人のアカウントも使用できるようにすることが今回のゴールです。</p> <p> </p> <p>・計測対象の操作</p> <p>サイドバーから最終アクティブをクリックするが、表示の反応が遅れるのと、最終アクティブのリストが表示されるのが遅いです。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20201013/20201013103743.gif" alt="f:id:yamamoto5555:20201013103743g:plain" title="f:id:yamamoto5555:20201013103743g:plain" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <p>・合計のレンダリング時間</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20201013/20201013101046.png" alt="f:id:yamamoto5555:20201013101046p:plain" title="f:id:yamamoto5555:20201013101046p:plain" class="hatena-fotolife" itemprop="image" width="586" /></p> <p> </p> <p>・APIのリクエストとレスポンス時間</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20201013/20201013101108.png" alt="f:id:yamamoto5555:20201013101108p:plain" title="f:id:yamamoto5555:20201013101108p:plain" class="hatena-fotolife" itemprop="image" width="608" /></p> <p> </p> <h3 id="Reduxの仕組み">Reduxの仕組み</h3> <p>Reactで<a href="https://www.wowtalk.jp/info/safety-confirmation.html" target="_blank">安否確認機能</a>というサービスも開発していますが、同じ規模のアカウントでユーザーの読み込みに時間に、これほど時間がかかることはなかったため、今回はRedux起因の問題であると予想を立てました。</p> <p> </p> <p>redux-loggerで出力しているLogでは、明らかにリストとして表示しているユーザー情報をStoreに更新した時の処理に時間がかかっていました。デバックしてみると、4000人のユーザー情報でループ処理が実行されていました。調べてみたところ、Reduxのimmutabilityに関係していました。</p> <p> </p> <h5 id="ReduxとImmutable-Data">ReduxとImmutable Data</h5> <p>Redux と React-Redux は、どちらもをshallow equality checkingを採用しています。shallow equality checking(またはreference equality)とは、2つの異なる変数が同じオブジェクトを参照しているかどうかをチェックします。 また、shallow equality checkingとよく比較されるのが、deep equality checking です。deep equality checkingは、2つのオブジェクトのプロパティのすべての値をチェックします。Immutable Dataに関しては、<a href="https://redux.js.org/faq/immutable-data" target="_blank">Reduxのドキュメント</a>にも詳しく記載されています。</p> <p>今回は、このshallow quality checkingによって、Storeに4000人のユーザー情報を入れる場合、それの差分を確認するためにループ処理が実行され、時間がかかっているのではと考えました。</p> <p> </p> <h3 id="改善内容">改善内容</h3> <p>Storeに入れてしまうと、shallow quality checkingで時間がかかってしまうため、ユーザー情報は、Storeに入れないで、グローバル変数の中に入れることにしました。local stateに入れるのは、UIなどの管理は向いているかなと思ったのですが、今回のユーザー情報は、最初に取得するだけで、そのあとに更新などは必要ないため、グローバル変数に格納することになりました。</p> <p><iframe class="embed-card embed-webcard" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" title="Organizing State | Redux" src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fredux.js.org%2Ffaq%2Forganizing-state%23do-i-have-to-put-all-my-state-into-redux-should-i-ever-use-reacts-setstate" frameborder="0" scrolling="no"></iframe><cite class="hatena-citation"><a href="https://redux.js.org/faq/organizing-state#do-i-have-to-put-all-my-state-into-redux-should-i-ever-use-reacts-setstate">redux.js.org</a></cite></p> <p> </p> <p>また、その他の改善点としては、最終アクティブの画面ではソート機能やフィルター機能があるため、毎回数万規模で部門のフィルターで時間がかかってしまうため、最初の20件のみを表示するように変更しました。</p> <p> </p> <h3 id="改善結果">改善結果</h3> <p>上記の改善施策やその他細々した修正をした結果、2秒かからないくらいになりました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20201030/20201030104344.png" alt="f:id:yamamoto5555:20201030104344p:plain" title="f:id:yamamoto5555:20201030104344p:plain" class="hatena-fotolife" itemprop="image" width="553" /></p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>Reduxのshallow equality checkingによって、データ量が多いものをStoreで管理しようとするとかなり時間がかかることが分かりました。サーバーの負荷を減らすためにも、今回はフロント側での処理で対応しました。</p> <p>数万人までのアカウントで利用できるようにするのが目標でしたが、今後10万人近いアカウントにも対応が必要になる可能性もあります。その時は今のやり方では限界がくると思うので、再度修正が必要です(またその時考える)。</p> <p>Reduxのパフォーマンス改善をする中でも、React部分の無駄な再レンダリングなど多々あり、まだまだ改善の余地がありそうです。これからも継続的に、改善していきたいです!</p> <p> </p> <h5 id="補足">補足:</h5> <p>今回が初めてのパフォーマンス改善だったので、チューニングの基礎などが書かれていたWebフロントエンド ハイパフォーマンス チューニングという本を読みました。Reduxのパフォーマンス改善には直接関係していませんが、いろいろなテクニックが書かれており、今後にも開発にも役立ちそうでした。</p> <p> </p> <div class="freezed"> <div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0728K5JZV/hatena-blog-22/"><img src="https://m.media-amazon.com/images/I/51Xdg77GAhL._SL500_.jpg" class="hatena-asin-detail-image" alt="Webフロントエンド ハイパフォーマンス チューニング" title="Webフロントエンド ハイパフォーマンス チューニング" /></a> <div class="hatena-asin-detail-info"> <p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/B0728K5JZV/hatena-blog-22/">Webフロントエンド ハイパフォーマンス チューニング</a></p> <ul> <li><span class="hatena-asin-detail-label">作者:</span><a href="http://d.hatena.ne.jp/keyword/%B5%D7%CA%DD%C5%C4%20%B8%F7%C2%A7" class="keyword">久保田 光則</a></li> <li><span class="hatena-asin-detail-label">発売日:</span> 2017/05/26</li> <li><span class="hatena-asin-detail-label">メディア:</span> Kindle版</li> </ul> </div> <div class="hatena-asin-detail-foot"> </div> </div> </div> <p> </p> <h5 id="参考リンク">参考リンク </h5> <ul> <li><a href="https://recruit-tech.co.jp/blog/2018/09/19/react_spa_performance_tuning/" target="_blank">React製のSPAのパフォーマンスチューニング実例 </a></li> <li><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;"><a href="https://qiita.com/Rui1009/items/b1e181d6c84dfefbadad" target="_blank">ReduxにおけるImmutableの概念についてまとめてみた</a></span></li> <li><a href="https://redux.js.org/faq/immutable-data" target="_blank">Redux FAQ: Immutable Data</a></li> </ul> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> yamamoto5555 絵文字リアクションの開発裏話 hatenablog://entry/26006613620789987 2020-09-14T14:36:22+09:00 2021-02-03T13:30:12+09:00 Web開発チームの山本です。 WowTalkに絵文字リアクション機能がリリースされ、時間が少し立ってしまったのですが、自分の中でも思い出深い機能でもあるので、今回は絵文字リアクション機能の開発裏話を紹介します。 絵文字リアクション機能とは? 絵文字リアクション機能は、トーク上に投稿されたメッセージに対して12種類の絵文字で反応(リアクション)することができる機能です。メッセージに対してリアクションできるため、スタンプよりもカジュアルに使うことができます。詳しくは、こちらから! なぜ絵文字リアクション機能を追加したのか? 日頃から多くのメッセージをWowTalkで行なっているため、スタンプがたく… <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200911/20200911113722.jpg" alt="f:id:yamamoto5555:20200911113722j:plain" title="f:id:yamamoto5555:20200911113722j:plain" class="hatena-fotolife" itemprop="image" /></p> <p><br />Web開発チームの山本です。</p> <p>WowTalkに絵文字リアクション機能がリリースされ、時間が少し立ってしまったのですが、自分の中でも思い出深い機能でもあるので、今回は絵文字リアクション機能の開発裏話を紹介します。</p> <p> </p> <h3>絵文字リアクション機能とは?</h3> <p>絵文字リアクション機能は、トーク上に投稿されたメッセージに対して12種類の絵文字で反応(リアクション)することができる機能です。メッセージに対してリアクションできるため、スタンプよりもカジュアルに使うことができます。詳しくは、<a href="https://www.wowtalk.jp/info/emoji.html" target="”_blank”">こちら</a>から!</p> <p> </p> <h3>なぜ絵文字リアクション機能を追加したのか?</h3> <p>日頃から多くのメッセージをWowTalkで行なっているため、スタンプがたくさん送信された際に、メッセージが流れてしまうという課題を感じていました。また、<a href="https://www.wowtalk.jp/concept.html" target="”_blank”">サービスコンセプト</a>の1つでもある「感情」と絵文字リアクションとの親和性も高く、機能を追加する後押しとなりました。</p> <p> </p> <h3>開発で苦労したこと</h3> <p>機能開発時に大変だったことは、仕様の決定でした。リアクションの種類はもちろんのこと、何個まで選択できるか、リアクションした人を見せるかなど、想像以上に決めることが多く、技術部内でも何度も話し合いをしました。またWowTalkのヘビーユーザーである社内の人の声も聞きたかったので、共有のアンケート機能を使用して、リアクションの種類について社内からも意見を募っていました。面白かったのは、技術部内ではポジティブなリアクションだけでいいのではと話していたのですが、意外と社内の人からはリアクションの感情の幅があった方が良いと言う意見があったことです。確かに現在使用する中で、喜び以外の感情のリアクションを使用することがよくあるなと思っています。</p> <p> </p> <p>個人的には、リアクションの追加・削除・更新などで起こるリアクション数の変更を適切に反映させることに、とても苦労しました。ただ追加するのに1リアクション増やす、削除するごとに1リアクション減らすでは、重複などのデータの矛盾が生じるため、メッセージが持つID(下のコードでいうと"messageId1"と"messageId2"のこと)をkeyにすることで、リアクションにアクセスしやすいようにしました。今回の実装で苦手に感じていた配列やオブジェクトの使い方にだいぶ慣れた気がします。</p> <p> </p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">{ messageId1: { clapping : { users : [ user1, user2, user3 ] }, heart : { users: [ user2, user4 ] }, }, messageId2: { rose : { users : [ user4, user5, user6 ] } } }</pre> <p> </p> <p> </p> <h3>リリース前のハプニング?!</h3> <p>テストも落ち着き、そろそろリリースだとなった時に、絵文字の入れ替えが発生しました。もともとは割れたハートと泣いている顔文字を入れる予定でしたが、上からのお達しで変更することになりました。そこで新しく入れる絵文字になったのが、👌と🌹です。バラは、もともと技術部でメンテナンス時などでお疲れ様という意味を込めて、バラのスタンプを送りあっていました。そのお疲れ様の意味を込めて、今回の絵文字に追加しました。皆さんも、ぜひ相手を労う時にたくさんバラを送ってください!</p> <p> </p> <h3>まとめ</h3> <p>嬉しいことに、社内からも「絵文字リアクション便利!」というポジティブな声を聞くと、頑張って開発して良かったなと思います。まだまだ改善の余地もあるとは思いますが、ユーザーの方にドンドン使っていただけると嬉しいです。これからも更にサービスを進化させられるよう、頑張っていきます!</p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> yamamoto5555 ReduxのStore設計で気をつけたこと hatenablog://entry/26006613554933969 2020-08-24T10:03:11+09:00 2021-02-03T13:30:46+09:00 Web開発チームのフロントを担当している山本です。 今年リリースをした集計機能でReduxを導入しました。集計機能とは、お客様によりWowTalkを使っていただくために、社内の浸透をサポートする機能です(詳しくはこちら)。Redux導入にあたり一番頭を悩ませたのが、Store設計でした。初めての設計ということもあり、いろいろと試行錯誤したので、今回はReduxのStore設計について紹介します。 公式のReduxのStore設計 他のプロジェクトのStoreは? 集計機能のStore構成 まとめ 参考URL: 公式のReduxのStore設計 Reduxのドキュメントでも、複数のデータを取り扱… <p>                 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200821/20200821112754.jpg" alt="f:id:yamamoto5555:20200821112754j:plain" title="f:id:yamamoto5555:20200821112754j:plain" class="hatena-fotolife" itemprop="image" width="468" /></p> <p><br /> </p> <p> Web開発チームのフロントを担当している山本です。</p> <p>今年リリースをした集計機能でReduxを導入しました。集計機能とは、お客様によりWowTalkを使っていただくために、社内の浸透をサポートする機能です(詳しくは<a href="https://www.wowtalk.jp/info/active_userdata.html" target="_blank" rel="noopener noreferrer">こちら</a>)。Redux導入にあたり一番頭を悩ませたのが、Store設計でした。初めての設計ということもあり、いろいろと試行錯誤したので、今回はReduxのStore設計について紹介します。 </p> <p> </p> <ul class="table-of-contents"> <li><a href="#公式のReduxのStore設計">公式のReduxのStore設計</a></li> <li><a href="#他のプロジェクトのStoreは"> 他のプロジェクトのStoreは?</a></li> <li><a href="#集計機能のStore構成">集計機能のStore構成</a></li> <li><a href="#まとめ">まとめ</a><ul> <li><a href="#参考URL">参考URL:</a></li> </ul> </li> </ul> <h3 id="公式のReduxのStore設計">公式のReduxのStore設計</h3> <p><a href="https://redux.js.org/recipes/structuring-reducers/basic-reducer-structure" target="_blank" rel="noopener noreferrer">Reduxのドキュメント</a>でも、複数のデータを取り扱う時の3つのポイントが書かれています。</p> <ul> <li><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">Domain data: アプリケーションが表示、使用、または変更する必要のあるデータ</span></li> </ul> <p><span style="margin-right: 15px;">  例)サーバーから取得するデータ</span></p> <ul> <li><span class="s1">App state: アプリケーション独自の動作データ </span></li> </ul> <p><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">  例)データの選択状態やリクエスト中の状態</span></p> <ul> <li><span class="s1">UI state: </span>現在のUIの表示方法を表すデータ</li> </ul> <p><span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;">  例)モーダルが表示か非表示か</span></p> <p> </p> <p>この3つの分け方は、データを取り扱う時に、明確な指針としてとても助かりました。基本的には上記のことを元に、今回の集計機能のStoreも構成しています。</p> <p> </p> <h3 id="他のプロジェクトのStoreは"> 他のプロジェクトのStoreは?</h3> <p>具体的な例も参考にしたかったので、<a href="https://github.com/gothinkster/react-redux-realworld-example-app">react-redux-realworld-example-app</a>というプロジェクトを見つけました。このプロジェクトでは、Twitterのように自分で簡単なメッセージを投稿できるようになっています。reducersを見てみると、editorやProfileといったパーツごとにreducerを分けており、フラットに整理されていました(プロジェクトの大きさによるかもしれませんが)。またcommon.jsでは、ユーザー情報など共通で管理を行っているデータをまとめています。</p> <p> </p> <p>正直、いろいろな具体的例を参考にしたかったのですが、その当時はなかなか見つからず。。。(私の探し方が悪かったのかな)。今は、 React Developer ToolsでStoreの中を覗けることを知ったのですが、早く知りたかったです!React Developer Toolsの使い方なども書いてあり、こちらの記事がとても参考になりました。私もこれからいろいろなStoreの中を覗いてみたいと思います。</p> <p><iframe class="embed-card embed-webcard" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" title="TwitterやSlackのRedux Storeを覗く" src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fengineer.recruit-lifestyle.co.jp%2Ftechblog%2F2019-12-20-dissecting-redux-store%2F" frameborder="0" scrolling="no"></iframe><cite class="hatena-citation"><a href="https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-20-dissecting-redux-store/"> </a></cite> </p> <h3 id="集計機能のStore構成">集計機能のStore構成</h3> <p>今回の集計機能では、ページごとに表示するデータが異なるので、パーツではなくページごとにStoreを構成しました。それは、今後のことを踏まえても、ページ数をかなり増やすことは考えていなかったこともあり、ページごとにStoreを作ることに踏み切ることが出来ました。 </p> <pre class="code" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;" data-lang="" data-unlink="">-cache |_common -page |_common |_activeUser |_lastActive -ui |_common |_activeUser |_lastActive</pre> <p> </p> <p>上記が考えたStore構成です。基本はドキュメントにあった3つのポイントから構成し、さらにページごとに分けています。ページ関係なく共通で使うものはcommonに入れて管理しています。今のところ、実装する中ではどこでデータを管理しているのかが見つけやすいです。</p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>今回はStoreの設計をしたのですが、どれが正解かは分からない部分が多かったです。集計機能では、ページごとに分けて良かったとは思っていますが、サービスによっては、やはりパーツごとに分けたほうがいいものもあり、一概にはどれがいいとは言えないと思いました。また、これからさらに集計機能を進化させたいので、その中で新たな壁にぶつかりそうな気がしています(既に壁にぶつかったので、その内また違う記事で書きます)。まだまだRedux初心者なので、積極的に他のサービスからも学んで行きたいです!</p> <p>  </p> <h5 id="参考URL">参考URL:</h5> <p><a href="https://blog.tai2.net/real-world-redux.html" target="_blank" rel="noopener noreferrer">React Redux Real World Examples 〜先人から学ぶReact Reduxの知恵〜</a></p> <p><a href="https://github.com/markerikson/redux-ecosystem-links/blob/master/apps-and-examples.md#applications" target="_blank" rel="noopener noreferrer">redux-ecosystem-links/apps-and-examples.md</a></p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> <p> </p> yamamoto5555 カラーユニバーサルデザイン認証を取得した時の開発話 hatenablog://entry/26006613530327347 2020-03-12T13:56:30+09:00 2020-12-23T11:07:35+09:00 Web開発チームのフロントを担当している山本です。 昨年ワウテックは、カラーユニバーサルデザイン(CUD)認証を取得しました。(詳しくはこちらから)より良いサービスを提供できるよう奮闘する中で、CUD認証の取得は新機能とは少し違う新たな取り組みでした。今回、なぜCUDマーク取得を行ったのか、どのような変更があったのかについてご紹介したいと思います。 CUDとは なぜCUD認証を取得したのか 変更点 まとめ CUDとは CUD認証とは、特定非営利活動(NPO)法人カラーユニバーサルデザイン機構が発行を行う、“製品が多くの人にわかりやすい配色であることを保証する第三者認証の証”です。(※1) 人そ… <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20140914/20140914125312.jpg" alt="f:id:yamamoto5555:20140914125312j:plain" title="f:id:yamamoto5555:20140914125312j:plain" class="hatena-fotolife" itemprop="image" /></p> <p>  </p> <p>Web開発チームのフロントを担当している山本です。</p> <p> </p> <p>昨年ワウテックは、カラーユニバーサルデザイン(CUD)認証を取得しました。(詳しくは<a href="https://www.wowtalk.jp/info/release20190808_cud.html" target="_blank">こちら</a>から)より良いサービスを提供できるよう奮闘する中で、CUD認証の取得は新機能とは少し違う新たな取り組みでした。今回、なぜCUDマーク取得を行ったのか、どのような変更があったのかについてご紹介したいと思います。</p> <p> </p> <ul class="table-of-contents"> <li><a href="#CUDとは">CUDとは </a></li> <li><a href="#なぜCUD認証を取得したのか">なぜCUD認証を取得したのか</a></li> <li><a href="#変更点">変更点</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <p> </p> <h3 id="CUDとは">CUDとは </h3> <blockquote> <p>CUD認証とは、特定非営利活動(NPO)法人カラーユニバーサルデザイン機構が発行を行う、“製品が多くの人にわかりやすい配色であることを保証する第三者認証の証”です。(<a href="http://www2.cudo.jp/wp/?page_id=43" target="_blank">※1</a>)</p> </blockquote> <p>人それぞれ色の伝わり方が異なるため、正しく情報が伝わるよう色の使い方に配慮しようという取り組みです。私が赤色に見えていても黒っぽく見えてしまう人もいるため、みんなにとって、より分かりやすい配色を選択していきます。</p> <p> </p> <h3 id="なぜCUD認証を取得したのか"><strong>なぜCUD認証を取得したのか</strong></h3> <p>弊社では「言語」「距離・時間」「個性」「温度・感情」「経験」という5つの視点からサービスの開発や新機能の追加を行なっています。その中の「個性」から色の伝わり方が違うことで生じるコミュニケーションの課題解決に取り組むことにしました。もともと、私が入社前の面接時より、代表の瀬沼からカラーユニバーサルの話を聞いており、会社としても取り組んでいきたいことだと前々から私も認識していました。</p> <p> </p> <h3 id="変更点">変更点</h3> <p>では、実際にCUD認証を取得するにあたっての変更した箇所をいくつかご紹介したいと思います。</p> <p> </p> <p><strong>1. 赤文字からオレンジへ</strong></p> <p>ワウトークで使用していた赤が暗めで黒と同じように見えてしまうため、「注意を集める、強調する、違いを表現できていない」という指摘がありました。改善案としては、明るいオレンジよりの色に変更することでした。ただ赤文字はあらゆる所で使っていたため、全てを変更するのがとても大変でした。ただ色を変更すればいいという訳ではなく、動的に色を変更しているところもあるため(例えば、クリックすると色が変化するなど)、見落としがないようにと神経を使った気がします。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200311/20200311094711.jpg" alt="f:id:yamamoto5555:20200311094711j:plain" title="f:id:yamamoto5555:20200311094711j:plain" class="hatena-fotolife" itemprop="image" width="492" /></p> <p> </p> <p><strong>2.グラフの色</strong></p> <p> 共有機能では、アンケートを取ることができ、その結果はグラフで表示されます。そのアンケートのグラフで使用している色が差が少ないということでいくつか変更しました。それぞれの項目に白縁をつけたり、グラフの項目をクリックすると情報が表示されるなど、もともと対応していたところもあり、全ての色を変更する必要はありませんでした。ただ僅かな色の変更なので、私も変更するときに色の差異を認識することが難しかったです。</p> <p> </p> <p><span style="text-decoration: underline;"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200311/20200311095241.png" alt="f:id:yamamoto5555:20200311095241p:plain" title="f:id:yamamoto5555:20200311095241p:plain" class="hatena-fotolife" itemprop="image" width="485" /></span></p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>正直CUD認証を取得にあたって、「色の変更が多くなるのでは」、「現在の製品とかなり違う色を提案されるのでは」と不安もありました。嬉しいことに、ワウトークの強みでもあるシンプルさが使用している色にも出ており、改善箇所の指摘も多くなかった気がします。意外と文字の色をオレンジに変更しても、私の中では違和感もあまりなく、今では馴染んでいるように感じます。</p> <p> </p> <p>今回のCUD認証の取得から、色自体を認識できないというのではなく、背景色との色の差が少ないため分かりずらさが生じたり、強調するなどの色の役割ができていなかったことを知りました。デザイン性だけではなく、新たな色に関する視点を意識するようになったので、今後の開発にも活かしていきます!</p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> <p> </p> yamamoto5555 駆け出しエンジニア1年目で学んだ3つのこと hatenablog://entry/26006613504093033 2020-02-05T09:19:49+09:00 2020-12-23T11:07:14+09:00 Web開発チームのフロントを担当している山本です。 最近では技術開発部の仲間も着々と増え、再来年入社の新卒採用まで始まりました。基本的に技術開発部は実務経験のある方が主なのですが、私は実務未経験からの採用でした。エンジニアを目指している人やエンジニアとして働き始めた人たちがエンジニアとして働くイメージが膨らむよう、エンジニア1年目で得た3つの学びを紹介したいと思います。 学び1: ReactとReduxの技術面での挑戦 学び2: 機能(サービス)リリースするまでの見積もりの大切さ 学び3: 仕様の決定からデザインについて考えるようになる まとめ 学び1: ReactとReduxの技術面での挑戦… <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yamamoto5555/20200203/20200203135617.jpg" alt="f:id:yamamoto5555:20200203135617j:plain" title="f:id:yamamoto5555:20200203135617j:plain" class="hatena-fotolife" itemprop="image" /></p> <p> </p> <p>Web開発チームのフロントを担当している山本です。 </p> <p>最近では技術開発部の仲間も着々と増え、再来年入社の新卒採用まで始まりました。基本的に技術開発部は実務経験のある方が主なのですが、私は実務未経験からの採用でした。エンジニアを目指している人やエンジニアとして働き始めた人たちがエンジニアとして働くイメージが膨らむよう、エンジニア1年目で得た3つの学びを紹介したいと思います。</p> <p> </p> <ul class="table-of-contents"> <li><a href="#学び1-ReactとReduxの技術面での挑戦">学び1: ReactとReduxの技術面での挑戦</a></li> <li><a href="#学び2-機能サービスリリースするまでの見積もりの大切さ">学び2: 機能(サービス)リリースするまでの見積もりの大切さ</a></li> <li><a href="#学び3-仕様の決定からデザインについて考えるようになる">学び3: 仕様の決定からデザインについて考えるようになる</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <p> </p> <h3 id="学び1-ReactとReduxの技術面での挑戦">学び1: ReactとReduxの技術面での挑戦</h3> <p>Reactでの開発時にデータ管理の複雑さが増していき、<a href="https://redux.js.org/" target="_blank">Redux</a>を取り入れようという話になりました。それからWeb開発チームでも定期的にRedux勉強会を開催されるようになりました。毎回テーマを決め、各自で事前に予習をし、勉強会でお互いに学んだことを共有するスタイルで行っています。Reduxを勉強する中で、Reactの復習にもなり、初期の頃より理解できることが増えてきたのを実感できたのも嬉しかったです。</p> <p> </p> <h3 id="学び2-機能サービスリリースするまでの見積もりの大切さ">学び2: 機能(サービス)リリースするまでの見積もりの大切さ</h3> <p>機能追加などを行う中で、現在の自分の力を考慮し、実装にかかる時間を正確に見積もることが大切だとよく上司から言われます。無限に時間があればいいのですが、そういうわけにはいきません。機能実装の時に必要な作業を書き出し、それに対してかかる作業時間を見積もるようにしています。数時間で終わるだろうと思っていても実際に取り掛かると手間取り、見積もりと差が出ることがよくありました。また作業時間の見積もりは正確だとしても他の作業が発生することもあるため、それも事前に想定しながら見積もることが必要です。見積もり時間が短いからいいというわけではなく、自分の力量を知るために自分と向き合う時間でもあると思っています。</p> <p>今は<a href="https://app.instagantt.com" target="_blank">Instagantt</a>でスケジュール管理をしており、タスク状況を視覚的に把握できるので便利です。他にもオススメのツールがあれば教えて欲しいです。</p> <p> </p> <h3 id="学び3-仕様の決定からデザインについて考えるようになる">学び3: 仕様の決定からデザインについて考えるようになる</h3> <p>初めて機能追加する時に、仕様の可能性が無限大だということに気がつきました。ユーザーが使う時のことを考えながら、開発期間内でできることを決めていく必要があります。既存の機能のことも考慮しないといけないため、色々なケースを想定する必要があり、何度も話し合いが必要なこともあります。</p> <p> </p> <p>また私の場合デザイナーにデザイン依頼する前に、自分が実現したいものを伝えるため、ワイヤーフレームを作成します。大まかな配置も伝えたいので、色々と調べたり部内メンバーからもフィードバックをもらうことがあります。ワイヤーフレーム自体は、HTMLやCSSでコードを書きながら調整したり、既にあるコンテンツをつなげながら作っています。ワイヤフレームを作るようになり、他のサイトのレイアウトなども意識するようになりました。</p> <p> </p> <h3 id="まとめ">まとめ</h3> <p>以前のエンジニアに対して、コーディングだけが仕事内容だというイメージがあったのですが、仕様決めからスケジューリングなどコードを書く上で必要な作業があることも知りました。また未経験でも勉強すればできるようになることも増えると実感し、今後も学び続けたいと思います。 </p> <p>昨年は目の前のことで精一杯でしたが、今年はES6などの基礎部分を固めたり、もう少しバックエンドのことにも挑戦したりすることで、自分でできることの幅を増やしたいです。</p> <p> </p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> <p> </p> yamamoto5555 ペアプログラミングを試してみた hatenablog://entry/26006613489669245 2020-01-09T18:16:54+09:00 2020-01-09T18:16:54+09:00 どうもこんにちは!web開発チームのしまねです。最近web開発チームでペアプログラミングに取り組んでみました。取り組んだ開発はまだリリースまでいっていませんが、どのように取り組んだのか・やってみた感想などを振り返ってみたいと思います。 目的 使ったツール やり方 いざ実践 まとめ 目的 目的としては大きく3つありました。 目的① レビューが十分にできていない状況を変えたい コードレビューの依頼は来るのですが自分の開発業務もあるため、十分にできていないなぁという思いを少し前から抱えていました。例えばある機能を実装するとしてそれに関するコードをすべてレビューするとなると、まとまった時間が必要となる… <p>どうもこんにちは!web開発チームのしまねです。<br />最近web開発チームでペアプログラミングに取り組んでみました。<br />取り組んだ開発はまだリリースまでいっていませんが、どのように取り組んだのか・やってみた感想などを振り返ってみたいと思います。</p> <p> </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></li> </ul> <h3 id="目的">目的</h3> <p>目的としては大きく3つありました。</p> <p><strong>目的① レビューが十分にできていない状況を変えたい</strong></p> <p> コードレビューの依頼は来るのですが自分の開発業務もあるため、十分にできていないなぁという思いを少し前から抱えていました。例えばある機能を実装するとしてそれに関するコードをすべてレビューするとなると、まとまった時間が必要となるためついつい後回しにしてしまっていました。そして結局十分な時間がとれず深く見れずに終わってしまうという。。<br />  ペアプログラミングで開発すればレビューせざるを得ないためレビュー不足な状況を変えることができるのではないかと考えました。</p> <p><strong>目的② 新卒が入った時に研修で使えるかを検証したい</strong></p> <p> ワウテックでは新卒採用を始めていて近い将来新卒が入社します。新卒の研修の一環としてペアプログラミングはいいんじゃないかなと思っていたので、その効果を検証する必要がありました。</p> <p><strong>目的③ テスト時のバグ改修の工数を減らしたい</strong></p> <p> 開発の工数は見積もりやすいのですが、テストに出したあとのバグ改修はバグの数によって工数が増減するため見積もりにくくスケジュールが押してしまうことがありました。<br />  もしもテスト提出の時点で極力バグを少なくできればテスト工数の振れ幅も小さくなり見積もりやすくなるはずです。<br />  2人で1人分のコードしか書けないので開発スピードは遅くなりますが、2人で確認しながら進めるのでバグが減ってテスト時のバグ改修の工数では短くなるのではないかという期待がありました。</p> <p> </p> <h3 id="使ったツール">使ったツール</h3> <ul> <li>Live Share</li> </ul> <p><a href="https://visualstudio.microsoft.com/ja/services/live-share/" target="_blank">https://visualstudio.microsoft.com/ja/services/live-share/</a></p> <p> Live Shareというプラグインを使えばVisual Studio Codeでソースコードを複数人で編集できるようになります。<br /> 誰か1人がホストになり共同編集する人を招待し、招待された人はホストのファイルを編集できるようになります。<br /> 他の人がどこを編集しているのかはカーソルで表示されてわかりやすかったです。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="VScodeのLive Shareでファイルを共同編集している様子"> <p><img class="hatena-fotolife" title="f:id:wt-shimane:20200107102151p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20200107/20200107102151.png" alt="f:id:wt-shimane:20200107102151p:plain" /></p> <figcaption>VScodeのLive Shareでファイルを共同編集している様子</figcaption> </figure> <p> 招待したり招待されたホストに接続する際にMicrosoftアカウントかGitHubアカウントが必要です。</p> <p> </p> <h3 id="やり方">やり方</h3> <p> 調べてみるとペアの中でドライバーとナビゲーターという役割に分ける方法があったので参考にしました。<br /> 参考サイト: ペアプログラミングのやりかた<br /> <a href="https://qiita.com/kanatatsu64/items/3f04d0116e22392efaca" target="_blank">https://qiita.com/kanatatsu64/items/3f04d0116e22392efaca</a></p> <p><br /><strong>ナビゲーターの役割</strong><br />・プログラムの構成を考える<br />・テストを書く<br />・ドキュメントを書く<br />・ドライバーの疑問をググる<br />・ドライバーが書いたプログラムをレビューする</p> <p><strong>ドライバーの役割</strong><br />・コードを書く</p> <p>その他にはルールとして次のことを決めました。<br />・開始時間と終了時間とどこまでやるかを決める<br />・1時間を目安に休憩</p> <p> </p> <h3 id="いざ実践">いざ実践</h3> <p> 今回は僕がナビゲーターでペアのメンバーがドライバーで進めていくことにしました。<br /> まずナビゲーターの僕がコーディングの前に大まかな作りを説明し、処理をコメントで書いていきました。<br /> この間はドライバーはやることがなくなってしまうので最初だけでもこの作業はあらかじめやっておくべきでした。<br /> キリの良いところまで処理を書いてドライバーにコーディングを始めてもらいました。<br />目の前でどんどんコードが書かれていくのが面白くてしばらく見ていたのですが、「いけない、いけない。次の処理のコメント書かなくちゃ」ということでいったん次に実装予定の処理をコメントでどんどん書いていきました。<br /> その後はドライバーの書いたコードを見たり、疑問点に答えたり、次の実装を書いたりと進めていって最初に目標にしていた部分まで実装してその日は終わりにしました。<br />1回やってみた時点で、2人とも他の業務もあり今回ペアプログラミングを行ったプロジェクトだけやるわけにはいかないので週に何回か2~3時間くらいでやってみよう、また、ペアプログラミングをする部分はそのプロジェクトのコアになるようなロジックの部分にしてcssの設定等の見た目の部分は1人で進めていこうということになりました。</p> <p> </p> <p>その後何度かやってみて・・・</p> <p> </p> <ul> <li>ペアプログラミングした部分のソースコードについては考え方まで理解しているのでレビューする必要が全くなかった。</li> <li>ナビゲーターはあまりやることがないのではないかと思っていたがドライバーとのやり取りが多く発生して想定していたよりも忙しかった。</li> <li>1時間を目安に休憩と決めていたがついつい進めてしまって休憩を取りそこねることが多かった。</li> <li>どこまでやるかを毎回最初に決めておいてよかった。</li> </ul> <p> というような感想をもちました。</p> <p> </p> <h3 id="まとめ">まとめ</h3> <p><strong>結論① ペアプログラミングをする上でLive Shareはすごく便利</strong></p> <p> 1つのPCではなくそれぞれのPCでできるようになるのでナビゲーター側の自由度が高くなります。昔は1つのPCに2人がはりついてやっていたそうですが、それだとお互いにかなりストレスになると思います。</p> <p><strong>結論② どこまでやるかを毎回最初に決めておくことが重要</strong><br /> ペアプログラミングをしていると1人でやるより疲れるので予めゴールを設定しておくと気が楽です。気が楽なだけですが続けていくには重要なことです。</p> <p><strong>結論③ 改めてコードレビューの時間をとる必要はない</strong><br /> ペアプログラミングを行った部分に関してはドライバー側が書いたコードもナビゲーター側が設計しているし、議論しながら進めているためコードを書き終わった時点でもうレビューはしなくてもいい状態になっています。</p> <p><strong>結論④ 新卒の研修としてはとてもアリ</strong><br /> マンツーマンでしっかりコードの書き方から見ることができるので新卒の研修時には非常に有用だと思いました。研修の際には逆にナビゲーターをやってもらってプログラムの構成を考えてもらうのもありかもしれません。</p> <p> 目的③についてはまだテストまで到達していないので検証できていないです。。<br /> ただ現時点の感想では、仮にペアプログラミングで開発した方が工数がかかったとしてもメンバー間の知識の共有や議論して進めることでの品質のアップと他に得られるものがあると思います。</p> <p> どうやってコードを書くのかについては体系的に説明するのは難しいため、書いているところを見てもらうのが一番早いです。まだまだ慣れませんがもうしばらく続けてみたいです。<br /> 今回はやりませんでしたがLiveShareを使えばリモートワーク中でもペアプログラミングできるかもしれないので近い将来チャレンジしてみたいです。</p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> <p> </p> wt-shimane 2020年もワウテックをよろしくお願いします hatenablog://entry/26006613494789251 2020-01-08T16:49:35+09:00 2020-01-08T16:49:35+09:00 新年あけましておめでとうございます。アプリ開発チームの岡野です。 今年もよろしくお願いします。 今年で2回目ですが、ワウテックでは毎年年明けに全社で神田明神に参拝に行っています。 9時に神田明神に到着。去年も天気がよくなった気がしますが、残念ながら今年も雨でした(2敗) 待ち時間の間にお守りを購入。我らが技術開発部部長(自分の上司)は、今年もIT情報安全祈願のお守りを買って早速会社のデスクに飾っていました。去年はこれのおかげでほぼ半年間重大な障害なく過ごせました。今年もご利益があるよう願います。 個人的には今年も金運のお守りを買いました。宝くじが当たりますように。。。(_人_) 上司の机に飾ら… <p>新年あけましておめでとうございます。アプリ開発チームの岡野です。</p> <p>今年もよろしくお願いします。</p> <p> </p> <p>今年で2回目ですが、ワウテックでは毎年年明けに全社で神田明神に参拝に行っています。</p> <p>9時に神田明神に到着。去年も天気がよくなった気がしますが、残念ながら今年も雨でした(2敗)</p> <p><img class="hatena-fotolife" title="f:id:okano4413:20200108111921j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20200108/20200108111921.jpg" alt="f:id:okano4413:20200108111921j:plain" /></p> <p> </p> <p>待ち時間の間にお守りを購入。我らが技術開発部部長(自分の上司)は、今年もIT情報安全祈願のお守りを買って早速会社のデスクに飾っていました。去年はこれのおかげでほぼ半年間重大な障害なく過ごせました。今年もご利益があるよう願います。</p> <p> </p> <p>個人的には今年も金運のお守りを買いました。宝くじが当たりますように。。。(_人_)</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="IT情報安全祈願"> <p><img class="hatena-fotolife" title="f:id:okano4413:20200108122037j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20200108/20200108122037.jpg" alt="f:id:okano4413:20200108122037j:plain" /></p> <figcaption>上司の机に飾られてるIT情報安全祈願</figcaption> </figure> <p>去年は文化交流館という箇所の2階で参拝しましたが、今年は御神殿で参拝できました。(去年は混雑日だったため代表者以外は文化交流館2階で参拝という形だったようです)</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="神田明神境内図"> <p><img class="hatena-fotolife" title="f:id:okano4413:20200108113215j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20200108/20200108113215.jpg" alt="f:id:okano4413:20200108113215j:plain" /></p> <figcaption>神田明神境内図</figcaption> </figure> <p><img class="hatena-fotolife" title="f:id:okano4413:20200108120828j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20200108/20200108120828.jpg" alt="f:id:okano4413:20200108120828j:plain" /></p> <p>会社として参拝して神社の方に商売繁盛の祈願をしてもらう間参列している会社の名前が呼ばれるのですがIT系の企業名が多く呼ばれていました。IT時代を感じますね。</p> <p>混雑日を避けたことや去年よりも開始時間が早いこともあってか人は比較的少なく、スムーズに参拝は終了しました。</p> <p> </p> <p>解散後、去年買ったお守りをお焚き上げ所に渡し、今年一年の運勢を占うべくおみくじを購入しました。</p> <p>結果は</p> <p>・</p> <p>・</p> <p>・</p> <p>・</p> <p>大吉でした :D</p> <p>うまくいかないことがあってもおおらかな心で構えていろというありがたいお言葉。</p> <p>忘れないように持って返って飾っておくことにします。</p> <p><img class="hatena-fotolife" style="width: 200px;" title="f:id:okano4413:20200108113750j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20200108/20200108113750.jpg" alt="f:id:okano4413:20200108113750j:plain" /></p> <p> </p> <p> </p> <p>今年もきっと慌しくなるでしょうが、公私共に最高な1年にできるように頑張りたいと思います。</p> <p>今年もワウテックをよろしくお願いします。</p> <p> </p> <h3>参考リンク</h3> <p>神田明神</p> <p><a href="https://www.kandamyoujin.or.jp/">https://www.kandamyoujin.or.jp/</a></p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> okano4413 大きなActivityは小さく分割したほうがいいお話 hatenablog://entry/26006613475020317 2019-12-05T12:22:19+09:00 2019-12-05T14:29:57+09:00 こんにちは。ワウテックAndroidアプリ担当の岡野です。 今回は珍しくAndroidに関する投稿です。 WowTalkには様々な機能がありますが、私が着任当時に実装した機能がタスク管理機能でした。当時はiOS担当だったため、Androidの実装は当時の担当の方がしてくれた(※1)のですが、最近不定期に機能改善をする際にAndroidのタスク管理機能のメンテナンスに苦心することが多かったので思い切ってアーキテクチャの変更に取り組みました。 1 タスク管理機能の概要 WowTalkでは作業内容をタスクとして管理する機能があり、テキスト情報に加えて画像やドキュメントファイルを添付し、タスクの担当者… <p>こんにちは。ワウテックAndroidアプリ担当の岡野です。</p> <p>今回は珍しくAndroidに関する投稿です。<br /><br /></p> <p>WowTalkには様々な機能がありますが、私が着任当時に実装した機能がタスク管理機能でした。当時はiOS担当だったため、Androidの実装は当時の担当の方がしてくれた(<a href="#1"><strong>※1</strong></a>)のですが、最近不定期に機能改善をする際にAndroidのタスク管理機能のメンテナンスに苦心することが多かったので思い切ってアーキテクチャの変更に取り組みました。</p> <h3>1 タスク管理機能の概要</h3> <p>WowTalkでは作業内容をタスクとして管理する機能があり、テキスト情報に加えて画像やドキュメントファイルを添付し、タスクの担当者・関係者間で共有できる仕組みです。タスクには期限が設定でき、期限切れ当日には通知が届くようにもなっています。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="自分はもっぱら個人のタスク管理に使うことが多い"> <p><img class="hatena-fotolife" title="f:id:okano4413:20191204135117p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191204/20191204135117.png" alt="f:id:okano4413:20191204135117p:plain" width="70%" /></p> <figcaption>自分はもっぱら個人のタスク管理に使うことが多い</figcaption> </figure> <p>このタスク管理機能は大きく3種類のActivityから構成されています。</p> <table> <tbody> <tr> <th>リスト画面</th> <td>タスクの一覧を表示する画面</td> </tr> <tr> <th>作成画面</th> <td>タスクを作成する画面</td> </tr> <tr> <th>詳細/編集画面</th> <td>作成したタスクの詳細を確認し、編集する画面</td> </tr> </tbody> </table> <p> </p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="タスク管理機能。詳細画面と編集画面は1つのActivityで実装されていた"> <p><img class="hatena-fotolife" title="f:id:okano4413:20191203153350p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191203/20191203153350.png" alt="f:id:okano4413:20191203153350p:plain" /></p> <figcaption>タスク管理機能。詳細画面と編集画面は1つのActivityで実装されていた</figcaption> </figure> <p>それぞれ独立したActivityで実装されており、サーバーからのデータ取得リクエスト呼び出しや描画に伴うデータ加工などもそれぞれのActivityで実装されていました。</p> <h3>2. 実装の問題点</h3> <p>上述したとおり複数の役割を1つのActivityで実装したため、多いものではソースコードの行数が2800行近くにまでなりました。多いと見るかは個人差があるかもしれませんが、個人的にはかなり苦労するレベルです。またメンテナンスも忘れた頃にするので、毎回作業時に「これなにしてるんだっけ...」と処理を見返すことが何度もあり、作業効率があまりに悪いということもありました。</p> <h3>3. 改善方針</h3> <p>今回はタスクの詳細・編集画面を改修することにし、下記の2つを実施することにしました。</p> <h5>画面ごとにActivityを切り分ける</h5> <p>タスクの詳細画面と編集画面は類似したインターフェース及びデータ構造のため、1つのクラスで実装されていましたが、それぞれ表示の仕方が変わるため、モードの切り替え時に逐次ViewのVisibilityを変更していました。</p> <p>これらを個別のActivityに分割することで状態の管理などを気にしなくて済むような実装にしていきます。 </p> <h5>データとViewの操作をActivityから切り分ける(MVVMアーキテクチャ)</h5> <p>合わせてデータ操作とViewの状態管理も切り分けて、それぞれが依存しないようにしていきます。今回はGoogleがAndroidアプリ実装時の推奨アーキテクチャとしているMVVM(<a href="#2"><strong>※2</strong></a>)を採用することにしました。</p> <p>簡単にMVVMについて書くと、データの取得/送信部分をModel(M)クラスに任せ、ViewModel(VM)はModelからデータを受け取り、View(V)が使いやすい形でデータを提供します(<a href="#3"><strong>※3</strong></a>)。</p> <p>今回2つの画面のデータ取得ロジックがほぼ同じため、下記の様にModelを共通化した形で変更を加えることにしました。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="1つの大きなActivityを画面と処理の種類で分割する"> <p><img class="hatena-fotolife" title="f:id:okano4413:20191204141623p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191204/20191204141623.png" alt="f:id:okano4413:20191204141623p:plain" /></p> <figcaption>1つの大きなActivityを画面と処理の種類で分割する</figcaption> </figure> <h3>4. 改善の効果</h3> <p>画面ごとにActivityを切り分けた上、MVVMにすることで1クラスあたりのソースコード量が減り、かなり見やすくなりました。合わせて以下の様な効果がありました。</p> <p><strong>・現在の状態を意識する必要がなくなった</strong></p> <p><strong>・クラス内部の変数が減り、各変数への依存関係も減ったためロジックの構築がシンプルになった</strong></p> <p><strong>・表示のための"ビューロジック"と"実際の表示処理"が個々のクラス内で完結しているため、実装時に頭の中で同時に考える負担が減り実装が早くなった</strong></p> <p>1つ目と2つ目は書いている通りで、今の画面が詳細画面なのか編集画面なのかを意識する必要もなく、またそれぞれの状態で利用していた変数を個別のClassで管理する様になったので、条件分岐などの記述が減り1つの関数の行数もだいぶ削減されました。</p> <p> </p> <p>3つ目に関しては実際に経験があるとイメージしやすいのですが、1つのActivity内でデータの取得から表示までの一連の流れを書く場合</p> <p><span style="color: #dd830c;">①バックグランドでデータの取得</span></p> <p><span style="color: #dd830c;">②受け取ったデータをメインスレッド(Handler)にポストする</span></p> <p><span style="color: #dd830c;">③メインスレッド内で変数への保存を行いデータの表示処理を行う</span></p> <p>みたいなことをするわけですが、一連の流れを描く際に1つで今回取り扱うデータとは関係ないデータを取り扱う処理(例えば描画や別の処理のエラーハンドリング関数)をかき分けながらHandlerを探すなんてことがよくあります。</p> <p>これが無くなるだけでもかなり実装時の負担は低減されました。 </p> <h3>5. 気づいたこと</h3> <p>今回実装していて思ったのがViewModelが思ったよりも肥大化したことでした。実装にあたりLiveData(<a href="#4"><strong>※4</strong></a>)を利用した値の自動更新を採用したのですが、これとビューロジックを一度に実装するとそこそこな分量になりました(1000行程)</p> <p>また通常ModelはSingleton(<a href="#5"><strong>※5</strong></a>)で実装してDBやサーバーから取得したデータをキャッシュすることを推奨(<a href="#6"><strong>※6</strong></a>)されているのですが、別のタスクを開いたはずが直前のタスクの情報が表示されてるなんてこともありました。ActivityとModelは依存関係を排除しているとはいえ、用途は想定して実装しないといけませんね。</p> <p> </p> <h3>6. 終わりに</h3> <p>今回はタスク管理機能のFatActivityをビューロジックの切り分けと整理によって2800行あったソースコードを200~1000行のクラスに分割し、保守性を改善しました。今後はこの画面以外でも同様の問題を抱えている箇所を1つずつ改善していければと思います。</p> <p> </p> <p>当たり障りのない内容でしたが、基本的なことの大切さを実感します。社内ではこういった既存実装が抱える問題を「負の遺産」と呼んでいますが、まだまだ負の遺産はありますので1つずつ返済していきたいと思います。</p> <p> </p> <h3><strong>7. 注釈</strong></h3> <p id="1">※1 : <strong>Androidの実装は当時の担当の方がしてくれた</strong></p> <p>決して前任者のソースコードがわかりにくいというわけではないです。</p> <p id="2">※2 : <strong>アプリのアーキテクチャガイド</strong></p> <p><a href="https://developer.android.com/jetpack/docs/guide?hl=ja#cache-data">https://developer.android.com/jetpack/docs/guide?hl=ja#cache-data</a></p> <p id="3">※3 :<strong> MVCと何が違うのか</strong></p> <blockquote> <p>いわゆる昔のModel-View-Controller(MVC)と同じに聞こえるかもしれませんが、個人的に思うControllerとViewModelの大きな違いはまさに<strong>View</strong>の<strong>Model</strong>である点です。</p> <p>画面で表示する際、仕様によって表現を変える必要が出てくる場合があります。例えばサーバー側ではLong型の UnixTimeで保存し、アプリ側ではこれを「<strong>2019-11-29</strong>」と表示する場合もあれば「<strong>明日</strong>」「<strong>今日</strong>」の様な表現にする場合もあります。</p> <p>これらの表示の上での計算(=ビューロジック)は、MVCにおいてControllerで記述することはせず、Viewで加工することになります。一方MVVMではこのビューロジックをViewではなくViewModelに担当させることでViewは複雑な処理を記述する必要がなく、表示だけに集中できます。</p> </blockquote> <p> </p> <p id="4">※4 :<strong> LiveDataの概要 </strong></p> <p> <a href="https://developer.android.com/topic/libraries/architecture/livedata?hl=ja">https://developer.android.com/topic/libraries/architecture/livedata?hl=ja</a></p> <p id="5">※5 :<strong> デザインパターン シングルトン</strong></p> <p><a href="https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1">https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1</a></p> <p id="6">※6 : <strong>Androidアプリ設計パターン入門</strong></p> <p><a href="https://peaks.cc/books/architecture_patterns">https://peaks.cc/books/architecture_patterns</a></p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> okano4413 1週間で作って社内リリースしたヘルプボットを性能改善した話 hatenablog://entry/26006613426123479 2019-10-04T10:29:20+09:00 2019-12-05T14:29:43+09:00 お久しぶりです。WowTalkのAndroid開発担当している岡野です。 前回1週間でヘルプボット作って社内リリースした話を書きました。 その際にいくつか課題が出ましたので、今回は前回ほど専門的な内容ではありませんが、課題解決までのアプローチを書いていきたいと思います。 1.前回の課題点 前回作成したボットですが、欠点として単一のキーワードを送った時した回答できないという課題がありました。通常ユーザーがボットに質問する時には適当な長さの文章を送ってくると考えると使い物にならないですね。 この課題を解決するため下記の流れでアプローチしていきました。 文章の形態素解析によるキーワード抽出 形態素解… <p>お久しぶりです。WowTalkのAndroid開発担当している岡野です。</p> <p class="p1" style="font-size: 14px; line-height: 1.8em; color: #454545; font-family: 'Helvetica Neue', Helvetica, Arial, 'Hiragino Kaku Gothic Pro', Meiryo, 'MS PGothic'; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-style: initial; text-decoration-color: initial;">前回<a href="https://engineer.wowtech.co.jp/entry/2019/07/24/104412" target="_blank">1週間でヘルプボット作って社内リリースした話</a>を書きました。</p> <p class="p1" style="font-size: 14px; line-height: 1.8em; color: #454545; font-family: 'Helvetica Neue', Helvetica, Arial, 'Hiragino Kaku Gothic Pro', Meiryo, 'MS PGothic'; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: #ffffff; text-decoration-style: initial; text-decoration-color: initial;">その際にいくつか課題が出ましたので、今回は前回ほど専門的な内容ではありませんが、課題解決までのアプローチを書いていきたいと思います。</p> <h3>1.前回の課題点</h3> <p>前回作成したボットですが、欠点として単一のキーワードを送った時した回答できないという課題がありました。通常ユーザーがボットに質問する時には適当な長さの文章を送ってくると考えると使い物にならないですね。</p> <p>この課題を解決するため下記の流れでアプローチしていきました。</p> <ol> <li><em>文章の形態素解析によるキーワード抽出</em></li> <li><em>形態素解析サービス/ツールの選定</em></li> <li><em>記事の検索方法の変更 </em></li> </ol> <p>1つずつ見ていきましょう。</p> <h3>2.文章の形態素解析によるキーワード抽出</h3> <p>形態素解析とはなんぞやというと方のためにお話しすると、要は文章を意味のある単語単位(形態素)で分割し、それぞれの品詞などを解析する作業です。</p> <table> <tbody> <tr> <th>例文</th> <td>Androidでログインができない</td> </tr> <tr> <th>形態素で分割</th> <td>Android/で/ログイン/が/でき/ない</td> </tr> <tr> <th>品詞の抽出 </th> <td>名詞 / 助詞 / 名詞 /助詞 /動詞 / 助動詞</td> </tr> </tbody> </table> <p>この中で意味のある単語=キーワードは「Android」「ログイン」= 名詞となることはわかると思います。あとはこのキーワードをうまくユーザガイドの検索ワードとして投げてあげるだけ。</p> <h3>3.形態素解析サービス/ツールの選定</h3> <p>SaaSサービス信者になりつつある自分はAWSにも形態素解析できるサービスがあるのではと探してみると<strong><a href="https://aws.amazon.com/jp/comprehend/" target="_blank">Amazon Comprehend</a></strong>というサービスがありました。</p> <p>やったぜ(*´-`)...と思ったけどこれ、<a href="https://docs.aws.amazon.com/ja_jp/comprehend/latest/dg/supported-languages.html" target="_blank">日本語に対応していなかった</a>...orz</p> <p>他にないかなと探してGoogle Cloud Platform(GCP)にも<strong><a href="https://cloud.google.com/natural-language/?hl=ja" target="_blank">Cloud Natural Language API</a></strong>という似たようなサービスがあるそうです。</p> <p>Googleさんなら日本語にも対応しているし安心かな。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="Cloud Natural Language Apiでの解析"> <p><img class="hatena-fotolife" style="border: solid 1px lightgray;" title="f:id:okano4413:20191003114754p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191003/20191003114754.png" alt="f:id:okano4413:20191003114754p:plain" /></p> <figcaption>Cloud Natural Language Apiでの解析</figcaption> </figure> <p>ちゃんと日本語も解析できてるようです。</p> <p>あとはコスト面が気になるので確認することにしました。<a href="https://cloud.google.com/natural-language/pricing?hl=ja" target="_blank">料金のページ</a>を見ると構文解析を含むNatural Language Apiはユニット単位の課金らしいです。(以下抜粋)</p> <p> </p> <blockquote> <p style="box-sizing: inherit; margin: 16px 0px; padding: 0px; color: #212121; font-family: Roboto, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Natural Language API の使用量は「ユニット」の数に基づいて計算されます。分析のためにこの API に送信される各<a href="https://cloud.google.com/natural-language/docs/reference/rest/v1beta2/documents?hl=ja" style="box-sizing: inherit; color: #1a73e8; outline: 0px; text-decoration: none;" target="_blank">ドキュメント</a>のユニット数は、少なくとも 1 ユニットとなります。ドキュメントに 1,000 文字を超える Unicode 文字が含まれている場合(空白文字や、HTML / XML タグなどのマークアップ文字も含まれます)、1,000 文字 = 1 ユニットの複数ユニットとしてカウントされます。</p> <p style="box-sizing: inherit; margin: 16px 0px; padding: 0px; color: #212121; font-family: Roboto, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">たとえば、Natural Language API に送信する 3 件のリクエストそれぞれに、800 文字、1,500 文字、600 文字が含まれている場合、1 番目のリクエスト(800)が 1 ユニット、2 番目のリクエスト(1,500)が 2 ユニット、3 番目のリクエスト(600)が 1 ユニット、合計 4 ユニットとして課金されます。</p> <p style="box-sizing: inherit; margin: 16px 0px; padding: 0px; color: #212121; font-family: Roboto, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Natural Language API の使用料は、使用された API の機能とその機能を使用して評価されたユニットの数に基づいて、月単位で計算されます。次の表に、請求月に分析されたユニットの総数に基づく 1,000 ユニットあたりの料金を示します。</p> </blockquote> <p>つまり1000文字ごとに1ユニットが消費され、最低でも1回のリクエストで1ユニットが消費されるそうです。</p> <p>通常チャットで質問する時には短文であることが多いため、これでは費用対効果が悪いと判断しました。無念。。。</p> <p>このAPI、そこそこの長さの分を構文解析する時には有用だけど、今回みたいな短文向きではなさそうです(料金的な意味で)。</p> <p>最後に以前Androidの開発の時にふと見つけた<a href="https://www.atilika.com/ja/kuromoji/" target="_blank">Kuromoji</a>というライブラリを思い出してnodeモジュールがないか探してみることに。<a href="https://www.npmjs.com/package/kuromoji" target="_blank">ありました</a>。</p> <p>最近はなんでもありますね。ほんと助かります。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="Kuromojiによる解析"> <p><img class="hatena-fotolife" style="border: solid 1px lightgray;" title="f:id:okano4413:20191003121327p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191003/20191003121327.png" alt="f:id:okano4413:20191003121327p:plain" /></p> <figcaption>Kuromojiによる解析</figcaption> </figure> <p> これなら日本語にも対応してくれてるし、料金も利用したLambdaのリソース分しかかからないので、(Lambdaの利用料金は実行時間と消費したメモリの量に依存するため)残念なコードを書かない限り料金も抑えれそうです。</p> <p> 以下今回の調査のまとめです。</p> <table width="572"> <tbody> <tr> <th style="font-weight: 400; background-color: #ffcc99;" width="73">目的</th> <td style="font-weight: 400;" width="190">文(sentence)を単語(word)に分割できる</td> <td style="font-weight: 400;" width="170"> </td> <td style="font-weight: 400;" width="139"> </td> </tr> <tr> <th style="font-weight: 400; background-color: #e0ffff;" width="73"> </th> <th style="font-weight: 400; background-color: #e0ffff;" width="190">amazon comprehend</th> <th style="font-weight: 400; background-color: #e0ffff;" width="170">cloud natural language api</th> <th style="font-weight: 400; background-color: #e0ffff;" width="139">Kuromoji</th> </tr> <tr> <th style="font-weight: 400; background-color: #e0ffff;" width="73">機能</th> <td style="font-weight: 400;" width="190">構文解析<br />キーフレーズ抽出<br />感情分析<br />エンティティ分析<br />言語検出など</td> <td style="font-weight: 400;" width="170">構文解析<br />エンティティ分析<br />感情分析<br />コンテンツ分類など</td> <td style="font-weight: 400;" width="139">構文解析</td> </tr> <tr> <th style="font-weight: 400; background-color: #e0ffff;" width="73">長所</th> <td style="font-weight: 400;" width="190">・ドキュメントからそのドキュメントが何を指すものなのかを学習させることでユーザの真に知りたい内容や行いたい行動を抽出できる</td> <td style="font-weight: 400;" width="170">・精度自体はAWSよりいいと思う。<br />・5000ユニットまで無料利用枠がある</td> <td style="font-weight: 400;" width="139">・node moduleを<br />入れるだけですぐ利用できる<br />・Lambdaの無料利用枠を利用した計算ができる</td> </tr> <tr> <th style="font-weight: 400; background-color: #e0ffff;" width="73">短所</th> <td style="font-weight: 400;" width="190">・日本語で利用できる機能が少ない。<br />・文章の解析がメインとなるため、ボットのように短文で質問を行う場合は費用対効果が悪い</td> <td style="font-weight: 400;" width="170">・1ユニットが1000文字で設定されているためAWSよりも費用対効果が悪い</td> <td style="font-weight: 400;" width="139">・名詞を分割して解析するので自分でどこまでがまとまった単語なのか判定するロジックを追加する必要がある。</td> </tr> <tr> <th style="font-weight: 400; background-color: #e0ffff;" width="73">料金体系</th> <td style="font-weight: 400;" width="190">10億文字まで<br />$0.0001/100文字</td> <td style="font-weight: 400;" width="170">10億文字まで<br />$0.001/1000文字</td> <td style="font-weight: 400;" width="139">Lambdaの利用枠内</td> </tr> </tbody> </table> <h3>4.記事の検索方法の変更</h3> <p>前回WP REST APIで検索を行う時、通常の質問形式や複数キーワードを投げても適切な回答が得られないという話をしました。これは</p> <p style="font-style: italic;">{domain}/wp-json/wp/v2/posts/?search="キーワード"</p> <p>のクエリで検索した時に、完全一致で検索しようとするため、投稿のタイトルや本文内に質問文と同じものがなければ検索結果に出てこないことが原因でした。</p> <p>またユーザの質問文に含まれるキーワードが必ずしもユーザガイドに含まれないこともあります。</p> <p>例えば「トークの<strong>一覧</strong>」という言い方をする人と「トークの<strong>リスト</strong>」という人がいる場合にユーザガイド内の言い方が「トークの<strong>一覧</strong>」だったとすると後者の検索で「リスト」というキーワードに一致しなくなります。</p> <p>このような揺らぎを少しでもなくすために、ある程度予測される表現については別途「タグ」として管理することにし、「タグ」による検索を行えるようにしました。</p> <p style="font-style: italic;">{domain}/wp-json/wp/v2/posts/?filter[tag]="キーワード1,キーワード2..."</p> <p>こうすることでかなりの期待したものに近い結果が得られるようになりました。</p> <p style="text-align: center;"><img class="hatena-fotolife" style="width: 200px; border: solid 1px lightgray;" title="f:id:okano4413:20191003143458p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20191003/20191003143458.png" alt="f:id:okano4413:20191003143458p:plain" /></p> <p>あとは細かいロジックの調整(連続する名詞を1つの単語として検索する、一致するキーワードが多い記事を優先して表示するなど)をして完成(?)と相成りました。</p> <h3>5.これからのお話</h3> <p>今回ユーザーガイドURLを取得するボットを作っていて気づいたのが、</p> <p>「1つの記事の中で複数の機能を説明している」</p> <p>記事があることです。</p> <p>もちろん、全く関係のない機能を説明しているわけではないのですが、本来ユーザーが調べたい機能以外のことが多く存在していると、欲しい回答を探す手間が発生します。</p> <p>せっかく回答の書かれたURLを教えてもらってもその回答がすぐにはわからないとストレスになってしまいますね。これは当社のユーザーガイド作成の至らない点です。。。</p> <p> </p> <p>今後はユーザーガイドの改善もしながらボットの改善を目指して行けるようにも動いていきたいと思います。そしていつの日か、このボットが皆さんの前に現れてくれることを願います。</p> <h3>6.おわりに</h3> <p>今回の記事では、以前作成した「キーワードに一致する記事のURLを返却するボット」へのリクエストフォーマットを改善してより意味のあるURLを返すようにするための取り組みについて記載しました。内容としてはあまり専門的なものではなかったですが、結局はこういう地道なことの積み重ねでUXがよくなるのかなとも思っています。</p> <p> </p> <p>次回はいい加減Androidについて記事を書きたいなと思いますので、その時は改めてよろしくお願いします。^^; </p> <h3>参考URL</h3> <ul dir="auto" data-sourcepos="90:1-92:0"> <li data-sourcepos="90:1-90:110"><a href="https://aws.amazon.com/jp/comprehend/" target="_blank">Amazon Comprehend</a></li> <li data-sourcepos="91:1-92:0"><a href="https://cloud.google.com/natural-language/?hl=ja#" target="_blank">Cloud Natural Language API</a></li> <li data-sourcepos="91:1-92:0"><a href="https://www.atilika.com/ja/kuromoji/" target="_blank">Kuromoji</a></li> </ul> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> okano4413 1週間でヘルプボット作って社内リリースしたお話 hatenablog://entry/26006613376399723 2019-07-24T10:44:12+09:00 2019-07-24T10:44:12+09:00 こんにちは。WowTalkのAndroid開発担当している岡野です。 今回はAndroidと全く関係ないのですが、AWSのサービスを使ってWowTalkのユーザサポート用のボットを作って1週間で社内リリースした話を書いていこうと思います。 1. なぜ作ろうと思ったのか WowTalkには電話やメールでのサポート窓口というものがあるですが、ここに来る問い合わせ件数を減らせないかというのがそもそもの始まりでした。 WowTalkには操作説明が載っているユーザーサイトもあるのですが、せっかくWowTalkには自分でカスタマイズできるボット機能と、WowTalkにメッセージを送信できるOpenAPIも… <p>こんにちは。WowTalkのAndroid開発担当している岡野です。</p> <p class="p1">今回は<span class="s1">Android</span>と全く関係ないのですが、<span class="s1">AWS</span>のサービスを使って<span class="s1">WowTalk</span>のユーザサポート用のボットを作って1週間で社内リリースした話を書いていこうと思います。</p> <h3>1. なぜ作ろうと思ったのか</h3> <p>WowTalkには電話やメールでのサポート窓口というものがあるですが、ここに来る問い合わせ件数を減らせないかというのがそもそもの始まりでした。</p> <p>WowTalkには操作説明が載っているユーザーサイトもあるのですが、せっかくWowTalkには自分でカスタマイズできるボット機能と、WowTalkにメッセージを送信できるOpenAPIもあるのでそれをうまく利用したいと考えました。</p> <p> </p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="今回考えたアーキテクチャの概要"> <p><img class="hatena-fotolife" title="f:id:okano4413:20190723154630p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20190723/20190723154630.png" alt="f:id:okano4413:20190723154630p:plain" /></p> <figcaption>今回考えたアーキテクチャの概要</figcaption> </figure> <p>今回は検証も兼ねて、社内からの問い合わせに対応できるよう社内公開しましたが、最終的にはユーザの皆様に公開し、利用していただけるようなボットを提供できればと考えています。</p> <h3>2.開発環境</h3> <p><strong>Lambda</strong> : node.js 8.10 </p> <p><strong>ServerlessFramework</strong> : 1.40 </p> <h3>3. サーバー立てずに実装したい</h3> <p>WowTalkは裏側でAWSを使っているのですが、今回は機能上EC2でインスタンスを立てる必要もないと判断したためAPIGateWayとLambdaを使うことにしました。</p> <p>ただしインスタンスを立てないことに伴いLambdaの設定を工夫する必要がありました。</p> <h3>4. IPアドレスがない問題</h3> <p>WowTalkのOpenAPIの利用にはセキュアアクセスのためアクセス元にIPアドレスが必要ですが、LambdaはサーバーレスなFaaSサービスのためIPアドレスがありません。</p> <p>ではどうするかというと、OpenAPIと同じサブネットにLambdaを設置することで対応しました。OpenAPIは社内で検証を行うことがしばしばあるので、同じサービスネットワーク内からのアクセスは許可されています。なのでLambda関数のVPC設定から、WowTalkのインスタンス群と同じサブネットに配置することでOpenAPIの呼び出しを行えるようにしました。</p> <p> </p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="Lambda関数のコンソールからVPCの設定をする"> <p><img class="hatena-fotolife" title="f:id:okano4413:20190723144448p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20190723/20190723144448.png" alt="f:id:okano4413:20190723144448p:plain" /></p> <figcaption>Lambda関数のコンソールからVPCの設定をする</figcaption> </figure> <h3>5.Lamdaから外部へアクセスできない問題</h3> <p>ただこれだけでは不十分で、今度はLambdaからユーザーサイトにアクセスできなくなりました。上の画像の注意書きにもありますが、NAT GateWayを設定していないため外部ネットワークとのアクセスができず、ドメイン解決ができなくなるので当然と言えば当然なのですが(汗) </p> <p>これはRoute53のプライベートホストゾーン設定にユーザーサイトへアクセスするためのレコードセットを設定することで解決しました。OpenAPIのサーバーへのアクセスも同様に通常のドメイン設定ではアクセスできなくなったので合わせて設定することで対応しました。</p> <h3>6.回答しよう</h3> <p>ここまでできたらあとは裏側の処理をnode.jsでガリガリ書くだけです。まずはシンプルにキーワードに関連する記事のURLをボットのそれっぽく返却するだけにしました。</p> <pre class="code lang-javascript" style="overflow: auto hidden; font-family: Monaco, Consolas, 'Courier New', Courier, monospace, sans-serif; font-size: 0.8rem; background: #f5f5f5; border: none; white-space: pre-wrap; text-overflow: ellipsis; line-height: 1.3; padding: 10px; color: #454545; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;" data-lang="javascript" data-unlink=""><span class="synConstant" style="color: #ff6666;">'use strict'</span>; <span class="synStatement" style="color: #d88a17;">const</span> crypto = require(<span class="synConstant" style="color: #ff6666;">'crypto'</span>); <span class="synStatement" style="color: #d88a17;">const</span> URLSafeBase64 = require(<span class="synConstant" style="color: #ff6666;">'urlsafe-base64'</span>); <span class="synStatement" style="color: #d88a17;">const</span> Buffertrim = require(<span class="synConstant" style="color: #ff6666;">'buffertrim'</span>); <span class="synStatement" style="color: #d88a17;">const</span> https = require(<span class="synConstant" style="color: #ff6666;">"request"</span>); <span class="synStatement" style="color: #d88a17;">const</span> Config = require(<span class="synConstant" style="color: #ff6666;">'./config'</span>); <span class="synIdentifier" style="color: #51cfcf;">var</span> util = require(<span class="synConstant" style="color: #ff6666;">'util'</span>); <span class="synIdentifier" style="color: #51cfcf;">var</span> configs; module.exports.handleMessage = async (<span class="synStatement" style="color: #d88a17;">event</span>, context, callback) =&gt; <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synStatement" style="color: #d88a17;">const</span> body = <span class="synStatement" style="color: #d88a17;">event</span><span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">'body'</span><span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synStatement" style="color: #d88a17;">const</span> alias = <span class="synStatement" style="color: #d88a17;">event</span><span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">'stageVariables'</span><span class="synIdentifier" style="color: #51cfcf;">][</span><span class="synConstant" style="color: #ff6666;">'alias'</span><span class="synIdentifier" style="color: #51cfcf;">]</span>;<span class="synComment" style="color: #4f80e5;">//APIGateWayから渡される環境変数</span> configs = <span class="synStatement" style="color: #d88a17;">new</span> Config(alias).getConfigs();<span class="synComment" style="color: #4f80e5;">//環境ごとに設定を変更</span> <span class="synStatement" style="color: #d88a17;">if</span> (body != <span class="synStatement" style="color: #d88a17;">null</span>) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> json = getJsonFromBody(body); <span class="synStatement" style="color: #d88a17;">const</span> userId = json<span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">'source'</span><span class="synIdentifier" style="color: #51cfcf;">][</span><span class="synConstant" style="color: #ff6666;">'userId'</span><span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synStatement" style="color: #d88a17;">const</span> content = json<span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">'message'</span><span class="synIdentifier" style="color: #51cfcf;">][</span><span class="synConstant" style="color: #ff6666;">'content'</span><span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synStatement" style="color: #d88a17;">const</span> companyId = json<span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">'source'</span><span class="synIdentifier" style="color: #51cfcf;">][</span><span class="synConstant" style="color: #ff6666;">'companyId'</span><span class="synIdentifier" style="color: #51cfcf;">]</span> getURLFromKeyWord(content, <span class="synIdentifier" style="color: #51cfcf;">function</span>(answer)<span class="synIdentifier" style="color: #51cfcf;">{</span> sendMessageBuddy(configs.BOT_ID, configs.BOT_PASSWORD, userId, companyId, answer); <span class="synIdentifier" style="color: #51cfcf;">}</span>); <span class="synIdentifier" style="color: #51cfcf;">}</span> callback(); <span class="synIdentifier" style="color: #51cfcf;">}</span>; <span class="synIdentifier" style="color: #51cfcf;">function</span> getJsonFromBody(body) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synComment" style="color: #4f80e5;">/** 暗号化解析処理 (省略) **/</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synComment" style="color: #4f80e5;">// ユーザーサイトからキーワードが該当する記事を取得する</span> <span class="synIdentifier" style="color: #51cfcf;">function</span> getURLFromKeyWord(keyword, callback) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> encodedKeyWord = encodeURI(keyword); <span class="synIdentifier" style="color: #51cfcf;">var</span> opt = <span class="synIdentifier" style="color: #51cfcf;">{</span> url: configs.SUPPORT_URL, headers: <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synConstant" style="color: #ff6666;">"Accept"</span> : <span class="synConstant" style="color: #ff6666;">"application/json"</span> <span class="synIdentifier" style="color: #51cfcf;">}</span>, qs : <span class="synIdentifier" style="color: #51cfcf;">{</span> search : encodedKeyWord, per_page : 100 <span class="synIdentifier" style="color: #51cfcf;">}</span>, qsStringifyOptions : <span class="synIdentifier" style="color: #51cfcf;">{</span> encode : <span class="synConstant" style="color: #ff6666;">false</span> <span class="synIdentifier" style="color: #51cfcf;">}</span>, rejectUnauthorized : <span class="synConstant" style="color: #ff6666;">false</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> https.get(opt, <span class="synIdentifier" style="color: #51cfcf;">function</span>(error, response, body) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synStatement" style="color: #d88a17;">if</span> (body != <span class="synStatement" style="color: #d88a17;">null</span>) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> JSONBody = JSON.parse(body); <span class="synIdentifier" style="color: #51cfcf;">var</span> answers = <span class="synIdentifier" style="color: #51cfcf;">[]</span>; <span class="synComment" style="color: #4f80e5;">//回答メッセージの配列</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> answer; <span class="synStatement" style="color: #d88a17;">if</span> (body.length == 0 || body == <span class="synConstant" style="color: #ff6666;">"[]"</span>) <span class="synIdentifier" style="color: #51cfcf;">{</span> answer = configs.HINT_TEXT_FOR_NO_RESULT; <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synStatement" style="color: #d88a17;">else</span> <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synComment" style="color: #4f80e5;">//ここで回答本文作成処理。</span> answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword); <span class="synStatement" style="color: #d88a17;">for</span> (<span class="synIdentifier" style="color: #51cfcf;">var</span> i=0; i&lt;JSONBody.length; i++) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> val = JSONBody<span class="synIdentifier" style="color: #51cfcf;">[</span>i<span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synIdentifier" style="color: #51cfcf;">var</span> title = val<span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">"title"</span><span class="synIdentifier" style="color: #51cfcf;">][</span><span class="synConstant" style="color: #ff6666;">"rendered"</span><span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synIdentifier" style="color: #51cfcf;">var</span> link = val<span class="synIdentifier" style="color: #51cfcf;">[</span><span class="synConstant" style="color: #ff6666;">"link"</span><span class="synIdentifier" style="color: #51cfcf;">]</span>; <span class="synComment" style="color: #4f80e5;">/** WowTalkに送れる文字数は800文字の文字制限があるため、</span> <span class="synComment" style="color: #4f80e5;"> * 回答が制限を超える場合は別のメッセージとして送信するようにする。</span> <span class="synComment" style="color: #4f80e5;"> **/</span> <span class="synComment" style="color: #4f80e5;">//判定用文字列</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> textAfterJoin = answer + title + <span class="synConstant" style="color: #ff6666;">"</span><span class="synSpecial" style="color: #c000c0;">\n</span><span class="synConstant" style="color: #ff6666;">"</span> + link + <span class="synConstant" style="color: #ff6666;">"</span><span class="synSpecial" style="color: #c000c0;">\n</span><span class="synConstant" style="color: #ff6666;">"</span>; <span class="synStatement" style="color: #d88a17;">if</span> (answer.length &lt; configs.MAX_LENGTH &amp;&amp; textAfterJoin.length &gt;= configs.MAX_LENGTH) <span class="synIdentifier" style="color: #51cfcf;">{</span> answers.push(answer); answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword); <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synStatement" style="color: #d88a17;">if</span> (link != <span class="synStatement" style="color: #d88a17;">null</span>) <span class="synIdentifier" style="color: #51cfcf;">{</span> answer += title + <span class="synConstant" style="color: #ff6666;">"</span><span class="synSpecial" style="color: #c000c0;">\n</span><span class="synConstant" style="color: #ff6666;">"</span> + link + <span class="synConstant" style="color: #ff6666;">"</span><span class="synSpecial" style="color: #c000c0;">\n</span><span class="synConstant" style="color: #ff6666;">"</span>; <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> answers.push(answer); <span class="synStatement" style="color: #d88a17;">for</span> (<span class="synIdentifier" style="color: #51cfcf;">var</span> j=0; j&lt; answers.length ; j++) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> ans = answers<span class="synIdentifier" style="color: #51cfcf;">[</span>j<span class="synIdentifier" style="color: #51cfcf;">]</span>; callback(ans); <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synStatement" style="color: #d88a17;">else</span> <span class="synStatement" style="color: #d88a17;">if</span> (error != <span class="synStatement" style="color: #d88a17;">null</span>) <span class="synIdentifier" style="color: #51cfcf;">{</span> callback(configs.HINT_TEXT_FOR_REQUEST_ERROR); <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synIdentifier" style="color: #51cfcf;">}</span>); <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synComment" style="color: #4f80e5;">//OpenAPIにリクエスト</span> <span class="synIdentifier" style="color: #51cfcf;">function</span> sendMessageBuddy(fromAccount, fromPassword, toAccount, companyId, content) <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synStatement" style="color: #d88a17;">const</span> message = <span class="synIdentifier" style="color: #51cfcf;">{</span> from_account : fromAccount, from_account_password: fromPassword, to_account : toAccount, message_content: content <span class="synIdentifier" style="color: #51cfcf;">}</span>; <span class="synStatement" style="color: #d88a17;">const</span> messageJsonString = JSON.stringify(message); <span class="synComment" style="color: #4f80e5;">/** 回答の暗号化処理 (省略) **/</span> <span class="synIdentifier" style="color: #51cfcf;">var</span> opt = <span class="synIdentifier" style="color: #51cfcf;">{</span> uri: configs.OPENAPI_URL, method: <span class="synConstant" style="color: #ff6666;">'POST'</span>, headers:<span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synConstant" style="color: #ff6666;">"Content-type"</span>: <span class="synConstant" style="color: #ff6666;">"application/x-www-form-urlencoded"</span>, <span class="synIdentifier" style="color: #51cfcf;">}</span>, form: <span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synConstant" style="color: #ff6666;">"company"</span>: companyId, <span class="synConstant" style="color: #ff6666;">"request_id"</span>: <span class="synConstant" style="color: #ff6666;">"req123"</span>, <span class="synConstant" style="color: #ff6666;">"language"</span>:<span class="synConstant" style="color: #ff6666;">"jp"</span>, <span class="synConstant" style="color: #ff6666;">"request_data"</span>:<span class="synConstant" style="color: #ff6666;">"暗号化された回答"</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> <span class="synIdentifier" style="color: #51cfcf;">}</span> https.post(opt,<span class="synIdentifier" style="color: #51cfcf;">function</span>(error,response,body)<span class="synIdentifier" style="color: #51cfcf;">{</span> <span class="synComment" style="color: #4f80e5;">/** Open APIにリクエストを投げた後の処理 **/</span> <span class="synIdentifier" style="color: #51cfcf;">}</span>); <span class="synIdentifier" style="color: #51cfcf;">}</span></pre> <p> </p> <p>こうして記事のURLを返却するボットを無事(?)社内リリースしました。</p> <p style="text-align: center;"><img class="hatena-fotolife" style="width: 200px;" title="f:id:okano4413:20190723145627p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20190723/20190723145627.png" alt="f:id:okano4413:20190723145627p:plain" /></p> <h3>7. これからのお話</h3> <p>ただこれでは通常の質問形式や複数キーワードで検索をかけようとした時に、WP REST APIでは投稿が引っかからないので正直ほとんど使い物になりませんでした orz</p> <p style="text-align: center;"><img class="hatena-fotolife" style="width: 200px;" title="f:id:okano4413:20190723150643p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okano4413/20190723/20190723150643.png" alt="f:id:okano4413:20190723150643p:plain" /></p> <p>というわけで「投げられたメッセージをキーワード解析しながらWP REST APIへの検索方法も改善しないと使い物にならないな〜」となりました。そちらについてはまた今後追記していきたいと思います。 </p> <h3>8. おわりに</h3> <p>今回の記事では(ボットのクオリティはさておき)お手製のサポートボットをリリースするまでを簡単に追いました。</p> <p>タイトルにもある通り、AWSのサーバーレスアーキテクチャを利用すると、アーキテクチャを考えてから社内リリースするまで1週間という短期間で実装ができました。EC2でインスタンスを立てると利用していないリソースにまで料金が発生したり負荷が高くなった時のスケーリングとかリソースの配分なども考えないといけませんが、そこを考慮しなくて済むのは魅力的です。</p> <p>Androidアプリ開発する傍ら、サービスがもっと使いやすくなるような機能の模索もしていきたいと思います。</p> <p> </p> <h3>参考URL</h3> <h5>AWS関連</h5> <ul dir="auto" data-sourcepos="90:1-92:0"> <li data-sourcepos="90:1-90:110"><a href="https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/aliases-intro.html" rel="nofollow noreferrer noopener" target="_blank">AWS Lambda エイリアスの紹介</a></li> <li data-sourcepos="91:1-92:0"><a href="https://dev.classmethod.jp/cloud/aws/version-management-with-api-gateway-and-lambda/" rel="nofollow noreferrer noopener" target="_blank">[API Gateway + Lambda]ステージとエイリアスを使ってバージョン管理してみた</a> </li> </ul> <h5>Serverless関連</h5> <ul dir="auto" data-sourcepos="94:1-96:105"> <li data-sourcepos="94:1-94:145"><a href="https://serverless.com/framework/docs/providers/aws/guide/credentials#using-the-aws-profile-option" rel="nofollow noreferrer noopener" target="_blank">Serverless Framework AWS Credential Guide</a></li> <li data-sourcepos="95:1-95:100"><a href="https://qiita.com/horike37/items/b295a91908fcfd4033a2" rel="nofollow noreferrer noopener" target="_blank">Serverless Frameworkの使い方まとめ</a></li> <li data-sourcepos="96:1-96:105"><a href="https://qiita.com/70_10/items/ae22a7a9bca62c273495" rel="nofollow noreferrer noopener" target="_blank">ServerlessでLambdaをVPC内にデプロイする</a></li> </ul> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" /> </a></p> okano4413 React+PHP (CodeIgniter) でWebアプリケーションを作る ~Storybookを添えて~ hatenablog://entry/17680117127001286880 2019-03-29T16:41:13+09:00 2019-03-29T16:43:01+09:00 こんにちは。Web開発チームの島根です。 今回のテーマはReactとCodeIgniterでのWebアプリケーション開発です。 Laravel使った方が楽なのでしょうが、現在動いている他のシステムでCodeIgniterが使われているので今回やったことを他のシステムに流用できるようCodeIgniterを選択しました。 やったことはと言えば、要はReactをビルドして作られたindex.htmlと同じことをCodeIginterのviewで行うということだけです。 ただし、2点ほどつまづいてしまいました。 1.create-react-appでreactプロジェクトを作ったのでビルドする度にフ… <p>こんにちは。Web開発チームの島根です。<br /> 今回のテーマはReactとCodeIgniterでのWebアプリケーション開発です。<br /> Laravel使った方が楽なのでしょうが、現在動いている他のシステムでCodeIgniterが使われているので今回やったことを他のシステムに流用できるようCodeIgniterを選択しました。<br /> やったことはと言えば、要はReactをビルドして作られたindex.htmlと同じことをCodeIginterのviewで行うということだけです。<br /> ただし、2点ほどつまづいてしまいました。<br /> <br /> </p> <div class="section"> <h3>1.create-react-appでreactプロジェクトを作ったのでビルドする度にファイル名が変わる</h3> <p> これは、create-react-appで作ったreacctプロジェクトでは、webpackの設定でビルド時にファイル名にハッシュ値を含むようにしてあります。<br /> ejectすればconfigファイルを設定できるようですが戻せなくなるため、今回はbuildした後にできるasset-manifest.jsonからファイル名を読み込むという方法をとりました。<br /> 読み込みたいのは<br /> ・main.<ハッシュ値>.chunk.js<br /> ・<ハッシュ値>.chunk.js<br /> ・main.<ハッシュ値>.chunk.css<br /> ・<ハッシュ値>.chunk.css<br /> の4つのファイルです。<div class="images-row mceNonEditable"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20190329/20190329121311.png" alt="f:id:wt-shimane:20190329121311p:plain" title="f:id:wt-shimane:20190329121311p:plain" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20190329/20190329120737.png" alt="f:id:wt-shimane:20190329120737p:plain" title="f:id:wt-shimane:20190329120737p:plain" class="hatena-fotolife" itemprop="image"></span></div>割と力技です。笑<br /> ・controller</p> <pre class="code lang-php" data-lang="php" data-unlink>public function index() { $assetJson = file_get_contents('./build/asset-manifest.json'); $asset = json_decode($assetJson, true); $base = '<span class="synIdentifier">&lt;</span>react<span class="synIdentifier">をビルドしてできたbuildディレクトリのパス&gt;</span>'; $filePaths = array(); foreach($asset as $key =<span class="synError">&gt;</span> $filename){ if($key === 'main.css'){ $filePaths['MAIN_CSS'] = $base . $filename; } if($key === 'main.js'){ $filePaths['MAIN_JS'] = $base . $filename; } if($key !== 'main.css' <span class="synError">&amp;&amp;</span> preg_match(&quot;/chunk\.css$/&quot;, $key)){ $filePaths['CHUNK_CSS'] = $base . $filename; } if($key !== 'main.js' <span class="synError">&amp;&amp;</span> preg_match(&quot;/chunk\.js$/&quot;, $key)){ $filePaths['CHUNK_JS'] = $base . $filename; } } $this-<span class="synError">&gt;</span>load-<span class="synError">&gt;</span>view('welcome_message', $filePaths); } } </pre><p>file_get_contentsでasset-manifest.jsonからファイル名のマッピング情報を取って正規表現で各ファイルを判断してます。main.jsとmain.cssはキーが固定なのでキーで判断し、残り2つは正規表現で判断。ただしmain.jsとmain.cssもchunk.jsやchunk.cssで終わるのでキーの名前ではじいています。(力技)<br /> こうしておけばビルドしたらCodeIgniter側から読み取れるフォルダにasset-manifest.jsonとjsファイルとcssファイルを移動すれば読み取れるようになります。<br /> ファイル移動の部分はshell書いてもいいですし、デプロイ時のフローに入れてもいいですし簡単に自動化できると思います。<br /> ・view</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;?php</span> <span class="synIdentifier">defined(</span><span class="synConstant">'BASEPATH'</span><span class="synIdentifier">) OR exit(</span><span class="synConstant">'No direct script access allowed'</span><span class="synIdentifier">);</span> <span class="synIdentifier">?&gt;</span><span class="synComment">&lt;!DOCTYPE html&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">html</span><span class="synIdentifier"> </span><span class="synType">lang</span><span class="synIdentifier">=</span><span class="synConstant">&quot;en&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">head</span><span class="synIdentifier">&gt;</span> <span class="synPreProc"> </span><span class="synIdentifier">&lt;</span><span class="synStatement">meta</span><span class="synIdentifier"> </span><span class="synType">charset</span><span class="synIdentifier">=</span><span class="synConstant">&quot;utf-8&quot;</span><span class="synIdentifier">&gt;</span> <span class="synPreProc"> </span><span class="synIdentifier">&lt;</span><span class="synStatement">title</span><span class="synIdentifier">&gt;&lt;/</span><span class="synStatement">title</span><span class="synIdentifier">&gt;</span> <span class="synPreProc"> </span><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;&lt;?=$CHUNK_CSS?&gt;&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> <span class="synPreProc"> </span><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;&lt;?=$MAIN_CSS?&gt;&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> <span class="synIdentifier">&lt;/</span><span class="synStatement">head</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">body</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">div</span><span class="synIdentifier"> </span><span class="synType">id</span><span class="synIdentifier">=</span><span class="synConstant">&quot;root&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">div</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span><span class="synSpecial">!</span><span class="synIdentifier">function</span>(<span class="synSpecial">l</span>)<span class="synIdentifier">{function</span><span class="synSpecial"> e</span>(<span class="synSpecial">e</span>)<span class="synIdentifier">{</span><span class="synStatement">for</span>(<span class="synIdentifier">var</span><span class="synSpecial"> r,t,n=e</span><span class="synIdentifier">[</span>0<span class="synIdentifier">]</span><span class="synSpecial">,o=e</span><span class="synIdentifier">[</span>1<span class="synIdentifier">]</span><span class="synSpecial">,u=e</span><span class="synIdentifier">[</span>2<span class="synIdentifier">]</span><span class="synSpecial">,f=</span>0<span class="synSpecial">,i=</span><span class="synIdentifier">[]</span><span class="synSpecial">;f&lt;n.length;f++</span>)<span class="synSpecial">t=n</span><span class="synIdentifier">[</span><span class="synSpecial">f</span><span class="synIdentifier">]</span><span class="synSpecial">,p</span><span class="synIdentifier">[</span><span class="synSpecial">t</span><span class="synIdentifier">]</span><span class="synSpecial">&amp;&amp;i.push</span>(<span class="synSpecial">p</span><span class="synIdentifier">[</span><span class="synSpecial">t</span><span class="synIdentifier">][</span>0<span class="synIdentifier">]</span>)<span class="synSpecial">,p</span><span class="synIdentifier">[</span><span class="synSpecial">t</span><span class="synIdentifier">]</span><span class="synSpecial">=</span>0<span class="synSpecial">;</span><span class="synStatement">for</span>(<span class="synSpecial">r </span><span class="synStatement">in</span><span class="synSpecial"> o</span>)<span class="synType">Object</span><span class="synSpecial">.prototype.hasOwnProperty.call</span>(<span class="synSpecial">o,r</span>)<span class="synSpecial">&amp;&amp;</span>(<span class="synSpecial">l</span><span class="synIdentifier">[</span><span class="synSpecial">r</span><span class="synIdentifier">]</span><span class="synSpecial">=o</span><span class="synIdentifier">[</span><span class="synSpecial">r</span><span class="synIdentifier">]</span>)<span class="synSpecial">;</span><span class="synStatement">for</span>(<span class="synSpecial">s&amp;&amp;s</span>(<span class="synSpecial">e</span>)<span class="synSpecial">;i.length;</span>)<span class="synSpecial">i.shift</span>()()<span class="synSpecial">;</span><span class="synStatement">return</span><span class="synSpecial"> c.push.apply</span>(<span class="synSpecial">c,u||</span><span class="synIdentifier">[]</span>)<span class="synSpecial">,a</span>()<span class="synIdentifier">}function</span><span class="synSpecial"> a</span>()<span class="synIdentifier">{</span><span class="synStatement">for</span>(<span class="synIdentifier">var</span><span class="synSpecial"> e,r=</span>0<span class="synSpecial">;r&lt;c.length;r++</span>)<span class="synIdentifier">{</span><span class="synStatement">for</span>(<span class="synIdentifier">var</span><span class="synSpecial"> t=c</span><span class="synIdentifier">[</span><span class="synSpecial">r</span><span class="synIdentifier">]</span><span class="synSpecial">,n=!</span>0<span class="synSpecial">,o=</span>1<span class="synSpecial">;o&lt;t.length;o++</span>)<span class="synIdentifier">{var</span><span class="synSpecial"> u=t</span><span class="synIdentifier">[</span><span class="synSpecial">o</span><span class="synIdentifier">]</span><span class="synSpecial">;</span>0<span class="synSpecial">!==p</span><span class="synIdentifier">[</span><span class="synSpecial">u</span><span class="synIdentifier">]</span><span class="synSpecial">&amp;&amp;</span>(<span class="synSpecial">n=!</span>1)<span class="synIdentifier">}</span><span class="synSpecial">n&amp;&amp;</span>(<span class="synSpecial">c.splice</span>(<span class="synSpecial">r--,</span>1)<span class="synSpecial">,e=f</span>(<span class="synSpecial">f.s=t</span><span class="synIdentifier">[</span>0<span class="synIdentifier">]</span>))<span class="synIdentifier">}</span><span class="synStatement">return</span><span class="synSpecial"> e</span><span class="synIdentifier">}var</span><span class="synSpecial"> t=</span><span class="synIdentifier">{}</span><span class="synSpecial">,p=</span><span class="synIdentifier">{</span>1<span class="synSpecial">:</span>0<span class="synIdentifier">}</span><span class="synSpecial">,c=</span><span class="synIdentifier">[]</span><span class="synSpecial">;</span><span class="synIdentifier">function</span><span class="synSpecial"> f</span>(<span class="synSpecial">e</span>)<span class="synIdentifier">{</span><span class="synStatement">if</span>(<span class="synSpecial">t</span><span class="synIdentifier">[</span><span class="synSpecial">e</span><span class="synIdentifier">]</span>)<span class="synStatement">return</span><span class="synSpecial"> t</span><span class="synIdentifier">[</span><span class="synSpecial">e</span><span class="synIdentifier">]</span><span class="synSpecial">.exports;</span><span class="synIdentifier">var</span><span class="synSpecial"> r=t</span><span class="synIdentifier">[</span><span class="synSpecial">e</span><span class="synIdentifier">]</span><span class="synSpecial">=</span><span class="synIdentifier">{</span><span class="synSpecial">i:e,l:!</span>1<span class="synSpecial">,exports:</span><span class="synIdentifier">{}}</span><span class="synSpecial">;</span><span class="synStatement">return</span><span class="synSpecial"> l</span><span class="synIdentifier">[</span><span class="synSpecial">e</span><span class="synIdentifier">]</span><span class="synSpecial">.call</span>(<span class="synSpecial">r.exports,r,r.exports,f</span>)<span class="synSpecial">,r.l=!</span>0<span class="synSpecial">,r.exports</span><span class="synIdentifier">}</span><span class="synSpecial">f.m=l,f.c=t,f.d=</span><span class="synIdentifier">function</span>(<span class="synSpecial">e,r,t</span>)<span class="synIdentifier">{</span><span class="synSpecial">f.o</span>(<span class="synSpecial">e,r</span>)<span class="synSpecial">||</span><span class="synType">Object</span><span class="synSpecial">.defineProperty</span>(<span class="synSpecial">e,r,</span><span class="synIdentifier">{</span><span class="synSpecial">enumerable:!</span>0<span class="synSpecial">,get:t</span><span class="synIdentifier">}</span>)<span class="synIdentifier">}</span><span class="synSpecial">,f.r=</span><span class="synIdentifier">function</span>(<span class="synSpecial">e</span>)<span class="synIdentifier">{</span><span class="synConstant">&quot;undefined&quot;</span><span class="synSpecial">!=</span><span class="synStatement">typeof</span><span class="synSpecial"> Symbol&amp;&amp;Symbol.toStringTag&amp;&amp;</span><span class="synType">Object</span><span class="synSpecial">.defineProperty</span>(<span class="synSpecial">e,Symbol.toStringTag,</span><span class="synIdentifier">{</span><span class="synSpecial">value:</span><span class="synConstant">&quot;Module&quot;</span><span class="synIdentifier">}</span>)<span class="synSpecial">,</span><span class="synType">Object</span><span class="synSpecial">.defineProperty</span>(<span class="synSpecial">e,</span><span class="synConstant">&quot;__esModule&quot;</span><span class="synSpecial">,</span><span class="synIdentifier">{</span><span class="synSpecial">value:!</span>0<span class="synIdentifier">}</span>)<span class="synIdentifier">}</span><span class="synSpecial">,f.t=</span><span class="synIdentifier">function</span>(<span class="synSpecial">r,e</span>)<span class="synIdentifier">{</span><span class="synStatement">if</span>(1<span class="synSpecial">&amp;e&amp;&amp;</span>(<span class="synSpecial">r=f</span>(<span class="synSpecial">r</span>))<span class="synSpecial">,</span>8<span class="synSpecial">&amp;e</span>)<span class="synStatement">return</span><span class="synSpecial"> r;</span><span class="synStatement">if</span>(4<span class="synSpecial">&amp;e&amp;&amp;</span><span class="synConstant">&quot;object&quot;</span><span class="synSpecial">==</span><span class="synStatement">typeof</span><span class="synSpecial"> r&amp;&amp;r&amp;&amp;r.__esModule</span>)<span class="synStatement">return</span><span class="synSpecial"> r;</span><span class="synIdentifier">var</span><span class="synSpecial"> t=</span><span class="synType">Object</span><span class="synSpecial">.create</span>(<span class="synStatement">null</span>)<span class="synSpecial">;</span><span class="synStatement">if</span>(<span class="synSpecial">f.r</span>(<span class="synSpecial">t</span>)<span class="synSpecial">,</span><span class="synType">Object</span><span class="synSpecial">.defineProperty</span>(<span class="synSpecial">t,</span><span class="synConstant">&quot;default&quot;</span><span class="synSpecial">,</span><span class="synIdentifier">{</span><span class="synSpecial">enumerable:!</span>0<span class="synSpecial">,value:r</span><span class="synIdentifier">}</span>)<span class="synSpecial">,</span>2<span class="synSpecial">&amp;e&amp;&amp;</span><span class="synConstant">&quot;string&quot;</span><span class="synSpecial">!=</span><span class="synStatement">typeof</span><span class="synSpecial"> r</span>)<span class="synStatement">for</span>(<span class="synIdentifier">var</span><span class="synSpecial"> n </span><span class="synStatement">in</span><span class="synSpecial"> r</span>)<span class="synSpecial">f.d</span>(<span class="synSpecial">t,n,</span><span class="synIdentifier">function</span>(<span class="synSpecial">e</span>)<span class="synIdentifier">{</span><span class="synStatement">return</span><span class="synSpecial"> r</span><span class="synIdentifier">[</span><span class="synSpecial">e</span><span class="synIdentifier">]}</span><span class="synSpecial">.bind</span>(<span class="synStatement">null</span><span class="synSpecial">,n</span>))<span class="synSpecial">;</span><span class="synStatement">return</span><span class="synSpecial"> t</span><span class="synIdentifier">}</span><span class="synSpecial">,f.n=</span><span class="synIdentifier">function</span>(<span class="synSpecial">e</span>)<span class="synIdentifier">{var</span><span class="synSpecial"> r=e&amp;&amp;e.__esModule?</span><span class="synIdentifier">function</span>()<span class="synIdentifier">{</span><span class="synStatement">return</span><span class="synSpecial"> e.</span><span class="synStatement">default</span><span class="synIdentifier">}</span><span class="synSpecial">:</span><span class="synIdentifier">function</span>()<span class="synIdentifier">{</span><span class="synStatement">return</span><span class="synSpecial"> e</span><span class="synIdentifier">}</span><span class="synSpecial">;</span><span class="synStatement">return</span><span class="synSpecial"> f.d</span>(<span class="synSpecial">r,</span><span class="synConstant">&quot;a&quot;</span><span class="synSpecial">,r</span>)<span class="synSpecial">,r</span><span class="synIdentifier">}</span><span class="synSpecial">,f.o=</span><span class="synIdentifier">function</span>(<span class="synSpecial">e,r</span>)<span class="synIdentifier">{</span><span class="synStatement">return</span><span class="synSpecial"> </span><span class="synType">Object</span><span class="synSpecial">.prototype.hasOwnProperty.call</span>(<span class="synSpecial">e,r</span>)<span class="synIdentifier">}</span><span class="synSpecial">,f.p=</span><span class="synConstant">&quot;/&quot;</span><span class="synSpecial">;</span><span class="synIdentifier">var</span><span class="synSpecial"> r=</span><span class="synStatement">window</span><span class="synSpecial">.webpackJsonp=</span><span class="synStatement">window</span><span class="synSpecial">.webpackJsonp||</span><span class="synIdentifier">[]</span><span class="synSpecial">,n=r.push.bind</span>(<span class="synSpecial">r</span>)<span class="synSpecial">;r.push=e,r=r.slice</span>()<span class="synSpecial">;</span><span class="synStatement">for</span>(<span class="synIdentifier">var</span><span class="synSpecial"> o=</span>0<span class="synSpecial">;o&lt;r.length;o++</span>)<span class="synSpecial">e</span>(<span class="synSpecial">r</span><span class="synIdentifier">[</span><span class="synSpecial">o</span><span class="synIdentifier">]</span>)<span class="synSpecial">;</span><span class="synIdentifier">var</span><span class="synSpecial"> s=n;a</span>()<span class="synIdentifier">}</span>(<span class="synIdentifier">[]</span>)<span class="synIdentifier">&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">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;&lt;?=$MAIN_JS?&gt;&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">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;&lt;?=$CHUNK_JS?&gt;&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">body</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">html</span><span class="synIdentifier">&gt;</span> </pre> </div> <div class="section"> <h3>2.BrowserRouteを使うとページを読み込めない。</h3> <p>これに関してはエラーが出なかったため困りました。<br /> jsやcssのファイルが読み込めていないわけでもなく、コンソールやサーバー側のログファイルにも気になるエラーは何もなし。<br /> ブラウザプラグインのReact Developer Toolで調べたところ、Component自体が読み込めていない様子でした。<br /> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20190329/20190329112201.png" alt="f:id:wt-shimane:20190329112201p:plain" title="f:id:wt-shimane:20190329112201p:plain" class="hatena-fotolife" itemprop="image"></span></p><p>rootの中が空なので一番トップのBrowserRouterがきちんとルーティングできていなかった。HashRouteに変えればちゃんと表示されました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink>ReactDOM.render( &lt;HashRouter&gt; &lt;div&gt; &lt;Route path=<span class="synConstant">'/'</span> exact component=<span class="synIdentifier">{</span>App<span class="synIdentifier">}</span>/&gt; &lt;Route path=<span class="synConstant">'/page1'</span> component=<span class="synIdentifier">{</span>Page1<span class="synIdentifier">}</span>/&gt; &lt;Route path=<span class="synConstant">'/page2'</span> component=<span class="synIdentifier">{</span>Page2<span class="synIdentifier">}</span>/&gt; &lt;/div&gt; &lt;/HashRouter&gt; , <span class="synStatement">document</span>.getElementById(<span class="synConstant">'root'</span>));   </pre> </div> <div class="section"> <h3>構築手順</h3> <p>構築手順を簡単に書いておきます。<br /> ・create-react-app</p> <pre class="code lang-sh" data-lang="sh" data-unlink>create-react-app ci-sample </pre><p>・codeigniter install</p> <pre class="code lang-sh" data-lang="sh" data-unlink>composer create-project kenjis/codeigniter-composer-installer codeigniter </pre><p>・reactやCodeIgniterでコードを書く。ReactのRouterにはHashRouterを使う。</p><p>・buildしてCodeIgniter側にソースコードを移動。</p><p>・CodeIgniterのトップページでbuildしたreactのソースコードを読み込めるようにする。index.html内に直接書かれているscriptをコピペする。</p> </div> <div class="section"> <h3>おまけ</h3> <p>最後におまけですが、Storybookというコンポーネントをカタログにして共有できるツールも入れました。<div class="images-row mceNonEditable"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20190329/20190329115346.png" alt="f:id:wt-shimane:20190329115346p:plain" title="f:id:wt-shimane:20190329115346p:plain" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wt-shimane/20190329/20190329115350.png" alt="f:id:wt-shimane:20190329115350p:plain" title="f:id:wt-shimane:20190329115350p:plain" class="hatena-fotolife" itemprop="image"></span></div></p><p>コンポーネントごとにPropsやコンポーネント自体の説明を書けたり渡す値を変えたりできるaddonがあったりしてすごくいいです。<br /> Storybookはまだまだ活用しきれていないので熟した頃に投稿させてもらいます。</p> </div> wt-shimane ワウテック技術開発部の開発環境について hatenablog://entry/17680117126992662229 2019-03-18T11:22:53+09:00 2022-10-05T12:12:21+09:00 こんにちは(^o^)!web開発チームの島根です。本日からワウテックの技術開発部もブログを運営していくこととなりました!技術的なトピックはもちろんのこと開発メンバーのことについても配信していく予定ですので、このブログを通じてワウテックの技術開発部の雰囲気も伝えられたらなぁと考えています。 さて、記念すべき1本目のテーマは「ワウテック技術開発部の開発環境について」です!! 開発環境といっても開発用のサーバーの設定や使っているツール等についてではなく、今回はPC・デスク・チェア・オフィス内の様子など、どういった環境で開発しているのかをご紹介します。 PC デスク 椅子 オフィスについて PC PC… <p>こんにちは(^o^)!web開発チームの島根です。<br />本日からワウテックの技術開発部もブログを運営していくこととなりました!<br />技術的なトピックはもちろんのこと開発メンバーのことについても配信していく予定ですので、<br />このブログを通じてワウテックの技術開発部の雰囲気も伝えられたらなぁと考えています。</p> <p>さて、記念すべき1本目のテーマは<br />「ワウテック技術開発部の開発環境について」です!!</p> <p>開発環境といっても開発用のサーバーの設定や使っているツール等についてではなく、<br />今回はPC・デスク・チェア・オフィス内の様子など、どういった環境で開発しているのかをご紹介します。</p> <ul class="table-of-contents"> <li><a href="#PC">PC</a></li> <li><a href="#デスク">デスク</a></li> <li><a href="#椅子">椅子</a></li> <li><a href="#オフィスについて">オフィスについて </a></li> </ul> <p> </p> <h3 id="PC">PC</h3> <p>PCはWindowsかMacかを選ぶことはできます。<br />テレワーク制度もあるので基本的にノートPCが配布されています。<br />僕はMacbook Proを使っています。<br />僕はiOSの開発はしないのですが、Macだとコマンドラインからbashのコマンドうてるのでほとんどの作業をコマンドライン内で済ませることができるので僕はWindowsよりMacのほうが好きです。<br />Windowsも10になってからbashが使えるようになったらしいですが試していません笑。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title=" PCとディスプレイ ( 画面に線が入っていますが反射です。割れているわけではないです笑 )"> <p><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313145640j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313145640.jpg" alt="f:id:wowtech-dev:20190313145640j:plain" /></p> <figcaption>PCとディスプレイ ( 画面に線が入っていますが反射です。割れているわけではないです笑 )</figcaption> </figure> <p>27型のディスプレイも配布されます。ソースコードを映すだけですがディスプレイが大きいと目が疲れにくいような気がします。<br />僕はもうこの大きさに慣れてしまって、家でテレワークをするときは別でディスプレイを持っていないので物足りなく感じてしまいます(-_-)</p> <p> </p> <h3 id="デスク">デスク</h3> <p><br />隣や前の席とのパーティションはありません。<br />必要ないものはロッカーに入れておくようにしているので、1人あたりの広さも十分かなーと思っています。</p> <p> </p> <h3 id="椅子">椅子</h3> <p> 椅子はErgohuman Fit(エルゴヒューマン フィット)のヘッドレスト有りのものを使わせてもらっています。<br />椅子に関しては事あるごとに、いいオフィスチェアを買って下さい!とねだっていたのですが、昨年12月にオフィス移転した際についに買ってもらえました( ゚Д゚ノノ"☆パチパチパチパチ<br />これ、ホント嬉しかったです。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="調整箇所が多く自分の体に合わせて自在に調整することができて、まさにFit!"> <p><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313145711j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313145711.jpg" alt="f:id:wowtech-dev:20190313145711j:plain" /></p> <figcaption>調整箇所が多く自分の体に合わせて自在に調整することができて、まさにFit!</figcaption> </figure> <p>さらに、開発メンバー全員でオフィスチェア専門店に行き、実際に試してこれに選定したという思い出エピソードもあります笑</p> <p> </p> <h3 id="オフィスについて">オフィスについて </h3> <p>現在入居しているオフィスのビルは1フロアごとの貸出となっているので6階にはWowTechの社員しかいません。1フロアには2つ執務室があって、技術開発部は管理部とマーケティング部と同じ執務室となっています。<br />これらの部署はあまり外部と電話をするような部署ではないので執務室内はとても静かで開発に集中しやすい環境になっています。</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="オフィス内の風景"> <p><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313150045j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313150045.jpg" alt="f:id:wowtech-dev:20190313150045j:plain" /></p> <figcaption>オフィス内の風景</figcaption> </figure> <p>また、リフレッシュルームにコーヒーメーカーが置いてあり、いつでも豆から淹れた美味しいコーヒーを飲むことができます。コーヒー豆は近くのコーヒー屋さんから総務の方が定期的に買ってきてくれています。いつもありがとうございます(*´∀`*)</p> <figure class="figure-image figure-image-fotolife mceNonEditable" title="コーヒーメーカーとコーヒー豆"> <p><img class="hatena-fotolife" title="f:id:wowtech-dev:20190313150211j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313150211.jpg" alt="f:id:wowtech-dev:20190313150211j:plain" /></p> <figcaption>コーヒーメーカーとコーヒー豆</figcaption> </figure> <p>僕はコーヒーを毎日飲むので美味しいものがオフィスでできるってのは非常にありがたいです。</p> <p> </p> <p>我々ワウテック技術開発部はこのような環境で開発をしています。<br />興味を持った方がいらっしゃればいつでもご連絡下さい。</p> <p><a href="https://www.wowtech.co.jp/recruit.html" target="_blank"> <img class="hatena-fotolife" title="f:id:wowtech-dev:20190313163146j:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wowtech-dev/20190313/20190313163146.jpg" alt="f:id:wowtech-dev:20190313163146j:plain" style="cursor: pointer;" /> </a></p> <p> </p> wowtech-dev