20200116のReactに関する記事は12件です。

React+Typescript+spring bootで勉強会情報をSlackに通知するサービスを作りました

概要

タイトル通りReact, Typescript, spring bootを用いて個人開発を行いましたので、備忘録としてここに記します。

作ったもの

名前

勉強会情報通知サービス「noroshi」

noroshi

スクリーンショット 2020-01-15 21.49.37.png

サービス内容

使い方をnoteに書きました✨
https://note.com/uusu/n/n8441acfd64da

connpassやDoorkeeperの勉強会情報をSlackに通知するサービス。
地域・キーワードで絞り込みができるので、欲しい勉強会情報を取得できます。
また、通知設定は複数登録することができます。
自分は java勉強会情報チャンネルJS勉強会情報チャンネル それぞれに通知がいくようにnoroshiを使っています。

モチベーション

興味があるイベントを知ったときには既に募集が終了していたり、満員で参加できなかった経験からこのサービスを開発しました。
また、新たな技術に挑戦したいという気持ちも強かったので、モチベーション維持のためReactやTypescript、kotlinを採用しました。

開発期間

全部合わせて二か月くらいです。

使用技術・ツール

技術

アプリケーション
- React (コンポーネントは全てhooksを用いた関数型コンポーネントで作成しました)
- Typescript
- spring boot
- java
- kotlin (サービス層や一部モデルなどをkotlinで書きました)

インフラ
- heroku
- netlify

ツール

  • intellij
  • vscode
  • postman

  • illustrator (ロゴやトップ画面イラスト作成に使用)

システム構成図

ざっくり構成図(フロー図?)はこんな感じです.
archi.png

フロントはreact/tsで書いたものをbuildしてnetlifyでホスティングしています.
バックエンドはspring bootで書いたアプリケーションをherokuで動かしています.
バックエンドはAPIとバッチが共存している構成です.

認証周りはJWTを利用しています.

(あまり詳しくないため断言できませんが,JAMstackに近い構成になっているかと思います)

技術的な知見

個人的に詰まった所などを記します。

spring boot

複数の実装があるinterfaceのDI方法

今回、勉強会情報をconnpassさん、doorkeeperさんのAPIから取得しています。
それぞれAPIレスポンスが違いますが、最終的には取得した情報をサービス共通で利用する勉強会モデル詰めて利用しています。
そのため、共通のイベント情報モデルリストを返すメソッドを定義したインターフェースを、connpassイベント取得クラス・doorkeeperイベント取得クラスで実装しております

そのような複数実装のあるインターフェースをDIする際は、利用する側でList型でラップすると複数の実装クラスがインジェクションされます。

以下のように書きます。
(RequiredArgsConstructorを用いたコンストラクタインジェクションの場合)

@Service
@RequiredArgsConstructor
class StudyGroupService(

// StudyGroupSiteがインターフェース
val studyGroupService: List<StudyGroupSite>?
・
・
・

foreachなどで共通メソッドを呼び出しを行えます。

studyGroupService?.forEach {
                e ->
                e.インターフェースに定義したメソッド()}
            }

とても簡単なことですが、今まで知りませんでした。
こういった基礎的な知識の見落としに気づけるのは個人開発をするメリットですね。

React

hooksでステートを更新すると一部ステートがundefinedになってしまう

言葉で説明するのがめんどくさいので、簡単に再現してみます。
以下二つのファイルをHTMLファイルに張り付けてブラウザで開くと動作します。

classComponent.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ClassComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 0,
b: 0
};

this.handleChangeA = this.handleChangeA.bind(this);
}

handleChangeA(a) {
this.setState({ a: this.state.a + a });
}

render() {
console.log(`a: ${this.state.a}, b: ${this.state.b}`);
return (
<div>
<p>a: {this.state.a}</p>
<p>b: {this.state.b}</p>

<button onClick={() => this.handleChangeA(1)}>a+1</button>
</div>
);
}
}

ReactDOM.render(<ClassComponent />, document.getElementById("root"));
</script>
</body>
</html>

FunctionComponent.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>FuntionComponent</title>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
const FuncComponent = props => {
const [state, setState] = React.useState({ a: 0, b: 0 });

const handleChangeA = a => {
setState({ a: state.a + a });
};

console.log(`a: ${state.a}, b: ${state.b}`);

return (
<div>
<p>a: {state.a}</p>
<p>b: {state.b}</p>

<button onClick={() => handleChangeA(1)}>a+1</button>
</div>
);
};

ReactDOM.render(<FuncComponent />, document.getElementById("root"));
</script>
</body>
</html>

二つは同じ処理をするクラス型コンポーネントと関数型コンポーネントになります。
処理内容はaとbというステートを持ち、a+1というボタンを押すとaのみ1加算されるといったものです。

実際に動かしてみると、setStateの内容は同じなのに関数型コンポーネントではbがundefinedになってしまいます。

このステート書き換えでundefinedになってしまう問題のせいで1日潰れました。
しかし,改めて公式ドキュメントを読むと以下のような記述があります。

しかしクラスでの this.setState とは異なり、state 変数の更新は、マージではなく必ず古い値を置換します。

hooksでは、古いステートが引き継がれないとしっかり公式のドキュメントで書いていました..

クラスコンポーネントと同じ動作にするためには、ステート更新処理を以下のように修正する必要があります。

 const handleChangeA = a => {
 // スプレッド構文を用いて、古いステートも記述する
          setState({ ...state, a: state.a + a });
        };

これで無事hooksを用いた関数型コンポーネントでステート更新を行うことができました。
これが起きてしまった原因は、自分自身Reactを触るのがこれで初で、最初はクラスコンポーネントベースの開発の勉強をしていたため、知識がごちゃごちゃになってしまったことと
単純に公式ドキュメントをしっかり読まなかったことですね。

これを反省し、新たなものを学ぶ際はできるだけ公式ドキュメントや公式が用意してるチュートリアルをやろうと思います。。

useEffectで無限にAPIをコールしてしまう

今回関数型コンポーネント+hooksで全て開発しています。
そこで何らかのリストなどをAPIから取得する際は、useEffect内でAPIコールを行っております。
useEffectは、コンポーネントのレンダーが終わるたびに実行されるメソッドです。
詳しくは公式ドキュメントを確認してください

最初に以下のようなコードを書きました.

useEffect(() => {
    APIをコールするメソッド()
    .then(success => {
        setState({...state, ステートの更新})
    })
})

