20201201のReactに関する記事は16件です。

TypeScript導入済みのミニマムなNext.jsのテンプレートを作った

レッドインパルスのたかけんです。
REDIMPULZ Advent Calendar 2020 の2日目のエントリーです。

ReactとNext.jsの解説動画

弊社のYouTubeチャンネルでは、ReactとNext.jsの解説動画を公開しています。



https://www.youtube.com/playlist?list=PLGqNraLKYNVdJgHZfey5c2pEw6_z8mIto

動画では、Next.jsの公式のチュートリアルをベースに解説をしていおり、
型情報についても解説した方が理解が深まると思い、TypeScriptを導入しています。

しかし、公式のチュートリアル手順で作成されたプロジェクトにTypeScriptやESLint等のツールを導入するのが若干手間なので、
設定を加えたNext.jsのテンプレートを作成しました。

今回は、その作成したテンプレートの紹介になります。

nextjs-typescript-starter

導入済みの設定

  • TypeScript
  • ESlint
  • Prettier
  • VScode Extentions

TypeScriptの設定以外に、ESlintやPrettier、VScodeの拡張機能の設定も入れています。
テンプレートからプロジェクトを作成し、VScodeで開けば、TypeScriptやESlintで快適に開発できるようになっています。

注意点

元々の公式のテンプレートにあまり手を加えていないシンプルな設定になっています。
これらの設定に加えて、haskyやjestなどが導入されているテンプレートからプロジェクトを作成したい方は、他に有志の方が公開しているテンプレートがあるので、そちらの利用も検討してみてください。

参考

テンプレートの使い方

Next.jsの公式のチュートリアルと同じように使えます。
以下、create-next-appでプロジェクトを作成し、開発サーバーを起動する手順です。

$ npx create-next-app app --example "https://github.com/redimpulz/nextjs-typescript-starter"
$ cd app
$ yarn dev

まとめ

TypeScript導入済みのシンプルなNext.jsのスターターテンプレートを紹介しました。

設定の記述量も最低限にしているので、分かりやすいかと思います。
TypeScriptでNext.jsのチュートリアルをやりたい人などは、ぜひ使ってみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

実践 React Native 運用チェックシート

この記事はReact Native Advent Calendar 2020の1日目の記事です。

実践 React Native 設計チェックシート 続編の運用編として書きます。

キッチハイクのアプリはReact Nativeで開発を始め、4年目に入りました。これまでのプロダクション運用で、7回のReact Nativeアップグレードと100回以上のリリースを行いました。他にも運用で得た知見・ノウハウをまとめたいと思います。

これからReact Native をプロダクションリリースする方、すでに運用中の方に読んでいただければ嬉しいです。これから導入したいライブラリや仕組みも多く、まだまだ運用は楽になると思っています。

2020年に公開されたReact Native開発・運用ストーリー

2020年は複数社のReact Native開発・運用ストーリーが公開された年になりました。その中でもこれから紹介するShopify, Khan Academy, Wixは多くのユーザーに使われており、大規模アプリのReact Native開発・運用ストーリーとその知見を知ることができます。

Shopify

Shopifyは自社のテックブログで、React Native is the Future of Mobile at Shopify という記事を2020年1月29日に公開し、React Nativeへの期待をあらわしました。またQiitaには、Shopifyから2020年4月15日に配信されたpodcast、ShopifyのVP Engineeringのインタビューセッションについて書かれた記事があり、 RNを辞めたAirbnb、RNを採択したShopify の部分は多くのReact Nativeエンジニアが知りたいことではないかと思います。

Khan Academy

2020年9月1日、InfoQに Khan AcademyのReact Nativeへの移行のメリットとデメリット が掲載されました。この記事は、Khan Academyのテックブログに投稿された Our Transition to React Native をもとに、2年間にわたる移行がまとめられています。既存のネイティブアプリをReact Nativeで再実装する過程での段階的なステップが書かれており、移行がスムーズに行われたものではなく困難な箇所もあったことがわかります。

Wix

Wixのテックブログで2020年11月10日にスタートしたReact Native At Wixを紹介します。この記事はWixでの5年間のReact Native開発をコミュニティに共有する目的で書かれており、8パートからなる長編が予定されています。現在はまだ第2パートまでしか公開されていませんが、すでに大人数チームでの開発を効率化するための自社フレームワーク開発というディープなものになっており、続きが楽しみです。

リクルート

日本の会社では2020年03月31日の記事でリクルートがB2Bのスマホアプリ開発でReact Nativeを採用した理由@ITに掲載されています。

React Native アプリ運用のポイント

4年間の運用で大切だと感じたのは、React, React Nativeの最新情報キャッチアップ力です。React Nativeは開発途中にあり、コアアーキテクチャレベルで変更が行われています。各バージョンのBreaking changesやDeprecatedの情報をキャッチアップすることで、React Nativeの進化についていくことが可能で、最新のアーキテクチャによる恩恵を受けることができます。逆に、少し目を離すとアップグレードが困難になります。冒頭に紹介した他社が公開している事例や開発ストーリーも貴重な情報源となります。公開に感謝しつつ参考にし、こちらも公開できる知見は積極的に公開するような取り組みで、React Nativeの大きなエコシステムが回ることに少しでも貢献できればと考えています。

まずは、React Native本体のアップグレードについてです。運用チェックシートの最初のポイントは、定期的にReact Nativeアップグレードする体制と工数があるか、という点です。

過去7回のReact Native アップグレード

キッチハイクは4年間で7回のアップグレードを行ってきました。

バージョン 開始時期 マージ 期間 コメント
0.42.3 2017/06/30 -- -- 開発開始
-> 0.46.2 2017/08/09 -- -- アプリ公開バージョン
-> 0.50.4 2017/11/24 2018/01/17 約2ヶ月 大変だった
-> 0.53.3 2018/04/11 2018/04/18 1週間 あっさり
-> 0.56.1 2018/10/18 2018/10/31 2週間
-> 0.59.6 2019/06/10 2019/07/10 1ヶ月 時間かかった
-> 0.59.10 2019/11/07 2019/11/15 1週間 マイナーアップデート
-> 0.62.2 2020/05/11 2020/05/27 2週間

アプリをストアに公開後、半年に1回のペースでアップグレードを行ってきました。短いものだと1週間、長くて約2ヶ月かかってます。

0.46.2 -> 0.50.4 の思い出

初めてのアップグレードだったのでとにかく大変だった印象です。11月から着手して、クリスマスがすぎ、大晦日がすぎ、正月がすぎてもなかなかエラーが解消せず、先が見えない不安がありました。今でも覚えているのが、AIRMap(いまはreact-native-maps)とfbsdkです。AIRMapはAirbnbが開発していたライブラリでその名の通り地図表示なのですが、ネイティブ層のビルドエラーで苦しんだ記憶があります。かなりObjective-Cのソースコードを読みました。のちのAirbnbのReact Nativeやめる宣言により、ライブラリ開発がどうなるか心配だったのですが無事にReact Native Communityに移管されてメンテナンスされてます。ありがたし。

もう一つのfbsdkはFacebookログインに必要なライブラリなのですが、React Nativeと開発元が同じなのになぜこうもエラーになるのか、と。結局、fbsdkの最新バージョンでのビルドはあきらめて、一つ戻したバージョンにしたと思います。

キッチハイクはWebエンジニアがReact Nativeでアプリ開発を始めたので、ネイティブに慣れてなかったこともあり、XcodeやAndroid Studioのビルドエラーの解決に時間がかかりましたが、いまとなってはここでチームに知見を貯めれたことが、この後数回行われるアップグレードに良い影響を与えていると思います。

0.50.4 -> 0.53.3の思い出

前回2ヶ月かかったので、そのくらいかかるのかなとのぞんだところ、結果的には1週間で終わったのがこのアップグレードです。しかし、またreact-native-mapsでエラーが。エラーというか、Androidだけ地図が表示されないというものでした。前回と今回の結果で、mapsライブラリはアップグレード時の最重要リスト入りします。この後のアップグレードでも地図まわりは特に慎重に確認することになりました。

前回の経験と、React Native本体のBreaking changesが少なかったことから、いくつかエラーは出たものの簡単な修正で解決できるものでした。react-native-maps以外は。

0.53.3 -> 0.56.1の思い出

この回からPod, Gradleも含めて各ライブラリもできるだけ最新に追従しようと試みます。今回はmapsのエラーこそなかったものの、各ライブラリをアップデートしたことによるスタイル崩れが発生し、時間を費やすことになります。また、この期間にReact Nativeを開発していたら覚えている方も多いと思われる、日本語キーボードの変換問題(CJK問題)にも遭遇します。

前回よりも時間がかかりましたが、2週間でアップグレードが終わりました。

React Native本体以外のライブラリ

キッチハイクではReact Native以外のライブラリを、ユーザー影響・影響範囲の2軸で分類し、バージョンアップの方針を決めています。

image.png

その中でもユーザー影響大・影響範囲広に位置する5つのライブラリを主要ライブラリとしています。

  • react-navigation
  • react-native-firebase
  • react-native-push-notification
  • expo-secure-store
  • @react-native-community/async-storage

主要ライブラリのアップグレード

主要ライブラリのようにユーザー影響が大きく・影響範囲が広いものは、個別プロジェクトとしてアップグレードを行っています。
特に react-navigationreact-native-firebase の2つは大きな変更が入ることが多く、情報のキャッチアップを行っています。

React Navigation

image.png

v3からv4になる時に、stack, tabs, drawerにパッケージ分離が行われました。
またv4からv5でAPIの刷新が行われ、互換性がなくなりました。

現在v6が開発中で、ロードマップがissueとして公開されています。
Roadmap for v6 · Issue #8981 · react-navigation/react-navigation

キッチハイクではv4を使っており、タイミングをみてv5に移行したいと考えています。

React Native Firebase

image.png

v5からv6になるときにアーキテクチャが変更され、各モジュールで細かく分割されました。自社で必要な個別モジュールのみに差し替え作業を行いました。

キッチハイクでは以下のモジュールを使用しています。

  • @react-native-firebase/app
  • @react-native-firebase/analytics
  • @react-native-firebase/dynamic-links
  • @react-native-firebase/messaging
  • @react-native-firebase/remote-config

リリース運用

日々リリースしていくなかで考慮が必要な点をピックアップします。

Apple / Google ベンダー対応

React Nativeアプリを運用していく中でもベンター対応は必須になってきます。こちらもReact Nativeto
同じくキャッチアップが重要になります。App Store ConnectやGoogle Play Consoleに目立つように表示されるものもあれば、メールで送信されるものもあります。

いくつかはReact Naitve本体のアップグレードで対応できるものもありますが、個別に対応するものもあります。これまでに対応したもので、印象に残っているものを事例として以下のものがありました。

iOS

iOS 13

  • Sign in with Apple
  • Launch Storyboard
  • ダークモード
  • UIWebView廃止

iOS 14

Android

クラッシュレポート

長期的に安定したアプリを提供するため、クラッシュレポートの運用が必要になります。
React Nativeはネイティブアプリとは違い、各アプリベンダーが提供しているクラッシュレポートではJavaScriptレイヤーのエラー情報を得ることができません。

そこでJavaScriptレイヤーのエラー情報を得るためにライブラリを導入する必要があります。
代表的なものは以下のものです。

キッチハイクではSentryを使っています。

アプリサイズ

リリースを重ねるごとにアプリサイズは大きくなっていきます。キッチハイクアプリはフォントを入れた時にサイズが大きく増えてしまいました。サイズが約100MB程度になっており、Wifiに接続していない屋外でのインストール・アップデートに時間がかかってしまうので、アプリのサイズは少しでも減らしたいところです。

Androidアプリについては比較的簡単にアナライズして、アプリサイズを減らすことにトライできます。弊社のテックブログに記事がありますので、参考にしてもらえればと思います。

React Nativeアプリのサイズを35%減らした話 Android編 - KitchHike Tech Blog

セキュリティ対策

アプリをプロダクション運用する際にセキュリティに関する対策も必要不可欠になってきます。
React Native公式ドキュメントにもSecurityのページがあり、以下の3つの観点が書かれています。

  • ストレージ(Async Storage, Secure Storage)
  • 認証とDeep Link(OAuth2とリダイレクト)
  • ネットワークセキュリティ(SSL Pinning)

キッチハイクではSecure Storageの対応を行いました。ライブラリはUnimodule化されたexpo-secure-storeを使っています。UnimoduleでExpoのライブラリを使えるのは便利です。

マーケティング運用

サービスを成長させていくなかで、デジタル広告戦略でアプリのインストールを増やしたい場合、広告トラッキングが必要になります。React Nativeアプリと連携できる代表的なアトリビューションプロバイダーは以下となります。

キッチハイクではAppsFlyerとGoogle Analyticsを連携させて、BigQueryで分析しています。(現在は戦略上広告ストップしています)

image.png

以前、React Nativeアプリの広告計測事例で発表した資料を貼っておきます。

image.png

https://speakerdeck.com/shoken/react-native-with-appsflyer-and-firebase-analytics

おまけ : RN Featuresについて

ここからは弊社テックブログの宣伝になります。
React Nativeアプリのプロダクション運用では、情報のキャッチアップが重要になることが実際に運用して感じたことです。そこで弊社では毎月1回、React Native関連の情報を有志が持ち寄ったものをまとめて、テックブログに公開するRN Featuresという活動を続けています。

直近のエントリーはこのようなものです。

これからも毎月1回、キャッチアップしたReact Nativeの情報を公開していきますので、興味がある方はぜひ購読してもらえればと思います。


最後まで読んでいただいてありがとうございました。

2日目は @nekoniki さんです。お願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactがわからないというよりJavaScriptがわからなかったのです

はじめに

