20210108のReactに関する記事は4件です。

React/Redux ドラッグ&ドロップイベントのアクション定義

ドラッグやドロップ時のイベントをReact/Reduxで実装する時に少し悩んだので、これはその時の覚書になります。例では、複数のファイルをドラッグ&ドロップした際に、それらのファイルを全て表示しています。

ドロップ時に呼び出す関数

  1. onDrop時に発生するe.dataTransfer.filesを渡すために引数にfilesを指定
  2. オブジェクトの2つ目に引数のfilesをそのまま指定
actions.js
import * as actionTypes from '../utils/actionTypes';

export const onDropFile = (files) => ({
  type: actionTypes.DROP_FILE,
  files,
});

Action

  1. initialAppStateには、ドロップしたファイルを表示するためにfilesを配列で定義
  2. actionには、e.dataTransfer.filesが渡ってくるため、returnのオブジェクトにaction.filesを指定
reducers/test.js
import * as actionTypes from '../utils/actionTypes';

const initialAppState = {
  files: [],
};

const test = (state = initialAppState, action) => {

  if (action.type === actionTypes.DROP_FILE) {    
    return {
      ...state,
      files: action.files,
    };
  } else {
    return state;
  }
};

export default test;

Reducer

reducers/index.js
import { combineReducers } from 'redux';
import test from './test';

const reducer = combineReducers({
    test,
});

export default reducer;

Container

  1. TestContainer自体に関数DragOverAction、DropActionを定義。
  2. ドラッグ&ドロップするdivのイベントに、1.の関数を指定。
  3. DropAction内で、定義したactionsの関数を呼び出す。
TestContainer.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import DropFiles from '../components/DropFiles';

class TestContainer extends Component {

  DragOverAction(e) {
    e.preventDefault();
  }

  DropAction(e) {
    const { test, actions } = this.props;
    e.preventDefault();
    const files = e.dataTransfer.files;
    actions.onDropFile(files);
  }

  render() {
    const { test, actions } = this.props;

    return (
      <div className="test">
        <h1>File Input</h1>
        <div className="upload_area">
          <div className="drop_area"
               onDragOver={(e) => this.DragOverAction(e)}
               onDrop={(e) => this.DropAction(e)}>
            Please drag and drop file here
          </div>
        </div>
        <div className="file-input-list">
          <DropFiles 
            files={test.files}
          />
        </div>
      </div>
    );
  }
}

const mapState = (state, ownProps) => ({
  test: state.test,
});