コンポーネントはステートが更新される度に再レンダーされます。
そのため、このコードではレンダー→useEffect内でAPIコール→ステートが変わるので再レンダー→useEffect内でAPIコール→ステートが変わるので再...
といったように無限ループが発生してしまいます。

この問題は以下のスタックオーバーフローを発見し解決することができました。
https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffect

useEffect(() => {
    APIをコールするメソッド()
    .then(success => {
        setState({...state, ステートの更新})
    })
}, []) // useEffectの第二引数に[]を置く

このように第二引数に[]を指定すると一度だけ実行される副作用となるため、無限ループを回避できます。

このスタックオーバーフローを見つけた後に公式ドキュメントを見ると、同じような事が書いてあり思わず自分を殴りました。

最後に

コードの品質などまだまだ改善できるところがあるので、暇を見つけてちょこちょこと修正していきたいです。
まず、もっとテストをしっかり書きたいですね。バックエンドはもちろんですが、フロントエンドのテストは書いたことがないので挑戦してみたいです。

あとは、使用技術の強みを活用できていない点も直したいです。
バックエンドだとkotlinを使用していますが、いまいちkotlinの強みを引き出せていません。
他にもreactでは関数型コンポーネントの「ステートを扱うロジックの再利用性が高い」などの利点をまだ活用しきれていません。

開発プロセスや(最低限のUXを考えた)機能設計などの意思決定プロセスなどは別記事にしてまとめようと思います。

もし誤字脱字やQiitaの利用規約に反するような内容あればご指摘をお願いします

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

[React]@babel/preset-env と @babel/ransform-runtime を core-js@3 で対応する

最近の勉強で学んだ事を、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

Babel と core-js の関係のおさらい

Babelが提供する @babel/polyfill や @babel/preset-env などのモジュールを利用すると
built-ins objects(Promise, WeakMap等)
static methods(Object.assign, Array.from等)
instance methods(Array.prototype.includes等)
といった新しい機能を使った実装が可能になりますよね。これらのBabelモジュールは core-js が提供するpolyfillを内部的に読み込んでいます。特に @babel/polyfill は core-js と regenerator-runtime を束ねて提供するpolyfill集です

core-jsのバージョンを指定して直接読み込むます!
古いブラウザをサポートするため、core-js を利用してポリフィルを含めた React 16 向けの環境を次のように設定

React 16 はコレクション型 Map および Set に依存しています。これらの機能をネイティブに提供しない(IE 11 未満など)、または標準非準拠な挙動をする(IE 11 など)古いブラウザやデバイスをサポートする場合は、core-js や babel-polyfill などのような、グローバル環境のポリフィルをバンドルしたアプリケーションに含めることを検討してください。

package.json
+    "core-js": "^3.6.4",
src/index.js
+ import 'core-js/es/set'; 
+ import 'core-js/es/weak-map'; 

これで完了です!

参考記事

JavaScript 環境の要件
Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法
ReferenceError: Can't find variable: WeakMap
@babel/polyfill と core-js

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

[React]axiosの処理が繰り返し、呼び出される問題

コンポーネント内で定義したstateをaxiosのresponseデータを使って書き換える処理を書きましたが、console.log()で取ってみると何度もresponseが返ってくる事象。。。

        const [posts, setPosts] = useState([]);

        axios
            .get('/api/posts')
            .then(response => {
                setPosts(response.data);
                console.log(posts)   //何度も通信が走っていることを確認

            })
            .catch(() => {
                console.log('通信に失敗しました');
            });

下記で解決しました。

    useEffect(() => {
        getPostsData();
    },[])

    function getPostsData(){
        axios
            .get('/api/posts')
            .then(response => {
                setPosts(response.data);

            })
            .catch(() => {
                console.log('通信に失敗しました');
            });
    }

axiosの処理を関数化し、第2引数を[]にすることで、空配列が渡ってきたときのみ、処理が走るようにします。

やはり公式、、、

https://ja.reactjs.org/docs/hooks-reference.html

空の配列 [] を渡すと、この副作用がコンポーネント内のどの値にも依存していないということを React に伝えることになります。つまり副作用はマウント時に実行されアンマウント時にクリーンアップされますが、更新時には実行されないようになります。

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

Next.jsのセットアップ手順

ディレクトリ作成

mkdir sample_app
cd sample_app

node_modules初期化

npm init -y
  • これでpackage.jsonを作ってくれる

Next.jsインストール

npm install --save next react react-dom

package.jsonのscriptsにコマンドを追加

"dev": "next -p 3001",
"build": "next build",
"start": "next start"
  • ポート番号3000だと他アプリと被りやすいので別ポートに変更

サンプルページ作成

mkdir pages
vi page/index.js
export default () => <div>Welcome to next.js!</div>

起動

npm run dev

参考記事

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

経年劣化に耐える ReactComponent の書き方

「経年劣化に耐えるコード」というのは、だれもが目指すものでしょう。「そもそもフロントエンドのコードは、今ある技術で最良のものを書き捨てるべき」という意見も理解できますが「備えあれば憂いなし」ということもありますので、ここにメモを残します。あくまで、私なりのベストプラクティスですのでご了承ください。

5層に別れた SFC

私はレイヤーによる技術の分離で、ReactComponent の経年劣化に備えています。ここでいうSFCとは「Stateless Functional Component」の略称ではありません。Vue.js の文脈にある「Single File Component」を指します。

// (1) import層
import React from 'react'
import styled from 'styled-components'
// (2) Types層
type ContainerProps = {...}
type Props = {...} & ContainerProps
// (3) DOM層
const Component: React.FC<Props> = props => (...)
// (4) Style層
const StyledComponent = styled(Component)`...`
// (5) Container層
const Container: React.FC<ContainerProps> = props => {
  return <StyledComponent {...props} />
}

記述順は「依存関係の上流下流」に従います。import や型定義が上流工程であることは言うまでもないので省略、重要なのは(3)〜(5)を構成するレイヤーです。

技術の分離

なぜこの区分になっているのか、なぜこの書式になっているのか、ひとつずつ解説していきます。

(3) DOM層

const Component: React.FC<Props> = props => (
  <div className={props.className}>
    <button onClick={props.handleClick}>
      {props.flag ? 'click me' : 'CLICK ME'}
    </button>
  </div>
)