Reactはユーザインタフェース構築のためのJavaScriptライブラリです。次のような利点があるため、HTMLCSSjQuery を学習し、Web制作者目指しているような方もReactにも興味を持つと良いと思いました。JavaScriptをある程度学習していれば、Reactはそんなに難しくないからです。

  1. コンポーネントベース
    • ブロックを組み立てるようにユーザインタフェースを構築できる
    • 見通しがよく、苦痛が少ない
  2. 一度学習すれば応用が効く
    • サーバー上でもレンダーできる
    • モバイルアプリケーションの中でも動く(React Native)

Reactを二ヶ月くらい学習し、真似してコードが多少書けるようになった時、私はそのコードにどうにも腑に落ちませんでした?。その頃の私はReact以前にJavaScriptがよくわかっていなかったのです。そんな少し前の自分に説明するつもりで書いてみた記事になります。

サンプルコード1

このサンプルは[Toggle]ボタンをクリックするたびに、ボタンの上の絵文字が?と?の間で切り替わるという簡単なものです。

image.png

import React, { useState } from "react";

const App = () => {
  const [liked, toggleLiked] = useState(false);
  const handleToggle = () => toggleLiked(!liked);

  return (
    <>
      <p>useState Sample #1</p>
      <p>{liked ? "?" : "?"}</p>
      <button onClick={handleToggle}>Toggle</button>
    </>
  );
};

export default App;

StackBlitz

今の自分useStateステートフック)を使用しています。シンプルで何の問題もないですね。」

昔の自分「なんだか、わかったようなわからないような。すっきりしないのです?。そもそもフックってどういうこと?意味があまりわからないです?。」

今の自分フックは何種類かあるのですが、ステートフックとはクラスコンポーネントでは普通に持つことができるステート(コンポーネントがその内部に変数のように持つオブジェクト)をファンクショナルコンポーネント(関数コンポーネント)にも持たせられるようにするための関数です。それでは、次のコードはわかりますか?」

サンプルコード2

こちら見た目や動きは先程のものと同じです。コードの行数は16から36(うち3行はコメントだけの行)と倍増しました。

import React from "react";

export default function App() {
  const stateArray = React.useState(false); // stateArray[0]の初期値をfalseにする
  // 戻り値は以下の配列
  // stateArray[0]: true(?) または false(?)が入る
  // stateArray[1]: stateArray[0]を設定する関数
  let liked = stateArray[0];
  const setLiked = stateArray[1];

  function handleToggle() {
    if (liked === true) {
      setLiked(false);
    } else {
      setLiked(true);
    }
  }

  if (liked === true) {
    return (
      <div>
        <p>useState Sample #2</p>
        <p>?</p>
        <button onClick={handleToggle}>Toggle</button>
      </div>
    );
  } else {
    return (
      <div>
        <p>useState Sample #2</p>
        <p>?</p>
        <button onClick={handleToggle}>Toggle</button>
      </div>
    );
  }
}

StackBlitz

昔の自分「コードが長くなりましたが、大分親しみやすいコードです。React.useSateが要素数2の配列を返し、0番目の要素が設定した値の参照用。1番目の要素が、その値を設定する関数を指し示すわけですね。」

今の自分サンプルコード1(StackBlitz)の以下の部分は、Reactライブラリのエントリポイント(名前空間)のReactをインポートし、Reactの中にあるuseStateを特定の名前付きでインポートをしています。この辺りの詳細はこちらを見てください。」

import React, { useState } from "react";

今の自分サンプルコード1(StackBlitz)の以下の部分は、分割代入(Destructuring assignment)という構文で、配列から値(あるいはオブジェクトからプロパティ)を取り出して、別個の変数に代入できる書き方になります。

const [liked, toggleLiked] = useState(false);

昔の自分アロー関数は知っているのですが、Appという関数の中に handleToggleという関数があるのはどういうことでしょうか?」
今の自分「これはクロージャというやつです。マトリョーシカ人形の親子で例えると、親(関数)から子(関数)の心の内(子の変数など)は見ることができませんが、子からは親の心の内(親の変数など)を見ることができる仕組みです。」

昔の自分「なるほど。この辺り、Reactの話しではなくて、全部JavaScriptの話ですね?」

今の自分「そのとおりです。そして、return で返しているHTMLみたいなタグ構文はJSXと呼ばれるJavaScriptを拡張した書き方です。」

昔の自分「それは教材で習いました。これがコンパイルされて、ブラウザで実行可能なコードに変換するのでしたね?」

今の自分「そうです。上のコードはReactのライブラリの関数を1つ呼んでいることと、JSXが使われている以外は表面上はただのJavaScriptとして理解できます。」

Reactを少し勉強してみて感じたこと

1. JSXを用いたUIコンポーネント作成は楽しくてわかりやすい

  • 見通しよく作成でき、他人と分担もしやすいです
   <Navbar />
   <TodoList />
   <Footer />

2. 親コンポーネントが子コンポーネントに props を渡す方法は1方向で単純でわかりやすい

3. ファンクショナルコンポーネントクラスコンポーネントよりシンプルで書きやすい

  • これから作るだけならファンクショナルコンポーネントだけでよいと思います
  • 必要ならファンクショナルコンポーネント以下のフックを組み合わせるなどして、ローカルステート、グローバルステート、ライフサイクルを扱うなど、クラスコンポーネントと同じようなことができる

4. BabelWebpackとかのややこしい設定は不要