function mapDispatch(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

export default connect(mapState, mapDispatch)(TestContainer);

ドロップしたファイルが読み込まれると、以下のコンポーネントによって、ファイル名が表示されるようにしています。

components/DropFiles.js
import React, { Component } from 'react';

class DropFiles extends Component {

  render() {
    const allFiles = [];

    for (let i=0;i<this.props.files.length;i++) {
      allFiles.push(
        <button className="drop-file" key={i}>
          {this.props.files[i].name}
        </button>
      );
    }

    return (
      <div>
        {allFiles}
      </div>
    );
  }
}

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

Reactでxmlファイルをアップロードして使う

use-file-uploadを使ってXMLファイルをアップロードしてパースする手順のメモです

use-file-upload

今回はuse-file-uploadを使用しました
https://www.npmjs.com/package/use-file-upload

use-file-uploadはReactフックとして動作するライブラリで、useFileUpload()でstateを作っておいて、buttonなどからonclickで呼ぶ事でアップロードしたファイルをstateとして扱えるようにしてくれます。

以下の例のsource, name, size, fileはそれぞれblob url, ファイル名、ファイルサイズ、ファイルオブジェクトになります。画像ファイルをアップロードして表示する場合は、blob urlであるsourceをimgタグのsrcに与える事でサーバー上のファイルと同様に表示できます。

src/app.js
import React from 'react'
import { useFileUpload } from 'use-file-upload'

const App = () => {
  const [file, selectFile] = useFileUpload()

  return (
    <div>
      <button
        onClick={() => {
          // Single File Upload
          selectFile({}, ({ source, name, size, file }) => {
            // file - is the raw File Object
            console.log({ source, name, size, file })
            // Todo: Upload to cloud.
          })
        }}
      >
        Click to Upload
      </button>

      {file ? (
        <div>
          <img src={file.source} alt='preview' />
          <span> Name: {file.name} </span>
          <span> Size: {file.size} </span>
        </div>
      ) : (
        <span>No file selected</span>
      )}
    </div>
  )
}

export default App;

上記の例ではbuttonタグにCSSを設定せずに表示するので以下のようなシンプルなボタンが表示されます
image.png

アップロードしたXMLファイルをオブジェクトに変換

fileオブジェクトを直接触るやり方が分からなかったので、blob urlをXMLHttpRequestで読んでくるやり方でやりました。XMLHttpRequestでblobを読みに行ってresponseXMLでXMLとして取得します。XMLとして読めなかった場合はresponseXMLにnullが入りますので、nullでなかった場合は表示するというようにしました。

import React from 'react'
import { useFileUpload } from 'use-file-upload'

function applyGPX(xml){
  console.log(xml);
};

function getXML(source) {
  console.log('source:');
  console.log(source);

  var xhr = new XMLHttpRequest();
  xhr.onload = function(e){
    const xml = xhr.responseXML;
    if (xml != null){
      applyGPX(xml);
    } else {
      console.log('not xml file');
    }
  };
  xhr.open("GET", source);
  xhr.send();
};

const App = () => {
  const [file, selectFile] = useFileUpload()

  return (
    <div>
      <button
        onClick={() => {
          selectFile({}, ({ source, name, size, file }) => {
            getXML(source);
          })
        }}
      >
        Click to Upload
      </button>

      {file ? (
        <div>
          <span> Name: {file.name} </span>
          <span> Size: {file.size} </span>
        </div>
      ) : (
        <span>No file selected</span>
      )}
    </div>
  )
};

export default App;

xmlオブジェクトからほしい情報を拾う

XMLオブジェクトが作れたのでほしい情報を拾っていこうと思います。XMLHttpRequest.responseXMLで取得したオブジェクトはgetElementsByTagName(), getElementById(), getElementsByClassName()などで取得できます。

tag名で探す
const elements = hoge.getElementsByTagName('fuga')
ID名で探す
const elements = hoge.getElementById('fuga')
class名で探す
const elements = hoge.getElementsByClassName('fuga')

また、取得したelementのattributeは以下のように取得できます

attributeの取得
const attribute = elements[0].getAttribute('foo')

今回はgpxファイルを読み込むので以下のようにして緯度、経度、高度を取得しました。

function getTracks(xml){
  console.log('getTracks');
  const trackTags = xml.getElementsByTagName('trkpt');
  console.log('track length: ' + trackTags.length);

  var tracks = [];
  var elevations = [];
  for(let k = 0; k < trackTags.length; k++) {
    let element = trackTags[k];
    let latitude = parseFloat(element.getAttribute('lat'));
    let longitude = parseFloat(element.getAttribute('lon'));
    tracks.push([latitude, longitude]);

    let element2 = element.getElementsByTagName('ele')[0]
    let elevation = parseFloat(element2.innerHTML)
    elevations.push(elevation);
  };
  console.log([tracks, elevations]);
  return [tracks, elevations]

余談

use-file-upload以外にもいくつかReact用のアップロードライブラリを試したんですが、blobぐらい分かるよね?的なノリで書いてあって地味に苦戦しました。ウェブ関係のライブラリ制作者が常識だから省いても良いと思っている部分を勉強できる本があったら読みたいので、ご存知の方がいらっしゃいましたら教えて下さい。

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

React.js + AWSでプログラミング無しで自動ブラウザテストが出来るサーバレスWebアプリを作ったお話

敵「リリースした後に既存のコンテンツがおかしくなってないか"目視"でチェックしてね」

ぼく「はい (やりたくねぇ〜〜〜〜〜大体どんなパターン網羅して、どこまでチェックしたらいいんだ? 大体人間が目視でチェックしてもそのうち精度が落ちて結局見落としggg) 」

というのが発端で、以下のようにリリース前後のWebページのスクリーンショットを取得・比較して変更点を視覚的に検出するブラウザテストWebアプリを作りました。

appvisual.png

実装したアプリ「Diffender」と利用した技術要素などについて紹介していきます。

作ったアプリ「Diffender」の紹介

何が出来るか?

Webページのスクリーンショットの取得、取得したスクリーンショット間の変更点の検出ができます。

appvisual2.png
こんな感じで2つスクリーンショットを取得した後に差分を取得することで、リリース前後で表示内容に想定外の変更がないことのチェックが行えます。

一度テスト設定を作成した後は、ワンクリックで繰り返し同じ内容のテストを行えます。

ここ見てもらうのが一番はやいかもです
https://diffender.hassoubeat.com/about

コンセプト

「エンジニア以外の人も簡単に自動ブラウザテスト」

従来の自動ブラウザテストはSeleniumとかPuppeteerなどを利用してブラウザ操作(クリック、テキスト入力)をプログラミングする必要があるため、エンジニア以外の人にはハードルが高いです。

そのハードルを下げるべく、プログラミングをしなくてもブラウザ操作が行えて、テスト結果がスクリーンショットとして出力されるため誰でも結果の正否が判断が行えるブラウザテストアプリを実装しました。

目視でデグレチェックという辛い作業からサヨナラ。

なんで作ったの?

① 非エンジニアのディレクターさんやステークホルダーの方でも見て分かるデグレ検証結果が欲しかった

品質の担保としてはユニットテストとかでもいいんですけど、そのユニットテストの実施内容が分からない非エンジニアの方でも実際に表示される画面のスクリーンショットの差分という形でリリース前後のテスト結果が見れると安心できるようにしたいなーと思ったのが一つ目です。

※ 勿論ユニットテストの代わりになるものではないです

② ブラウザテストの度プログラムを書きたくない、すぐ実行できるようにしたい

すでに触れていますが、従来のブラウザテストはSelenium、Puppeteerなどを利用してプログラミングを行う必要がありました。ハードルが高すぎる。。。

しまいにはプログラムを書いてもテスト大量のページが沢山あると、全ページのテスト完了までにすごい時間がかかります。
そこらへんを並列でササッと実行してくれるやつが欲しかったのが2つ目です。

③ サーバレスSPAを実装する経験値が欲しかった

サーバレスSPAをゼロから構築する経験がなかったので、実際に設計・実装を通して経験値を積みたい!というのが3つ目。(理由の8割くらい)

最終的には形になるものが実装できましたが、効率の良い開発環境はもう少し改善の余地がありそうでした。
※ 主にAWSのローカルモック周りの最適解がよくわからない...

アーキテクチャ

Diffenderのアーキテクチャはこんな感じです。

arch.png

フロントエンドはReact.js、バックエンドはAWS関連サービスで実装しています。
言語はフロント、バックエンド両方JavaScript(Node.js)です。

フロントエンド

JavaScript

js.png
今回はプレーンなJavaScriptで実装しました。
TypeScriptはまた今度...。

React.js

react.png
UI構築用のJavaScriptフレームワーク。
Vue.jsと悩んだんですけど、JavaScriptのレベルアップにはReact.jsがいいみたいな記事をどっかで読んで選んでみました。
確かにJavaScriptの理解が深まった気がします。
(でも終始create react appの優しさに抱かれていた気が...自前でビルド設定のカスタマイズとかメンテナンスするのしんどそう)

次はVue.jsでチャレンジしたいです。

Redux

redux.png
アプリケーション全体の状態管理ライブラリ。
一番設計が難しかった気がする。
どこにビジネスロジックを集約するのがいいのか未だに最適解が分からない...。

AWS Amplify

amplify.png

バックエンドリソースへのアクセス用ライブラリとして利用しました。
再考するとaxiosとかでも十分だったかもしれない。。。

バックエンド

AWS Lambda

lambda.png

イベントトリガーで好きなコードを実行できるAWSサービス。
ランタイムはフロントと揃えてJavaScript(Node.js)

サーバサイドの処理は全部Lambdaで実装しています。サーバレスバンザイ。
Provisioned Concurrencyはケチってコールドスタートなので、初回アクセス時はちょっと時間かかります。。。

後述するSQSと組み合わせて、スクリーンショットの取得や差分の取得といった時間のかかる処理は並列処理にしています。

AWS API Gateway

apigateway.png

簡単にREST APIの構築ができるようにするフルマネージドなAWSサービス。
フロントエンドからコールしたいLambdaのコードと連携してREST APIにしています。

AWS SQS

sqs.png

AWSのフルマネージドなメッセージキューイングサービス。
Lambdaで時間のかかる処理を非同期で並列処理するために採用しました。
この構成お手軽なのに強力すぎる...。

AWS DynamoDB

dynamodb.png

フルマネージドなNoSQLデータベースを提供するAWSサービス。
LambdaのRDS Proxyを使って使い慣れてるRDSを使うという方法も検討したんですが、RDS Proxyが高かったんでDynamoDBにしました(貧困)
RDBMSと違って独特の制約が多くて設計に苦労しました。GSIサイッキョ!

でも後から取得したいクエリの種類が増えると、GSIの貼り直しが必要になったりしてつらみ。
早い段階での要件の洗い出しが大事だ。。。

AWS Cognito

cognito.png

モバイル・Webアプリケーションでのユーザ作成・認証・管理機能をフルマネージドで提供するAWSサービス。
面倒なユーザ管理周りをスクラッチしたくなくて使いました。
分かったら便利だけど、やっぱり使い方が分かるまでのハードルが高いよ...
でもAPI Gatewayと認証の連携が簡単にできるのはAWSサービスで統一する良さを実感できました。

AWS S3

s3.png

容量無制限のデータ保存ができるオブジェクト型ストレージを提供するサービス。
撮影したスクリーンショットを格納先として利用してます。

他にもWebページそのもののデプロイ先としてもAWS CloudFrontとセットで活用してます。

その他

AWS SAM

awssam.png

テンプレートファイルに記載された通りにAWSリソースをデプロイするInfrastructure as Codeを実現してくれるサービス。
AWSマネジメントコンソールでポチポチしてデプロイするよりも遥かにデプロイとリソース管理が楽になったけれど、
最終的には800行ぐらいの巨大ファイルになってビルド・デプロイに時間がかかるのがストレスフルでした。

後発のAWS CDKを使ったらストレスフリーになったりするのかな。

作成期間

学習期間込みで大体週35〜40時間くらいで3ヶ月ちょっとくらい...。

課題

・(UI・UXが)哀れ
必要なクリック数、入力項目が多い。。。
実装する前にちゃんとしたワイヤーフレームを引いて、メインストリームの機能を利用するまでのクリック数などを検討すべきでした。

・AWSサービスを絡めた開発のススメ方
開発当初は毎回コードをAWSにデプロイしてた。。。(時間がかかりすぎる)
単体テストコードを書いてローカルで実行するのが最速?
localstackみたいなサービス公式から出して...
AWS SAMのテンプレートの管理がヤバい。800行ぐらい行ってる。AWS CDK使うべき。。。

他にも色々ありますが、キリがないので割愛。。。

最後に

やっぱり一つのサービスを全て一人で作るのは大変だけど、得るものも多い。。。
始める前はサーバレスSPAの実装ってまったくイメージがついてなかったんですが、今となっては動くものが作れてどこが良くなかったかという観点の振り返りまで出来るようになりました。
ただ有識者に聞いたら一発なところも手探りで進めていたのは非効率だなぁと思わずには居られない場面も多々ありました。
(React.js, AWSサービスの仕様・制約、効率的な開発環境などなど)

気軽に聞けるエンジニア仲間みたいな人がいたら、もっと詰まらずにいいものが出来てたんじゃないかと強く思ったので
今年の抱負として、"えんじにあともだちをふやす" を掲げたいと思います。まる。

その前に新しいお仕事が見つかるといいなぁ...(切実)

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

target="_blank"にはrel="noopener noreferrer"をつける

忘れがちなのでメモ

<a
  src="https://example.com"
  target="_blank"
  rel="noopener noreferrer"
>
  リンク
</a>

Reactでeslintを使ってるならreact/jsx-no-target-blankerror にする

eslintrc.js
module.exports = {
  // ~ 略 ~
  rules: {
    'react/jsx-no-target-blank': 'error',
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む