JSX(TSX)は、React のためだけのものではなく、他ライブラリでも利用される技術です。そのため、React に依存する Hooks API などはここから取り除いています。return を用いない記法(props => (...))にすることで、Hooks API の介入を阻みます。この純粋な TSX にはビジネスロジックが無く、Array.mapや真偽値による出し分け程度です。「ボタンを押下された事で何が発生するのか?」という知識も存在しません。ここは副作用のない、真に Stateless なレイヤーです。このconst Componentだけを抜き出し(export)した場合、テストのしやすさは想像に易いでしょう。

(4) Style層

const StyledComponent = styled(Component)`
> button {...}
`

React CSS in JS のメジャーどころとして、styled-components がまず挙がると思いますが、Style層もあくまで CSS の話です。styled-components が解決している名前空間の解決は、BEM(MindBEMding)が解決したことと同じです。テンプレート文字列に記述されたCSSは、BEM にフォールバックしたり、CSS Modules に移行しても成立する記述となっています。私が styled-components のstyled.divstyled.button を敬遠している理由はここにあります。>による、children への指定漏洩防御も忘れない様にします。

BEM へのフォールバックはまれなケースかと思いますが、部分的に React を表示している様な折衷 html では、この切り分けが生きてきます。

(5) Container層

const Container: React.FC<ContainerProps> = props => {
  const [flag, setFlag] = React.useState(false)
  const handleClick = React.useCallback(() => {
    setFlag(!flag)
  }, [flag])
  return (
    <StyledComponent
      {...props}
      flag={flag}
      handleClick={handleClick}
    />
  )
}

Redux の経験がある方なら、PresentationalComponent / ContainerComponent というワードに馴染みがあるでしょう。Redux のコードベースには、Store に connect するコンポーネントとして、ContainerComponent という区分が明確にあります。これは React Hooks 全盛期のいまでも、踏襲すべきベストプラクティスであると私は考えています。ここは Stateful なレイヤーであり「依存の注入」を行う場所でもあります。

  • useState による状態管理が、Redux Store へ移行することになった
  • Storybook の為に、モックを注入する層に差し替えたい
  • テストの為に、モックを注入する層に差し替えたい

もしこのレイヤーに、useEffectを利用した fetch 処理が介入していたとしても、Storybook やテストにおいては、代替 Container を用意すれば良いわけです。

(1)〜(4)は、ここの都合による影響を受けることがありません。Hooks か? Redux か? GraphQL か? という配慮も当然不要なものとなります。(3) DOM層から知識を剥奪することが重要な理由はこれに起因します。

この様に「賢いレイヤー」を分離することで生まれるメリットは、依存注入技術の差し替えだけではなく、ビジネスロジックの移行(純関数の切り出し・Hooks から Redux への状態移譲)も容易にします。「Hooks API が過去のものになる…」という杞憂は当分先の話かと思いますが、将来の変化への備えとしては十分でしょう。

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

atomic designとは

ページ単位ではなく、コンポーネント単位でデザインを考える手法

・Atoms
・Molecules
・Organisms
・Templates
・Pages

の5つの単位に分けて、ページを作っていきます。

または日本語で、

・原子
・分子
・有機体
・テンプレート
・ページ

とも言われています。

Atoms

ボタン1つやフォーム1つなど

例)テキスト
818f81e08b73d262c11dce25abd2397a.png

例)フォーム
2ceca238355bc9a9828dfb5f8158fe4d.png

例)ボタン
68a81c6eacaccb643b8286fa4fbff68a.png

Molecules

Atomsを組み合わせたもの

例)フォームとボタンとテキストを組み合わせたもの
6730c3783a6356e206db752538996915.png

Organisms

Atoms,Molecules、他のOrganismsを組み合わせたもの

例)ヘッダー
d3a256d116b373ecba73574d450c2338.png

Templates

Atoms,Molecules、他のOrganismsを組み合わせ、ページ構造を表す

Pages

Templatesに実際の文章や画像が入ったもの


atomic designのメリット

・変更に強い
・再利用性が高い


参考
https://blog.kubosho.com/entry/using-atomic-design



ではまた!

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

React TypeScript で Propsに HTMLタグを入れる方法

Propsで渡すデータに改行タグを入れたかったので、
このように設定してみました。

import React from 'react';

// 型を定義する
type HeadlineType = {
  en: string | Array<string|JSX.Element>;
  ja: string;
}

// 子コンポーネント
const TheHeadline: React.FC<HeadlineType> = props => {
  return (
    <h1 className="headline">
      <div className="headline__en">{props.en}</div>
      <div className="headline__ja">{props.ja}</div>
    </h1>
  );
};

// 親コンポーネント
const App: React.FC = () => {
  return (
    <div id="app">
      <TheHeadline en={["Long Long", <br key="Some key" />, "Long Title"]} ja="長いタイトル" />
    </div>
  );
};

ポイントは、定義する型は、Element ではなく、JSX.Element ということ。
ここで時間かかってしまった。


[追記]
どうやら props で渡す引数にHTMLのタグがある場合、HTMLタグには任意のkey を指定しないと、
Warning: Each child in a list should have a unique "key" prop.
と警告が出るようです。


これでうまくいきました!

参考:https://stackoverflow.com/questions/33381029/react-how-to-pass-html-tags-in-props

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

とりあえずreact-hook-form

はじめに

react系のフォームバリデーションライブラリreact-hook-formのざっくり使い方です。

公式のドキュメントでは、

  • 超軽量なパッケージ
  • 再レンダリングを最小に押さえて、マウントの高速化
  • フォームの値がローカル管理される為、他パッケージに依存しない

等々の利点が挙げられている。

formikとの比較

download

スクリーンショット 2020-01-16 0.49.24.png
https://www.npmtrends.com/redux-form-vs-formik-vs-react-hook-form

size

formik

スクリーンショット 2020-01-16 0.54.11.png

react-hook-form

スクリーンショット 2020-01-16 0.54.30.png

API

useForm

useFormでいろいろなapiを受け取れる

useFormのoption(default)
const { register } = useForm({
  mode: 'onSubmit', // | 'onBlur' | 'onChange'
  reValidateMode: 'onChange', // onBlur | onSubmit 再度バリデーションされるタイミング
  defaultValues: {},
  validationSchema: {}, // スキーマレベルで Yup を使用してフォームバリデーションルールを適用
  validateCriteriaMode: "firstErrorDetected",
  submitFocusError: true, // エラーのある最初のフィールドがフォーカスされる
  nativeValidation: false, // ブラウザバリデーションの活用
})