5. CSSは好みに合わせて色々な書き方ができる(CSS とスタイルの使用

  • 個人的にはstyled-componentsが良さそうに思っていますが、賛否両論あるみたいですね。

参考資料

  1. 現代の JavaScript チュートリアル
  2. JavaScript学習の格安教材( LinkdIn Premium、ドットインストールPREMIUM、YouTube ) // 私のブログのメモです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Backstageを完全に理解した

はじめに

Ateam Group Manager & Specialist Advent Calendar 2020の2日目は株式会社エイチームライフスタイル 執行役員CTOの @tsutorm がお送りします。例年マニアックなネタで好評を頂いておりますので、今年もマイペースで行こうと思います。 :smile:

一応 完全に理解した1 はネタです。

この記事の概要

  • エンジニア向けポータル Backstage をうまく使って社内外サービスの "あれどこ / これ誰が見てるの / 詳しいのだれ" を減らせそうだよ
  • Spotify社が作ったOSSでpluggableなreact-appとして見てもとっても勉強になるよ

Backstageとは

Backstage logo

開発組織が関わるサービスが少なく開発チームも小規模で、1~2サービスだけ見ていれば良い場合はそれほど問題は起きづらいですが、複数のサービスを複数の組織が連携しながら見るような開発組織規模になってきた場合、次のような問題が起きやすくなります。

  • 誰がメンテナか不明なミニ社内サービス (意外と重要な事をしている)
  • 標準化の不備によるノウハウ欠如
  • 全体把握不良によるオンボーディングの長期化
  • 車輪の再開発

ありがちですよね。

Backstage とは、Spotify社で上記の課題を解消するため、4年前から開発され今年3月にOSS(Apache License, Version 2.0) として公開された、開発者向けポータルシステム2 で、社内のソフトウェア資産(マイクロサービス、ライブラリ、データパイプライン、Webサイト、MLモデルなど)を サービスカタログ として管理することにより、開発者自身が民主的,自律的に上記問題へ対処し... 要するに快適開発ライフが維持できますというやつ です。3

ThoughtWorks TECHNOLOGY RADAR Vol.23 でも今回からAssess評価に追加されて今後注目されていくかと思いますが、意外とQiita含めて日本語での触ってみた事例が少ない状況でしたので、今回はざっと触って "完全に理解した" 感を皆さんと共有できればと思います。

主な機能

image.png

サービスカタログ(Backstage Service Catalog)

開発組織が関係するサービス等を"カタログ"としてポータルへまとめることができます。一覧は上記の感じです。
後述するタイプ別に大きく区分けされ、その中で自分がオーナーのサービス、関係が深い(スターをつけた)サービス、全体での一覧化や、タグ、キーワードでの検索UIがあり、カタログ化されたサービスを素早く見つけられる様になっています。

カタログで管理される主な項目は

  • 基本情報
    • 識別名と、その概要
    • ライフサイクル
      • 実験的/非production: experimental
      • 本番稼働: production
      • 廃止予定: deprecated
    • タイプ
      • (基本的に)APIを持つサービス service、 Webサイト website、 ライブラリ library
    • オーナーとなる人物(識別子としては メールアドレス), 組織
    • タグ(ex: typescript, ruby とか検索する際に使う
  • ソースリポジトリ
    • github, gitlab, etc...
  • API仕様
  • 関連する技術文書(TechDocs)

と言ったものを kubernetesのconfigに似たyaml形式で定義することによって、以下のようなダッシュボードから扱えるようになります。

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: petstore
  description: Petstore
spec:
  type: service
  lifecycle: experimental
  owner: pets@example.com
  implementsApis:
    - petstore
    - streetlights
    - hello-world

こんな感じ image.png

ソフトウェアテンプレート(Backstage Software Template)

記事作成時点ではまだα版機能です。

社内で新しくアプリケーションを開発し始めるときのテンプレートを管理できる機能です。ここから新規リポジトリを社内の所定リポジトリに向けて展開することで、いちいち「あのxxxってリポジトリのyyyをzzzに変えると今んとこ一番社内でいい感じの構成になるよ」みたいなのが撲滅されます。

image.png

今回はあまり触ってないので紹介としてはこの程度ですが、Cookiecutterを利用してテンプレート構成から所定のリポジトリに初期プロジェクトが自動展開されるような感じです。ちょっと後述します。

技術文書(Backstage TechDocs)

Markdown形式で、技術的なドキュメントを記載できる機能です。
これも今回あまり触ってないのでさらっと公式からイメージだけ

TechDocs

触ってみる

まぁともあれ触ってみましょう。公式のGetting Started にわかりやすく書いてますが、node(v14), npx と python と yarn と docker がセットアップされてる必要があります。

それぞれの導入は割愛します。

公式からgit cloneする手順が書いてありますが、backstage自体のメンテをするわけではなければ、Create an AppからがBackstageを自分の所用に作っていく手順になるので、こちらで進めます。

(もちろん git clone の手順でも動きます)

ローカルで動かしてみる

動作前提が揃っている状態で以下で終わりです。簡単。

$ npx @backstage/create-app

アプリ名とbackendに使うDB(PostgreSQLかSQLiteの二択)を指定すれば、後はスクリプトが走って終わりです。

$ npx @backstage/create-app
? Enter a name for the app [required] lifestyle
? Select database for the backend [required]
  PostgreSQL
❯ SQLite

Creating the app...

 Checking if the directory is available:
  checking      lifestyle ✔

 Creating a temporary app directory:
  creating      temporary directory ✔

 Preparing files:
  copying       .eslintrc.js ✔
  templating    .gitignore.hbs ✔
(中略)
  copying       EntityPage.tsx ✔

 Moving to final location:
  moving        lifestyle ✔

 Building the app:
  executing     yarn install ✔
  executing     yarn tsc ✔

?  Successfully created lifestyle

See https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers

アプリケーションとしては、フロントのReactAppとバックエンドにexpressでAPIが立ってる構成ですので、実行もそれぞれ行います

$ cd lifestyle
$ yarn start & # フロントアプリの起動
# (割愛)
$ yarn workspace backend start # バックエンドアプリの起動
# (割愛)

あとはブラウザから http://localhost:3000/ にアクセスすれば冒頭の画面が出てくるはずです。

アプリケーション全体の設定は app-config.yaml に定義されたものをロードして利用しています。
親切な事に、デモ用のexampleが豊富に入った app-config.yaml を配備してくれた状態になってるので、本来なら同階層の app-config.production.yaml を軸に自由に変えていく。というのがセオリーかと思います。

今回はさらっと触るので、このデモの環境をベースに進めます。

サービスカタログに手動で追加してみる

任意のエディタで app-config.yaml を開いて、catalog:まで読み進めてください。

catalog:
  rules:
    - allow: [Component, API, Group, User, Template, Location]
  locations:
    # Backstage example components
    - type: url
      target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml

アプリ側ではAllで6つ, Ownedとして3つのアプリが表示されますが、ここでは target として githubのリンクが示されているだけです。

おもむろにリンクをブラウザで開いてもらうと、以下の yaml になっています。

apiVersion: backstage.io/v1alpha1
kind: Location
metadata:
  name: example-components
  description: A collection of all Backstage example components
spec:
  type: github
  targets:
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/petstore-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-order-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/podcast-api-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/queue-proxy-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/searcher-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-lib-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/www-artist-component.yaml
    - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/shuffle-api-component.yaml

おー。なんとなく掴めましたかね。試しに、petstore-component.yamlを見に行きましょう。

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: petstore
  description: Petstore
spec:
  type: service
  lifecycle: experimental
  owner: pets@example.com
  implementsApis:
    - petstore
    - streetlights
    - hello-world

はい。ここで冒頭のカタログの設定が現れました。Backstageではこの単位を Component と呼ぶようです。

petstore-component.yaml に相当するものを自分たちが開発しているサービスのリポジトリルートに catalog-info.yaml みたいにおいておいて、それを指し示してあげれば、自動的にBackstageのカタログプロセッサが解釈してリストとして表示してくれる・・・と。そういう機構になってます。

したがって、試しにご自身のgithubなんかに適当に上記を改変したものを突っ込んで、暫く待つと...

image.png

jwt-example 追加されましたね!

今回は触れませんが、metadata.annotationsでTechDocsを関連付けたり、spec.implementsApisでOpenAPIで書いた仕様書をAPIとして公開したり、、Componentを起点に様々な情報を集約していくことができます。

ソフトウェアテンプレートの裏側(Cookiecutters)

他にも、組織内のベストプラクティスを詰め込んだソフトウェアテンプレートを登録しておいて、新規アプリを立ち上げる時にオレオレベスト構成じゃなく、組織としてのベスト構成を標準化する機構があります。

こいつ内部でどういう動きになっているか気になりますよね?(ならないですかね?)

./packages/backend/src/plugins/scaffolder.ts を見てもらうとわかるのですが cookiecutter というプロジェクトテンプレートのツールを使っています。
実際に動作させてる所はここですね

ちょっとまだ試せてないですが、cookiecutterの流儀に乗せればある程度いろんな環境が作れる(ためしてないですが、gem installとか走らせるならdocker側の調整も必要かな・・・)っぽいです。

独自のtemplaterの作成もできるようなんで、頑張れば何でもって感じですかね。

それ以外色々

コアコンポーネント以外にも TechRadarCost Insightsなど、プラグインとして機能強化可能な仕組みと、それを取り巻くマーケットプレイスエコシステムもあり、拡張性は非常に高そうです。

image.png

またアプリケーション自体もLernaを利用した大きなReact + Typescriptなappで、デザインについてもBackstage DesignTeamが全体設計に基づいてマテリアルUIを採用していて 参考になるかなと感じました。

ただ、記事作成時点でまだまだ仕様も変わるし3つのフェーズの最初が終わった段階だよ。 となっています。本番利用はそれなりに人柱上等の前提で進めた方が良さそうです。

まとめ

  • ソフトウェア資産が増えてきて全体視界不良感の解消を目的として、開発者ポータル/サービスカタログツール Backstage 注目してみても良いかなと思いました。
  • 内部のアーキテクチャ Lerna, cookiecutter も面白いので見てみると ○
  • まだロードマップの前半だからご利用は計画的に!

次回告知

Ateam Group Manager & Specialist Advent Calendar 2020の3日目は @rf_p がお送りします。
ニーズを捉えてMVPを超高速リリースする彼のアドカレ、是非ご期待ください!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OOUI の考え方を拝借して Atomic Design を再考してみる話

こんにちは。ひらやま(@rhirayamaaan)です。
この記事は、GLOBIS Advent Calendar 2020 に参加している記事です。

さて、Atomic Design という言葉が巷で話題になってからだいぶ日は経ちますが、私の「Atomic Designってデザイナーには難しくない!?という話」という記事の LGTM 数も現時点(2020/12/08 時点)でゆるやかに増加傾向にあり、まだまだ Atomic Design を考えている人たちは多いのではないかと思います。

そんな中で、昨今「OOUI」という言葉をよく耳にするようになりました。
ソシオメディアの上野さんが「オブジェクト指向UIデザイン」という本を刊行されたことで、より一層この言葉が世に広がったように思えます。

オブジェクト指向というものを UI デザインの領域に持ってくることで、エンジニアがオブジェクト指向でプログラムを書くように、ユーザがオブジェクト指向で UI を使えるようになるはずです。

例えば「商品を購入したい」と考えた場合、「購入する」という行動を先に選ぶのではなく「商品」という「オブジェクト」を選択するはずです。

このようにオブジェクトを選択してから、どう行動をするかを考えるのが自然です。

それを可能にするのが OOUI という設計方法であり、これは Atomic Design を設計する上でも参考にできるのではないかと考えました。

この考えをベースに持った状態で、Atomic Design による設計をもう一度考えてみたいと思います。

Atomic Design の「ユーザ」は誰なのか

Atomic Design は、UI を一つ一つ分解して共通化を図り、車輪の再開発を減らしていくために作られることが多いのではないでしょうか。

例えば、Button のようなコンポーネントを作成しておけば、いちいち角丸の矩形を作成して色を塗って文字を入れ…という作業が不要になり、簡単に Button を配置できるようになります。

今では、Figma, Sketch 等の UI 作成ツールでコンポーネント化できますし、言わずもがなですが、プログラミングにおいても React, Vue, Angular 等で同様にコンポーネント化できます。

使い回しが効くように Atomic Design を設計するということは、その出来上がったコンポーネントライブラリを誰かに使ってもらうために作ることが目的となります。

では、誰に使ってもらうためなのでしょうか?

それはもちろん、デザイナーやエンジニアです。

Atomic Design はデザイナーやエンジニアがものづくりをする過程においてより創造的な活動を行えるように、ものづくりにおける無駄を省きながら、ものづくりにより一層集中できる環境を提供する一要素として位置づけられるべきです。

つまり、Atomic Design を設計する上で重要なのは、デザイナーやエンジニアがオブジェクトベースでものづくりをできるようにすることだと私は考えています。

Atomic Design の分解・分類の難しさ

例えば、以下のような UI があったとしましょう。
Empty_A_5.png
モーダルで確認を促す UI です。

この UI をユーザ目線で考えるとタスク指向寄りなデザインではありますが、今回はその視点では考えません
今回考えたいのはあくまでも、「この UI を作る人に対して、どう要素を分解しておくとものづくりがしやすいか」なので、この視点で考えを進めていきます。

上記のデザインを見たときに、みなさんならどのようにコンポーネントを分解していくでしょうか?
一緒に見ていきたいと思います。


Atoms を定義してみる
まず、Heading, Button, Checkbox というような要素を Atoms として定義できそうです。
私の記事でも紹介していますが「Atomic Design ~堅牢で使いやすいUIを効率良く設計する」という本に以下の文章が記述されています。

つまり、Atoms層は、「それ以上UIとしての機能性を破壊しない最小要素」となるように分割します。

機能性を破壊しない形で最小単位になっていればいいので、Atoms は上記の分解で良さそうです。

Molecules を定義してみる
次に、もう一つ大きな要素を考えていくと、白い矩形で囲われている箇所でコンポーネントにできそうでしょうか。
Atoms 群で構成されている要素なので、こちらは Molecules として定義してみましょう。
こちらは確認を促すので Confirm という名前で定義しておくと、使い回しが効くような感じがします。

Organisms を定義してみる
最後に、さらに大きな要素を考えると、この Confirm が黒い半透明のオーバーレイを活用してモーダルで表示されている状態で一つのコンポーネントにできそうでしょうか。
名前を ConfirmModal という名前にして共通化しておくと良さそうな感じがします。


というように、小さい要素から順に分解してみましたがいかがでしょうか?
みなさん、何か違和感を感じませんでしたか?

Atoms まではそこまで違和感がなかったと思いますが、Molecules あたりから、え?本当にそれでいいの?という、なんとも言えないもやもやがあります。
(私が Molecules 以降、曖昧な言い回しをしているからというのもあるとは思いますが……)

これは、 ConfirmConfirmModal を作ったときに、果たしてそれらのコンポーネントは本当に使いまわしの効くコンポーネントなのか?という疑問が残るからではないでしょうか。

二つのコンポーネントは、実は限定的な使われ方をするものであって、わざわざ Molecules や Organisms として共通化したパーツとして定義する必要がないものかもしれない、という考えを拭いきれません。

もしかしたら、Templates に定義した1ページの中の一つの要素として ConfirmConfirmModal を定義していた方が、逆にスッキリするかもしれません。

このような混乱は、Atomic Design としてパーツを分割することだけを盲目的に考えているときに発生してしまいます。

しっかりと、利用者であるデザイナーとエンジニアのことを考えた上で、もう一度要素を分解してみたいと思います。

アクションをオブジェクト化する

前提として、一旦便宜上 Molecules に Confirm が定義されているとして、ConfirmModal について考えていきたいと思います。
Confirm については後ほど考察します。)

まずは、ConfirmModal を作る際に、作り手の思考を簡単に言語化してみます。

Confirm をモーダルで表示する」

この文言をそのままにコンポーネントを作成すると、ただ Confirm にモーダル機能を付随させたオブジェクトが出来上がってしまいそうです。
そうなると、使い回しが効きそうなコンポーネントになっているかという疑問は拭いきれません。

なので、ここで OOUI の考え方を使って考えていきます。

上記の文章をもう少し良く見てみると、以下のように分解できるはずです。

  • Confirm というオブジェクト」
  • 「対象をモーダル表示するというアクション」

OOUI では、この「アクション」自体もオブジェクトとして捉えていきます

アクションを「オブジェクト化」した上で言語化した文章を整理すると、以下のようになります。

  • Confirm というオブジェクト」
  • 「○○をモーダル表示するオブジェクト」

このように、「モーダルで表示する」というアクション自体をコンポーネントにしてしまうことができるのです。

さらに「○○をモーダルで表示するオブジェクト」というものは、Atoms の「機能性を破壊しない最小要素」にも一致します。

なので、Organisms として作っていた ConfirmModal を作らなくても、Confirm という Molecules と Modal という Atoms を組み合わせるだけで実装できてしまうのです。

もし、React で UI を作成することを想定した場合、以下のようにシンプルにコーディングできます。
(トップページ用の Template を作成している想定のコードです。)

components/templates/top.tsx
import React, { FC } from 'react'

import { Modal } from '../atoms/Modal'
import { Confirm } from '../molecules/Confirm'

export const Top:FC = () => {
  <>
    <div>
      something...
    </div>
    <Modal>
      {/* この children が ○○ にあたる部分です */}
      <Confirm />
    </Modal>
  </>
}

今回のように OOUI の考えを借りながらしっかりとコンポーネントを定義できていれば、わざわざ Organisms を作らなくても Template に直接 Atoms と Molecules を組み合わせて UI を作成できるのです。

アクションのオブジェクト化のメリット

Modal というアクションをオブジェクト化したコンポーネントを作成したことによって、どのようなメリットがあるのかをもう少し説明していきます。

Confirm 以外のオブジェクトもモーダル化できる

保守をしていく上で、いくら最初に「確認でしかモーダルは使わない!」と決めたとしても、ビジネス要件によってどうしても他のものをモーダルにしたくなるケースは出てきてしまうのが常です。

今回のようにアクションをオブジェクト化しておけば、Confirm 以外コンポーネントをモーダル化することができます。

何かしらの Organisms を入れるのでもいいし、ただの文字列や、緊急的に style 属性でスタイリングしたものもモーダル化できます。

モーダル自体の表現(オーバーレイのスタイリングや中央配置の仕方等など)の仕方は固定しつつ、中身のコンテンツだけ可変にできます。

このように、使い方に余白が生まれるため、便利にしつつも表現を狭めないような設計にすることができます。

Confirm をモーダル以外の表現をしても違和感がない

例えば Confirm を、画面の下側にフローティングさせて表示させたくなったとします。

その場合に ConfirmModal というものが前例としてあると、ConfirmFloating というコンポーネントを作って法則性を担保したくなります。

しかし、今回のように設計しておけば、Floating というコンポーネントに切り出すことができるので、好きなものをフローティングさせられるようになるわけです。

このような考えを持っておけば、Accordion, Carousel というような機能も Atoms としてコンポーネントを切り出すことができます。

アクションを「機能性を破壊しない最小要素」で分解していることになるので、Atoms として切り出してよいのです。

ConfirmModal というコンポーネントも作れる

もし、ConfirmModal というコンポーネントの出現頻度が高い場合には、上記の設計から再度 ConfirmModal を作ることもできます。

components/organisms/ConfirmModal
import React, { FC } from 'react'

import { Modal } from '../atoms/Modal'
import { Confirm } from '../molecules/Confirm'

export const ConfirmModal:FC = () => {
  return (
    <>
      <Modal>
        <Confirm />
      </Modal>
    </>
  )
}

もともと考えていた ConfirmModal のようにモーダルの機能と Confirm が密結合になっていると分解するのは大変です。

しかし、今回のように疎結合なものを作っておいて結合するのは容易になので、Atoms をしっかりと定義することはとても重要です。

見た目を頼りにグルーピングしない

さて、次に Molecules の Confirm について考えていきたいと思います。

先ほど、Confirm を考える際に、以下のような考えでコンポーネント化しました。

白い矩形で囲われている箇所でコンポーネントにできそうでしょうか。

このように、「矩形で囲われている」というような形で「見た目」をベースにコンポーネント化してしまうと、密結合になりやすく、使い回しが効かないコンポーネントになってしまいがちです。

それを避けながら使いやすいコンポーネントを作るためには、Confirm に必要なオブジェクトとアクションは何かを考えてみると良いです。

もう一度 UI を見てみましょう。

Empty_A_5.png

まずは、Enroll, Cancel というアクションを実行するものは必ずないといけません。
これがないと「確認」に対しての「回答」をユーザから得ることができないからです。

なので、「押したら○○する」というアクションをオブジェクト化した Button というオブジェクト(Atom)を Confirm 内で利用するのが良さそうです。
ただし、Button 内の Enroll, Cancel という文言自体は、Confirm の利用する文脈に依存するので、外から流せるようにすべきです。

次に、タイトルと説明文に関してです。
これも、存在していないとユーザに質問を行えないので確認が取れないため必須です。
説明文は文章を外から流せるようにし、見出しはそのサービス内で適切な見出しを表現する Heading というオブジェクト(Atom)を Confirm 内で利用するようにします。
見出しの文言自体は Enroll と同様に、文脈によって変わるので、外から流せると良いでしょう。

そして最後に、チェックボックスについてです。
このチェックボックスは、「Don't show this message(今後このメッセージを表示しない)」と書かれているため、Confirm の利用文脈においては必要なチェックボックスです。
なので、このチェックボックスを使わなくても良いケースも考えられるため、この機能はオプションとして位置づけられると考えられます。

この場合に、このチェックボックスを闇雲に Confirm 内で利用するようにしたとします。
そうしてしまうと、もし保守をしていく中でチェックボックスを使わないケースの方がメジャーになっていったとしたら、チェックボックスの機能は完全に過多になってしまいます。

なので、ここはチェックボックスの機能を Confirm には含めずに、外部から様々なオブジェクトを代入できるようにした方が使い勝手が良さそうです。

上記を踏まえた上で、React でコード化してみます。

components/molecules/Confirm.tsx
import React, { FC, ComponentProps, MouseEventHandler } from 'react'

import { Button } from 'components/atoms/Button'
import { Heading } from 'components/atoms/Heading'

type Props = {
  title: string,
  description: string,
  cancelButton: {
    onClick: MouseEventHandler<HTMLButtonElement>,
    text: string,
  },
  continueButton: {
    onClick: ComponentProps<typeof Button>,
    text: string,
  },
  className?: string,
}

export const Confirm: FC<Props> = ({
  title,
  description,
  cancelButton,
  continueButton,
  className = '',
  children,
}) => {
  return (
    <div className={['confirm', className].join(' ')}>
      <Heading>{title}</Heading>
      <p className="confirm__description">{description}</p>
      {children ? <div className="confirm__note">{children}</div> : null}
      <div className="confirm__buttons">
        <button onClick={cancelButton.onClick} className="confirm__cancel">
          {cancelButton.text}
        </button>
        <Button onClick={continueButton.onClick}>{continueButton.text}</Button>
      </div>
    </div>
  )
}

考慮したいことを踏まえた上で、上記のようにコーディングしてみました。

ポイントとなる箇所は何箇所かありますが、今回ピックアップしたいのは、children を受け付けている点です。

children は、オプションであるチェックボックスの受け入れ口となります。

こうすることで「様々なオブジェクトを受け付ける」という要件を満たせています。
例えば、ラジオボタンによる選択式の UI でも、「詳細はこちら」というようにリンクを含んだ UI でも実装可能となります。

このように実装しておけば、Confirm という機能を保ちつつ、ある程度フレキシブルに UI を作成できるようになっているはずです。

ただ、一点注意していただきたいのは、今回の実装はあくまでも例であり、上記コードがベストプラクティスであるということを述べたいわけではありません。

どこを必須としてどこをオプションにするかは、サービスや事業の環境に応じて変わってくるので、最適解を明示することはできません。

しかし今回の例から、汎用性が高く、使い勝手の良いコンポーネント作成をする際のきっかけになればと思っています。

作成すべきコンポーネントがサービスや事業の中でどういう振る舞いをすべきを考え、それを一般化・抽象化し、その上でもう一度目の前にある UI の見たときに、どの要素と機能が必須・オプションなのかを考えていくことこそが、Atomic Design を基にコンポーネントを作成する上で重要だと考えています。

さいごに

いかがだったでしょうか。

ここまで散々つらつらと述べてきましたが、私自身、OOUI に対してとても詳しいというわけではないです。
しかし、OOUI の考え方をベースに Atomic Design を捉え直してみると、より使いやすいコンポーネント設計を考えやすくなる感覚があったので記事に起こしてみました。

実際に使いやすいコンポーネントライブラリを作るのはなかなか難しく、やってみないとわからないことも多いですが、今回の記事によって少しでも良いコンポーネントライブラリが作れるようになることを願っています。

また、今回 React でコンポーネント実装をしていますが、ツールやフレームワークが違っても、考え方自体は転用できるはず。
Figma や Sketch、Vue や Angular 等などの様々なツール上で、かつ職能を超えて、この記事の中身を参考にしていただけたら、大変嬉しい限りです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

完全個人用のURL管理サービスをReact×Firebase×Go×Herokuで作った話

はじめに

こんにちはRIN1208です。

この記事はタイトルに書いてある通り、完全に個人で使う用に作成したのでそれについて書いていきたいと思います。
今回作ったものはこちら

この記事はITRCアドベントカレンダーの1日目の記事になります。

 今回の構成

スクリーンショット 2020-11-25 14.17.15.png

今回は上記のような構成で作成しています。サーバーサイドはHeroku、フロントエンドはFirebase Hosingにデプロイしています。
またJWTが無効な値だった場合は6, 7の部分の処理はせずにエラーを返すようにしました。

できたもの

以下のようなURLを貼り付けて管理するだけのサービスです。完全個人用なので規約等はありません。
シンプルなのもにしたかったので機能は特にありません。
スクリーンショット 2020-12-01 16.15.20.png

使用技術

 環境

  • golang
  • react(yarn)
  • firebase cli
  • heroku cli

上記の環境がある前提で説明していきます

フロントエンド

  • React.js
    reduxは使用しておりません
  • axios
  • material-ui
    cssが大の苦手な為使用しました
  • Firebase
    フロントエンドは認証と Firebase Hosingを使用しました

バックエンド

  • Golang (Gin)
  • Heroku
  • FireStore
    フロントで使用しても良かったのですがサーバーサイド書きたかったのでしようしました。

CI

  • GitHub Actions
    masterにpushされた際にフロントはFirebase Hosing、バックエンドはHerokuにデプロイれるようにしました

この記事で説明する部分

今回作ったものを説明するにあたり、全て説明するとさすがに長くなるので以下の要所のみ説明していきます

フロントエンド

  • JWTの部分

サーバーサイド

  • JWTの部分
  • FireStoreの部分
  • GitHub Actionsについて

フロントエンド

まずはフロントエンドjwtの取得の部分です

    const [loading, setLoading] = useState(true);
    const [user, setUser] = useState(null);

    useEffect(() => {
        firebase.auth().onAuthStateChanged(user => {
            setLoading(false)
            setUser(user)
            if (user) {
                user.getIdToken().then(function (idToken) {
                    localStorage.setItem('jwt', idToken)
                });
                localStorage.setItem('uid', user.uid)
            }
        })
    })
    const logout = () => {
        firebase.auth().signOut();
        localStorage.removeItem('uid')
    }

ログイン時にuser.getIdToken().then(function (idToken) でjwtを取得し、
localStorage.setItem('jwt', idToken)でローカルストレージにjwtを保存しています

バックエンド

GoでFirestoreを扱うための下準備

FireBaseのプロジェクを開きスクリーンショット 2020-12-01 16.42.22.png
上の画像のように プロジェクトの設定 > サービスアカウント > 新しい秘密鍵の生成 をタップし生成して下さい。このjsonファイルはGitHubに上げたりしないで下さい。

CORSの設定

以下のように書きCORSの設定をします

    port := os.Getenv("PORT")
    if port == "" {
        err := godotenv.Load(fmt.Sprintf("./%s.env", os.Getenv("GO_ENV")))
        if err != nil {
            fmt.Println(err)
        }
        port = os.Getenv("LOCAL_PORT")
    }
    r := gin.Default()
    r.Use(cors.New(cors.Config{
        AllowMethods: []string{
            "POST",
            "GET",
            "OPTIONS",
            "PUT",
            "DELETE",
        },
        AllowHeaders: []string{
            "Content-Type",
            "Content-Length",
            "Authorization",
            "Uid",
        },
        AllowOrigins: []string{
            "http://localhost:3000",
            os.Getenv("FRONT_URL1"),
            os.Getenv("FRONT_URL2"),
        },
        MaxAge: 24 * time.Hour,
    }))

    pkg.Serve(r, ":"+port)

Firestoreの処理

FireStoreの処理の部分です。interfaceを使用してClean Architectureっぽく書いています。

type FirestoreAuth struct {
    Type                        string `json:"type"`
    Project_id                  string `json:"project_id"`
    Private_key_id              string `json:"private_key_id"`
    Private_key                 string `json:"private_key"`
    Client_email                string `json:"client_email"`
    Client_id                   string `json:"client_id"`
    Auth_uri                    string `json:"auth_uri"`
    Token_uri                   string `json:"token_uri"`
    Auth_provider_x509_cert_url string `json:"auth_provider_x509_cert_url"`
    Client_x509_cert_url        string `json:"client_x509_cert_url"`
}

type FireBaseClient struct {
    FireBase      *firebase.App
    FireStore     *firestore.Client
    Ctx           context.Context
    CollectionRef *firestore.CollectionRef
    DocumentRef   *firestore.DocumentRef
    Auth          *auth.Client
}

type FireBaseHandler interface {
    Collection(path string) *FireBaseClient
    Set(ctx context.Context, data interface{}) error
    Doc(id string) *FireBaseClient
    Documents(ctx context.Context) *firestore.DocumentIterator
    Delete(ctx context.Context) error
    VerifyIDToken(ctx context.Context, idToken string) error
}

type FireBase struct {
    FireBaseHandler
}

func Init_firebase() FireBaseHandler {

    ctx := context.Background()
    sa := option.WithCredentialsFile("./firestore.json")
    app, err := firebase.NewApp(ctx, nil, sa)
    if err != nil {
        return nil
    }
    client, err := app.Firestore(ctx)

    if err != nil {
        return nil
    }
    auth, err := app.Auth(ctx)
    if err != nil {
        return nil

    }

    return &FireBaseClient{
        FireBase:  app,
        FireStore: client,
        Ctx:       ctx,
        Auth:      auth,
    }
}
//データを書き込み
func (fb *FireBase) InsertData(data model.Content) {

    updateError := fb.Collection(data.Uid).Doc(data.Content_id).Set(context.Background(), map[string]interface{}{
        "content_id": data.Content_id,
        "comment":    data.Comment,
        "url":        data.Url,
        "date":       data.Date,
    })
    if updateError != nil {
        log.Printf("An error has occurred: %s", updateError)
    }
}

//データを削除
func (fb *FireBase) DeleteData(uid, id string) error {

    err := fb.Collection(uid).Doc(id).Delete(context.Background())

    if err != nil {
        return err
    }
    return nil
}
//データを取得
func (fb *FireBase) GetData(uid string) []model.Content {

    var res_data []model.Content
    iter := fb.Collection(uid).Documents(context.Background())

    for {
        doc, err := iter.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Fatalf("Failed to iterate: %v", err)
        }
        data := doc.Data()
        var content model.Content
        content.Comment = data["comment"].(string)
        content.Url = data["url"].(string)
        content.Content_id = data["content_id"].(string)
        content.Date = int(data["date"].(int64))

        res_data = append(res_data, content)
    }

    return res_data

}
//jwt認証
func (fb *FireBase) AuthJWT(jwt string) error {

    idToken := strings.Replace(jwt, "Bearer ", "", 1)
    err := fb.VerifyIDToken(context.Background(), idToken)
    if err != nil {
        return err
    }
    return nil
}

func (fb *FireBaseClient) VerifyIDToken(ctx context.Context, idToken string) error {
    _, err := fb.Auth.VerifyIDToken(ctx, idToken)
    return err
}

func (fb *FireBaseClient) Collection(path string) *FireBaseClient {
    fb.CollectionRef = fb.FireStore.Collection(path)
    return fb
}

func (fb *FireBaseClient) Set(ctx context.Context, data interface{}) error {
    _, err := fb.DocumentRef.Set(ctx, data, firestore.MergeAll)
    return err
}

func (fb *FireBaseClient) Doc(id string) *FireBaseClient {
    fb.DocumentRef = fb.CollectionRef.Doc(id)
    return fb
}
func (fb *FireBaseClient) Documents(ctx context.Context) *firestore.DocumentIterator {
    res := fb.CollectionRef.Documents(ctx)
    return res
}

func (fb *FireBaseClient) Delete(ctx context.Context) error {
    _, err := fb.DocumentRef.Delete(ctx)
    return err
}

GoでFireStoreをを使用する際にFireBaseの認証のjsonを読み込ませるのですがgithubにpushするわけにも行かないので今回はjsonを作成するようにしました

func CreateFireStoreJson() {
    fp, err := os.Create("./firestore.json")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer fp.Close()

    file := fmt.Sprintf(` {
    "type": "%s",
    "project_id": "%s",
    "private_key_id": "%s",
    "private_key": "%s",
    "client_email": "%s",
    "client_id": "%s",
    "auth_uri": "%s",
    "token_uri": "%s",
    "auth_provider_x509_cert_url": "%s",
    "client_x509_cert_url": "%s"
}`,
        os.Getenv("FS_TYPE"),
        os.Getenv("FS_PROJECT_ID"),
        os.Getenv("FS_PRIVATE_KEY_ID"),
        os.Getenv("FS_PRIVATE_KEY"),
        os.Getenv("FS_CLIENT_EMAIL"),
        os.Getenv("FS_CLIENT_ID"),
        os.Getenv("FS_AUTH_URI"),
        os.Getenv("FS_TOKEN_URI"),
        os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL"),
        os.Getenv("FS_AUTH_PROVIDER_X509_CERT_URL"))

    _, err = fp.Write(([]byte)(file))
    if err != nil {
        fmt.Println(err)
    }
}

GitHub Actionsを使ってFirebaseとHerokuにデプロイする

ymlを作成する

.github/workflows/deploy.ymlをプロジェクトのルートディレクトリに作成して下さい。
これはGithub Actionsの設定ファイルです。今回はdeploy.ymlにしていますが.yml形式のファイルであれば問題ないです。

name: ci

on:
  push:
    braches: 
      - master

jobs:
  firebase:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup node
        uses: actions/setup-node@v1
        with:
          node-version: 14.4.0

      - name: Install dependencies
        run: |
          yarn
      - name: Build React app
        env:  #FireBaseの環境変数を定義しています
          REACT_APP_FB_API_KEY: ${{ secrets.REACT_APP_FB_API_KEY }}
          REACT_APP_FB_AUTH_DOMAIN: ${{ secrets.REACT_APP_FB_AUTH_DOMAIN }}
          REACT_APP_FB_DATABASE_URL: ${{ secrets.REACT_APP_FB_DATABASE_URL }}
          REACT_APP_FB_PROJECT_ID: ${{ secrets.REACT_APP_FB_PROJECT_ID }}
          REACT_APP_FB_STORAGE_BUCKET: ${{ secrets.REACT_APP_FB_STORAGE_BUCKET }}
          REACT_APP_FB_MESSAGEING_SENDER_ID: ${{ secrets.REACT_APP_FB_MESSAGEING_SENDER_ID }}
          REACT_APP_SERVER_URL: ${{ secrets.REACT_APP_SERVER_URL }}
        run: |
          yarn install && yarn build
      - name: Setup Firebase CLI
        run: |
          npm install -g firebase-tools
      - name: Deploy Firebase
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}  #firebaseのcitoken
        run: |
          firebase deploy --token $FIREBASE_TOKEN
  backend:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
          fetch-depth: 0
    - name: Deploy to Heroku
      env:
        HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }} # herokuのcitoken
        HEROKU_APP_NAME: herokuのプロジェクト名
      if: github.ref == 'refs/heads/master' && job.status == 'success'
      run: |
        git push https://heroku:$HEROKU_API_TOKEN@git.heroku.com/$HEROKU_APP_NAME.git origin/master:master

