- 投稿日:2020-12-07T21:10:07+09:00
Reactで画像を表示させる方法
近頃Reactでいいサイトを作ろうとしているのですが、画像を表示させるのに3日かかってしまったのでたどり着いたコードを書いていきます。
表示のさせ方
test.reactimport ReactDOM from 'react-dom'; import Icon from './logo.png'; const IconComponent = () =>{ return <img src={Icon} alt="アイコン" /> } const rootElement = document.getElementById("root"); ReactDOM.render( <IconComponent />,rootElement );こんな感じになりました。
import Icon from ''でパスを指定するとそのものがIconに
入るみたいです。
const rootElement = document.getElementById("root");
をしてからReactDOM.renderで、
,rootElement
としないとTarget container is not a DOM element
と出ます。ここら辺の理解はまだまだなのでこれから調べます。終わり
- 投稿日:2020-12-07T19:14:48+09:00
【初めてのDApps開発②】ReactでシンプルなDAppsを作ってみよう!~フロントエンド編~
はじめに
[※注意!!]この記事は【初めてのDApps開発①】の続編です。まだ読んでいない人はそちらを読んでからこの記事を読んで下さい。
今回は最後の項目のReactを用いたフロントエンド開発を行います。
- Remixを用いたスマートコントラクト開発
- Truffleを用いたスマートコントラクトのデプロイ
- Reactを用いたフロントエンド開発
「Reactというフロントエンドフレームワークを使ってweb3.jsを経由してmetamaskを動かし, スマートコントラクトを動作させる」ということをやっていきます。
つまりこのようなイメージになります。作っていくアプリケーション
本記事では、【初めてのDApps開発①】でデプロイしたスマートコントラクトを使いながらアプリケーションの見た目の部分を作っていきます。
完成するアプリケーションは、以下のようになります。
では、一緒に作っていきましょう!!!
セットアップ
まずディレクトリを作ります。
そこにweb3がすぐに使えるようになっているReact Truffle Boxというレポジトリをクローンします.
npm install
を実行することでpackage.json
書かれているdependencies
をインストールしてくれます。$ git clone https://github.com/truffle-box/react-box.git $ cd react-box/client $ npm install私は【初めてのDApps開発①】で作成した
dapps-deploy
ファイルと一まとめにして管理することにしました!
さて、【初めてのDApps開発①】で作成した
dapps-deploy
ディレクトリ下のbuild/contract
にResister.json
ファイルが生成されています。これはコントラクトのコンパイル時に生成されたものです。中身を丸ごとコピーしましょう。そして
react-box
ディレクトリのclient/src
下にResister.json
というファイルを作ってそこに貼ります。Reactを記述していく!
まず、
client/src
下のApp.js
をこのように書き換えましょう。App.jsimport React from "react"; import Resister from "./Resister.json"; import getWeb3 from "./getWeb3"; import "./App.css"; class App extends React.Component { constructor(props) { super(props); this.state = { web3: null, accounts: null, contract: null, name: null, age: null, hobby: null, address: "", outputName: null, outputAge: null, outputHobby: null, }; } componentDidMount = async () => { try { const web3 = await getWeb3(); const accounts = await web3.eth.getAccounts(); const networkId = await web3.eth.net.getId(); const deployedNetwork = Resister.networks[networkId]; const instance = new web3.eth.Contract( Resister.abi, deployedNetwork && deployedNetwork.address ); this.setState({ web3, accounts, contract: instance }); } catch (error) { alert( `Failed to load web3, accounts, or contract. Check console for details.` ); console.error(error); } const { accounts } = this.state; console.log(accounts); }; render() { return ( <div className="App"> </div> ); } } export default App;
componentDidMount
関数 は react のライフサイクルの一種でレンダリングがされた直後に実行される関数です。web3とコントラクトを使えるようにする処理を書いています。
render
はHTML的なものを書いていく所です。
とりあえず起動してみましょう。$ npm startまだ何も表示されないはずです。
関数の作成
関数の作成をしていきます。
// アカウント情報の登録をする関数 writeRecord = async () => { const { accounts, contract, name, age, hobby } = this.state; const result = await contract.methods.registerAccount(name, age, hobby).send({ from: accounts[0], }); console.log(result); if (result.status === true) { alert('会員登録が完了しました。'); } };コントラクトで定義した
registerAccount 関数
を実行する関数です。ユーザーのname、age、hobby
という入力値を受け取って、引数に入れています。registerAccount 関数
はコントラクトのStorageにデータを書き込む関数なのでGas代を支払うアカウントを明記しています。ここでは自分のデータを書き込むユーザーが支払うように設定されています。// アカウント情報を読み込む関数 viewRecord = async () => { const { contract, accounts } = this.state; console.log(contract); const result = await contract.methods.viewAccount(accounts[0]).call(); console.log(result); const outputName = result[0]; const outputAge = result[1]; const outputHobby = result[2]; this.setState({ outputName, outputAge, outputHobby }); };コントラクトで定義した
viewAccount 関数
を実行する関数です。
registerAccount 関数
と違うところはGas代を支払うユーザーを明記していない点です。viewAccount 関数
はStorageにデータを書き込まない(見るだけ)なのでGas代を支払う必要がありません。また受け取った結果を表示するためにstateに入れています。handleChange = (name) => (event) => { this.setState({ [name]: event.target.value }); };ユーザーの入力値を受け取ってstateに入れる関数です。
以上で関数の作成は完了です。UIの作成に移ります。UIの作成
ReactだけでシンプルなUIを作っていきます!
render() { return ( <div className="App"> <br /> <form> <div> <label>氏名:</label> <input onChange={this.handleChange("name")} /> </div> <div> <label>年齢:</label> <input onChange={this.handleChange("age")} /> </div> <div> <label>趣味:</label> <input onChange={this.handleChange("hobby")} /> </div> <button type='button' onClick={this.writeRecord}> 会員登録 </button> </form> <br /> <br /> <form> <label>検索したいアドレスを入力してください。</label> <input onChange={this.handleChange("address")} /> <button type='button' onClick={this.viewRecord}> 検索 </button> </form> <br /> <br /> {this.state.outputName ? <p>氏名: {this.state.outputName}</p> : <p></p>} {this.state.outputAge ? <p>年齢: {this.state.outputAge}</p> : <p></p>} {this.state.outputHobby ? <p>趣味: {this.state.outputHobby}</p> : <p></p>} </div> ); }以上の関数、UIの記述を行った後のApp.jsがこちらです。↓
App.jsimport React from "react"; import Resister from "./Resister.json"; import getWeb3 from "./getWeb3"; import "./App.css"; class App extends React.Component { constructor(props) { super(props); this.state = { web3: null, accounts: null, contract: null, name: null, age: null, hobby: null, address: "", outputName: null, outputAge: null, outputHobby: null, }; } componentDidMount = async () => { try { const web3 = await getWeb3(); const accounts = await web3.eth.getAccounts(); const networkId = await web3.eth.net.getId(); const deployedNetwork = Resister.networks[networkId]; const instance = new web3.eth.Contract( Resister.abi, deployedNetwork && deployedNetwork.address ); this.setState({ web3, accounts, contract: instance }); } catch (error) { alert( `Failed to load web3, accounts, or contract. Check console for details.` ); console.error(error); } const { accounts } = this.state; console.log(accounts); }; // アカウント情報の登録 writeRecord = async () => { const { accounts, contract, name, age, hobby } = this.state; const result = await contract.methods.registerAccount(name, age, hobby).send({ from: accounts[0], }); console.log(result); if (result.status === true) { alert('会員登録が完了しました。'); } }; // アカウント情報の読み込み viewRecord = async () => { const { contract, accounts } = this.state; console.log(contract); const result = await contract.methods.viewAccount(accounts[0]).call(); console.log(result); const outputName = result[0]; const outputAge = result[1]; const outputHobby = result[2]; this.setState({ outputName, outputAge, outputHobby }); }; handleChange = (name) => (event) => { this.setState({ [name]: event.target.value }); }; render() { return ( <div className="App"> <br /> <form> <div> <label>氏名:</label> <input onChange={this.handleChange("name")} /> </div> <div> <label>年齢:</label> <input onChange={this.handleChange("age")} /> </div> <div> <label>趣味:</label> <input onChange={this.handleChange("hobby")} /> </div> <button type='button' onClick={this.writeRecord}> 会員登録 </button> </form> <br /> <br /> <form> <label>検索したいアドレスを入力してください。</label> <input onChange={this.handleChange("address")} /> <button type='button' onClick={this.viewRecord}> 検索 </button> </form> <br /> <br /> {this.state.outputName ? <p>氏名: {this.state.outputName}</p> : <p></p>} {this.state.outputAge ? <p>年齢: {this.state.outputAge}</p> : <p></p>} {this.state.outputHobby ? <p>趣味: {this.state.outputHobby}</p> : <p></p>} </div> ); } } export default App;ここで確認のために起動を行います。
npm start
を打ち込みます。
すると以下のように表示されるはずです。シンプルなUIが完成しました。?
ここまでのreact-box/client/src内のファイル構成はこのようになっています。
テスト
では、実際に動かしてみましょう。
その前に!MetamaskとReact Appとの接続を許可する必要があります。
- Metamaskを起動。
画像上部のネットワーク名を
Rinkebyテストネットワーク
に設定。水色で囲まれた右上のボタンをクリック→、
Connected sites
をクリック→、Manually connect to current site
→、次へ→、Connect
。では、いよいよテストしていきます。
適当に氏名、年齢、趣味を入力して会員登録ボタンを押してみてください。自動的にmetamaskが起動するので確認ボタンを押してください。
すると会員情報がブロックチェーン上に記録されます。ディベロッパーツールでは、ブロックに関する情報を見ることができます。次に登録した情報を閲覧をしてみましょう。metamaskを開いてアドレスをコピーします。
コピーしたアドレスを入力して検索ボタンを押してみてください。
会員情報の閲覧ができました!!!??
次は、このアプリケーションのデザインをカッコよくしていきたいと思います。
React-Bootstrapを用いてアプリケーションの見た目をカッコよくしていく
ここから先は、冒頭で紹介したようなアプリケーションの見た目にしていく作業になります。
(※アプリケーションの見た目にこだわらない方であればこのセクションはとばしても全然問題ないです)では、React-Bootstrapを用いてApp.jsを編集していきます。
react-bootstrapのインストール方法と使い方は以下の記事を参考にして下さい。
React Bootstrap 公式ドキュメント編集後のApp.jsがこちらになります。
App.jsimport React from "react"; import Resister from "./Resister.json"; import getWeb3 from "./getWeb3"; import { Row, Col, Button, Form, Modal } from "react-bootstrap"; import "bootstrap/dist/css/bootstrap.min.css"; import "./App.css"; class App extends React.Component { constructor(props) { super(props); this.state = { web3: null, accounts: null, contract: null, name: null, age: null, hobby: null, address: "", outputName: null, outputAge: null, outputHobby: null, //// lines: [], // モーダル show: false, // フォームチェック validated: false, }; } // モーダル設定 handleClose = async () => { await this.setState({ show: false }); // ページリロード document.location.reload(); } handleShow = async () => this.setState({ show: true }); componentDidMount = async () => { try { const web3 = await getWeb3(); const accounts = await web3.eth.getAccounts(); const networkId = await web3.eth.net.getId(); const deployedNetwork = Resister.networks[networkId]; const instance = new web3.eth.Contract( Resister.abi, deployedNetwork && deployedNetwork.address ); this.setState({ web3, accounts, contract: instance }); } catch (error) { alert( `Failed to load web3, accounts, or contract. Check console for details.` ); console.error(error); } const { accounts, contract } = this.state; console.log(accounts); const item = await contract.methods.accounts(accounts[0]).call(); this.state.lines.push({ item }); console.log(this.state.lines); }; // アカウント情報の登録 writeRecord = async () => { const { accounts, contract, name, age, hobby } = this.state; const result = await contract.methods.registerAccount(name, age, hobby).send({ from: accounts[0], }); console.log(result); if (result.status === true) { this.handleShow(); } }; // アカウント情報の読み込み viewRecord = async () => { const { contract, accounts } = this.state; console.log(contract); const result = await contract.methods.viewAccount(accounts[0]).call(); console.log(result); const outputName = result[0]; const outputAge = result[1]; const outputHobby = result[2]; this.setState({ outputName, outputAge, outputHobby }); }; handleChange = (name) => (event) => { this.setState({ [name]: event.target.value }); }; // フォーム最終確認 handleSubmit = (event) => { const form = event.currentTarget; if (form.checkValidity() === false) { event.preventDefault(); event.stopPropagation(); } this.setState({ validated: true }); }; render() { return ( <div className="App"> <Row className="text-left m-5"> <Col md={{ span: 4, offset: 2 }}> <Form className="justify-content-center" noValidate validated={this.state.validated} > <Form.Group controlId="validationCustom03"> <Form.Label>Name</Form.Label> <Form.Control type="name" onChange={this.handleChange("name")} placeholder="Enter Name" required /> <Form.Control.Feedback type="invalid"> Please enter name. </Form.Control.Feedback> </Form.Group> <Form.Group controlId="validationCustom03"> <Form.Label>Age</Form.Label> <Form.Control type="text" onChange={this.handleChange("age")} placeholder="Enter Age" required /> <Form.Text className="text-muted"> We'll never share your age with anyone else. </Form.Text> <Form.Control.Feedback type="invalid"> Please enter age. </Form.Control.Feedback> </Form.Group> <Form.Group controlId="validationCustom03"> <Form.Label>Hobby</Form.Label> <Form.Control type="text" onChange={this.handleChange("hobby")} placeholder="Enter Hobby" required /> <Form.Control.Feedback type="invalid"> Please enter hobby. </Form.Control.Feedback> </Form.Group> {/* フォームチェック */} <Form.Group> <Form.Check required label="Agree to terms and conditions" feedback="You must agree before submitting." onChange={this.handleSubmit} /> </Form.Group> <Button variant="primary" type="button" onClick={this.writeRecord}> 会員登録 </Button> {/* モーダル */} <Modal show={this.state.show} onHide={this.handleClose}> <Modal.Header closeButton> <Modal.Title>Wellcome to BcDiary!!</Modal.Title> </Modal.Header> <Modal.Body>会員登録が完了しました。</Modal.Body> <Modal.Footer> <Button variant="secondary" onClick={this.handleClose}> Close </Button> </Modal.Footer> </Modal> </Form> </Col> <Col md={{ span: 4, offset: 0 }} className='ml-5'> <Form className="justify-content-center"> <Form.Group controlId="formBasicEmail"> <Form.Label>検索したいアドレスを入力してください。</Form.Label> <Form.Control onChange={this.handleChange("address")} placeholder="Search" /> </Form.Group> <Button variant="primary" type="button" onClick={this.viewRecord}> 閲覧 </Button> </Form> <br /> <br /> {this.state.outputName ? <p>Name: {this.state.outputName}</p> : <p></p>} {this.state.outputAge ? <p>Age: {this.state.outputAge}</p> : <p></p>} {this.state.outputHobby ? <p>Hobby: {this.state.outputHobby}</p> : <p></p>} </Col> </Row> </div > ); } } export default App;DApp完成!
上記のプログラムの元、完成したアプリケーションはこのようになります。
Githubにソースコードをあげているのでそちらも良ければ参考にして下さい。最後に
ここまで記事を最後まで読んで頂いて本当にありがとうございます!そしてお疲れ様です!
初めてDApps開発をする方にとって少しでも力になっていれば幸いです。
是非LGTMボタンをポチッとお願いします!!
- 投稿日:2020-12-07T18:03:41+09:00
WEB開発未経験から一人でアプリを作りました
この記事は「NEXTSCAPE Advent Calendar 2020」の11日目の記事です
WEB開発未経験の私が、基礎的なWEB開発の知識を身に着けるために書評WEBアプリを題材に作成しました。
この記事では最初にアプリの紹介をしたのちに
- インフラ構成について
- 書籍照会のためにGoogle Books APIsを採用
- React Hooksで自分がハマってしまったポイントについて
といったところについて、お話ししようと思います。
どんなアプリを作ったのか
すでにお話に出ていますが、書評WEBアプリです。
ユーザーが本についてのレビューを書き込み、他のユーザーと共有できるといったものです。なぜこのアプリを作ったのか
ネクストスケープでは本を読む方が多く、お互いに本を薦めることが頻繁にあると聞いたからです。せっかく作るなら、使っていただける可能性のあるテーマで作成するほうがより、モチベーション持って続けられると感じたからです。
アプリの写真と紹介
トップページ
トップページでは新着レビューを閲覧したり、人気のレビューをランキング形式で見れるようにしてみました。
レビューページ
レビューを選択することで、詳細の閲覧やレビューに対して「いいね」することもできます。
マイページ
マイページでは投稿したレビューを管理できます。また、自分が過去にいいねしたレビューも閲覧できます。
使用技術
フロントエンド
React, Material-UI
サーバーサイド
.NET Core3.1
インフラ
Microsoft Azure, AzureDevOps
サーバーサイドは、.NET Core で作成しました。
オブジェクト指向設計を意識して、業務システムを作成する際の練習になるようにしました。また、AzureDevOpsを用いてパイプラインを作成し、masterブランチにpushされたら、ビルド、単体テスト、デプロイが自動で実行されるようにしました。
インフラ構成について
書籍照会のためにGoogle Books APIsを採用
アプリで書籍の情報を取得する必要がありました。
当初は以下の三つで比較・検討それぞれ実装の容易さ、書籍情報量、書籍情報の質を中心に紹介できればと思います。
まず、紹介に入る前にざっと内容をまとめてみました。
実装の容易さ 書籍情報量 書籍情報の質 Google Books APIs 認証必要なし ページ数やカテゴリもとれる ISBNがなかったり、フォーマットがまばら。JSONで取れる 楽天ブックス書籍検索API 認証あり ページ数が取れない レスポンスが綺麗(ただしAPIによって形式が異なる)JSONで取れる Amazon Product Advertising API APIの内容みるために登録が必要 未検証 未検証(レスポンスはxml形式のみ) Google Books APIs
単純な書籍情報が欲しい時に一番おすすめです。APIを利用する際に登録は不要で
https://www.googleapis.com/books/v1/volumes?q=【ここに書籍名または著者など】
だけで、すぐに使えます。
ここを押してみてください⇒https://www.googleapis.com/books/v1/volumes?q=相沢沙呼また、書籍情報に関しても書籍のカテゴリや、ページ数も取得できるので結構充実してると思います。
しかし、ISBNがなかったり、フォーマットが決まっていないこと(123-xxxx or 123xxxxのようにハイフンがない場合)があるので注意は必要になると思います。
ちなみに、レスポンスはJSON形式で取れます。今回は実装の容易さと情報量がある程度欲しいことから、こちらを採用しました。
楽天ブックス系API
楽天ブックスAPIを利用するためには、事前にアプリ情報の登録をしてアプリIDの発行が必要になります。
用途に合わせてAPIが複数あるので、ややこしいと感じました。
(それぞれレスポンス構成が異なる)少し触ってみたところ、書籍のページ数が取れないという点もあり、もし必要な場合はほかのAPIと組み合わせる必要があります。
良い点としてはISBNはきちんと取得できて、また、書籍以外の検索にも利用できそうでありました。
GoogleBooksAPIsとの比較はISBNをきちんと管理したいかで判断するとよいと思います。
ちなみに、レスポンスはJSON形式で取れます。Amazon Product Advertising API
まず、APIの内容を確認しようとアクセスすると登録からするように案内されました。
また、レスポンスもxmlのみしか対応していないみたいです。今回は内容を確認する前に登録が必要なこともあり、上記二つと比較して容易に扱えないと判断しました。
React Hooksで自分がハマってしまったポイントについて
書評WEBアプリではフロントエンドの開発にReactを採用しております。
参考にさせていただいたサイト
- https://overreacted.io/ja/a-complete-guide-to-useeffect/
- https://snamiki1212.com/react-useeffect-pitfall
useEffect内での非同期関数の書き方
非同期関数をuseEffect内で直接呼ぶことはできない!!!
つまり、下記のような書き方はできないということです。sampleNgUseEffect.jsReact.useEffect(async () => { await func1(); }, [fun1]);なぜ、できないかといいますと、async で定義された非同期関数は、返り値に Promise オブジェクトを返す通常の関数のシンタックスシュガーだから、という点です。
useEffectに渡す関数の戻り値はcleanup関数だからです。useEffect内で非同期関数を使う場合は、下記のように非同期関数を定義してから実行しましょう。
sampleUseEffect.jsReact.useEffect(() => { // 1)定義 const func1 = aysnc () => { // ... await ... // ... } // 2)実行 func1() }, [...])useStateのsethogehoge()は即座に反映してくれない
useEffectと合わせて、内部でsethogehoge()を使うことが多々あると思いますが、今回のNGな書き方を下記に載せます。
useStateNg.jsconst [userId, setUserId] = React.useState('default'); React.useEffect(() => { setUserId('newValue'); console.log(`userId is ${userId}`); }, [userId]);この場合logとして出力されるのは、「userId is default」となります。
なぜかといいますと、その関数が実行された時点のuserIdを使うからです。setした値を利用したい場合は、変数に置き換えて利用するなどの工夫が必要です。
useState.jsconst [userId, setUserId] = React.useState('default'); React.useEffect(() => { const newValue = 'newValue'; // << ここで変数に入れてあげる setUserId(newValue); console.log(`userId is ${userId}`); }, [userId]);まとめ
今後、増やしたい機能
- 別のユーザーにこの本おすすめだよといったレコメンド機能
- レビューに対してコメント機能
- 他ユーザーのページ(その人のレビューとかいいねしているのが見れる)
- カテゴリやタグなんかも入れたい
ここまでお読みいただきありがとうございます。今回は局所的であまり参考にはならない内容になってしまいましたが、書籍管理をどうしようと考えている方、React Hooksで苦戦中の方に少しでも参考になればと思います。
- 投稿日:2020-12-07T16:37:43+09:00
【React】Fast Refresh を有効にする
以前から
react-hot-loader
を使って、HMR (Hot Module Reloading)
を行ってきましたが、
React
がFast Refresh
というネイティブ機能が提供されましたので、こちに移行します。
今回はwebpack
を使った場合、具体的にな設定を説明します。ライブラリインストール
Fast Refresh 機能は、
react-refresh
を追加でインストール必要です。
webpack
使う場合、babel
とプラグインもインストール必要があるInstallyarn add react-refresh yarn add @babel/core babel-loader @pmmmwh/react-refresh-webpack-plugin -D
設定
意外と設定する場所が少ないが、プラグインなしの場合死ぬほど難しいそうです。
webpack.tsimport ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; const configs: Configuration = { ... module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', // *** ここから *** options: { plugins: ['react-refresh/babel'] }, // *** ここまで *** }, { loader: 'ts-loader', options: { transpileOnly: true, }, }, ], }, ], }, plugins: [ ... // *** ここから *** new ReactRefreshWebpackPlugin(), // *** ここまで *** ], ... };試して見る
デモ用ソースは GitHub で公開中です。
https://github.com/wwalpha/react-refresh-typescript-exampleDEMOgit clone https://github.com/wwalpha/react-refresh-typescript-example.git cd react-refresh-typescript-example yarn install yarn start全部5種類コンポーネントがテストできます。
- Class Default
- Class Named
- Function Default
- Function Named
- Lazy Component
- 投稿日:2020-12-07T15:41:54+09:00
Firebaseでfunctionsの関数をdeploy時にasyncのエラー
firebaseでデプロイ時にfuncionsへ関数のデプロイに失敗
エラー文
Function failed on loading user code. Error message: Code in file lib/index.js can't be loaded. Is there a syntax error in your code? Detailed stack trace: /srv/node_modules/@google-cloud/firestore/build/src/collection-group.js:54 async *getPartitions(desiredPartitionCount) { ^ SyntaxError: Unexpected token * at createScript (vm.js:80:10) at Object.runInThisContext (vm.js:139:10) at Module._compile (module.js:617:28) at Object.Module._extensions..js (module.js:664:10) at Module.load (module.js:566:32) at tryModuleLoad (module.js:506:12) at Function.Module._load (module.js:498:3) at Module.require (module.js:597:17) at require (internal/module.js:11:18) at Object.<anonymous> (/srv/node_modules/@google-cloud/firestore/build/src/index.js:39:28) ⚠ functions: Warning: Node.js 8 functions are deprecated and will stop running on 2021-03-15. Please upgrade to Node.js 10 or greater by adding an entry like this to your package.json: { "engines": { "node": "12" } } The Firebase CLI will stop deploying Node.js 8 functions in new versions beginning 2020-12-15, and deploys from all CLI versions will halt on 2021-02-15. For additional information, see: https://firebase.google.com/support/faq#functions-runtime Functions deploy had errors with the following functions: addDataset To try redeploying those functions, run: firebase deploy --only "functions:addDataset" To continue deploying other features (such as database), run: firebase deploy --except functions Error: Functions did not deploy properly.async *getPartitions(desiredPartitionCount)
非同期通信の関数データ付近でエラーが起きているとわかった。エラー対処方法
functionsのpackage.jsonの中身を
"dependencies": { "firebase-admin": "^9.2.0", "firebase-functions": "^3.11.0" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^3.9.1", "@typescript-eslint/parser": "^3.8.0", "eslint": "^7.6.0", "eslint-plugin-import": "^2.22.0", "typescript": "^3.8.0", "firebase-functions-test": "^0.2.0" },から
"dependencies": { "firebase-admin": "^8.6.0", "firebase-functions": "^3.3.0" }, "devDependencies": { "tslint": "^5.12.0", "typescript": "^3.2.2", "firebase-functions-test": "^0.1.6"へ
削除及び修正し変更。eslintと相性があわなかったのかもっと予想・・・。
Typescriptに詳しくないので、詳しいことがわかりません。ヒントになった参考記事
https://stackoverflow.com/questions/64575650/firebase-functions-error-after-update-what-can-i-do
以上となります。
ここは違う、ここはこうしたほうがいいかも?
等々ございましたら、ご指摘いただけますと幸いです。最後までみていただきありがとうございます。
- 投稿日:2020-12-07T11:28:50+09:00
Reactでファイル拡張子を".js"から".jsx"にするとエラーが起きる(Error: ENOENT: no such file or directory, open '実行パス')
はじめに
こんにちは。Qiita3記事目です。
最近はよく起きるエラーの解決法備忘録集みたいになってきています…笑
今回もまたよく起きそうなエラーが発生したのでその解決策をまとめておきます。Markdown記法にも少しずつ慣れてきました。
開発環境
- エディタ:VSCode
- OS:Mac OS X
- node:v15.2.1
- yarn:1.22.10
- React:17.0.1
- React-dom:17.0.1
- React-script:4.0.1
結論(解決策)
そのプロジェクトフォルダ内の
node_modules
を削除し、yarn install
してパッケージをインストールし直す。
npmを使っている方はyarn install
の部分をnpm install
に。$ rm -rf node_modules && yarn install原因:拡張子を".js"から".jsx"にすると発生する
VSCodeでは拡張子に応じてファイルのアイコンを変更してくれる機能があります。
コンポーネントを記述したファイルは拡張子を.jsx
にすることで、ファイルアイコンがReactのアイコンになり判別がしやすくなるということで早速変えてみたのですが、以下のようなエラーが出ました。App.jsをApp.jsxに変更したあとのターミナル./src/App.js Error: ENOENT: no such file or directory, open '/Users/creditbba/Desktop/programming/practice/js-react/jake-react/react-practice/src/App.js'
解決策
解決策としては、一度プロジェクトフォルダ内の
node_modules
を削除し、再度yarn install
でパッケージをインストールし直すことによって解決しました。$ rm -rf node_modules && yarn install
-r
オプションは「ディレクトリを削除する」オプションで、-f
オプションは「メッセージを表示しない(「本当に削除していいですか?」みたいなもの)」オプションになります。懸念点
そんなに頻繁にやることではないと思うのでよいのですが、この拡張子の変更によるエラーは原因となる拡張子を変更する度に毎回起こるということを何回か実験して確認しました。
例としては、今回のような
.jsから.jsxへの変更
、逆に.jsxから.jsへの変更
、また.jsxから.tsxへの変更
など、これらの変更を行った段階で今回のエラーが発生します。その度に今回の解決策のコマンドを実行すれば解決はするのですが、如何せん面倒臭いなあという気もします。
何かもっと良い解決策はないのでしょうかね。
今回参考にしたもの
- 投稿日:2020-12-07T07:04:25+09:00
Reactのカラーテーマはこうしてこう[Material-UI, styled-componentsの例]
TL;DR
この記事は、メタップス Advent Calendar 20207日目の記事です。
本当にやりたかったテーマ(Reactで新規プロジェクト立ち上げるときはこうしてこう)に間に合わなかったため、やや細かい内容になってしまいました。お許しください。ここでは、ReactでThemingするとき、カラーテーマをどんな感じでやると良いか、Material-UIやstyled-componentsのThemeProviderを例にとって、In My Opinionで解説します!
方針
原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。
- primaryyssecondaryは、プロジェクトで決めているテーマカラーを素直に入れていく
- それで足りない色は型を注入してユーザー定義していく
- Material-UIとstyled-componentsのThemePrivider両方で共通のthemeオブジェクトを利用する
- 当然typescriptでコードヒントを効かす
解説と実装例
Material-UIのThemeProvider
Material-UIのColor paletteは、primary, secondary, error, warning, ...と存在し、以下のような構造のインタフェースになっています。
export interface SimplePaletteColorOptions { light?: string; main: string; dark?: string; contrastText?: string; } ... export interface Color { 50: string; 100: string; 200: string; 300: string; 400: string; 500: string; 600: string; 700: string; 800: string; 900: string; A100: string; A200: string; A400: string; A700: string; } ... export type ColorPartial = Partial<Color>; ... export type PaletteColorOptions = SimplePaletteColorOptions | ColorPartial; ... export interface PaletteOptions { primary?: PaletteColorOptions; secondary?: PaletteColorOptions; ... }かなり細かく設定できますが、primary、secondaryを除けば、infoやsuccessなどの汎用的なカラーリングであり、プロジェクト規模がある程度まで行くと、これでは足りなくなってしまいます。
例えば、Material-UIで定義されているパレットに加えて、red, pink, greenを、定義済みのパレットと同じようなインタフェースで新設したいときは、どうすればよいのでしょうか。
たまに見かけるやり方で、、theme.tsxなどに、Material-UIの
createMuiTheme
とは別にカラーのオブジェクトを定義してエクスポートし、利用する場所でimportするというのがあります。theme.tsxexport const theme = createMuiTheme({ ... }); export const userPalette = { red: { ... } }しかし、毎回importするのは面倒ですし、これではコードヒントも効きません。
themeはネストが深くなるので、きちんと型をつけてコードヒントの恩恵を受けたいです。なのでここは型を注入します。
types/createPalette.d.tsimport * as createPalette from "@material-ui/core/styles/createPalette"; import { PaletteColorOptions } from "@material-ui/core/styles/createPalette"; interface CustomPalette { red: PaletteColorOptions; // light, main, dark, 50, 100, ..., 900, A100, ..., A700 pink: PaletteColorOptions; green: PaletteColorOptions; } declare module "@material-ui/core/styles/createPalette" { // eslint-disable-next-line interface PaletteOptions extends CustomPalette {} // eslint-disable-next-line interface Palette extends CustomPalette {} }これで型が拡張されました。
ThemePrivider.tsx
で、拡張した型を追加でthemingしてみます。またここでも、Material-UIのカラーパレットシステムを利用します。(自分で一つずつ設定しても良いですが)
Material-UIでは、ある程度一般的な色のパレットをimportできるように準備されています。
https://material-ui.com/customization/color/#color-paletteこれを利用すると、以下のように非常にシンプルに記述することができます。
ThemePrivider.tsximport { red, pink, green } from "@material-ui/core/colors"; export const theme = createMuiTheme({ palette: { red: { ...red, light: "#e57373", main: "#f44336", dark: "#d32f2f", }, pink: { // redと同様 }, green: { // redと同様 }, }, });これで新たに、17色 * 3の色がテーマから使えるようになっています。
あとは、受け取ったchildrenをラップするようにコンポーネントを定義し、ルートコンポーネントでラップするだけです。
ThemeProvider.tsximport { ThemeProvider as MuiThemeProvider } from "@material-ui/styles"; export const ThemeProvider: React.FC<Props> = ({ children }: Props) => { return ( <MuiThemeProvider theme={createMuiTheme(theme)}> {children} </MuiThemeProvider> ); };styled-componentsのThemeProvider
以上でMaterial-UIでのThemingはできました。
次はstyled-componentsのThemeProviderにも、同じthemeオブジェクトを渡して、styled内でpropsとして利用できるようにしたいです。しかし、そのままThemeProviderにthemeを渡すと、tscでエラーが出るので、ここでも型を注入します。
Material-UIのThemeインタフェースを利用してシンプルに書くことができます。
types/styled-components.d.tsimport "styled-components"; import { Theme } from "@material-ui/core"; declare module "styled-components" { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DefaultTheme extends Theme {} }こうしておいて、ThemeProviderでさらにラップすれば良いです。
ThemeProvider.tsximport { ThemeProvider as MuiThemeProvider } from "@material-ui/styles"; import { ThemeProvider as StyledThemeProvider } from "styled-components"; export const ThemeProvider: React.FC<Props> = ({ children }: Props) => { return ( <MuiThemeProvider theme={createMuiTheme(theme)}> <StyledThemeProvider theme={theme}>{children}</StyledThemeProvider> </MuiThemeProvider> ); };あとは普通に使うだけ。
styled-componentsのThemeProviderは、下位にある全てのコンポーネントツリーに対してこのthemeを送るため、全てのstyledでthemeにアクセスできるようになり、importする必要はなくなります。
コードヒントも有効になります。const StyledRedWrapper = styled.div` background-color: ${(props) => props.theme.palette.red[50]}; `;おわり
これで、Material-UIのテーマを拡張して適用し、また、styled-compoenentsでも同じものを利用できるようになりました。
今回はMaterial-UI & styled-componentsでしたが、Material-UIがAntDesignでも、styled-componentsがemotionでも、あるいはどちらか片方しか使わなくても、やることは基本的に同じだと思います。方針に書きましたが、
原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。で良いのかなと思います。
- 投稿日:2020-12-07T03:35:07+09:00
(React)npm start起動時のCORS回避
前提
フロントエンド - React.js
バックエンド - Go問題
npm start
でサーバを起動した際、React App内で外部APIをfetchすると、CORSエラーが発生する。すでに行った対処(参考にした記事)
- fetchの第二変数に
{mode: 'cors'}
を追加する。- GoのResponse Headerに
w.Header().Set("Access-Control-Allow-Origin", "*")
を追加。この二つの対処では、CORSエラーは解消されなかった。
解決法
ブラウザからのAPIの送り先が
localhost:4000
の場合は、React Appのpackage.jsonに、
"proxy": "http://localhost:4000",
と追加で記述すると良い。fetchで
localhost:4000/api
にリクエストを投げたい時は、
fetch("/api", {mode: 'cors'}).then....
と書く。
- 投稿日:2020-12-07T01:17:13+09:00
[初心者向け]React Redux Basic Tutorial�にあるTodoAppを今書き直したら
はじめに
枠が空いていましたので、Advent Calendarに参加させていただきました。
10月からReactの学習を始めた新参者です。
間違っているところなどあればご指摘頂けると幸いです。React Reduxを学ぼうと思った時に Basic Tutorial | React Redux にあるTodoAppをみる方も多いと思います。
私はReactの学習を くるみ割り書房 ft. React - BOOTH さんから出ているりあクト!シリーズで勉強しているのですが、改めてBasic TutorialのTodoAppを見るとこれはこのまま実務のコードとして参考に出来ないなと思いました。
例えば、function TodoApp()
など関数でなく、アロー関数で書くなど。
これからReactでプロジェクトを進めていく時はおそらくどこもアロー関数になると思います。これからReactを学ぶ上で最小限の小さいアプリを作っておいて見返すのは大事だと思いますので、せっかくならReact Redux Basic TutorialのTodoAppを書き直そうと思いました。
記事では、下記リポジトリの補足コメントを書いてるところが大きいです。
https://github.com/nori1994/todo-app-reactTodoApp
React Redux Basic TutorialにあるTodoAppを書き直してみた pic.twitter.com/HZ0RZilcry
— nori (@nori0__) December 6, 2020Basic Tutorial | React Redux の全ての機能が揃っているわけでなく、最低限React Reduxとは?が分かる範囲で書いています。
そして、りあクト!の11-redux/04-toolkit/src/features/todo.ts
で紹介されているSliceを実際に使うとどうなるのかを検証したかったためコードの参考引用しているところがあります。Slice
Sliceは、Action と Reducerをまとめて定義できるためすっきり記述できます。
Action と Reducerとは?という方は下記にざっくりと書いたので参考になれば嬉しいです。
Action
Actionは、イベント、例えばStateをどう変化させるかを定義します。
ActionType(文字列)からActionCreatorと呼ばれる Actionを作る関数を生成します。Reducer
Reducerは、以下のように古いStateとActionから新しいStateを取得するようになっています。type Reducer<S, A> = (state: S, action: A) => SThree Principles | Redux
Actionは、Dispatcherを介してReducterに渡されます。
createSlice
を使うとActionTypeの定義とActionCreator、Reducerをまとめて定義できます。
今回のTodoAppにはTodoを管理する、表示内容をフィルターするの大きく2つの機能があると考えたので、それぞれSliceを生成して、combineReducers
で1つのReducerにしました。
createSlice | Redux Toolkit
combineReducers | Redux例えば表示内容をフィルターする場合は、以下のようにAction と Reducerを定義しました。
visibilityFilterSlice.tsexport const visibilityFilterSlice = createSlice({ name: 'visibilityFilter', initialState: { visibilityFilter: VISIBILITY_FILTER.INCOMPLETE, } as VisibilityFilterState, reducers: { changed: (state, action: PayloadAction<VISIBILTY_FILTER_TYPE>) => { const changedValue = action.payload; state.visibilityFilter = changedValue; }, }, });Store
Storeは、アプリでの状態Stateを保管するためのものです。
configureStore
でStoreを生成しています。
Stateの変化を履歴で表示してくれるデバックツールRedux DevTools extension がデフォルトでONになっているなど設定が優秀なので、configureStore
でStoreを作っています。
configureStore
などに含まれている内容は以下を参照。
Redux Toolkit: Overview | Reduxindex.tsxconst rootReducer = combineReducers({ todo: todoSlice.reducer, visibilityFilter: visibilityFilterSlice.reducer, }); const store = configureStore({ reducer: rootReducer }); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'), );Dispath
実際にStateを変更するにはDispatcherを介してReducterにActionが渡されることで変更されます。
VisibilityFilters.tsxconst VisibilityFilters: FC = () => { const dispatch = useDispatch(); const { changed } = visibilityFilterSlice.actions; const visibilityFilterState = useSelector( (state: RootState) => state.visibilityFilter, ); return ( <div> {Object.entries(VISIBILITY_FILTER).map(([_, currentFilter]) => { return ( <text key={`visibility-filter-${currentFilter}`} className={`filter ' + ${ currentFilter === visibilityFilterState.visibilityFilter ? 'filter--active' : '' }`} onClick={() => { dispatch(changed(currentFilter as VISIBILTY_FILTER_TYPE)); }} > {currentFilter} </text> ); })} </div> ); };まとめ
この小規模単位のアプリを手元に持っておけばこれどうなっているんだっけとなった時に振り返りやすくなったと感じています。
あと、くるみ割り書房 ft. React - BOOTHさんで出ている本は旧版を除いて全て購入しました。TodoAppも多大に影響を受けています。とてもおすすめです。
- 投稿日:2020-12-07T00:24:50+09:00
【脱create-react-app】React開発環境を1から作ってみる
はじめに
この記事は限界開発鯖 Advent Calendar 2020の7日目です。
何気にAdvent Calendar初挑戦です。もともと「高速化のためのWebpack」というタイトルでWebpackのチューニングについて書こうと思っていたのですが、あまりにもニッチ過ぎたのでやめました。
1コマンドで環境構築ができる
create-react-app
皆さんReactの開発はやったことありますか?Reactのチュートリアルでは環境構築として
create-react-app
が紹介されています。このcreate-react-app
は以下のコマンドを叩くだけですぐにReactの開発環境を作れる夢のようなアイテムです。npx create-react-app my-appこのコマンドはたしかにReact初心者の方にはおすすめのコマンドですが、React(Webフロント)中級者になってくると、ビルド時のコンフィグをいじったり新しいツールチェインを導入したくなると思います。
create-react-app
はコンフィグをほとんど隠してしまうため、いじることのできる箇所が非常に少ないです。一応
npm run eject
でcreate-react-app
が隠しているコンフィグをすべてファイルにエクスポートすることができますが、今度は膨大なコンフィグファイルの量で逆にわからなくなります。今回はそんな
create-react-app
から卒業するために各種ツールチェインの設定を1から丁寧に解説していこうと思います。今回の構築する開発環境
Webpack
複数のソースコードを1ファイルにまとめる(バンドルと言います)バンドラーと呼ばれるツールです。バンドルする際に各種ツールにソースコードを通したり画像のファイルパスを自動的に解決してくれたりと万能です。「モダンなWebフロント開発=バンドラーを使った開発」 とも言えるでしょう。
TypeScript
JavaScriptに安全性をもたらす上位互換な言語です。JavaScriptは他言語だとエラーになるようなことも当たり前のようにエラーにならなかったりするので型やnull安全性をもたらしてくれるTypeScriptはほぼ必須の存在です。
Babel
新しいJS/TSの文法を古い文法に変換してくれるツールです。Webフロントでは実行環境がブラウザに依存するため様々なブラウザ・バージョンをサポートしなければなりません。新しいJSの文法が使いたいのに古いブラウザへのサポートもしたい…!そんなときにはぜひともBabelを使いましょう。
SCSS
CSSに変数やfor文、if文を追加したCSSの上位互換な言語です。他にもスタイルをネストして書けたりするので快適にCSSを書くことができます。
PostCSS
CSSに対して色々な処理を行うことができるツールです。今回は自動的にベンダープレフィックスを付けてくれるautoprefixerとCSSのコードサイズを縮小するcssnanoを導入します。
※ESLintやPrettierも導入したいのですが、そこまで解説するととんでもない文字数になってしまいそうなので今回は見送らせていただきます。
環境構築
Node.js
はじめにNode.jsプロジェクトのセットアップを行います。今回はnpmではなくyarnを用いて解説します。npmでもpnpmでもお好きなパッケージマネージャをお使いください。
yarn init -y
package.json
が作成されます。デフォルトで問題ありませんが、必要に応じて変更してください。TypeScript
yarn add -D typescript # TypeScriptのヘルパーライブラリです。 yarn add tslib次に
tsconfig.json
を作成します。古いバージョンへのトランスパイルはbabelに任せるので、TypeScriptで出力するコードはesnextにします。tsconfig.json{ "compilerOptions": { "target": "esnext", "module": "esnext", "jsx": "react", "sourceMap": true, "removeComments": true, "importHelpers": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }Webpackの初期設定
Webpackの設定ファイルはJavaScriptで記述することが多いですが、どうせなら型による安全性を設定ファイルにも導入しましょう。いくつかのTypeScriptのパッケージを導入してWebpackの設定を行います。
yarn add -D webpack webpack-cli @types/webpack ts-node @types/node
webpack.config.ts
を作成する前に、tsconfig.json
に少し手を加えます。ts-node
を用いて実行したときのみ反映される設定を追加します。tsconfig.json{ "compilerOptions": { // ... }, "ts-node": { "compilerOptions": { "target": "es2020", "module": "commonjs", "lib": ["es2020"] } } }
webpack.config.ts
を作成します。loaderやpluginの設定は後回しにして、ベースとなる設定のみをここで行います。
src/index.tsx
をエントリーポイントにして、dist/
ディレクトリを出力とします(お好みで変更してください)。
Webpackにはmodeという概念があり、production modeとdevelopment modeの2種類がありますが、今回はNODE_ENV
という環境変数を読み込んで判断することにします(未設定の場合はdevelopment modeになります。)。同様に、ベースとなるパス関してもBASE_URL
という環境変数を読み込んで設定します(未設定の場合は/
になります。)webpack.config.tsimport { Configuration } from "webpack"; import * as path from "path"; const isProduction = process.env.NODE_ENV === "production"; const isDevelopment = !isProduction; const baseURL = process.env.BASE_URL ?? "/"; const config: Configuration = { target: "web", mode: isProduction ? "production" : "development", entry: { index: path.join(__dirname, "src", "index.tsx"), }, output: { path: path.join(__dirname, "dist"), publicPath: baseURL, filename: "assets/scripts/[name].[contenthash:8].js", chunkFilename: "assets/scripts/chunk.[contenthash:8].js", }, }; export default config;まだloaderを一つも設定していないので
src/index.tsx
というファイルを作って実行してもバンドルに失敗します。Babel
Babelでは過去の文法への変換は行ってくれますが、最新の標準ライブラリの補完は行ってくれません。そこで
core-js
も導入します。yarn add -D @babel/core @babel/preset-env yarn add core-js
Babelの設定ファイルである
.babelrc
を作成します。.babelrc{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ] }Babelでは
.browserslistrc
というファイルを読み込んでどのバージョンの文法へトランスパイルするかを判断する機能があります。基本的にデフォルト設定で構いませんが、「IE許すまじ」の意志を込めてIEをサポートから外しておきます。.browserslistrcdefaults not ie 11具体的にどんなブラウザをサポートする設定になっているかは、以下のコマンドで確認することができます。必要に応じてサポートするブラウザやバージョンを変更しましょう。
npx browserslistloaderの設定
とりあえずTypeScriptとBabelの設定が行えたのでWebpackに各種loaderを設定します。
TypeScriptとBabelのloaderを導入します。yarn add -D ts-loader babel-loader
webpack.config.ts
にloaderの設定を追加します。webpack.config.tsconst config: Configuration = { // ... resolve: { extensions: [".tsx", ".ts", ".js"], }, // ... module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: "babel-loader", }, { loader: "ts-loader", }, ], }, ], }, // ... };Reactの導入
yarn add react react-dom yarn add -D @types/react @types/react-dom
ここまでの動作確認も含めていくつかファイルを作って実際に動作させてみます。
src/index.tsximport React, { StrictMode } from "react"; import { render } from "react-dom"; import App from "./App"; render( <StrictMode> <App /> </StrictMode>, document.getElementById("root"), );src/App.tsximport React, { FC } from "react"; const App: FC = () => ( <div> <h1>Hello, world!</h1> </div> ); export default App;package.json{ // ... "scripts": { "build": "webpack" } // ... }yarn build
dist/
下にファイルが出力されたら成功です。SCSS
SCSSのプロセッサーにはC++製の
node-sass
とDart製のdart-sass
の2種類がありますが、公式の推奨がdart-sass
のため、こちらを導入します。yarn add -D sass fibers @types/sass @types/fibers style-loader css-loader sass-loader
webpack.config.ts
にCSS系のloader設定を追加します。CSS Moduleを使用したいため、[name].module.(css|sass|scss)
だった場合はCSS Moduleを有効にするようにします。webpack.config.ts// ... import sass from "sass"; import fibers from "fibers"; const config: Configuration = { // ... module: { rules: [ // ... { test: /\.(?:c|sa|sc)ss$/, use: [ { loader: "style-loader", }, { loader: "css-loader", options: { sourceMap: isDevelopment, importLoaders: 1, modules: { auto: true, localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]", exportLocalsConvention: "dashesOnly", }, }, }, { loader: "sass-loader", options: { sourceMap: isDevelopment, implementation: sass, sassOptions: { fiber: fibers, }, }, }, ], }, // ... ], }, // ... };このままではTypeScriptのコンパイル時に、「CSS/Sass/SCSSを読み込むことができない」というエラーが発生してしまうので
src/global.d.ts
を作成して読み込めるようにします。src/global.d.tsdeclare module "*.module.css" { const classes: { readonly [key: string]: string }; export default classes; } declare module "*.module.scss" { const classes: { readonly [key: string]: string }; export default classes; } declare module "*.module.sass" { const classes: { readonly [key: string]: string }; export default classes; }動作確認のためにSCSSのファイルを作成します。
src/App.module.scss.title { font-size: 3rem; color: lightblue; }
src/App.tsx
を変更してSCSSを適用させます。src/App.tsximport React, { FC } from "react"; import styles from "./App.module.scss"; const App: FC = () => ( <div> <h1 className={styles.title}>Hello, world!</h1> </div> ); export default App;
yarn build
を実行してエラーが発生しなければ成功です。PostCSS
今回はPostCSSのプラグインとしてautoprefixerとcssnanoを導入します。
yarn add -D postcss autoprefixer cssnano postcss-loader
.postcssrc
を作成します。.postcssrc{ "plugins": ["autoprefixer", "cssnano"] }
webpack.config.ts
にPostCSSのloaderを追加します。webpack.config.tsconst config: Configuration = { // ... module: { rules: [ // ... { test: /\.(?:c|sa|sc)ss$/, use: [ // ... { loader: "css-loader", options: { // ... importLoaders: 2, // ... }, }, { loader: "postcss-loader", options: { sourceMap: isDevelopment, }, }, { loader: "sass-loader", // ... }, ], }, // ... ], }, // ... };プラグインの導入
Webpackのプラグインを導入します。今回は、バンドルしたJavaScriptをHTMLタグとして注入してくれる
html-webpack-plugin
とWebpackの出力先にファイルをそのままコピーするcopy-webpack-plugin
を導入します。yarn add -D html-webpack-plugin copy-webpack-plugin @types/html-webpack-plugin @types/copy-webpack-plugin
webpack.config.ts
にプラグインの設定を追加します。webpack.config.tsconst config: Configuration = { // ... plugins: [ new HtmlWebpackPlugin({ inject: "head", minify: isProduction, template: path.join(__dirname, "src", "index.html"), scriptLoading: "defer", }), new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, "public"), }, ], }), ], // ... };
html-webpack-plugin
で用いるHTMLを作成します。src/index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Hello, world!</title> </head> <body> <noscript> <strong>This site uses JavaScript. Please enable it.</strong> </noscript> <div id="root"></div> </body> </html>
copy-webpack-plugin
の動作確認のためにpublic/
ディレクトリを作成し、robots.txt
を作成します。public/robots.txtuser-agent: *
yarn build
を実行して動作確認をします。開発用のローカルサーバーのセットアップ
webpack-dev-server
を導入します。ホットリロード付きなので開発中の動作確認が楽になります。yarn add -D webpack-dev-server @types/webpack-dev-server
webpack.config.ts
に設定を追加します。webpack.config.tsconst config: Configuration = { // ... devServer: { historyApiFallback: true, }, // ... };
package.json
にscriptを新しく追加します。package.json{ // ... "scripts": { // ... "dev": "webpack serve" } // ... }
yarn dev
を実行してWebページがきちんと表示されれば成功です。ソースマップの設定
ソースマップがあると開発時のデバッグがやりやすくなります。
webpack.config.tsconst config: Configuration = { // ... devtool: isDevelopment ? "eval-source-map" : "nosources-source-map", // ... };これで終了です。お疲れさまでした。
おわり
最後に何度も書き換わった
package.json
とwebpack.config.ts
の全体像を示しておきます。package.json{ "scripts": { "build": "webpack", "dev": "webpack serve" }, "dependencies": { "core-js": "^3.8.1", "react": "^17.0.1", "react-dom": "^17.0.1", "tslib": "^2.0.3" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/preset-env": "^7.12.7", "@types/copy-webpack-plugin": "^6.3.0", "@types/fibers": "^3.1.0", "@types/html-webpack-plugin": "^3.2.4", "@types/node": "^14.14.10", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/sass": "^1.16.0", "@types/webpack": "^4.41.25", "@types/webpack-dev-server": "^3.11.1", "autoprefixer": "^10.0.4", "babel-loader": "^8.2.2", "copy-webpack-plugin": "^6.3.2", "css-loader": "^5.0.1", "cssnano": "^4.1.10", "fibers": "^5.0.0", "html-webpack-plugin": "^4.5.0", "postcss": "^8.1.14", "postcss-loader": "^4.1.0", "sass": "^1.30.0", "sass-loader": "^10.1.0", "style-loader": "^2.0.0", "ts-loader": "^8.0.11", "ts-node": "^9.1.0", "typescript": "^4.1.2", "webpack": "^5.10.0", "webpack-cli": "^4.2.0", "webpack-dev-server": "^3.11.0" } }webpack.config.jsonimport { Configuration } from "webpack"; import HtmlWebpackPlugin from "html-webpack-plugin"; import CopyWebpackPlugin from "copy-webpack-plugin"; import sass from "sass"; import fibers from "fibers"; import * as path from "path"; const isProduction = process.env.NODE_ENV === "production"; const isDevelopment = !isProduction; const baseURL = process.env.BASE_URL ?? "/"; const config: Configuration = { target: "web", mode: isProduction ? "production" : "development", entry: { index: path.join(__dirname, "src", "index.tsx"), }, output: { path: path.join(__dirname, "dist"), publicPath: baseURL, filename: "assets/scripts/[name].[contenthash:8].js", chunkFilename: "assets/scripts/chunk.[contenthash:8].js", }, resolve: { extensions: [".tsx", ".ts", ".js"], }, module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: "babel-loader", }, { loader: "ts-loader", }, ], }, { test: /\.(?:c|sa|sc)ss$/, use: [ { loader: "style-loader", }, { loader: "css-loader", options: { sourceMap: isDevelopment, importLoaders: 2, modules: { auto: true, localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]", exportLocalsConvention: "dashesOnly", }, }, }, { loader: "postcss-loader", options: { sourceMap: isDevelopment, }, }, { loader: "sass-loader", options: { sourceMap: isDevelopment, implementation: sass, sassOptions: { fiber: fibers, }, }, }, ], }, ], }, plugins: [ new HtmlWebpackPlugin({ inject: "head", minify: isProduction, template: path.join(__dirname, "src", "index.html"), scriptLoading: "defer", }), new CopyWebpackPlugin({ patterns: [ { from: path.join(__dirname, "public"), }, ], }), ], devServer: { historyApiFallback: true, }, devtool: isDevelopment ? "eval-source-map" : "nosources-source-map", }; export default config;最後に
明日は @Siketyan による「『限界開発鯖』を支える技術」です。私の所属している限界開発鯖の雰囲気がわかる(?)と思うのでぜひご覧ください!
- 投稿日:2020-12-07T00:15:56+09:00
react-dndのimportで起こった事
react-dndのimport時にエラーが出た
「react-dnd」を利用してサンプルを作ろうと思った時に「import」でエラーが出たので対処方を書こうと思います。
大したものではないです。import { DragDropContext } from 'react-dnd'がない
import { DragDropContext } from "react-dnd"DragDropContextが見つからないと言われてしまいます。
最新ではこれが変わっていて…import { DndProvider } from 'react-dnd';になっています。
import ReactDnDHTML5Backend from 'react-dnd-html5-backend'がない
import ReactDnDHTML5Backend from "react-dnd-html5-backend"ReactDnDHTML5Backendが見つからないと言われてしまいます。
最新ではこれが変わっていて…import { HTML5Backend } from 'react-dnd-html5-backend';になっています。
調べても意外とスンナリ見つからなかったので、参考になれば嬉しいです。
ありがとうございました。
- 投稿日:2020-12-07T00:15:19+09:00
React HooksでWebSocket通信を行うサンプル
はじめに
WebSocketを使ってリアルタイム通信アプリを作ろうとした場合、React Hooksなら「はいはい、副作用はuseEffect使って書けばいいんでしょ」というのはわかるものの、いざ書くとなるとこに何を書けばいいやら案外迷うものです。
本記事では、簡単なチャットアプリを題材にその方法をまとめました。useEffectフックや、カスタムフックの使いどころについて少しでも参考になれば幸いです。なお、WebSocketのライブラリはsocket.ioを用いています。
動作検証環境:
- Mac OS Catalina 10.15.7
- react 17.0.1
- socket.io 3.0.3
- express 4.17.1
サンプルアプリケーション概要
名前を入れて入室後、メッセージを送信あるいは他のユーザーが送信したメッセージを受信するという簡単なチャットアプリです。
サーバー側の実装
socket.ioライブラリを使ってWebSocket通信を行う簡単なプログラムを作成し、Expressにホストします。
app.jsconst app = require('express')(); const server = require('http').createServer(app); const io = require('socket.io')(server); io.on('connection', (socket) => { console.log('A client connected.'); socket.on('send', (payload) => { console.log(payload); socket.broadcast.emit('broadcast', payload); }); socket.on('disconnect', () => { console.log('Conenction closed.'); }); }); server.listen(3001, () => { console.log('Listening..'); });簡単な解説:
send
イベントを受信すると、他のクライアントに対してブロードキャストを行います。- 3001番ポートで待ち受けます。
React Hooksアプリの実装
まず、3000番ポートで立ち上げたdev-serverで動くReactアプリから、3001番ポートで待ち受けるサーバーアプリとそのまま通信しようとするとクロスオリジンのエラーとなるため、クライアント側の
package.json
を編集してプロキシ設定を行っています。"proxy": "http://localhost:3001",チャットコンポーネントの実装の概略です(イベントハンドリングや、WebSocket通信を除く部分)。
Chat.jsconst Chat = ({name}) => { const [messages, setMessages] = useState([{ name: '管理人', text: `ようこそ、${name}さん` }]); const [text, setText] = useState(''); //(途中略) return ( <div> <div className="input"> <input type="text" placeholder="メッセージ" value={text} onChange={handleInputChange} /> <button disabled={!text} onClick={handleButtonClick}>送信</button> </div> <ul> { messages.map((msg, idx) => { return ( <Message key={idx} name={msg.name} text={msg.text} /> ); }) } </ul> </div> ); }
useState
フックを使って定義したステート変数messages
には、入室時の初期メッセージ、自分が送信したメッセージ、他のユーザーから受信したメッセージが時系列に格納された配列が入ります。
JSXではその配列をmap
関数で変換して箇条書きリストに表示しています。さて、ここからが本題のWebSocket通信です。
Chat.jsconst socketRef = useRef(); useEffect(() => { console.log('Connectinng..'); socketRef.current = io(); socketRef.current.on('broadcast', payload => { console.log('Recieved: ' + payload); setMessages(prevMessages => [...prevMessages, payload]); }); return () => { console.log('Disconnecting..'); socketRef.current.disconnect(); }; }, []);まず、 socket.io-clientの
io
関数により作られるソケットオブジェクトをどこに宣言するべきかという問題があります。
ソケット接続はコンポーネントのマウント時に一度だけ行いたいため、useEffect
の第1引数に渡す関数内で初期化を行いますが、このオブジェクトは他の場所からも参照したいため、useEffect
の外側のスコープで定義する必要があります。
初めの頃、私は以下のように関数の外側に定義をしていました。let socket; const Chat = ({name}) => {しかし、このような参照の保持にはまさに
useRef
フックが使えるということに気づきました。const socketRef = useRef();さきほど「ソケット接続はコンポーネントのマウント時に一度だけ行いたい」と書きましたが、これを実現するためには、
useEffect
関数の第2引数の依存配列に空配列を指定します。
この第2引数の配列に含まれるプロパティやステート変数の値が変更されると、Reactがコンポーネントの再レンダリングを行う際にuseEffect
の第1引数の関数を再実行します。そのため、空配列を指定すると(値変更の監視対象変数が存在しないため)最初の一度だけ実行されるという挙動になるのです。useEffect(() => { // 略 }, []);さて、ソケット接続を最初に一度だけ行いたいのと同じように、接続のクローズも最後に一度だけ行いたいです。(ここで「最後」とは、コンポーネントがアンマウントされる時点を指します)。
これは、useEffect
の第1引数の関数の戻り値として後始末用の関数を返すことが実現可能です。return () => { console.log('Disconnecting..'); socketRef.current.disconnect(); }サンプルアプリでは、退室ボタンをクリックすると
Chat
コンポーネントがレンダリング対象から外れアンマウントされるので、そのタイミングでコンソールのログにDisconnecting..
というメッセージが出るのを確認できます。App.jsreturn ( <div> <Operation onEnter={handleEnter} onLeave={handleLeave} entered={entered} /> { entered && <Chat name={name} />} </div> )(上記コードで、
entered
の値が偽のとき、Chat
コンポーネントはレンダリング対象から外れる)
useEffect
の第1引数の関数では、ソケットオブジェクトを生成後、サーバーからのbroadcast
イベントを受信した際のコールバック関数を登録します。コールバック関数では、受信したメッセージをステート変数で管理されるメッセージ配列へ追加します(正確に言うと、新しいメッセージ配列をステート変数に格納)。Chat.jssocketRef.current = io(); socketRef.current.on('broadcast', payload => { console.log('Recieved: ' + payload); setMessages(prevMessages => [...prevMessages, payload]); });一方、送信ボタンクリックで呼び出されるイベントハンドラでは、新しいメッセージを作成して
send
イベントをサーバーに送出するとともに、ステート変数へも格納します。Chat.jsconst handleButtonClick = (e) => { const aMessage = { name: name, text: text, }; socketRef.current.emit('send', aMessage); setMessages(prevMessages => [...prevMessages, aMessage]); setText(''); }
Chat
コンポーネントのソース全量は以下のとおりです。Chat.jsimport {useState, useEffect, useRef} from 'react'; import Message from './Message'; import {io} from 'socket.io-client'; const Chat = ({name}) => { const [messages, setMessages] = useState([{ name: '管理人', text: `ようこそ、${name}さん` }]); const [text, setText] = useState(''); const socketRef = useRef(); useEffect(() => { console.log('Connectinng..'); socketRef.current = io(); socketRef.current.on('broadcast', payload => { console.log('Recieved: ' + payload); setMessages(prevMessages => [...prevMessages, payload]); }); return () => { console.log('Disconnecting..'); socketRef.current.disconnect(); }; }, []); const handleInputChange = (e) => { setText(e.target.value); }; const handleButtonClick = (e) => { const aMessage = { name: name, text: text, }; socketRef.current.emit('send', aMessage); setMessages(prevMessages => [...prevMessages, aMessage]); setText(''); } return ( <div> <div className="input"> <input type="text" placeholder="メッセージ" value={text} onChange={handleInputChange} /> <button disabled={!text} onClick={handleButtonClick}>送信</button> </div> <ul> { messages.map((msg, idx) => { return ( <Message key={idx} name={msg.name} text={msg.text} /> ); }) } </ul> </div> ); } export default Chat;カスタムフックを用いたリファクタリング
前述の
Chat
コンポーネントには socket.io-client ライブラリを使った WebSocket 通信処理を記述していますが、これには以下の問題があります。
- 特定の実装ライブラリへの依存が発生していること
- 責務の分離ができていないこと
とくに、
useEffect
内ではなく、イベントハンドラ関数の内部にソケットを使った副作用コードが書かれているのは気持ち悪いです。const handleButtonClick = (e) => { const aMessage = { name: name, text: text, }; socketRef.current.emit('send', aMessage); setMessages(prevMessages => [...prevMessages, aMessage]); setText('');このような場合、カスタムフックを使えば簡単に処理を抽出してコードをリファクタリングすることが可能です。
関連するソースコードをごそっと別ファイルにコピーして手を加え、以下のようなカスタムフック関数を作成します。useChatService.jsimport {useState, useEffect, useRef} from 'react'; import {io} from 'socket.io-client'; const useChatService = (initialMessage) => { const [messages, setMessages] = useState([initialMessage]); const socketRef = useRef(); useEffect(() => { console.log('Connectinng..'); socketRef.current = io(); socketRef.current.on('broadcast', payload => { console.log('Recieved: ' + payload); setMessages(prevMessages => [...prevMessages, payload]); }); return () => { console.log('Disconnecting..'); socketRef.current.disconnect(); }; }, []); const sendMessage = (name, text) => { const aMessage = { name: name, text: text, }; socketRef.current.emit('send', aMessage); setMessages(prevMessages => [...prevMessages, aMessage]); } return [messages, sendMessage]; } export default useChatService;
Chat
コンポーネントから利用したい、メッセージの配列とメッセージ送信関数を戻り値として返却します。
Chat
コンポーネントは以下のようにすっきりしました。Chat2.jsimport useChatService from './useChatService'; const Chat2 = ({name}) => { const [messages, sendMessage] = useChatService({ name: '管理人', text: `ようこそ、${name}さん` }); const [text, setText] = useState(''); const handleInputChange = (e) => { setText(e.target.value); }; const handleButtonClick = (e) => { sendMessage(name, text); setText(''); } // 以下変更なしおわりに
サンプルソースコードは GitHub にアップしました。
サンプルの動作方法はそちらのREADME.md
を参照ください。
- 投稿日:2020-12-07T00:03:45+09:00
新しいマシンにVue.js Reactの環境をインストールする方法
自分用に新しいマシンにVue.jsやReactをインストールする方法をメモとして残す
マニュアルインストールの方法は別にまとめる(予定)Macに『HomeBrew』をインストールする
- 何故インストールするのか?
- CUIベースで環境構築がしやすくなる
- 『$ brew install xxx』でCUIツールをインストールできる
- 『$ brew cask install xxx』でGUIツールをインストールできる
HomeBrewインストール手順
- 1. HomeBrewのHPを開く
- 2. Terminalを開く
- 3. インストールコマンドを実行する
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"brewコマンドでMacの開発環境を構築していく
- Google Chrome
$ brew cask install google-chrome
- VSCode
$ brew cask install visual-studio-code
- IntelliJ
- IntelliJのHPを開く (Ultimate版は有料なので、適宜検討する)
https://www.jetbrains.com/ja-jp/idea/download/#section=macNode.jsをインストールする
- 1. Terminalを開く
- 2. 下記コマンドを実行する (この時は最新のnodeのバージョンがインストールされる)
$ brew install node
- 3. インストールされたNodeのバージョンを確認する
$ node -v ex). v12.14.1
- 4. npmコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ npm -v ex). 6.14.8
- 5. nodeのバージョン管理コマンドをインストールする (今回は『n』を使用する)
$ npm install -g n
- 6. nコマンドが実行できるか確認する (試しにバージョン情報を確認する)
$ n -V ex). 6.7.0nについて
- Nodeのバージョンを管理することができる
- Nodeのバージョンを確認する
- https://nodejs.org/ja/download/releases/
- nodeの最新バージョンをインストールする
$ n latest
- インストール済みのNodeのバージョンを確認する
$ n ls
- Nodeのバージョンを切り替える
- 実行後にインストール済みのNodeのバージョンが表示されるので変更したいバージョンを選択する
$ nVue.jsの環境構築方法
vue/cliコマンドを実行できるようにする
- 1. @vue/cliのHPを開く
- 2. Terminalを開く
- 3. @vue/cliをインストールする
$ npm install -g @vue/cli
- ※1 バージョンを指定してインストールする場合は『@』をつける (Version 4.5.7をインストールする場合)
$ npm install -g @vue/cli@4.5.7
- ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g @vue/cli
- インストール済みの@vue/cliのバージョンを確認する
$ npm -g ls | grep @vue/cli ex). @vue/cli@4.5.7Vueのプロジェクトを作成する
- 1. Terminalを開く
- 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ vue create sampleReactの環境構築方法
create-react-appコマンドを実行できるようにする
- 1. create-react-appのHPを開く
- 2. Terminalを開く
- 3. create-react-appをインストールする
$ npm install -g create-react-app
- ※1 バージョンを指定してインストールする場合は『@』をつける (Version 3.4.1をインストール)
$ npm install -g create-react-app@3.4.1
- ※2 updateの仕方 (updateするときもinstallを実行することにより最新バージョンを取得できる)
$ npm install -g create-react-app
- 4. インストール済みのcreate-react-appのバージョンを確認する
$ npm -g ls | grep create-react-app ex). create-react-app@3.4.1Reactのプロジェクトを作成する
- 1. Terminalを開く
- 2. プロジェクトを新規に作成する (今回はプロジェクト名を『sample』とする)
$ create-react-app sample
- 投稿日:2020-12-07T00:00:07+09:00
チェックボックスのアクセシビリティ対応をしたお話
この記事はフラー Advent Calendar 2020 の 7日目の記事です。
6日目は、aikosogabeさんで、フラーに入社して1ヶ月経ったでした。最近、仕事でWebアクセシビリティについて調べる機会があったので、今回はその中でもつい最近調べた、チェックボックスのアクセシビリティについてやったことを書いていこうと思います。
前提
仕事で画像のような、クリックするとチェックされるようなカスタムチェックボックスをReactで作っていました。
で、その時作成したコードはだいたいこんな感じでした。
(TypeScriptを使用しているのでtsx形式になっていますが、見る上ではjsxだと思っていただけると......。)Checkbox.tsxconst Checkbox = () => { const [isChecked, setIsChecked] = useState(false); const handleChange = () => { setIsChecked(!isChecked); }; return ( <div className="content"> <label className={`${isChecked? "checkbox cheked" : "checkbox"}`}> <input type="checkbox" checked={isChecked} onChange={handleChange} /> <span>スイッチ</span> </label> </div> ); };checkbox.css.content { width: 100px; } .checkbox { display: block; border: 2px solid grey; text-align: center; } .checked { background: yellow; } input { display: none; }コードをぱっとみても分からない人向けに説明をすると、
Checkbox
はHTMLを返す関数です。
isChecked
でチェックボックスの状態を管理して、チェックボックスが押されたタイミングで状態を反転させるようにしています。(handleChange)さて、このチェックボックスにアクセシビリティ対応をすることになったのですが、一言でWebアクセシビリティと言っても「キーボードナビゲーション」や「スクリーンリーダー」など、様々な要素があります。この記事では「スクリーンリーダー」への対応に関して書いていきます。
また、これから紹介する結果は以下のOS、ブラウザで確認しています。
OS: macOS Catalina バージョン 10.15.7
ブラウザ: Chrome バージョン 87.0.4280.67
スクリーンリーダー: VoiceOver(PC)対応策
display:noneを使用しない
前提の項目で紹介したデモのコードですが、スクリーンリーダーではうまく動作しません。
手元の環境ではspan要素内の文字を読むことまでは確認できましたが、この要素がチェックボックスであることまではスクリーンリーダーで読み取ることができません。これはinput要素に
display:none
が設定されているためです。
ある要素に対してdisplay:none
やvisibility:hidden
が設定されていると、スクリーンリーダーのような支援技術はその要素を認識しなくなります。
そのため、前提の項目で例示したデモのコードでは、input要素そのものがスクリーンリーダーから認識されないので、チェックボックスであるというinput要素そのものが持つ情報も抜け落ちてしまいます。参考: コンテンツの非表示と更新
display:none
はもともと、チェックボックスの見た目をカスタマイズするときにinput要素を見えないようにするためのものです。
なのでdisplay:none
を消しただけでは「チェックボックスをカスタマイズする」という目的は達成できません。
以下の画像のようにinput要素が見えてしまいます。
opacity: 0でinput要素を視覚的に見えないようにする
次はinput要素をどうやって視覚的に消すかですが、調べた限りではいくつか方法があると思います。
例えば、clip: rect(0px, 0px, 0px, 0px)
で可視領域を狭める手法です。
参考: chakra Checkboxなので、ここで使用した手法はあくまでいくつかある実装の一つだと思っています。
ここで使用した手法は
opacity: 0
で視覚的に見えなくするという方法です。
変更はcssだけなのでcssだけ載せます。checkbox.css.content { width: 100px; } .checkbox { position: relative; /* 追加 */ display: block; border: 2px solid grey; text-align: center; } .checked { background: yellow; } input { position: absolute; /* 追加 */ margin: 0; /* 追加 */ width: 100%; /* 追加 */ height: 100%; /* 追加 */ opacity: 0; /* 追加 */ }
opacity
の他にもいくつかcssを追加しています。
position
はinputの領域をlabelの領域に重ねるため、width
、height
はスクリーンリーダーのカーソルを当てた時の領域を見た目通りにするためです。以下の画像はcss変更後のチェックボックスですが、前提の項目で紹介したデモとは違い、スクリーンリーダーにチェックボックスと認識されていることが分かります。
まとめ
今回はスクリーンリーダーにやさしいチェックボックスの一例として自分がやったことをまとめました。
普段はあまり意識しない場所かと思いますが、スクリーンリーダーのことも考えてみるきっかけになればと思います。
それはそれとして、自分でも引き続きWebアクセシビリティについて勉強していこうと思います。