register

input/selectのRefとバリデーションルールをreact-hook-formに登録する

<input name="form1_1" defaultValue="test" ref={register} />

registerでバリデーションをかけられる。下記のコードではrequired, minLengthを5にせっていしている。
'1_2は必須です', '5桁以上必要です'はエラーメッセージ。

<input
  name="form1_2"
  ref={
    register({
      required: '1_2は必須です',
      minLength : {
        value: 5,
        message: '5桁以上必要です'
      }
    })
  }
/>

フィールドフォームネストして扱うこともできる

name output
name="firstName" { firstName: 'value' }
name="firstName[0]" { firstName: [ 'value' ] }
name="name.firstName" { name: { firstName: 'value' } }
name="name.firstName[0]" { name: { firstName: [ 'value' ] } }

errors

errorsオブジェクトにはフォーム内の各フィールドのエラーオブジェクト。
react-hook-formにはエラーメッセージ用の表示にErrorMessageコンポーネントも用意されている。

import { useForm, ErrorMessage } from 'react-hook-form'
// ~~
  <ErrorMessage errors={errors} name="form1_2" />
// ~~

watch

指定されたnameのinputを監視して、その値を返す。

defaultValue が定義されていない場合、watch の初回のレンダリングは register の前に呼び出されるため undefined を返しますが、 第2引数として defaultValue を設定して値を返すことができます。

ただし、引数として useForm で defaultValues が初期化された場合、 初回のレンダリングは defaultValues で指定された値を返します。

<p className="form1-watch-text">watch output: {watch('form1_1')}</p>

handleSubmit

フォームバリデーションを通るとデータを渡す。

// ~~
const onSubmit = (data: Object) => { console.table(data) };
// ~~
<input type="submit" />
// ~~
async関数も渡すことができる
handleSubmit(async (data) => await fetchAPI(data))

form1.gif

Form1
import React from 'react';
import { useForm, ErrorMessage } from 'react-hook-form'

export default function Form1() {
  const { register, handleSubmit, watch, errors } = useForm({  validateCriteriaMode: 'all' });
  const onSubmit = (data: Object) => { console.table(data) };

  return (
    <div className="form form1">
      <h1>Form1</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="form-section">
          <span>1_1: </span>
          <input name="form1_1" defaultValue="test" ref={register} />
          <span className="sub-text">*watched</span>
        </div>
        <div className="form-section">
          <span>1_2: </span>
          <input
            name="form1_2"
            ref={
              register({
                required: '1_2は必須です',
                minLength : {
                  value: 5,
                  message: '5桁以上必要です'
                }
              })
            }
          />
          <span className="sub-text">*required</span>
        </div>
        <div className="form1-watch">
          <p className="form1-watch-text">watch output: {watch('form1_1')}</p>
        </div>
        <div className="errors">
          <ErrorMessage errors={errors} name="form1_2" />
        </div>
        <input type="submit" />
      </form>
    </div>
  )
};

controller

Controllerコンポーネント(UIコンポーネントライブラリと併せて使用するコンポーネント)用。
コンポーネント登録するためのメソッドが含まれている。
以下ではmaterial-uiを使用。

form2.gif

Form2
import React from 'react';
import { useForm, Controller, ErrorMessage } from 'react-hook-form';
import { TextField, Button } from "@material-ui/core";

export default function Form2() {
  const { handleSubmit, errors, control } = useForm()
  const onSubmit = (data: Object) => { console.table(data) };

  return (
    <div className="form form2">
      <h1>Form2</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <Controller
          as={<TextField />}
          name="form2_1"
          control={control}
          rules={{ required: "必須です" }}
          defaultValue=""
        />
        <div className="errors">
          <ErrorMessage errors={errors} name="form2_1" />
        </div>
        <Controller
          as={<Button color="primary" ><span>送信</span></Button>}
          name="submit"
          control={control}
          defaultValue=""
          onClick={handleSubmit(onSubmit)}
        />
      </form>
    </div>
  )
}

reset

フォーム内のvaluesとerrorsをリセットできる関数。
リセット時に値を渡すとデフォルトの値としてリセットできる。
form3.gif

Form3
import React, { useCallback } from 'react';
import { useForm, ErrorMessage } from 'react-hook-form'

type Reset = (values?: Record<string, any>) => void;

export default function Form3() {
  const { register, handleSubmit, reset, errors }: ({
    register: Function,
    handleSubmit: Function,
    reset: Reset,
    errors: any,
  }) = useForm();
  const onSubmit = (data: Object) => { console.table(data) };
  const onReset = useCallback(() => reset(), [reset])
  const onDefaultReset = useCallback(() => reset({ first_name: 'ジョン', last_name: '万次郎' }), [reset])

  return (
    <div className="form form3">
      <h1>Form3</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <span>姓:</span>
        <input
          name="last_name"
          ref={register({ required: '姓は必須です。' })}
        />
        <div className="errors">
          <ErrorMessage errors={errors} name="last_name" />
        </div>
        <span>名:</span>
        <input
          name="first_name"
          ref={register({ required: '名は必須です。' })}
        />
        <div className="errors">
          <ErrorMessage errors={errors} name="first_name" />
        </div>
        <input type="submit" />
        <input
          type="button"
          onClick={onReset}
          value="reset"
        />
        <input
          type="button"
          onClick={onDefaultReset}
          value="reset + default set"
        />
      </form>
    </div>
  )
};

setError / clearError

inputのエラーを手動で設定したりクリアする。

setValue

値を動的に設定できる。

form4.gif

Form4
import React, { useCallback } from 'react';
import { useForm, ErrorMessage } from 'react-hook-form'

export default function Form4() {
  const { register, handleSubmit, setError, clearError, errors, setValue } = useForm();
  const onSubmit = (data: any) => {
    const goodAnswer = 12 * 12;
    if (parseInt(data.answer, 10) !== goodAnswer) return setError('answer', 'notMatch', '不正解です');
    clearError('answer');

    console.log('正解');
  };
  const onSetValue = useCallback(() => {
    setValue('answer', 12 * 12);
  }, [setValue]);

  return (
    <div className="form form4">
      <h1>Form4</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <span>12 × 12 = </span>
        <input
          name="answer"
          ref={register({ required: '入力してください' })}
        />
        <div className="errors">
          <ErrorMessage errors={errors} name="answer" />
        </div>
        <input
          type="submit"
          value="回答"
        />
        <input
          type="button"
          value="諦める"
          onClick={onSetValue}
        />
      </form>
    </div>
  )
};