CIトークンを取得する

FireBaseのトークンを取得する

firebase login:ci

上記のコマンドを打つと以下のようにトークンが表示されます

✔  Success! Use this token to login on a CI server:

{TOKENの文字列}

Herokuのトークンを取得する

heroku auth:token

上記のコマンドを打つと以下のようにトークンが表示されます

 ›   Use heroku authorizations:create to generate a long-term token
{TOKENの文字列}

GitHub ActionsでHerokuにデプロイする際にProfile等は必要ないみたいです

環境変数を定義する

リポジトリを開き Setting > Secrets > New repository secret で環境変数を設定します
設定する際は${{ secrets.FIREBASE_TOKEN }}のような書き方で取得できます。

上記のが完了したらmasterにpushするとFireBaseとHerokuにデプロイされるようになります。

おわりに

ここまで読んでくださりありがとうございます。
今回は個人で使用するURLを管理するサービスについて書きました。
初めてGitHub Actionsを使用しましたがめっちゃ便利でした。ただ個人用ですのでエラーハンドリングやログもかなり適当になってます......

また間違っている点などがございましたらコメントなどで指摘していただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React入門 未経験から1週間でReactをマスターする #05. フォームと親子間のデータのやり取り

目次

1. Reactの新規プロジェクトの立ち上げ
2. コンポーネントのプロパティ(props)とステート(state)
3. Class Components と Function Components
4. 条件分岐 (if) と繰り返し (loop)
5. フォームと親子間のデータのやり取り←今ここ
6. コンポーネントのライフサイクル (準備中)
7. スタイル (準備中)
8. Higher-Order Component (準備中)
9. Portalを利用したモーダル (準備中)
10. refによるエレメントの取得 (準備中)
11. Contextを利用したテーマの変更 (準備中)

