- 投稿日:2021-02-26T23:33:55+09:00
create-next-app --example with-reduxを使った際の各ファイルの相関図
- 投稿日:2021-02-26T23:15:58+09:00
ReactModal と EventListener と useCapture
react 2年目がいろいろ学んだことやハマったことの記録。
ファンクションキーのイベントリスナー登録で悩んだお話です。ボタンにファンクションキーを割当てる
この機能は、他の方によってすでにフック関数で実装されていました。
ファンクションキー押下の検知は、keyDownイベントリスナーを登録することで制御しています。export const useFunctionKey = ( code: string, disabled: boolean, callback: VoidFunction ): void => { const fn = useCallback( e => { if (e.key === code && !disabled) { callback(); e.cancelBubble = true; e.returnValue = false; e.stopImmediatePropagation(); e.preventDefault(); } }, []); useEffect(() => { document.addEventListener('keydown', fn, false); return () => { document.removeEventListener('keydown', fn, false); }; }); };モーダルと親画面でファンクションキーを割り当てる
いくつかの画面でモーダルを表示する機能があり、モーダルにもファンクションキーを割り当てたボタンが配置されています。親画面と同じファンクションキーであったり、そうでないものもあります。
モーダルが表示されている状態でファンクションキーを押したら、現状どうなるのか?それを調べた上、以下の機能を満たすよう、必要な修正をすることになりました。
- モーダルが表示されている間はモーダルのファンクションキーのみ有効にする
- 上記以外のキーは、親画面に割り当てられたファンクションキーも含めて無効になる
- モーダルが閉じられたら、親画面のファンクションキーのみ有効になる(モーダルのファンクションキーは解除される)
イベントリスナー登録・解除
モーダルもすでに作成されていましたが、ファンクションキーの制御は入ってません。
モーダル上で入力されたキーを判定してイベント伝搬を制御する処理を追加します。const AllFunctionKeys = ['F1',...(略)...,'F12']; ReactModal.setAppElement('body'); const Modal: React.FC<Props> = ({ show, functionKeys = [], children, }) => { const handleModalKeyDown = React.useCallback( e => { if (!AllFunctionKeys.includes(e.key) || functionKeys.includes(e.key)) { // ファンクションキー以外の入力、またはボタンに割り当てたキーであればスルー return; } // 対象外のファンクションキーの場合はイベント伝搬を止める(キーを無効にする) e.cancelBubble = true; e.returnValue = false; e.stopImmediatePropagation(); e.preventDefault(); }, [functionKeys], ); return ( <> <ReactModal isOpen={show} shouldReturnFocusAfterClose={false} > {children} </ReactModal> </> ); };次にこのイベントリスナーをどのタイミングで登録して解除すべきか。要件は「モーダルが開いている間のみ」です。
モーダルはReactModalを用いた共通コンポーネントとして実装されています。
ReactModalには、モーダルを開いたとき・閉じたときのコールバック関数を定義できるとわかったので、モーダルが開いたタイミングでイベントリスナーを登録し、閉じたタイミングで解除する、としました。// モーダルを開いた時 const handleAfterOpen = React.useCallback(() => { document.addEventListener('keydown', handleModalKeyDown, true); }, [handleModalKeyDown]); // モーダルを閉じた時 const handleAfterClose = React.useCallback(() => { document.removeEventListener('keydown', handleModalKeyDown, true); }, [handleModalKeyDown]); return ( <> <ReactModal isOpen={show} shouldReturnFocusAfterClose={false} onAfterOpen={handleAfterOpen} // 追加 onAfterClose={handleAfterClose} // 追加 > {children} </ReactModal> </> );モーダルのイベントリスナーのほうを優先してほしいため、
addEventListener
の第3引数をtrue
に指定します。イベント伝搬の制御
上記の実装でテストしてみます。
親画面に、F1,F3を割り当てたボタンを、モーダルにF1,F2を割り当てたボタンを配置します。const EventListenerTester: React.FC = () => { const [show, setShow] = React.useState(false); const handleClick = React.useCallback((message: string) => { console.log(message); }, []); const handleClickModal = React.useCallback(() => { setShow(!show); }, [show]); return ( <> <FunctionKeyButton shortcutKey={'F1'} onClick={() => handleClick('page F1')} >ボタン</FunctionKeyButton> <FunctionKeyButton shortcutKey={'F3'} onClick={() => handleClick('page F3')} >ボタン</FunctionKeyButton> <Button onClick={handleClickModal} >モーダル</Button> <Modal functionKeys={['F1', 'F2']} show={show}> <> <div>モーダルのテスト</div> <div> <FunctionKeyButton shortcutKey={'F1'} onClick={() => handleClick('modal F1')} >ボタン</FunctionKeyButton> <FunctionKeyButton shortcutKey={'F2'} onClick={() => handleClick('modal F2')} >ボタン</FunctionKeyButton> <Button onClick={handleClickModal}>閉じる</Button> </div> </> </Modal> </> ); };まず親画面のみ出ている状態で、F1->F2->F3の順でファンクションキーをクリックします。
page F1 page F3これは想定通りの動きとなってます。
その後、モーダルを表示して同じようにF1->F2->F3の順でファンクションキーをクリックします。page F1 modal F2となりました。期待したのは両方ともモーダルで登録したほうのリスナーが実行されることでしたがF1のみNGでした。
最後のF3については正常にキャンセルされた、ということも確認できました。モーダルで使うファンクションキーは、そのままボタンのリスナーへイベント伝搬してほしいため、スルーするよう実装しました。しかし、ボタンに登録したリスナーはデフォルトの「登録された順番で実行」されるため、親画面のほうのリスナーが最初に実行されて、イベント伝搬が止められました。そのため、モーダルのボタンで登録されたリスナーが実行されなかったということになります。
ボタンに登録する場合のリスナーも「モーダルに配置するボタンなのかどうか」を考慮してリスナーの実行順序をコントロールしないといけないということです。
フック関数に引数を追加することにします。
ファンクションキーは親画面で使うほうが多いと思われたため任意設定とし、未指定時はfalse
でよいようにします。export const useFunctionKey = ( code: FunctionKey, callback: VoidFunction, disabled: boolean, modal?: boolean, // 追加 ): void => { // 省略 useEffect(() => { document.addEventListener('keydown', fn, modal); // 第3引数を修正 return () => { document.removeEventListener('keydown', fn, modal); // 同上 };テストのほうも、モーダルに配置するボタンは「モーダル指定」に修正します。
<div>モーダルのテスト</div> <div> <FunctionKeyButton shortcutKey={'F1'} onClick={() => handleClick('modal F1')} modal>ボタン</FunctionKeyButton> <FunctionKeyButton shortcutKey={'F2'} onClick={() => handleClick('modal F2')} modal>ボタン</FunctionKeyButton> <Button onClick={handleClickModal}>閉じる</Button> </div>さきほどと同じようにF1〜F3まで順番にファンクションキーを押していきます。
page F1 page F3さきほどと同様な動きのままで影響ありません!
そしていよいよモーダル表示時です。modal F1 modal F2無事にモーダルのほうのイベントリスナーが実行されました。
これで、機能要件の1つめ、2つめはOKとなりました。最後は3つめ、モーダルを閉じたらモーダルで登録したイベントリスナーは削除し、もとの親画面のイベントリスナーが有効になることです。
実はここもNGでした。。
モーダルのイベントリスナー登録の条件が間違っており、閉じたときに正常にイベントが削除されていませんでした。
原因は、前回の記事と同様のため詳細は書きませんが、useCallback
のdepsに、リスナーを指定してしまったこと。イベントリスナー登録時と解除時で中身が変わってしまうため、リスナーの解除ができてませんでした。こちらもこのように書き換えて、機能要件3つめも無事解決となりました。
const handleAfterOpen = React.useCallback(() => { document.addEventListener('keydown', handleModalKeyDown, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleAfterClose = React.useCallback(() => { document.removeEventListener('keydown', handleModalKeyDown, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []);参考サイト
- 投稿日:2021-02-26T14:35:32+09:00
TypeScript + React プロジェクト作成法
- TypeScript1 + React2 環境構築のために検索していたけれど、 だいたいのブログでこのコマンドを推奨される。
npx create-react-app react-ts-app --typescript
しかしこのコマンドを打ってもうまくプロジェクトが作れず、地味に詰まったのでメモ。前提 :
- Node.js インストール済 (筆者環境v14.15.1)
パッケージ管理別.bin 実行コマンド
npm(Node.jsのデフォルトパッケージ管理) の場合 :
npx
yarn の場合 :yarn run
手順 :
1. tsconfig.jsonの作成・初期化
- ローカルインストールされたスクリプトの実行
npx tsc --init2.TypeScript + React プロジェクト作成コマンドを打つ
npx create-react-app {プロジェクト名} --template typescript
npx create-react-app react-ts-app --template typescript備考 :
version 確認コマンド
npx tsc --version
- 投稿日:2021-02-26T12:57:40+09:00
【NoCode】5年目iOSエンジニアが、アプリのE2Eテスト自動化サービスを作った話
iOSエンジニア歴も早5年目となりました。
その経験を活かして、iOSアプリのテスト自動化サービス「SmartQA」を作りました!
今回の開発では、Firebaseが大活躍してくれました。
SmartQAをリリースする上で、AppiumやFirebaseの技術的な知見をここに残せたらと思います。
概要
ビルドをアップロードするだけで、ブラウザ上で簡単にテストの自動化ができます。
UIが変わってしまった場合でも、要素探索を自動で修復してくれます。
複数端末対応しているので、「iPhone SEやiPadだとクラッシュしてしまった」なんてことも検知できます。
現在はiOSアプリのみ対応しております。
私について
10歳の頃にアセンブリに出会ったのがきっかけで、プログラミングの世界にのめり込みました。
どんなプログラムも足し算・引き算・比較・ジャンプで動いてることに感動したことを今でも覚えています。
京都大学工学部情報学科を卒業して、Levetty株式会社を立ち上げました。
在学中にiOSエンジニアとして働き始めました。
当時京都では学生がエンジニアとして働ける会社がほとんどなく、大学生協のアルバイト募集に稀に出現するエンジニア募集を見つけるしかありませんでした。
大学生協を毎日クロールするスクリプトを組んで、LINEに通知して最速で応募したりしてました。
開発体制
開発期間としては、9月に実現できるのか技術検証を行い、10月から開発を始めました。
基本的に僕1人で開発を進めていたのですが、仲の良いエンジニア3人に手伝ってもらえたのがすごく大きかったです。
設計を綺麗にしてくれたり、インフラ整えてくれたり、なかなか手が回らない部分の実装をしてくれたり、サービスのアイデアをいただけたり。
とても感謝しています。
使用した技術
Appium
iOSやAndroidでシミュレータを操作するのに必要なライブラリです。
iOSで使う場合は、内部でXCUITestを使用しています。
Firebase
- Authentication
- Cloud Firestore
- Cloud Functions
- Cloud PubSub
- Cloud Storage
- Hosting
React.js / Redux / TypeScript
フロントは、SPA(シングルページアプリケーション)で作成しました。
Material-UIが大活躍してくれました。
Express / Node.js / TypeScript
テスト実行サーバー、macOS上で動作する簡易的なサーバーは全てExpressで実装しました。
AWS CDK / Ansible
サーバー側の実装は、Cloud Functionsに寄せていました。
しかし、540秒の実行時間の制限がきつく、テスト実行処理はAWSのEC2で運用することになりました。
手伝ってくれている凄腕エンジニアがAWS CDKとAnsibleを使って構築してくれていました。
macOSを何台も用意する必要があるので、Ansibleを触ったのですが、革命を感じました。
CDKについては、僕は触ってないので分かりません、、。が、めちゃくちゃ良いみたいです。
苦労したこと
大量のMacのセットアップ
iOSのシミュレータを使用するには、macOS環境が必要です。
SmartQAでは、macOSを貸し出してくれるMacStadiumというサービスを利用することにしました。
借りたMacを1つ1つ設定していくのですが、それはもう大変でした。
途中からAnsibleで自動化され楽にはなったものの、それでもXcodeをインストールするのは手動じゃないとダメだったりします。(2ファクタ認証の関係で)
平成時代から続くiOSシミュレータ自体のバグ
5年前に、AppleのDeveloperフォーラムに投稿されている内容です。
Hardware keyboard not working in simulator (toggle in on)Macのキーボードをシミュレータに接続しない設定で起動しても、接続されているバグがあるんですよね。
キーボードが接続されていると、UITextFieldにフォーカスするために、2回タップが必要だったり。
実際とは違う挙動をしてしまうんですよね。
もちろんApple信者としては、仕様として受け入れることにしました
調べた限り対策は1つしかなく、ここを2回クリックしてオンオフするしかないみたいです。
AppleScriptというものをこの時初めて知り、起動直後にConnect Hardware Keyboardを2回クリックするスクリプトを用意しました。
tell application "System Events" set hwKB to value of attribute "AXMenuItemMarkChar" of menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator" click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator" click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator" if ((hwKB as string) is equal to "missing value") then do shell script "echo 'hardware keyboard is off'" else click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator" end if end telliOSアプリのUI要素のXMLの情報量が極端に少ない!
Appiumでは、XCUITestのdebugDescriptionを利用してWebでいうところのDOMを取得しています。
そのDOMをXML形式で取得できるのですが、classやidなどの情報があるWebに比べて情報量が圧倒的に少ないです。
<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="パスワード" name="パスワード" label="パスワード" enabled="true" x="24" y="87" width="75" height="20"/> <XCUIElementTypeSecureTextField type="XCUIElementTypeSecureTextField" value="Abc1234" name="" label="" enabled="true" x="190" y="87" width="161" height="21"/> <XCUIElementTypeTextField type="XCUIElementTypeTextField" value="ピッカー入力" name="" label="" enabled="true" x="24" y="131" width="327" height="22"/> <XCUIElementTypeButton type="XCUIElementTypeButton" name="Login" label="Login" enabled="true" x="165" y="185" width="45" height="34"> <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="Login" name="Login" label="Login" enabled="true" x="165" y="191" width="45" height="22"/> </XCUIElementTypeButton>こちらがXMLの一部です。
UIButtonやUITextField、UILabelについて得られる情報は、要素の種類・座標・サイズ・値があります。
<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" x="171" y="598" width="45" height="44"/> <XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" x="235" y="612" width="17" height="16"/>しかし、UIImageViewやUIViewの場合は、座標とサイズだけが頼りになります。
そこで、何かしらの方法を用いて要素の推定を行う必要があります。
今回は、ルールベースとAIを組み合わせて推定することにしました。
ちなみに、ソースコードを変更できるのであれば、Accessibility Identifierを割り当てると、nameが固有の値になるので、追従がしやすくなります。
Appiumの謎のエラーでシミュレータが起動しなくなる
クラウドのmacOSで、いつも通り何度もシミュレータを立ち上げていたところ、Appiumでエラーが出力され、シミュレータが強制終了するようになりました。
Appiumの再インストール、Xcodeの最新版をインストールなど様々な方法を試しても治りませんでした。
ふと、macOS自体を再起動したところ正常に動き始めました。
????
憶測ですが、シミュレータを立ち上げごとにゾンビプロセスが生成されてしまってるのかもしれません。
この日を境にmacOSは、定期的に再起動される運用になりました
よかったこと
Appiumサーバーを複数立てることで並列化が簡単
テストの実行を早く終わらせるために、並列化をしようと試みました。
Appiumサーバーを複数起動することで実現することができました!
(WebDriverAgentとAppiumのポートがかぶらないように気をつけてください)
M1 Mac上で同一シミュレータを複数立ち上げる、人類の夢を達成しました。
— こーや (@koyataroo) February 12, 2021
E2Eテストが一瞬で終わる世界線作る! pic.twitter.com/P3JdxByXhRFirestoreのCollection Groupが強力
SmartQAでは、セキュリティを最大限考慮した設計になっています。
project
配下に情報を置くことで、セキュリティルールで縛りやすくしています。その代わり、横断検索が大変になってしまいます。
2019年にリリースされたCollection Groupで、解決することができました。
/project/{projectID}/collectionA/{documentID}通常、このような階層で
collectionA
に対して検索する場合、それぞれのプロジェクトに対して実行しなければいけません。しかし、CollectionGroupでは、同じコレクション名であれば横断的に検索をすることができます。
どんな階層に存在していても可能です。
インデックスを設定するだけで使用できて、非常に簡単でした。
さいごに
インフラからバックエンド・フロント、更にはmacOSのシミュレータの資源割付まで、幅広い実装でエンジニア人生の集大成ともいえるプロダクトでした。
テスト実行の安定化や、機能追加に励んでいきたいと思います!
長期的には、Android対応などやることがたくさんあります。
全然人手が足らなくて、エンジニアさんやQAさんを絶賛募集しております。
少しでも興味があれば、contact@levetty.co.jpか@koyatarooに連絡お待ちしております!
- 投稿日:2021-02-26T06:53:42+09:00
Next.js + RailsでポートフォリオサイトをISR対応&メンテナンスフリー化した
2年ほど前にNuxt.jsを使ってポートフォリオサイトを作成しました。
今回、このサイトをNext.js + Railsでリニューアルしたので、経緯を記事にまとめます。リニューアル後のページ
デザインは前回のものを踏襲していて、ほとんど変わっていません。
リポジトリ
リニューアルの目的
Next.jsを使って何か作りたい
昨年からReactやNext.jsを触ってノウハウを蓄積するようにしています。私自身普段はRailsを使った開発をしているので、Next.jsを採用するとしたらRailsと組み合わせて使う可能性が高いです。
昨今のフロントエンド界隈の盛り上がりを横目に、フロントエンドにNext.js/バックエンドにRailsを用いて、何か作りたいと思っていました。
デプロイなしで内容を更新したい
前回、勢いでポートフォリオサイトを作成したものの、単なる静的ページとして公開していたので記載内容を変更するためにはVueコンポーネントを直接編集する必要がありました。
今回は管理ページを別途作成し、ログインすることで記載内容を容易に追加・変更できるようにしています。
メンテナンスフリーにしたい
リニューアル後のサイトではQiitaやZennに公開した記事、SpeakerDeckに公開したスライドをを自動的に収集し、メンテナンスしなくても内容が自動更新されるようになっています。
システム構成
Next.jsのデプロイ先としてVercelを、Railsのデプロイ先としてherokuを使っています。
また、画像の格納先としてAWSのS3を利用しました。ライブラリ・フレームワーク
フロントエンド
- React
- Next.js
- React Hook Form
- react-dropzone
- react-spinners
- react-tippy
- axios
- SWR
- Tailwind CSS
バックエンド
- Ruby on Rails
- devise
- faraday
- rails_same_site_cookie
- AWS SDK for Ruby V3
実装上のポイント
ISR(Incremental Static Regeneration)
Next.jsを使ってISRを実現しています。herokuがレスポンスを返す時間に関わらず、来訪者がすぐにページを閲覧できるようにする狙いです。
以下はISRの挙動の解説です。
Next.js(ISR有効)は、アクセスがあった際に生成済みの静的ページをレスポンスします。このとき、herokuへのアクセスは発生しません。
前回のページ生成から指定した時間を経過した後にアクセスが発生すると、静的ページを再生成します。このときNext.jsはサーバーサイドでページを再生成するのを待たず、いったん前回の静的ページをレスポンスします。
再生成が完了すると、以降その静的ページをレスポンスします。
前提として、herokuのFreeプランだと30分間アクセスがない場合にdynoがSleepするので、次にアクセスがあった場合にdynoが起動するまで数十秒ほど待たされてしまうという問題があります。本来の使い方ではないかもしれませんが、バックエンドの処理に時間がかかる場合でも生成済みのページを即時にレスポンスできるという点で、ISRは有用だと感じました。
記事・スライドの自動収集
Heroku Schedulerを使うことで、日次でrakeタスクを実行して、記事・スライドを自動収集しています。
QiitaはAPIを、ZennはFeedを使って、自分自身の記事を収集しDBに保存しています。
namespace :qiita do desc "Fetch articles from qiita" task fetch: :environment do res = Faraday.get('https://qiita.com/api/v2/users/Y_uuu/items?per_page=100') return if res.status != 200 items = JSON.parse(res.body) items.each do |item| next if Article.find_by(link: item['url']).present? item_res = Faraday.get(item['url']) next if res.status != 200 Article.create( title: item['title'], body: item['body'].truncate(100) + '...', published_at: Time.zone.parse(item['created_at']), link: item['url'], ) end end endSpeakerDeckは収集方法を悩んだのですが、よくよく調べると https://speakerdeck.com/yuuu.atom のように、自身のアカウント名の末尾に
.atom
を付与することでFeedを取得できることがわかったので、これを使って収集するようにしました。認証
最初は「SPAの認証はJWT」という思い込みがあったのですが、いろいろ調べていくうちに「cookieを使った認証でも問題ない」との結論に至りました。認証のバックエンドもRailsで、deviseというGemを使ったよくある実装です。
ただし、今回はCrossOriginな構成のためつまづきポイントが多くありました。具体的な実装方法は別記事にまとめたので、興味のある方は参照ください。
Rails 6.1対応版: APIモードのRailsに対してCrossOriginなSPAからSession認証する方法
ファイルアップロード
当初は、バックエンドがRailsということで、Active Storageを使ってファイルアップロードを実現する予定でした。実装をしていく上で「わざわざActive Storageを使う必要があるのか?」という疑問が生じ、最終的にはS3の署名付きURLを使ってアップロード・閲覧する方式に変更したという経緯があります。
こちらも別記事にまとめたので、興味のある方は参照ください。
RailsをバックエンドとしたSPAでのファイルアップロード機能の作り方に悩んだ話
感想
ISRが良い
SSRとSSGのいいとこ取りができていて良いです。SSGのようにデプロイのビルドが長くなることもなく、かつSSRを使った場合に比べてページの表示が高速なので満足です。
Vercelが良い
今回初めてVercelを使ってみたのですが、GitHubのリポジトリを指定するだけで簡単にCI/CDを構築できました。前回Netlifyを使った時も同様の感動があったのですが、とかくNext.jsを使う場合はほとんど設定が不要で、噂通りVercelとの組み合わせがベストだと実感しました。
バックエンドのRails・herokuも良い
死んだと言われて久しいRailsですが、自分にとってはやはり最速で実装ができるフレームワークです。バックエンドは必要最低限実装しつつ、フロントエンドの実装に注力するスタイルで開発が進められました。
herokuを使うことでデプロイも非常に簡単でした。
まとめ
個人的には十分満足できるポートフォリオサイトが完成しました。
Next.js + Railsで何か作ろうとしている人の参考になれば幸いです。質問・感想などありましたら、ぜひコメントをお願いします。