getValues

フォーム全体のデータを返す。

<input name="test" ref={register} />
<input name="test1" ref={register} />
<button
  type="button"
  onClick={() => {
    const values = getValues()
    console.log(values) // e.g: { test: [1, 2], test1: { data: '23' } 
  }}
>
  GetValues
</button>

triggerValidation

バリデーションのトリガーを手動で設定できる。

<input name="lastName" ref={register({ required: true })} />
  <button
    type="button"
    onClick={async () => {
      triggerValidation("lastName");
    }}
  >
  Trigger
</button>

unregister

inputにunregisterを適用すると、フォームデータに含まれなくなる。

これは、 useEffect でカスタム登録として input を登録 (register) し、 コンポーネントのアンマウント後に登録を解除する場合に便利です。

らしい。

formState

フォームの状態がオブジェクトで入っている。

name description
dirty 入力が行われた後trueになる
isSubmitted submitされた後trueになる
isSubmitting 送信中はtrue, 送信後falseになる
touched 操作されたnameが配列で入る
submitCount submitされた回数
isValid errorがない場合true

おわりです。込み入ったことは試していないのですが、記述が簡単な印象でした。

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

redux-sagaのテストライブラリを比較してみた

はじめに

redux-sagaでテストを書くことになりました

[https://redux-saga.js.org/docs/advanced/Testing.html:title]

↑が公式の説明です

ですが、いくつかライブラリがあってどれも評判の差があまりなく、どれがいいのかわからなかったので、実際にコードを書いて比較しましてみました

サンプルリポジトリ

例のごとくtypescriptです

選択肢

名称 特徴
何も使わない 実際の動作に沿ったstep-by-stepで行える
redux-saga-test unitテストが楽になる、step-by-stepで行える
redux-saga-testing unitテストが楽になる、step-by-stepで行える
redux-saga-test-engine unitテストが楽になる、全effectが配列で記録され、その配列でassertionする
redux-saga-test-plan unitテストからintegrationまでサポート
redux-saga-tester integrationのテストをサポート

それぞれ結構書き味が違ったので感想含めて紹介していきます

サンプル

それぞれのライブラリで以下のコードのテストをしていきます

export function* fetchTodosSaga() {
  yield put(actions.requestTodos());
  try {
    const todos = yield call(todoApi.getList);
    yield put(actions.successTodos(todos));
  } catch (error) {
    yield put(actions.failedTodos());
  }
}

シンプルにapiを呼ぶだけのsagaです

何も使わない

import { fetchTodosSaga, actions, todoApi } from "../todo";
import { put, call } from "redux-saga/effects";
import { cloneableGenerator } from "@redux-saga/testing-utils";

describe("step by step", () => {
  describe("fetchTodosSaga", () => {
    // sagaのgeneratorをcloneできるようにしておく
    const gen = cloneableGenerator(fetchTodosSaga)();

    // 共通部分をテスト
    it("should request todo list", () => {  
      expect(gen.next().value).toEqual(put(actions.requestTodos()));
      expect(gen.next().value).toEqual(call(todoApi.getList));
    });

    // 共通部分から続けて成功パターンをテスト
    it("should success", () => {   
      // 共通部分まで進めたgeneratorをcloneすることで、条件分岐のテストができる
      const clone = gen.clone();  
      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      expect(clone.next(mockRes).value).toEqual(
        put(actions.successTodos(mockRes))
      );
    });

    // 共通部分から続けて失敗パターンをテスト
    it("should failed", () => {
      const clone = gen.clone();

      // throwがoptionalなのでthrowがあるかの確認をしている
      expect(clone.throw).toBeTruthy();
      clone.throw &&
        expect(clone.throw().value).toEqual(put(actions.failedTodos()));
    });
  });
});

コードの中に説明がありますが、@redux-saga/testing-utilsを使うことで条件分岐のテストが簡単になってます

何も使わないと言っておいて早速ライブラリを使ってますが、これは公式なのでセーフです

sagaのeffectがオブジェクトを返してくれるのでそれをtoEqualで比較するとeffectがyieldされてるかを確認できます

シンプルにGeneratorのテストをしている形ですね、expect(gen.next().value)が長ったらしい感じもしますが、すでに十分わかりやすいです

ここまでは素のsagaのテストのしやすさが生きてますね、thunkだとこうはいきません

redux-saga-test

import { fetchTodosSaga, actions, todoApi } from "../todo";
import fromGenerator from "redux-saga-test";
import { cloneableGenerator } from "@redux-saga/testing-utils";

describe("redux-saga-test", () => {
  describe("fetchTodosSaga", () => {
    const gen = cloneableGenerator(fetchTodosSaga)();

    it("should request todo list", () => {
      // ライブラリからexpectを作成する
      // Jestのassertを使うようにdeepEqualを作成
      const libExpect = fromGenerator(
        { deepEqual: (a: any, b: any) => expect(a).toEqual(b) },
        gen
      );

      // 作成したexpectを使う
      libExpect.next().put(actions.requestTodos());
      libExpect.next().call(todoApi.getList);
    });

    it("should success", () => {
      const libExpect = fromGenerator(
        { deepEqual: (a: any, b: any) => expect(a).toEqual(b) },
        gen.clone()
      );

      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      libExpect.next(mockRes).put(actions.successTodos(mockRes));
    });

    it("should failed", () => {
      const libExpect = fromGenerator(
        { deepEqual: (a: any, b: any) => expect(a).toEqual(b) },
        gen.clone()
      );

      libExpect.throwNext().put(actions.failedTodos());
    });
  });
});

redux-saga-testにはd.tsがなかったので、サンプルリポジトリでは適当にモックしてます

アサーションでchaiか何かを使う前提っぽいのですが、今回はJestで使っているのでdeepEqualを作ってます

expectがsaga仕様になるので、何も使わないよりは書きやすくなってます

redux-saga-testing

import { fetchTodosSaga, todoApi, actions } from "../todo";
import sagaHelper from "redux-saga-testing";
import { put, call } from "redux-saga/effects";

describe("redux-saga-test-plan", () => {
  describe("fetchTodosSaga", () => {
    describe("success", () => {
      // 今度はitを作成する
      const it = sagaHelper(fetchTodosSaga());

      it("should put requestTodos", nextValue => {
        expect(nextValue).toEqual(put(actions.requestTodos()));
      });

      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      it("should call todoApi", nextValue => {
        expect(nextValue).toEqual(call(todoApi.getList));
        return mockRes;
      });

      it("should select from state", nextValue => {
        expect(nextValue).toEqual(put(actions.successTodos(mockRes)));
      });
    });

    describe("failed", () => {
      const it = sagaHelper(fetchTodosSaga());

      it("should put requestTodos", nextValue => {
        expect(nextValue).toEqual(put(actions.requestTodos()));
      });

      it("should call todoApi", nextValue => {
        expect(nextValue).toEqual(call(todoApi.getList));
        return new Error();
      });

      it("should select from state", nextValue => {
        expect(nextValue).toEqual(put(actions.failedTodos()));
      });
    });
  });
});

今度はitを作ってますね

1effectごとにテストケースを作るのは、個人的には鬱陶しさを感じます

条件分岐にも弱いです

redux-saga-test-engine

import { fetchTodosSaga, todoApi, actions } from "../todo";
import { put, call } from "redux-saga/effects";
import { createSagaTestEngine, throwError } from "redux-saga-test-engine";

describe("redux-saga-test-engine", () => {
  describe("fetchTodosSaga", () => {
    // どんなeffectを記録するか指定する
    const collectEffects = createSagaTestEngine(["CALL", "PUT"]);

    it("should fetch todo list", () => {
      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      // nextで渡したいものは第二引数を使ってモックする
      // call(todoApi.getList)がyieldされたあとの`next()`で`mockRes`が渡される
      const actualEffects = collectEffects(fetchTodosSaga, [
        [call(todoApi.getList), mockRes]
      ]);

      // 配列として`actualEffects`にyieldされたeffectが入る
      expect(actualEffects[0]).toEqual(put(actions.requestTodos()));
      expect(actualEffects[1]).toEqual(call(todoApi.getList));
      expect(actualEffects[2]).toEqual(put(actions.successTodos(mockRes)));
    });

    it("should noop when failed", () => {
      const actualEffects = collectEffects(fetchTodosSaga, [
        [call(todoApi.getList), throwError()]
      ]);

      expect(actualEffects[0]).toEqual(put(actions.requestTodos()));
      expect(actualEffects[1]).toEqual(call(todoApi.getList));
      expect(actualEffects[2]).toEqual(put(actions.failedTodos()));
    });
  });
});

これもd.tsなかったです

effectが配列としてactualEffectsで取れるのでそれをテストしてます

いちいちnextする煩わしさはなくなりました

これも条件分岐に弱いです

redux-saga-test-plan

これが一番スター数が多かったです

import { fetchTodosSaga, actions, todoApi } from "../todo";
import { testSaga } from "redux-saga-test-plan";

describe("redux-saga-test-plan", () => {
  describe("fetchTodosSaga", () => {
    const labels = {
      waitApi: "wait api"
    };
    it("should fetch todo list", () => {
      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      testSaga(fetchTodosSaga)
        .next()
        .put(actions.requestTodos())
        .next()
        .call(todoApi.getList)
        // 共通部分までをsave
        .save(labels.waitApi)
        // 成功パターンをテスト
        .next(mockRes)
        .put(actions.successTodos(mockRes))
        .next()
        .isDone()
        // 共通部分までをrestoreして失敗パターンをテスト
        .restore(labels.waitApi)
        .throw(new Error())
        .put(actions.failedTodos())
        .next()
        .isDone();
    });
  });
});

});