今回の学習内容

今回は、

  • Reactでのフォームの利用方法
  • 親子間でのデータのやりとり

をやっていきます。

YouTubeでの解説動画

YouTubeでも解説しています。
動画で確認したい方はこちらもどうぞ。
【YouTube動画】 未経験から1週間でをマスターするReact入門 #05. フォームと親子間のデータのやり取り
未経験から1週間でをマスターするReact入門 #05. フォームと親子間のデータのやり取り

この記事のソースコード

ソースコードはGitHubで公開しています。

https://github.com/yassun-youtube/ReactTutorial

今回のコミット一覧↓

スクリーンショット 2020-12-01 13.16.00.png

フォームの利用

まずはReactでフォームを利用してみます。
Form.js にフォームを入れます。

このWebアプリでは、言語のリストと追加を行うアプリにしたいので、新しい言語を追加するフォームを作ります。

src/Form.js
import { useState } from 'react'; // 追加

export const Form = () => {
  const [text, setText] = useState(''); // 追加

  const submitForm = (e) => { // 追加
    e.preventDefault();
    console.log(`submitForm(${text})`);
  }

  return (
    <div>
      { /* 変更 */ }
      <h4>新しい言語の追加</h4>
      <form onSubmit={submitForm}>
        <div>
          <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
        </div>
        <div>
          <button>追加</button>
        </div>
      </form>
    </div>
  )
}

コードの説明

  const [text, setText] = useState(''); // 追加

まずここで、 text という state を追加しています。

  const submitForm = (e) => { // 追加
    e.preventDefault();
    console.log(`submitForm(${text})`);
  }

次に、 form をサブミットされたときの関数を定義しました。

ここでは、 preventDefault() をしてコンソールにログを表示するようにしています。

<h4>新しい言語の追加</h4>
<form onSubmit={submitForm}>
  <div>
    <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
  </div>
  <div>
    <button>追加</button>
  </div>
</form>

form タグに onSubmit を定義しています。これで form がサブミットされると 先程定義した submitForm 関数が呼び出されます。

<input type="text" value={text} onChange={(e) => setText(e.target.value)} />

次にこの input タグですが、これは text state を値としてもち、onChange を利用することで変更検知を行って text stateを setText を行うことで変更しています。

こうすることで、 text を常に input の内容と一致させることができます。

実際に動かしてみると下図のようになります。

7c9b5ec202b5788520e1189e570a9f41.gif

ここまでのコミット

https://github.com/yassun-youtube/ReactTutorial/commit/1fa65388d4662879c48b15ffd4f0d2ab2c0d915a

親子間のデータのやりとり

次に、フォームに入力されるとリスト側の言語が増えるようにしてみましょう。

今は List.js が言語データを管理しているのdせうが、言語データを App.js が管理するようにします。
そこでまず、前回 List.js で定義していた LANGUAGE 定数を別ファイル( src/const/languages.js )に移します。

src/const/languages.js
export const LANGUAGES = [
  'JavaScript',
  'C++',
  'Ruby',
  'Java',
  'PHP',
  'Go'
];

それを利用して、 App.js を修正します。

src/App.js
import { useState } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { LANGUAGES } from "./const/languages"; // 追加

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState(LANGUAGES); // 追加

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List /> : <Form />
      }
    </div>
  );
}

export default App;

言語データを管理するために langs という state を定義しました。

Form.js => App.js のデータのやり取り

次に、子コンポーネントである Form.js から App.js へのデータの受け渡しをします。

子から親へのデータの受け渡しは、 props を利用します。
具体的には、props に関数への参照を渡すことで、 子コンポーネントからその関数を呼び出してもらいます。

src/App.js
import { useState } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { LANGUAGES } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState(LANGUAGES);

  const addLang = (lang) => { // 追加
    console.log(lang);
    setLangs([...langs, lang])
  }

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List /> : <Form onAddLang={addLang}/> // 変更
      }
    </div>
  );
}

export default App;

App.js 側に addLang 関数を作成し、 Form.js のプロパティとして渡してみました。
これを Form.js から呼び出してみます。

src/Form.js
import { useState } from 'react';

export const Form = ({ onAddLang }) => { // 変更
  const [text, setText] = useState('');

  const submitForm = (e) => {
    e.preventDefault();
    onAddLang(text); // 変更
  }

  return (
    <div>
      <h4>新しい言語の追加</h4>
      <form onSubmit={submitForm}>
        <div>
          <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
        </div>
        <div>
          <button>追加</button>
        </div>
      </form>
    </div>
  )
}

コード解説

Form.js では、 onAddLang という props を追加しています。これは関数への参照になります。
submitForm 関数内で onAddLang 関数を呼び出すことにより、 App.js でイベントを起こします。

App.js では、

 <Form onAddLang={addLang}/>

と定義していますので、 Form.js から onAddLang が呼ばれると、 App.js 内ので addLang 関数が呼ばれることになります。

  const addLang = (lang) => { // 追加
    console.log(lang);
    setLangs([...langs, lang])
  }

addLang 関数では、 App.jslangs に対してデータを追加するようになっているので、データが追加されます。

実際に動かしてみる

実際に動かしてみて、 Form.js から App.js の関数が呼ばれていることを確認します。

今は App.js 内でコンソールに渡された値を表示することで確認できます。

a304ddb31fd27e65e4b1145478a12b19.gif

追加された言語をリストへ反映

次に、追加された言語を List.js に反映してみます。

src/App.js
import { useState } from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { LANGUAGES } from "./const/languages";

function App() {
  const [tab, setTab] = useState('list');
  const [langs, setLangs] = useState(LANGUAGES);

  const addLang = (lang) => {
    setLangs([...langs, lang]);
    setTab('list'); // 追加
  }

  return (
    <div>
      <header>
        <ul>
          <li onClick={() => setTab('list')}>リスト</li>
          <li onClick={() => setTab('form')}>フォーム</li>
        </ul>
      </header>
      <hr />
      {
        tab === 'list' ? <List langs={langs} /> : <Form onAddLang={addLang}/> // 変更
      }
    </div>
  );
}

export default App;

フォームから言語が追加されたら List.js を表示するために、addLang 関数で追加されたらタブも変更するようにしました。
また、 List.jslangs stateを渡すようにしています。

次に、 List.js でこの渡された langs を利用するようにしましょう。

export const List = ({ langs }) => { // 変更
  return (
    <div>
      {
        langs.map((lang, index) => { // 変更
          return <div key={index}>{ lang }</div>
        })
      }
    </div>
  )
}

List.js からは、 src/const/languages.js に移動した LANGUAGES を削除しました。
また、 props から langs を受け取り、それを利用するようにしています。

これにより、下図のようにフォームで追加された内容が List.js に反映されるようになりました。

bbd3365b2696cd719367109b84227851.gif

今日やったこと

JSXでのフォームの利用

<form onSubmit={submitFunc}>
  <input type="text" value={text} onChange={(e) => setText(e.target.value)} />
  <button>ボタン</button>
</form>

JSXでフォームを利用するためには、 onSubmit イベントで関数を呼び出します。
input 属性は、 valueonChange を利用して状態を管理します。

親子間のデータのやり取り

子コンポーネント=>親コンポーネント

function Parent() {
  const testFunc = () => {}
  return <Child parentFunc={testFunc} />
}

function Child({ parentFunc }) {
  return <div onClick={() => parentFunc(text)}>テスト</div>
}

子コンポーネントから親コンポーネントにデータを渡すには、 props に渡された関数を子コンポーネントから呼び出すようにします。

親コンポーネント=>子コンポーネント

function Parent() {
  const test = 'テストデータ';
  return <Child parentData={test} />
}

function Child({ parentData }) {
  return <div>{ parentData }</div>
}

親コンポーネントから子コンポーネントにデータを渡すには、 props でデータを渡します。

Class Components


Class Components でのコードはこちら

App.js

  • stateの追加
  • List.jsへlangsを渡す
  • Form.jsから新しいlangを受け取る

を追加しています。

src/App.js
import React from 'react';
import { List } from "./List";
import { Form } from "./Form";
import { LANGUAGES } from "./const/languages";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      tab: 'list',
      langs: LANGUAGES,
    };
  }
  addLang(lang) {
    this.setState({
      langs: [...this.state.langs, lang],
      tab: 'list',
    });
  }
  render() {
    const { tab, langs } = this.state;
    return (
      <div>
        <header>
          <ul>
            <li onClick={() => this.setState({ tab: 'list' })}>リスト</li>
            <li onClick={() => this.setState({ tab: 'form' })}>フォーム</li>
          </ul>
        </header>
        <hr />
        {
          tab === 'list' ? <List langs={langs} /> : <Form onAddLang={(lang) => this.addLang(lang)}/>
        }
      </div>
    )
  }
}

export default App;

Form.js

Form.js は全体的に変更します。
this のことをケアしてアロー関数で書くことに注意してください。

src/Form.js
import React from 'react';

export class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: '' }
  }
  submitForm(e) {
    e.preventDefault();
    this.props.onAddLang(this.state.text);
  }

  render() {
    const { text } = this.state;
    return (
      <div>
        <h4>新しい言語の追加</h4>
        <form onSubmit={(e) => this.submitForm(e)}>
          <div>
            <input type="text" value={text} onChange={(e) => this.setState({ text: e.target.value })} />
          </div>
          <div>
            <button>追加</button>
          </div>
        </form>
      </div>
    )
  }
}

List.js

src/List.js
import React from 'react';
// LANGUAGES定数を削除

export class List extends React.Component {
  render() {
    const { langs } = this.props; // propsからlangsを利用
    return (
      <div>
        {
          langs.map((lang, index) => {
            return <div key={index}>{ lang }</div>
          })
        }
      </div>
    )
  }
}

languages.js

ファイルを追加

src/const/languages.js
export const LANGUAGES = [
  'JavaScript',
  'C++',
  'Ruby',
  'Java',
  'PHP',
  'Go'
];


こちらのコミットの方が変更点がわかりやすく良いかもしれません。

https://github.com/yassun-youtube/ReactTutorial/commit/61654d4069d3888f14f90aced3aee73e1a292b53?branch=61654d4069d3888f14f90aced3aee73e1a292b53&diff=split

おわりに

これで、 Form.jsApp.js の子コンポーネントから親コンポーネントへのデータ渡し、および App.jsList.js の親コンポーネントから子コンポーネントへのデータ渡しを実装できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのドラッグ&ドロップを使ったソート

おすすめはreact-sortable-hocです。
https://mebee.info/2020/05/03/post-7246/

ちなみに、複数選択して同時にドラッグ&ドロップすることもできました。(hooksでは書き直せない。別記事参考)
https://github.com/clauderic/react-sortable-hoc/issues/318

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスコンポーネントのtips

hooksに書き直せないパターンに初めて出会って、クラスコンポーネントで書かなければならず、まともにやってなかったのでいろいろ忘れないようにメモしておく。

ちなみにhooksに書き直せないパターン

handleUpdateBeforeSortStart = ({index}) => {
    return new Promise((resolve) =>
      this.setState(
        ({items}) => ({
          sortingItemKey: items[index],
          isSorting: true,
        }),
        resolve,
      ),
    );
  };

PromiseのなかでsetStateして、第二引数のコールバックにresolveを指定している場合。
hooksのuseStateではコールバックを指定できず、大抵の場合はuseEffectの中で書くが、promiseのresolveはむり?みたい

tips

  • クラスコンポーネント内ではletやconstなどは書かない
  • クラス内で定義した変数にはthisでアクセス
  • 子コンポーネントからstateを変更したい場合には親コンポーネントでstateを変更する関数を作ってそれを子コンポーネントに渡す
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2020】モダンなフロントエンド環境を構築する手順

TL;DR

新規サービス・プロダクト開発を行う部署に所属しているため、業務で新たに環境構築を行うことが頻繁にあります。そのため、自分がいつも行う手順をまとめたいと思います。この記事では手順の列挙のみでやり方には言及しませんので、詳細はリンクをご確認ください。

Gatsbyを利用される方はこれ1つ

これから説明する手順の1~3をコマンド1つで行うことができます。
Reactのフレームワークの1つであるGatsby(Reactの公式ページもこのフレームワークが用いられています)には、starterというパッケージが存在します。そのパッケージを使ってコマンド1つで環境構築が可能です。

1~3の手順による環境を構築するstarterを作成しました
gatsby-starter-typescript-sass

手順

ReactやVueなどのライブラリや、Sassの使用をおすすめしますが、そうでなくても以下の手順は適用できるかと思います。

  1. Gitリポジトリの作成
  2. TypeScriptの導入
  3. ESLint・Prettierの導入
  4. VSCodeの各種設定を行い、設定ファイルを保存
  5. CI/CDの設定

各ステップの必要性

2. TypeScriptの導入

型によって無用なエラーを防げたり、補完が便利に使えます。

3. ESLint・Prettierの導入

コード作成のルールやフォーマットを自動でしてくれます。人間がやらなくてもいいことはとことん減らしたいので導入します。Linterが走るタイミングは、実際のその変更を確認できるためonSaveがおすすめです。(VSCodeの場合設定できます。)ただ確実にLinterが走ることを担保するため、huskyを使ってprecommitでLinterを走らせます。

Prettier 入門 ~ESLintとの違いを理解して併用する~
ESLint と Prettier の共存設定とその根拠について

4. VSCodeの各種設定を行い、設定ファイルを保存

前述したように、LinterなどをonSaveなどで走らせるために設定を行います。またその際にワークスペースに設定を保存すると、設定が書かれたファイルが保存されるので他の開発者と共有することができます。

vscodeの利用に必須な”ワークスペース”の概念

5. CI/CDの設定

GitHubなどにcommitしたタイミングでビルド・デプロイできるような仕組みを導入しておくことで、開発するたびにそれらのことを行う手間が省けます。

heroku 中級編 - 1分で実現するCI/CDをHeroku Pipelinesで

注意点

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Amplifyを使った開発で役立つ逆引きTips集(2020年版)

本記事は Fusic Advent Calendar 2020 の1日目の記事です。

今年もいよいよはじまりました!Fusic社員が「個性をかき集めて、驚きの角度から世の中をアップデートしつづける」記事を書いていきますので、クリスマスまでどうぞよろしくお願いします。

というわけで、1日目は「AWS Amplify」に関する記事を書こうと思います。

はじめに

68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6177732d6d6f62696c652d6875622d696d616765732f6177732d616d706c6966792d6c6f676f2e706e67.png

個人的に今年は「Amplify元年」といっても過言ではない年でした。Amplify SNS Workshop との出会いをきっかけに、AmplifyとNext.jsを組み合わせてモダンかつスピーディな開発ができないか模索するようになりました。

Amplifyを触るうちにさまざまなTipsを蓄積できたので、1年分まとめた逆引きTips集として公開することにしました。

※普段使用しているJavaScriptフレームワークがReact/Next.jsなので、記載内容もReact寄りです。ご了承くださいm(_ _)m

目次

  1. Auth
    1. サインアップ時に電話番号を不要としたい
  2. API(GraphQL)
    1. API.graphql()の結果をTypeScriptでいい感じに型解決できない
    2. Authでログインしたユーザ限定でAPIを実行できるようにしたい
    3. 項目をソートして取得したい
    4. schema.graphqlを更新しても諸々反映されない
    5. createdAt、updatedAtをスキーマから消したい
  3. Hosting
    1. 開発、ステージング、本番のインフラを分けたい
    2. hostingしたNext.jsのSPAが「403 Access Denied」となる
    3. 先にCI/CDを構築して後からauthやapiといったカテゴリのリソースを追加するとフロントエンドでエラーが発生する
  4. Function
    1. ランタイムにPythonを指定するとCI/CDがコケる
    2. 定期的にタスクを実行したい
  5. その他
    1. Amplifyの基本的な使い方をマスターしたい
    2. Categoryとして提供されていないAWSリソースを追加したい
    3. team-provider-info.jsonって何者?
    4. 上記で解決できない・よくわからない問題が起こっている
  6. まとめ

Auth

サインアップ時に電話番号を不要としたい

Amplify UI Componentsで作ったサインアップフォームは入力項目が多くて不便です。

amplify_2020_signup_form.png

コンポーネントに対してオプション指定することで、電話番号などの入力項目を減らせるようになっています。

amplify_2020_signup_form_without_phone.png

注意点として、Reactに対応したAmplify UI Componentsは2つ存在しています。

  • aws-amplify-react
  • @aws-amplify/ui-react

どちらを使うかによって事情が変わってくるため、それぞれ解説します。

aws-amplify-reactの場合

aws-amplify-react はLegacyなnpmパッケージです。

https://docs.amplify.aws/ui-legacy/q/framework/react

こちらのパッケージではReactのHOCとして、 withAuthenticator が提供されています。Reactコンポーネントをexportする際に、このHOCでラップすることでその画面を認証必須にすることが可能です。

export default withAuthenticator(App);

この引数でオプション指定することによって、サインアップフォームから電話番号を省略できます。

export default withAuthenticator(App, {
  signUpConfig: {
    hiddenDefaults: ['phone_number']
  }
});

@aws-amplify/ui-reactの場合

@aws-amplify/ui-react はLatestなnpmパッケージです。

https://docs.amplify.aws/ui/q/framework/react

Latestなので現在ではこちらを使用することが推奨されています。こちらのパッケージでも withAuthenticator が提供されています。しかし、 aws-amplify-react とは異なり signUpConfig をオプション指定することができないようです。

https://docs.amplify.aws/ui/auth/authenticator/q/framework/react

このため、HOCではなく AmplifyAuthenticator コンポーネントを使ってフォームをカスタマイズします。

<AmplifyAuthenticator usernameAlias="email">
  <AmplifySignUp slot="sign-up" formFields={[ { type: "email" }, { type: "password" } ]} />
  <AmplifySignIn slot="sign-in" />
  <AmplifySignOut />
  <Component {...pageProps} />
</AmplifyAuthenticator>

API(GraphQL)

API.graphql()の結果をTypeScriptでいい感じに型解決できない

Amplifyを使うと、バックエンドと通信するためのクライアント(GraphQLラッパー)を自動生成してくれるので便利です。もちろん、これはTypeScriptにも対応しています。

TypeScript初心者な私は、API.graphql()の戻り値が型推論されず、対処方法に悩みました、

import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api'
import { listDevices } from '../src/graphql/queries'

const asyncFunc = async () => {
  // これだと型推論が効かずエラーとなる
  const result = await API.graphql(graphqlOperation(listDevices))

  // 以降の処理...
}

いろいろ模索した結果、どうやら as を使って型を明示するのが正解のようです。

import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api'
import { listDevices } from '../src/graphql/queries'
import { ListDevicesQuery } from '../src/API'
const asyncFunc = async () => {
  const result = (await API.graphql(graphqlOperation(listDevices))) as GraphQLResult<ListDevicesQuery>

  // 以降の処理...
}

Authでログインしたユーザ限定でAPIを実行できるようにしたい

GraphQLスキーマにて @auth ディレクティブを指定することで、リクエスト時の権限チェックが可能です。

type Device @model
  @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String!
  logs: [Log] @connection(keyName: "byDevice", fields: ["id"])
}

type Log @model
  @auth(rules: [{ allow: owner }])
  @key(name: "byDevice", fields: ["deviceId", "timestamp"]) {
  id: ID!
  deviceId: ID!
  timestamp: AWSTimestamp!
  value: Float!
  device: Device @connection(fields: ["deviceId"])
}

上記では、allow: owner としていますが、所有者以外にも権限指定したり、readやcreate権限を部分的に付与することも可能です。詳しくはドキュメントを参照ください。
https://docs.amplify.aws/cli/graphql-transformer/auth

項目をソートして取得したい

Amplify SNS Workshopで解説されている通り、 @key ディレクティブを使ってDynamoDBにGSIを追加します。

type Post
  @model (subscriptions: { level: public })
  # パーティションキーをtype, ソートキーをtimestampに指定
  # typeには常に 'post' を格納することで、全Postをtimestampでソートして取得する
  @key(name: "SortByTimestamp", fields:["type", "timestamp"], queryField: "listPostsSortedByTimestamp")
{
  type: String!
  id: ID
  content: String!
  owner: String
  timestamp: AWSTimestamp!
}

こうしておくと、次のコードのようにソート指定をしたFetchが可能となります。

const res = await API.graphql(graphqlOperation(listPostsSortedByTimestamp, {
  type: "post", // パーティションキーとして 'post'(固定) を指定
  sortDirection: 'DESC' // 降順指定
}));

schema.graphqlを更新しても諸々反映されない

amplify add api を実行した時に、プロジェクト直下に schema.graphql を作成するケースが多いのではないでしょうか?

後々になって、テーブルを追加する必要が出てきたりディレクティブを追記したりする場合に、このファイルを更新して amplify update api しても反映されません。

なぜなら、 amplify/backend/api/{リソース名}/schema.graphql が実際に構築されたGraphQL APIのスキーマとして読み込まれているためです。更新するときはこちらを更新して、 amplify update api するようにしましょう。

schema_graphql_—_tatami-fm-music-library.png

createdAt、updatedAtをスキーマから消したい

DynamoDBにAppSync以外のリソースから値を書き込む場合など、createdAtupdatedAt が邪魔になるシチュエーションがあります。

そんなときは @model(timestamps: null) を指定することで、createdAtupdatedAt を省略できます。

type Program
  @model(timestamps: null) # createdAtとupdatedAtを削除
  @key(name: "byStation", fields: ["stationId", "startedAt"]) {
  id: ID!
  name: String!
  startedAt: AWSTimestamp!
  endAt: AWSTimestamp!
  stationId: ID!
  station: Station @connection(fields: ["stationId"])
}

Hosting

開発、ステージング、本番のインフラを分けたい

Amplifyで作成したプロジェクトは環境を複数作成して、切り替えることが可能です。環境は amplify env add することで追加できます。

AWS_Amplify_Console.png

Amplify SNS Workshop で構築手順が詳細に記載されていますので、こちらを参照ください。

hostingしたNext.jsのSPAが「403 Access Denied」となる

https _qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_60996_1dc862a3-7cd8-e51b-0ad8-6b91c889fdfd.png

こちらの記事で解説しています。

AWS AmplifyでhostingしたNext.jsのSPAが「403 Access Denied」となったときの対処法

先にCI/CDを構築して後からauthやapiといったカテゴリのリソースを追加するとフロントエンドでエラーが発生する

CI/CD構築後にauthやapiといったバックエンドのリソースを追加すると、CI/CDでバックエンドリソースが構築されず、デプロイしたフロントエンド側でエラーが発生します。

推測ですが、Amplify hostingのCI/CDのビルド設定(amplify.yml)は設定時点で、バックエンドのリソースが存在するかどうかを自動判定して、記述を変更しているようです。先にCI/CDを設定すると、バックエンドリソースは不要と判断して、ビルドが省略されます。

Next.js+TypeScript+AWS Amplify+RecoilでToDoリストを作るに記載したとおり、amplify.ymlを本来の形に書き換えることで、エラーを回避できます。

Function

ランタイムにPythonを指定するとCI/CDがコケる

Amplify側で指定したPythonのバージョンと、ビルド時に使用しているAmazon Linux2のPythonバージョンが異なることが原因です。

AWS AmplifyでPythonのFunctionをCI/CDするとbuildに失敗する問題の対処方法に記載したとおり、amplify.yml を書き換えてPythonを再インストールすることで回避できます。

定期的にタスクを実行したい

amplify add function する時に質問される Do you want to invoke this function on a recurring schedule? という質問に Yes と答えることで、LambdaのトリガーにCloudWatchEventが指定されます。

$ amplify add function
Scanning for plugins...
Plugin scan successful
? Select which capability you want to add: Lambda function (serverless function)
? Provide a friendly name for your resource to be used as a label for this category in the project: test
? Provide the AWS Lambda function name: test
? Choose the runtime that you want to use: Python
Only one template found - using Hello World by default.
? Do you want to access other resources in this project from your Lambda function? No
? Do you want to invoke this function on a recurring schedule? Yes
? At which interval should the function be invoked: Minutes
? Enter the rate in minutes: 3
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? No
Successfully added resource test locally.

その他

Amplifyの基本的な使い方をマスターしたい

はじめてAWS Amplifyを使うのであれば、Amplify SNS Workshopを1周することをお勧めします。

一方、このWorkshopだとカバーできていない部分があったり、使用しているパッケージのバージョンが一部古かったりという問題もあるので、他のチュートリアルもお勧めします。watilde/awesome-aws-amplify-ja にて日本語で書かれたチュートリアル記事がまとめられているので参考にしてみてください。

Categoryとして提供されていないAWSリソースを追加したい

Amplifyとして提供しているカテゴリ(authやapi)だけでは、構築したいシステムの要件を満たせないことがあります。

そんなときには Custom CloudFormation stacks を定義することで、AWSのリソースを自由にAmplifyのプロジェクトに組み込むことができます。もちろん、他のカテゴリからパラメータを渡すこともできます。例えば次のスライドに書いているような、apiで作ったDynamoDBのテーブル名をCustom CloudFormation stacksに渡してDynamoDBに値を書き込むIoT CoreのACTを定義する、といった使い方ができます。

https://speakerdeck.com/yuuu/aws-amplifytomockmockdeiotbatukuendowosupideinigou-zhu-suru?slide=25
image.png

team-provider-info.jsonって何者?

team-provider-info.json にはAmplifyのプロジェクトで構築したリソースのARNなどが記載されています。IAMのSecret Access Keyなどが記載されているわけではないので、直接的にセキュリティリスクがある訳ではありませんが、AccountIdが記載されていることを踏まえるとあまりpublicにすべきではありません。

次のスライドにも記載されている通り、プライベートリポジトリの場合以外は.gitignoreに追加する ようにしましょう。

https://speakerdeck.com/jaguar_imo/amplify-cli-deep-dive-awsdevday-2020?slide=7

上記で解決できない・よくわからない問題が起こっている

残念ながら、Amplifyを使っていると上記に挙がっていない問題にぶつかることはあると思われます。自分自身は以下のような方法で対処するようにしています。

エラーメッセージを理解する