今回はtestSagaを使ってユニットテストを書いていますが、expectSagaを使うとreducerを含めたインテグレーションテストもできます

まとめて1テストケースで書くと読みにくいですね

testSaga(fetchTodosSaga)をテストケースの外に出して変数化してあげて、成功・失敗パターンは別のテストケースにしてあげればもっと読みやすくなると思います

書き方を考えれば十分効率的に条件分岐のテストが書けます

メソッドチェーンのコード補完でテストを書いていけたので、一番早くテストをかけたと思います

restoreするとsaveポイント消えるので、そこだけは注意です

個人的にはこのライブラリが一番好きです

redux-saga-tester

wixが作ってるライブラリです

import reducer, { fetchTodosSaga, actions, todoApi } from "../todo";
import SagaTester from "redux-saga-tester";
import { Status } from "../types";

describe("redux-saga-tester", () => {
  const defaultState = reducer(undefined, { type: "noop" });

  const sagaTester = new SagaTester();

  beforeEach(() => {
    sagaTester.reset(true);
  });

  describe("fetchTodosSaga", () => {
    it("should fetch todo list", async () => {
      const mockRes = [{ id: 1, title: "test title", content: "test content" }];

      todoApi.getList = jest.fn().mockImplementation(() => {
        return Promise.resolve(mockRes);
      });

      sagaTester.start(fetchTodosSaga);

      await sagaTester.waitFor(actions.successTodos.toString());

      expect(
        sagaTester.wasCalled(actions.requestTodos.toString())
      ).toBeTruthy();
      expect(
        sagaTester.wasCalled(actions.successTodos.toString())
      ).toBeTruthy();

      expect(sagaTester.getState()).toEqual({
        list: mockRes,
        status: Status.success
      });
    });

    it("should noop when failed", async () => {
      todoApi.getList = jest.fn().mockImplementation(() => {
        return Promise.reject();
      });

      sagaTester.start(fetchTodosSaga);

      await sagaTester.waitFor(actions.failedTodos.toString());

      expect(
        sagaTester.wasCalled(actions.requestTodos.toString())
      ).toBeTruthy();
      expect(sagaTester.wasCalled(actions.failedTodos.toString())).toBeTruthy();

      expect(sagaTester.getState()).toEqual({
        list: [],
        status: Status.failed
      });
    });
  });
});

sagaのテストはeffectの順番が変わるだけでも壊れたりするので、このライブラリではブラックボックステストのアプローチを提供しています

なのでwasCalledなどにより壊れにくいテストが書けるようになっています

まとめ

テストは勉強中なのですが、個人的にはユニットテストはホワイトボックステストをして、インテグレーションテストならブラックボックステストをするものだと考えています

ブラックボックステストはredux-saga-test-planもサポートしてるので、個人的にはやっぱりredux-saga-test-planを使いたいと思います

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

material-ui-dropzoneでapplication/octet-streamのMime typeが登録できなかった話

内容

ずっと出来ると思っていたcadファイルをアップロードしようと思ってやってみたら、拒否されてしまったのでそれを解決したお話

結論

acceptedFilesのjwwみたいな感じで拡張子を指定してあげればうまく出来たよ

<DropzoneDialog
    open={open}
    onSave={handleSave.bind(this)}
    dialogTitle={"ファイルをアップロードする"}
    acceptedFiles={['image/*', 'application/*, .jww , .dwg , .dxf , .jwc , .p21']}
    dropzoneText={"アップロードしたいファイルをドラッグアンドドロップするかクリックして選択ください"}
    cancelButtonText={"アップロードをやめる"}
    submitButtonText={"アップロードする"}
    onDrop={handleCheck.bind(this)}
    showPreviews={true}
    maxFileSize={5242880}
    onClose={handleClose}
            />
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodePipelineでS3にデプロイしてCloudFrontでコンテンツを配信する

CodeStarでさくさくCI/CD作りもいいのだが、とりあえず一旦はCodeCommitからDeployまでCodePipelineで連携する方法を理解しておこうと思ったので、軽く試してみた。

CloudFrontで配信するところまでやってみる。

やること

  • CodePipelineを利用して、CodeCommit, CodeBuildを連携させ、ReactクライアントをS3にアップロードする(デプロイ)。
  • S3に配置されたReactクライアントをCloudFrontで配信する。

やる順番

  1. CodeCommitでリポジトリを作成
  2. CodeBuildでビルドの設定(テストの設定とかはしない)
  3. CodePipelineでCommitからDeployまでを一貫して行う(S3へビルドファイルをアップロード)
  4. CloudFrontでコンテンツを配信(細かい設定はしない)

CodeCommitでリポジトリを作成

AWSコンソールのCodeCommitを開き、リポジトリを作成する。

仮にリポジトリ名testを作成すると以下のような「接続のステップ」が表示される。

Screenshot 0032-01-14 at 10.14.19 PM.png

ソースコードをプッシュするには、まずこのリポジトリをクローンする必要がある。
右上の「URLのクローン」から「HTTPSのクローン」を選択すると、URLがコピーされるのでローカルでgit cloneする。

git cloneの際に尋ねられるユーザー名等はIAMの認証情報から取得する。
IAMのアクセス管理>ユーザーからアカウントを選択して、「AWS CodeCommit の HTTPS Git 認証情報」の「認証情報を生成」から証明書をダウンロードする。

中のユーザー名とパスワードを使ってgit cloneできるようになる。

cloneしたディレクトリにソースコードを置いてプッシュすればCodeCommitにソースコードが表示される。

CodeBuildでビルドの設定

AWSコンソールのCodeBuildを開き、ビルドプロジェクトを作成する。

公式の解説ページも参考に。
CodeBuild でビルドプロジェクトを作成する

「ビルドプロジェクトを作成」へ入ると、

  • プロジェクトの設定
  • 送信元
  • 環境
  • Buildspec
  • アーティファクト
  • ログ

の設定項目が目につくが、ここでは「送信元」、「環境」、「Buildspec」のみに触れる。

「アーティファクト」と「ログ」の設定は触れずにビルドプロジェクトを作成する。

CodeBuild 送信元設定

Screenshot 0032-01-14 at 10.39.50 PM.png

ここで指定するソースプロバイダは、CodeCommitの入力アーティファクトを出力するソースコードを指す。

CodePipeline(CodeCommit, CodeBuild, CodeDeployをCI/CD機能)では入力アーティファクトと出力アーティファクトが各フェイズで受け渡される。
アーティファクトとはそれぞれのフェイズの成果物のことで、CodeCommitの出力アーティファクトはコードそのものであり、CodeBuildはそれを入力アーティファクトとして受け取って、ビルドしたファイルを出力アーティファクトとしてCodeDeployへ渡す。

入力および出力アーティファクト

CodeBuild 環境設定

Screenshot 0032-01-14 at 10.56.50 PM.png

ビルド環境設定では、ビルドを実行するためにCodeBuildが使用するオペレーティングシステム、プログラミング言語ランタイム、およびツールの組み合わせを設定することができる。

特にこだわりがないのであれば、
OS、ランタイム、イメージ、イメージのバージョン、環境タイプは上記のように設定すればたいして困らないと思う。

サービスロールではCodeBuildの実行に必要なポリシーが組まれたロールが作成される。
すでにある場合は既存のものを使える。

CodeBuild のビルド環境リファレンス
CodeBuild に用意されている Docker イメージ

CodeBuild Buildspec設定

Screenshot 0032-01-14 at 11.25.09 PM.png

ビルドコマンドの挿入の選択肢もあるが、ここではbuildspecファイルを使ったビルド設定について触れる。
ここまでにCodeBuildがビルドを行うソースコードの設定と、ビルドを行う環境の設定について書いたが、Buildspecではビルドの実行時に実行する細かい処理についての設定を行うことができる。

例えば、reactクライアントをCodeCommitへのプッシュをトリガーとしてビルドしたい場合は、以下のようにディレクトリ内にbuildspec.ymlを配置する。

Screenshot 0032-01-14 at 11.32.04 PM.png

これがこのリポジトリのルートディレクトリだとすると、Buildspecの項目におけるBuildspec名にはファイルそのものを指定すれば良い。
もしリポジトリ内の特定のディレクトリに存在するBuildspecファイルを指定したい場合は、そのパスを書く(client/buildspec.ymlのように)。

また、このymlファイルの名前がbuildspecである必要はない(clientspec.ymlとかでも良い)。

buildspec.ymlの例
例ではreactクライアントのビルドを念頭に置いているため、runtime-versionsはnodejs10.xを指定しており、 typescriptをインストールしている。

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 10
    commands:
      - npm install -g typescript
  pre_build: # ビルド実行前に実行する処理等の設定
    commands:
      - cd client
      - npm install
  build: # ビルド実行処理等の設定
    commands:
      - npm run build
  # post_build:
  #   commands:
  #     - some command
  #     - some command
artifacts:
  files:
    - 'client/dist/*' # 出力するビルドファイル
  discard-paths: yes # 出力するビルドファイルからパスを省く(client/dist/などを省く)

CodeBuild のビルド仕様に関するリファレンス

CodePipelineでCommitからDeployまでを一貫して行う

AWSコンソールのCodePipelineを開き、パイプラインを作成する。