Amplifyに限った話ではないですが、Amplifyのエラーメッセージには問題の原因や対処方法が記載されています。まずはこれを読んで理解します。

内容があまり理解できなかったとしても「Amplify + エラーメッセージから抜き出したいくつかのキーワード」で検索すると、似たような現象がGitHub Issuesで見つかるケースが多いです。

Amplify CLI/Frameworkのアップグレードをする

Amplifyは開発が活発で、CLI/Frameworkが頻繁にバージョンアップされます。公式ドキュメントに書かれているような基本的な操作が上手くいかない場合は、CLI/Frameworkをバージョンアップすることで問題を解決できるかもしれません。

AmplifyのGitHub Issuesを検索する

先にも挙げた通り、AmplifyのGitHub Issuesにはさまざまな事例が挙がっています。大抵の問題はIssuesを読んで解決するケースが多いのでぜひ参照してください。

https://github.com/aws-amplify/amplify-js/issues

まとめ

記事を書いて、AWS Amplifyはバックエンドを気にせずフロントエンド開発に注力できる環境を提供してくれる嬉しいサービスだなと改めて思いました。加えて、バックエンド周りも細かいカスタマイズに対応できるようになっている点が好印象です。

2021年は弊社の業務でのAmplifyの導入事例を、もっと世の中に発信できるよう精進します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm installとexpo installの違いって何?

結論

expo installは、

「使用中のExpo SDKのバージョンと互換性のある依存関係のバージョンをインストールする」

点が異なります。

挙動の違い

npm installもexpo installも実際のインストールはnpmやyarnを呼び出します。

expo installでインストールする場合

expo install expo-secure-store

image.png

yarnを使ってSDK39と互換性のあるネイティブモジュールをインストールと書いてありますね。
以下のようにバージョンをしてくれています。

expo-secure-store@~9.2.0

npm installでインストールする場合

image.png

npm installでも結果的に同じ9.2.0がインストールされました。ただバージョンの指定はされていないですね。

おわりに

expo installを使うとexpoのSDKと互換性のあるバージョンのnpmをインストールしてくれるので、Expoで紹介されている公式コンポーネントは全てexpo installでインストールしましょう。

npm installだとライブラリによっては、バージョンの差異でexpoが動かなくなることがあるので注意してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】学習コストゼロのステート管理は unreduxed で。

redux を代表するステート管理ライブラリは学習コストが高めですよね。ライブラリ独自の記法を覚えなくちゃいけないしライブラリ同士の組み合わせ方も考えなければならない。

React hooks の useStateuseReducer で宣言するステートがそのまま他のコンポーネントと共有できたら楽だと思いませんか?
普段からコンポーネント単位では使い慣れているであろうフック関数でグローバルステートも管理できれば、ステート管理ライブラリについて学習する必要はないはずです。

そんなライブラリがあるのでしょうか?

あります。私が作りました。

unreduxed の紹介

リポジトリ

https://github.com/y-hiraoka/unreduxed

特徴

  • 軽量
    unreduxed は非常に軽量であなたのアプリケーションの負担になりません。ライブラリをビルドした後のjsファイルはたった88行で、 React にのみ依存しているため気兼ねなくインストールできます。

  • フックベース
    React hooks についてのみ知っていればすぐにプロジェクトに導入することができます。ライブラリのために何かを学ぶ必要はありません。あなたの作ったカスタムフックを unreduxed で味付けするだけで、すぐにステートを複数コンポーネントに共有することができます。

  • TypeScript フレンドリー
    ライブラリは TypeScript で作成されています。ライブラリを使用する際は型推論が効くようになっているため開発者体験は高いでしょう。もちろん JavaScript からも使用可能です。

  • 高パフォーマンス
    React における悩みの一つに、余計な再レンダリングをチューニングしなければならないことが挙げられます。 React context で共有されたステートを useContext で取得したらどうでしょう?再レンダリング避けられませんね。 props バケツリレーと React.memo でがんばりますか?つらいですね。react-redux 導入しますか?学習コストがかかりますね。unreduxed を使えば、共有されているステートがどんなに巨大でも、各コンポーネントは関心のある値の変化のみ検知して再レンダリングされます。無駄な再レンダリングは責任持って抑制させていただきます。

使い方

ライブラリの使い方を説明していきいます。ソースコードの記載が多くなりますがまったく読むことに苦痛はないはずです。なぜなら普段からカスタムフックに読み慣れているはずですから。

これから提示していくサンプルコードを含んだデモアプリはこちらの codesandbox に用意しています。なお、サンプルコードは TypeScript を使用しています。
https://codesandbox.io/s/unreduxed-demo-app-for-qiita-74zow

インストール方法

npm i unreduxed

コンテナフックを作成する

共有したいステートを持つカスタムフックを、コンテナフックと呼ぶことにしましょう。名前をつけただけです。あなたがいつも作っているカスタムフックを用意していただければ良いです。
このコンテナフックに初期値を注入したい場合は、フックの引数から受け取ることができます。一つ注意してもらいたいことは、引数は undefined を考慮してほしいということです。TypeScript なら ? マークをつけて宣言してください。これはライブラリの型定義上避けられません。

container.ts
import React from "react";
import unreduxed from "unreduxed";

const useCounter = (init?: number) => {
  const [count, setCount] = React.useState(init ?? 0);

  const increment = React.useCallback(() => setCount((prev) => prev + 1), []);
  const decrement = React.useCallback(() => setCount((prev) => prev - 1), []);

  return { count, increment, decrement };
};

export const [ContainerProvider, useContainer] = unreduxed(useCounter);

繰り返しになりますが、 useCounter はただのカスタムフックです。つまり、 フックのルールさえ守られていれば、何をやってもいいのです。いくつも useState を宣言してもいいでしょう。 useEffect を使用してもけっこうです。サードパーティライブラリが提供するカスタムフックを使用してももちろんかまいません。

作成したコンテナフックは unreduxed 関数に渡してください。戻り値は固定長のタプルになっており、1つ目が ContainerProvider で、2つ目が useContainer です(固定長タプルなので、受け取る変数は好きな名前がつけられます)。

私は ContainerProvideruseContainer という名前をつけてみました。この名前から使い方のイメージが湧く人も多いでしょう。ContainerProvider はコンテナ(ステート)を共有したいコンポーネントツリーのトップに配置します。 useContainerContainerProvider のツリー内部で使用される必要があります。これは React context の ProvideruseContext の関係と似ていますね(実際にライブラリの内部で React context が用いられているのでこの構造になっています)。

ContainerProvider を設置する

unreduxed 関数から受け取った ContainerProvider を設置します。設置する場所はステートを共有したい範囲のトップレベルです。グローバルステートなら index.tsxApp.tsx といった場所に置くことになるでしょう。

App.tsx
import React from "react";
import { ContainerProvider, useContainer } from "./container";
import styles from "./styles.module.css";

export default function App() {
  return (
    <div className={styles.root}>
      <ContainerProvider>
        <Count />
        <CountButtons />
        <ContainerProvider initialState={100}>
          <Count />
          <CountButtons />
        </ContainerProvider>
      </ContainerProvider>
    </div>
  );
}

<Count /><CountButtons />ContainerProvider に囲われています。
デモアプリの見栄えのため CSS Modules でスタイリングしていますが、本質では有りませんのでここでは言及しません。

ContainerProvider は React context の Provider をベースにしているため、ネストすることも可能です。サンプルコードでもネストさせてみました。内側の ContainerProvider には props.initialState が渡されていますね。ここで渡す値が、あなたがさきほど定義したコンテナフックの引数に渡されます。

useContainer でコンテナから値を取り出す

<Count /><CountButtons /> を定義しましょう。

<Count /> は現在のカウントを表示する役割を持ちます。
<CountButtons /> はカウントを増減させるボタンを表示する役割を持ちます。

App.tsx
const getRandomNum = () => Math.floor(Math.random() * 255);
const getColor = () =>
  `rgb(${getRandomNum()},${getRandomNum()},${getRandomNum()})`;

const Count: React.FC = () => {
  const count = useContainer((container) => container.count);

  // コンポーネントが再レンダリングされるたびに文字色が変わります
  const style = { color: getColor() };

  return (
    <p className={styles.countText} style={style}>
      count: <span className={styles.countValue}>{count}</span>
    </p>
  );
};

const CountButtons: React.FC = () => {
  const increment = useContainer((container) => container.increment);
  const decrement = useContainer((container) => container.decrement);

  // コンポーネントが再レンダリングされるたびに文字色が変わります
  const style = { color: getColor() };

  return (
    <div className={styles.countButtons}>
      <button className={styles.countButton} onClick={increment} style={style}>
        increment
      </button>
      <button className={styles.countButton} onClick={decrement} style={style}>
        decrement
      </button>
    </div>
  );
};

useContainer の使われ方に注目してください。(container) => container.count のようなアロー関数が渡されています。これはセレクター関数といって、指定することでコンテナからほしい値だけを選択して取得することができます。セレクター関数で関心のある値のみに絞り込むことで、それ以外の値の変更による再レンダリングを抑制します。
セレクター関数を指定せずに const container = useContainer() としてコンテナ全体を取得することもできます。が、基本的にはセレクター関数を渡して値の絞り込みをすべきでしょう。

サンプルコードのコメントにもありますが、再レンダリングを視覚化するためにレンダリングのたびに文字色を変える動作を仕込んでいます。これで本当に余分な再レンダリングが発生していないのかがわかりますね。

動作確認

demo-app-qiita.gif

ボタンをクリックしても(count の値が変化しても)テキストのカラーが変わる、つまり再レンダリングされるのは <Count /> だけで <CountButtons /> はそのままなのがわかります。

謳い文句通り、余分な再レンダリングは抑制されています。

コンテナをモック化できる

ContainerProvider には props.initialState 以外に props.mock を渡すことができます。これを渡すとコンテナフックのロジックを停止して常に同じ props.mock を配信し続けることになります。

ContainerProviderprops.mock を渡せることの何が嬉しいかというと、見た目の確認に専念できるということです。
例えばコンテナフックの内部で fetch を使って Web API から取得した値をステートに保持しているとしましょう。

type User = { id: string, name: string };

const useLoginUserInfo = () => {
  const [user, setUser] = React.useState<User>();

  React.useEffect(() => {
    (async() => {
      const response = await fetch("/api/user").then(r => r.json());
      setUser(response)
    })();
  }, []);

  return { user };
};

export const [LoginContainerProvider, useLoginContainer] = unreduxed(useLoginUserInfo);

useLoginContainer を使用しているコンポーネントの見た目の確認をしたいだけなのに、コンテナフックのロジックが動いてしまう以上 API サーバーを起動してリクエストを送信できるようにしておかないとエラーになって表示させることができません。

こんなときに LoginContainerProviderprops.mock を渡せば、 fetch が実行されなくなるので見た目の確認が非常に楽になります。

function App() {
  const mockUser: User = {
    id: "mock-id",
    name: "mock-name"
  };

  return (
    <LoginContainerProvider mock={{ user: mockUser }}>
      {...}
    </LoginContainerProvider>
  );
}

この場合、 useLoginContainer(container => container.user) で取得できるオブジェクトは常に mockUser オブジェクトと等しくなります。
この機能は Storybook などで重宝するはずです。

まとめ

私が開発した unreduxed というステート管理ライブラリを紹介しました。

使い方はカスタムフックを定義してわたすだけ。非常に簡単に導入が可能です。
他のステート管理ライブラリとも共存できるので部分的な導入も良いでしょう。

ぜひ使っていただきフィードバック等いただければ喜びます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアルやった後くらいに学んだ方が良いことをUdemyで学ぶ

はじめに

自分は去年から未経験でアプリ開発の現場に就職しました。
当時のレベルはRailsチュートリアルを3周くらいして、まあ多少はRailsのコードは書けるのかな?程度のレベルでしたが、もちろん実務ではそんなものでは全然足りずUdemyで色々受講したりして勉強しました。

そこで自分が受講して実際に役にたっている講座と内容を紹介したいと思います。
良い講座ばかりですが、注意点も多少あるので各章の最後に書いておきます。

Docker

開発環境を整えるのにもはやDockerは必須ですよね。
Dockerという言葉は聞いたことあるけど、よく分かってない。そんな声がちらほら。

私も環境構築に使うんでしょ?くらいの認識でした。
ということで下記講座を受けました。

ゼロからはじめる Dockerによるアプリケーション実行環境構築

Dockerとは何かから、実際にコンテナを立てたり、docker-composeでRails環境作るところまで分かりやすく解説されています。

※注意点
最後にSwarmの解説ありますが、今のデファクトスタンダードはKubernetes、略してk8s(かっこいい)なので、ここは特に見なくても良いのかなって気がしました。(もちろん勉強になると思います)

React

Dockerで環境構築できたらRailsはAPIモードにしてバックエンド専用、フロントはモダンなやり方で実装したいですね。そこでReactやります。

Railsチュートリアル終わったくらいだと、おそらくjQueryは分かると思いますし、RailsのViewをSlimで書いたりしていたと思いますが、バックエンドとフロントエンドを分離して、RailsはAPIでデータを返すだけ、フロントでの描画はReactで書きましょう。

Reactは二種類の書き方があり、一つはクラスコンポーネント、もう一つは関数コンポーネントです。
公式ではクラスコンポーネントはサポートを廃止する予定はないものの、新しいプロジェクトでは関数コンポーネントで書いても良いんじゃないかと言っていますので、おそらく関数コンポーネント推しなのではないでしょうか。(関数コンポーネントを絶対に使えとは書いてなかったです)

Reactのv16.8からHooksというものが導入されて、関数コンポーネントでstateを扱えるようになりました。最近は関数コンポーネントでしかコード書いてません。というか関数コンポーネントからしか勉強してないので関数コンポーネントしか書けません笑

以下動画ではHooksを使った関数コンポーネントの使い方が学べます。

【はむ式】React Hooks 入門 - HooksとReduxを組み合わせて最新のフロントエンド状態管理手法を習得

※注意点
クラスコンポーネントの解説は基本的にありません。クラスコンポーネント使わなくてもコーディングできますが、一応なんとなく読める程度にはなっておいた方が良いとは思いますので別で勉強必要かと思います。
個人的に普段書くことはありませんが、クラスコンポーネントは多少読める程度には勉強しました。

GraphQL

RailsチュートリアルでREST APIがどんなものかはなんとなく理解したかと思います。
実際は、APIとか意識してなかったと思いますが、routes.rbでURLとアクション紐付けてますよね。あれがREST APIです。
そうしたら次は別のAPIプロトコル学んでみましょう。

簡単に説明しますと例えば、REST APIでは/usersのようなurlにアクセスすると全てのuserの全ての情報が取れます。でも場合によっては全userの名前だけ欲しい時がありますよね?
そうすると、全userのidとかアバター画像のurlとか無駄なデータもフロント側に渡されます。

GraphQLでは欲しいものだけリクエストができるので、全userの名前だけ取得するといったことが可能です。

さらにスキーマ自体がAPIの仕様書になるので開発スピードが上がるということです(正直この辺は時と場合にもよるので絶対正しい訳ではないと思います)

ものすごくざっくり言うとREST APIでは、あるurlに対して何を渡すと何が返ってくるかという仕様書を作る必要があったのですが、GraphQLでは何を渡すと何が返ってくるかという仕様書を作るそれ自体がAPIの実装になるのでわざわざ別で作る必要がないといった感じです。

【はむ式】フロントエンドエンジニアのためのGraphQL with React 入門

※注意点
この講座ではGraphQLとはどういうものか学べますが、実際にAPIをどのように実装するかはカバーしていません。Railsで実装する場合にはgraphql-rubyというgemを使って実装していきます。

まとめ

これでDockerで環境構築して、RailsでGraphQLのAPI作成して、フロントをReactで作れるようになりました。
ReactでGraphQL扱うのにGraphQLクライアント必要なのですが、Apollo Clientが一番有名だと思うので、色々参考にしながら実装してみて下さい。

ちなみにReactの動画でReduxも多少学ぶことができるのですが、普段Redux使っていません。
なぜかというと前にReduxを使おうとしたらApollo Clientと競合したためです。

Apollo Client自体にstateを管理する機能があったり、useContextというHooksを使えば余程複雑ではない限りRedux使わなくても十分対応できると思います。

ということでRailsチュートリアル終わったらこの辺クリアしていくと良い感じに順番に技術を学べるんじゃないかというちょっとした経験談でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

D言語くんと秘密のレポート2

はじめに

さて、今年もD言語くんアドベントカレンダーが始まりました!

昨年に引き続き、 @simd_nyan 氏によって1日目に公開されたD言語くんのツイート一覧を見るために独自のビューアーを作っていますが、こちらをより扱いやすくアップデートしていきます。

大阪万博のロゴで脚光を浴びることもありましたが、VRChatへの参戦やイラスト投稿など継続的に発展を遂げており、
そんな細かいことはいいんだよ!というD言語くんファンの方は以下URLをご覧ください。

https://lempiji.github.io/dman-tweet-viewer/

今回からは自動再生もあるので、垂れ流しインお布団のスタイルもおすすめです。(寝てください)

ちなみに昨年の記事は以下になります。

https://qiita.com/lempiji/items/e46056ddf3f829f793fb

概要

さて、今回も技術的には React + TypeScript + Material-UIです。

Vueも3.0になったし別に作ろうかと考えましたが時間不十分と判断して断念しました。
雑に作るのはD言語くんに失礼なので仕方ありません。

というわけで更新しました。以下のような感じです。

image.png

更新点

前回からの変更点は以下3つです。

  • 総件数表示
  • 自動再生
  • ツイート読み込み中および読み込み失敗表示の追加
  • インクリメント周りのバグ修正

総件数表示は文字通り件数表示しただけなので割愛します。
自動再生と読み込み中表示と読み込み失敗表示について軽く説明していきます。

読み込み中表示

Twitter公式で提供されるスクリプトを使えば埋め込みツイートは簡単に表示できます。
細かいコードは昨年記事を参照してください。

変えた部分は、Reactの useRef を使って適当なdivに埋め込む以下のコードのあたりです。

const t = twttr.widgets.createTweet(tweet, containerRef.current, {});

createTweet の戻り値は Promise<HTMLElement> となっているのですが、これの完了を待機することで読み込み完了が判定できます。

読み込めないツイートに関しては完了後にノードの中が空になるため、それを使って判定します。(catch ではない点に注意)

まず表示部のイメージですが、エラーフラグなど見ながら表示を変えること、コンテナにあたるノードは常に置いておくことの2点に留意して以下のようになります。

return <>
    <div key="container" ref={containerRef}></div>
    {loading && (<div>Loading...</div>)}
    {error && (<div>Not Found</div>)}
</>;

あとはフラグを用意して、Promiseの完了を待って切り替えればOKです。

const t = twttr.widgets.createTweet(tweet, containerRef.current, { width: Math.min(window.innerWidth - 20, 500) });
// 追加
setLoading(true);
setError(false);
t.then(() => { 
    setLoading(false);
    setError(!containerRef.current.firstChild);
});

return () => {
    t.then(e => {
        if (e && e.parentNode) {
            e.parentNode.removeChild(e);
        }
    });
};

自動再生

自動再生は、再生ボタンを押すと2秒待機して次に進む動作を行い、読み込みが終わったら待機を繰り返します。

何も考えず setInterval などをコールバックで呼べば簡単なのですが、せっかくなので読み込みを待って適切に処理しつつ、副作用は useEffect に収まるよう少し手間をかけました。
useReducer を使ってやるところですが、管理がちょっと面倒だったのでベタ書きしてしまいまったのは反省)

ポイントとしては、自動再生用フラグ以外にもう1つフラグ用意しておき、任意のタイミングで useEffect をトリガーできるようにする点です。
フラグはある程度隠蔽したいので、独自のHooksということで useSignal というのを作ります。

useSignal
function useSignal(): [boolean, () => void] {
    const [signalCount, setSignalCount] = React.useState(false);
    const notify = React.useCallback(() => { setSignalCount(current => !current); }, [setSignalCount]);
    return [signalCount, notify];
}

これを使って必要な状態やコールバックを準備します。

状態
const [autoPlay, setAutoPlay] = React.useState(false);
const [tweetLoaded, notify] = useSignal(); // 独自のHooks

const handleTweetLoaded = React.useCallback(() => { notify(); }, [notify]);

一定時間待って更新するあたりを用意します。(見るからに useReducer にすべき感じですが、、)

クリック処理のエミュレート
React.useEffect(()=>{
    if (autoPlay) {
        const timer = setTimeout(() => {
            setCount(c => Math.min(c + 1, tweets.length - 1));
        }, AUTOPLAY_INTERVAL);
        return () => {
            clearTimeout(timer);
        };
    }
}, [autoPlay, tweetLoaded, tweets]); // ここにトリガー変数を入れておくと任意のタイミングで発火できる

あとはロード時にトリガー変数を更新するハンドラを設定しておけば、 useEffect の依存変数が切り替わることでuseEffect の処理がトリガーされます。

onLoadでトリガー
<Tweet tweet={tweets[count]} onLoad={handleTweetLoaded} />

おわりに

useStateuseEffect を使ったプログラミングにもだいぶ慣れてきた感じですが、自動再生あたりになってくるとやや入り組んできた感じになりますね。

この際 ContextuseReducer など使ってしっかり書いておくべきというのはあるのですが、時間足らず今回はここまでとします。

それではみなさんもD言語くんと同じ視点に立って、この年末も無事に過ごしてまいりましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssemblyをただC#実行プラットフォームとして使って既存のReactのWebアプリを拡張した話

Blazor Advent Calendar 2020 1日目の記事です

おことわり

特定技術の1日目の記事なんだし、Blazorとはみたいな前提の共有するよね!?と思った方すみません…
Blazor WebAssemblyがGAされて半年経過していますし、いろんな解説記事が溢れているはずと信じてその辺りは割愛します。

公式ドキュメントはこちら
ASP.NET Core Blazor の概要 | Microsoft Docs

…さて、ドキュメント読みましたか?
Blazor(の中のBlazor WebAssembly)についてわかりましたか?
概要の所を一部抜粋すると

  • Blazor WebAssembly は、.NET を使って対話型のクライアント側 Web アプリを構築するためのシングルページ アプリ (SPA) フレームワークです。
  • WebAssembly (略称 wasm) によって、Web ブラウザー内で .NET コードを実行することが可能になります。

「.NETでSPAクライアントが書けて、ブラウザ上で.NETコードを実行できるいい感じのフレームワーク」らしいです、すごいフレームワークですね。
そんなすごいBlazor WebAssemblyを使ってみたら良かった話をします。

今回の話のテーマとなるアプリケーション

Repository: https://github.com/yamachu/cognitive-cv-visualizer
WebSite: https://cognitive-cv-visualizer.yamachu.dev/

React.jsでUIが書かれていて、C#でAPIが書かれている素朴なアプリケーションが今回のテーマです。

ざっくりとこのアプリケーションは何をするかと言うと、ユーザがこのページにドラッグ&ドロップした画像に対してOCRをかけて、その領域を描画します。
動作こそしますが、このアプリケーション(あるいは作り)には問題点が一つありました。

このアプリケーションのOCRの部分はAzure Cognitive ServicesのComputer Visionに含まれるものを使用しています。
このOCRのAPIを使用するためにはCognitive Servicesのサブスクリプションが必要ですが、そのサブスクリプションのキーやAPIのエントリポイントの情報はユーザに入力してもらって、自分の実装したAPIに送信し、APIからCognitive Servicesの機能を叩くみたいことをしています。

送られてきたパラメータをdumpしたりログに残していない限りそのキーが漏れることはないでしょうが、自分で実装したAPI側で何らかの例外を起こしてRequestParameterなどもトレースに表れてしまった場合見てはいけないユーザのCredentialsを自分が見れてしまう状態になってしまいます。

問題解決に向けて

上記の問題解決に向けて実装時に考えたことが書いてあったり実装を行ったPRがこちら
https://github.com/yamachu/cognitive-cv-visualizer/pull/7

Uncontrollableな外部APIと連携している自分で実装したAPIが一番のネックとなっています。
なのでこの問題を解決するにはこの自分の実装しているAPIから引き剥がすのが一番の近道と言えます。
それを達成するためにAPI側で行っていることをJSでリライトする手もありましたが、今回はBlazor WebAssemblyを採用しました。

理由としては

  • APIがC#で書かれていたので、Interfaceを調整すればそのまま移植できそうだった
  • APIに投げていたパラメータが文字列の集合で容易に表現可能だった

という点が挙げられます。

公式ドキュメントには『Blazor WebAssembly は、.NET を使って対話型のクライアント側 Web アプリを構築するためのシングルページ アプリ (SPA) フレームワークです』と書かれていて既に仮想DOMをJSで管理していた場合はどうなるんだろうという不安がありましたが、Blazor WebAssemblyも静的なHTMLの特定のDOMツリーの下にマウントするという形でDOMを管理しているため相互に参照し合うことがなく、また別のツリーの出来事なので問題はありませんでした。

それでは実際に行った移行プロセスを紹介します。

  1. [C#] dotnetコマンドでBlazor WebAssemblyのテンプレートを作成
  2. [C#] APIとして提供していた機能の切り出し
  3. [C#] 当該機能を呼び出すJSInvokableアノテーションのついたロジックをDOMツリーにマウントするコンポーネントに実装
  4. [JS] JSInvokableアノテーションを呼び出すメソッドを実装
  5. [JS] APIとBlazor WebAssembly側が提供しているメソッドを呼び出す箇所の差し替え
  6. [HTML] Blazor WebAssemblyを実行するためのエントリポイントを追加

の大まかに6ステップです。

1はドキュメント通りで、2はアプリケーションに依ることなので省略します。
3、4は公式ドキュメントのASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出すに沿って行いました。

コードを例に挙げると、
C#(Blazor)側でこんな感じのコンポーネントを実装して(https://github.com/yamachu/cognitive-cv-visualizer/pull/7/commits/c4b0d32f564e00b352ca014be0e240306d5066fa)

@using System.Net.Http
@using CVVisualizer.Core

@code {
    private static HttpClient httpClient = new HttpClient();

    [JSInvokable]
    public static Task<string> RunOCR(string endpoint, string subscriptionKey, string imageBase64)
    {
        var image = Convert.FromBase64String(imageBase64);
        return VisionOCRService.AnalyzeAsync(httpClient, endpoint, subscriptionKey, image);
    }
}

JS側で(https://github.com/yamachu/cognitive-cv-visualizer/pull/7/commits/a50833857959438219598256c9fdd1d08926328a)

return window.DotNet.invokeMethodAsync(
          "CVVisualizer.Blazor",
          "RunOCR",
          formEndpoint,
          formSubscriptionKey,
          base64File
        );

こんな感じのコードを書くだけです。
C#側のBlazor WebAssemblyは『シングルページ アプリ (SPA) フレームワークです』と言われているのに一切タグを吐き出さない構成となっています。

これこそがタイトルにした「Blazor WebAssemblyをただC#実行プラットフォームとして使って」という意味です(タイトル回収)。

5、6も実装に依るのでこの記事ではスキップします。

おわりに

Blazor WebAssemblyを既存のWebアプリケーションに付け加えて、不安のあるAPIを一つ消し去ることが出来ました。
この様に全てBlazorのライフサイクルに乗せたりUIやStateの管理を行わなければいけないということはなく、ただ.NETのコードを実行する環境だけとして使うことも可能なのです。
例えばドメインロジックなどを移植してフロント側を固くしたりすることも出来そうですね。
様々な可能性を秘めたBlazor、多くの人が使って更に知見が増えていけばいいなと思います。

明日は @jsakamoto さんのデザインコンポーネントのお話です。

フルBlazorで作ると苦労しがちなデザインなので、デザインフレームワークの話は気になりますね、お楽しみに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む