パイプライン名とサービスロールを設定すると、

  • ソースステージ
  • ビルドステージ
  • デプロイステージ

の設定に入る。

ソースステージの設定

Screenshot 0032-01-14 at 11.54.58 PM.png

CodeCommitのリポジトリ名とブランチ名を指定する。
これはビルドステージへの出力アーティファクトとなる。

ビルドステージの設定

Screenshot 0032-01-14 at 11.57.00 PM.png

CodeBuildで設定したプロジェクトを指定する。

デプロイステージの設定

Screenshot 0032-01-15 at 12.01.07 AM.png

CodeDeployはS3のデプロイに対応していないため、S3デプロイを行うためにはCodePipelineを使う必要がある。

デプロイプロバイダーにS3を指定し、デプロイ先のバケットを選択する。
ここでは「デプロイする前にファイルを抽出する」にチェックを入れている(何もしないとzipがバケットに配置されるが、ここでは解凍された状態で配置したいため。デプロイパスはバケット内に展開されるディレクトリの構成を設定できる)。

全ての設定を終えて確認画面から「パイプラインを作成する」と、パイプライン一覧に新規パイプラインが表示される。
ソースコードに変更を加え、CodeCommitへプッシュすると、自動的にCodeBuildが起動し、S3へのビルドファイルのデプロイが行われる。

デプロイ設定で指定したバケット内にビルドしたファイルが表示されるはず。

Screenshot 0032-01-15 at 12.07.45 AM.png

CloudFrontでコンテンツを配信

CloudFrontとはオリジンサーバーが直接アクセスに対応する機会を減らし、キャッシュ化されたエッジロケーションのリソースに対してユーザをルーティングする機能のこと。
Amazon CloudFront とは

AWSコンソールでCloudFrontを開き、CreateDistributionを選択する。

Screenshot 0032-01-15 at 11.33.52 PM.png

WebのGet Startedからディストリビューションの設定を行う。

Origin Settingでは以下の2点を設定する。

  • Origin Domain Name
    CodePipelineでビルドファイルをデプロイしたS3バケットを選択する。

  • Restrict Bucket Access
    バケットのリソースへのアクセスを、S3のURLを使わずに常にCloudFrontのURLを利用したアクセスのみに絞りたい場合はこの項目をYesに設定する。

Default Cache Behavior Settingsは飛ばして、
Distribution Settingsでは以下の項目だけを設定する。

  • Default Root Object
    index.htmlを指定する。 これはバケット内のインデックスドキュメントの設定。 設定しないと${URL}/index.htmlとしてアクセスしなければならない。

Create Distributionをクリックして一覧に表示されるディストリビューションのStatusがDeployedになるまで待つ。

Screenshot 0032-01-15 at 11.44.21 PM.png

DeployedとなったらDomain Nameに表示されているURLにアクセスしてページが動いているかを確認する。

完。

まとめ

ここまででCodeCommitに新しい変更をコミットしていくと、S3バケットに自動的にビルドファイルがアップロードされるようになる。

厳密なデプロイはそのあとにCloudFrontのディストリビューションのInvalidate(CloudFrontのエッジロケーションのキャッシュを削除して更新されたコンテンツを再配布する)を行ったときに行われる。

InvalidateはCloudFrontのディストリビューションの一覧画面からディストリビューションを選び、Invalidationタブを選択した画面で行える。

Screenshot 0032-01-15 at 11.55.39 PM.png

Create InvalidationからInvalidateする項目を指定してInvalidateボタンをクリックするだけ。
バケット内の全てのファイルを指定してInvalidateする場合は*(アスタリスク)を指定すれば良い。

今後デプロイの手順としては、

  1. CodeCommitへコードのプッシュ
  2. CloudFrontでInvalidateを実行

をするだけでよくなる。

おしまい。

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

S3に静的HTMLファイルやSPAのスクリプトを配置して公開する

S3はファイルを補完するだけでなく、以下の手順を踏むことでHTTPサーバとしても利用することができます。
EC2などのサーバを立てなくても簡単にHTMLページが公開できます。
また、ReactやVueなどで作ったスクリプトを配置することもできます。費用はファイルサイズやアクセス数をベースに計算され、安価で簡単に情報公開ができるのでおすすめです。

S3バケットの作成

バケットを作成していない場合は、Create bucketボタンを押して、S3バケットを作ります。

最初のBucket nameは同じ名前のバケット名を(同じリージョンで)ほかの人が使っているとエラーが出ます。
重複しないバケット名を入れて、Nextを押して次に行きます。②~④は、一旦そのままの設定で構いません

Public access ブロックの解除

次に、Public access Blockの解除を行います。
前の手順で作成したバケットのチェックボックスをチェックした状態でEdit public settingsボタンを押します。
image.png

Block all public accessのチェックボックスを外し、Nextを押します。

すると、以下の様にPublic accessを許可してもよいかを確認する画面が出ます。ウインドウ下の入力欄に”confirm”と入力し、Confirmボタンを押します。

私はこの手順を踏むことでデータが公開された!と思ってしまいましたが、この段階ではPublic access blockを外しただけで公開設定をいれたわけではないので、公開はされていません。
次にBucketポリシーを変更して公開設定を行います。

Bucketポリシーの設定

次に、対象のバケット名をクリックし、バケットの中身の一覧画面に移動し、上に表示されるPermission>BucketPolicyの順にタブをクリックします。
タブの下にBucketPolicyEditorが表示されますので、以下に示したBucket policyを設定します。
example-bucketとなっている部分については、作成したバケット名に変更してください。

"bucketpolicy"
{
  "Version":"2012-10-17",
  "Statement":[{
    "Sid":"PublicReadGetObject",
        "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::example-bucket/*"
      ]
    }
  ]
}

Static website hosting の設定

最後に、Propertiesタブ>Static website hostingをクリックし, index document欄に初期ページとして表示するファイル名を設定します。ファイル名の指定なしにルートディレクトリを参照するURLを表示しようとした場合、ここで設定したファイルが自動的に読み込まれるようになります。
また、Error documentを設定すると、存在しないページなどを表示しようとした場合にここで指定したページが表示されるようになります。
image.png

これで設定完了です!
タイトルのすぐ下に表示されているEndpointのURLをクリックすると、S3のルートディレクトリに登録されているindex.htmlを参照できます。

参考ページ
https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteAccessPermissionsReqd.html

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