- 投稿日:2020-02-17T22:42:54+09:00
SPAの知識ゼロの状態から、React+Reduxの開発環境構築までにやったこと
Webアプリケーションを開発する際に、「今ならSPA(Single Page Application)っしょ」となり、フレームワークの選定からやらせてもらう機会があったので、そのときにやったことを共有します。
前提
本記事作成時の筆者のスキル
- Webアプリケーションは作ったことがあるが、言語はJava
- SPA is 何?
- Javascriptはかじった程度の知識
フレームワークの選定
Googleトレンドや、実績からSPAのフレームワークを以下に絞りました。
- React
- AngularJS
- Vue.js
その時人気だったReactにしておけばいいだろと、Reactにしました。
※近年、Vue.jsが勢力を伸ばしてきているらしいです。React + α
Reactは様々なライブラリと組み合わせるのが普通のようです。
npm installでどんどんライブラリを追加していくことになりますが、ここでは開発手法に大きく関わるライブラリだけ。
- TypeScript:Javascriptに静的型付けをしてくれる、AltJS。正直これ初心者には辛い。
- Redux:stateの管理方法に大きく関わるライブラリ
参考記事:https://qiita.com/mpyw/items/a816c6380219b1d5a3bf- @reduxjs/toolkit:ReduxのActinとReducerをまとめて記述できるツール。Reduxチーム公式。
参考記事:https://qiita.com/Ouvill/items/a76e9cbce569d01f2931- redux-saga:タスクをうまいこと処理してくれる"ミドルウェア"という概念。APIリクエストなどの非同期処理で使用する。
参考記事:https://qiita.com/kuy/items/716affc808ebb3e1e8ac
※while(true)よりも、takeLatestをよく使いました。- Material-UI:見た目をいい感じにしてくれるライブラリ。これは無くてもよかったかも。
- react-intl:メッセージ管理や多言語対応をできるようにするライブラリ
公式ドキュメント:https://github.com/formatjs/react-intl/blob/master/docs/README.md執筆時点での最新バージョンはそれぞれ以下の通りです。あまり古くなっているようなら、参考にならないかもしれません。
プロダクト version React 16.12.0 Redux 4.0.5 TypeScript 3.7.5 redux-saga 1.1.3 Material-UI 4.9.2 react-intl 3.12.0 エディター
これも流行りなのでVSCodeを使用しました。
結果的に、nodeやgitのコマンドもターミナルのペインで実行できるので、かなり使いやすかったです。設定と拡張機能をチーム内で共有する
エディタの設定は、プロジェクト直下の.vscode/settings.jsonというファイルで共有できます。
拡張機能は、.vscode/extensions.jsonというファイルを置くことで、VSCode起動時にポップアップが出るようになります。
フォーマッター、保存時の自動フォーマットなどの設定は共有しておくと便利です。プロジェクトの作成
create-react-appというコマンドを使います。これを使うと起動スクリプトやwebpackのコンフィグなどが隠された状態で、カスタマイズできないので、すぐにejectというコマンドで素のnodeで扱えるようにします。
npx create-react-app my-app cd my-app npm run eject
プロジェクトの設定
ディレクトリ構成を考える
こんな感じになりしました。「これが正解!」というパターンは無いようなので、あくまで一例として。
ディレクトリ構成(クリックで開く)
<root> ├─public 画像ファイルなど。srcから見たとき、"/"がpublic直下 ├─build ビルドされたファイルが作成される先 ├─config create-react-appで勝手の作成される ├─scripts create-react-appで勝手の作成される+自作のnpmスクリプトを作る場合はここ ├─node_modules └─src src以外のファイルはほとんど触らない ├─i18n メッセージのyamlと、それを管理するJS ├─modules actionとreducerをまとめたもの ├─containers 1画面1コンテナという考え方で、コンテナコンポーネントを作成する。 ├─comopnents コンポーネント。画面ごとにサブディレクトリを切る ├─sagas 非同期処理を扱う処理 ├─scss .scssファイル └─utils 共通で使う処理。Validatorなどimportを絶対パス指定でできるようにする
これデフォルトじゃないんかい。。。と思いながら設定しました。
方法はいくつかあるようですが、TypeScriptを使っているのでtsconfigで設定できる方法を採用しました。
これをやらないと、"../../../../"地獄になります。いつものnpm i --save tsconfig-paths
tsconfig.json{ "compilerOptions": { "baseUrl": "src" ~~~後略~~~ }メッセージ管理方法を考える
yamlでメッセージ管理できるようにする
yaml-flat-loaderというライブラリを使用して、yamlをjsonとして読み込めるようにします。
いつものnpm i --save-dev yaml-flat-loader
webpackconfig.jsのrulesに追記{ test: /\.yml$/, use: [{ loader: 'json-loader' }, { loader: 'yaml-flat-loader' }] },メッセージのキーを補完できるようにする
yamlでメッセージ管理する弊害として、コピペがし難いこともあり、補完できるように一手間加えました。
メッセージのキーをオブジェクトとして参照できるようにすることで、メッセージキーの変更やキーの指定ミスがコンパイルエラーになるというメリットもあります。
react-intlの正しい使い方をしているなら、こちらの記事が参考になりますが、JSでデフォルトメッセージを定義して、各言語はyamlというのは面倒です。
メッセージはyamlファイルだけで完結したいので、yamlファイルのキーをjsonにします。スクリプトを作成し、messageKeysというオブジェクトが各ファイルから参照できるようにします。
ja.ymlに存在するキーをJSONにしています。
スクリプト詳細(クリックで開く)
まずは必要ライブラリのインストールnpm install --save-dev js-yamlscripts/messageGen.js'use strict'; var yaml = require('js-yaml'); var fs = require('fs'); // Get document, or throw exception on error try { let doc = yaml.safeLoad(fs.readFileSync('src/i18n/ja.yml', 'utf8')); let str = "export const messageKeys ="; str += JSON.stringify(keyToValue("", doc)); str += ';\n'; str += "export default messageKeys;" fs.writeFile("src/i18n/messageKeys.tsx", str, (err) => { if (err) { throw err; } }); } catch (e) { console.log(e); } function keyToValue(parentKey, param) { let ret = param; for (let [key, value] of Object.entries(param)) { if (typeof value === 'object') { keyToValue(parentKey + key.toString() + ".", value); } else if (typeof value === 'string') { ret[key] = parentKey + key; } } return ret; }package.json(抜粋)"scripts": { - "start": "node scripts/start.js", + "start": "node scripts/messageGen.js && node scripts/start.js", + "message": "node scripts/messageGen.js" },使用例import messageKeys from "src/i18n/messageKeys"; ~~~中略(コンポーネント内)~~~ const intl = useIntl(); intl.formattedMassage({ id: messageKeys.some.message.id });"npm run message"でsrc/i18n/ja.ymlに存在するキーをjsonにしてくれます。
※エディタ上で補完できるようにするには、messageKeys.jsを一度開く必要がありました。スタイリング方法を考える
こちらの記事を参考に、"1. クラス名によるスタイリング"と"3. CSS Modules"で悩みましたが、CSS(SCSS)によるスタイリングを採用しました。
理由は、
- 業務ロジックやイベント処理を記述しているコンポーネントファイルに、見た目のことまで書きたくない
- 普通のSCSSの方が書ける人が多い
- ぶっちゃけCSS Moduleよくわからなかった
あたりです。
ただし、これには以下のデメリットがあります。
- 普通にscssをimportすると、Reactはstyleタグを生成するので影響範囲は全体ということになる
- 上記動作により、複数回importされると無駄なstyleタグが増えることになる
よって、コンポーネントのrootとなる要素にclassを設定し、その下にネストしたSCSSを書くようにしました。
(結局は開発者の運用努力なので、改善すべき部分ではあります。)結び
まだまだ色々なことをやった(ような気がします)が、以上が開発開始時の"進め方"を決める段階で時間をかけて考えた部分になります。
これからReactの開発を始める際の参考になれば幸いです。
- 投稿日:2020-02-17T21:45:04+09:00
【React】フォーカスアウトするとカンマ区切りで表示される入力欄
経緯
- 数値を入力する項目がある
- 桁が大きくなると額がわからなくなる
- だからカンマ区切りで表示したい
- それを実現可能な要素はHTMLには無い
- input=text だと数値以外を入力できる
- input=number だとカンマを表示できない
- じゃあ作ろう
仕様
- input=textをベースとする
- 入力値は右寄せで表示する
- バリデーション機能を追加する
- 正の数、および負の数を入力可とする
- マイナスは先頭のみ入力可とする
- 小数点は一度のみ入力可とする
- 小数点の前には必ず数値の入力があるものとする
- 末尾が小数点で終わる場合を許容する
- 未入力、及びバリデーション通過時の背景色を #FFF とする
- バリデーション不通過時の背景色を #FFBEDA とする
- 入力欄にフォーカス時に入力した文字列をそのまま表示する
- 入力欄からフォーカスアウト時にバリデーションを通過した場合はカンマ区切りの文字列を表示する
- 入力欄からフォーカスアウト時にバリデーションが不通過となる場合は入力した文字列をそのまま表示する
import React, { useState } from 'react'; const InputText = ({ ...rest }) => { const [value, setValue] = useState(''); const [isFocus, setFocus] = useState(false); const handleFocus = () => setFocus(true); const handleBlur = () => setFocus(false); const handleChange = e => setValue(e.target.value); const isBlank = value === ''; const isValid = /^[-]?(\d+)[.]?(\d+)?$/.test(value); const displayValue = (() => { if (isFocus || !isValid) { return value; } if (isValid) { return (+value).toLocaleString(); } return ''; })(); const displayStyle = { textAlign: 'right', backgroundColor: isValid || isBlank ? '#FFF' : '#FFBEDA' } return ( <input {...rest} type="text" onFocus={handleFocus} onBlur={handleBlur} onChange={handleChange} value={displayValue} style={displayStyle} /> ); } export default InputText;
- 投稿日:2020-02-17T16:04:59+09:00
React/Vueを利用したサービス
概要
SPAの代表的なフレームワークであるReactとVue(+Nuxt.js)を利用したサービスについて。
サービス一覧(React)
- Yahoo Japan
- freee
- Airbnb
- Kibela
Abeja
- ABEJAの技術スタックを公開します(2019年11月版)
- https://tech-blog.abeja.asia/entry/tech-stack-201911
Backlog(ヌーラボ)
- ヌーラボの川端が、React勉強会@福岡 vol.2 に登壇します #react_fukuoka
- https://nulab.com/ja/blog/nulab/2019-6-5-react-fukuoka-announcement/
Slack
Uber
Vue.js
Vue.jsを導入している国内の企業・サービス一覧
- Nintendo
- https://my.nintendo.com/
- ZOZOテクノロジーズ
- https://speakerdeck.com/amatsukiku/frontend-architecture-design-of-zozo
- 一休com
- 一休.comレストランのスマートフォン検索ページがSPAになりました
- https://user-first.ikyu.co.jp/entry/2018/10/09/080000
- Retty
- Webサービスを支えるユーザログ基盤開発@Retty
- https://engineer.retty.me/entry/2018/12/01/120019
- DMM
- DMM動画サービスの問題を解決しようとしている話(コンポーネント編)
- https://inside.dmm.com/entry/2018/07/12/components
- Gunosy
- 社内管理画面を Vue + Go で作る
- https://tech.gunosy.io/entry/admin-vue-go
- マンガZERO
- https://blog.nagisa-inc.jp/archives/2980
- ITプロパートナーズ
- サービスの管理画面で Vue + element.ui を活用する(table編)
- https://tech.itpropartners.jp/entry/2018/12/04/132144
- M3
- 「レガシーアプリケーションのリニューアルにNuxt.jsで戦う」というタイトルでVue Fes Japan 2018 Reject Conferenceに登壇してきました
- https://suzan2go.hatenablog.com/entry/2018/11/10/225810
- DeNA
- アバター着せ替えアプリ開発におけるフロントエンド技術(Vue.js活用事例) #denatechcon
- https://www.slideshare.net/dena_tech/vuejs-denatechcon-72603570
- Codeal
- コデアルリニューアルと技術的なトピック
- https://www.codeal.work/contents/archives/6448
- CoupLink
- 弊社マッチングアプリ「CoupLink」をVue.jsでSPA化しました
- https://tech.linkbal.co.jp/4834/
- ALIS
- 【ALISのシステム】フロントエンドアーキテクチャ:その1
- https://alis.to/AB2/articles/34ZkxZ1pwylQNuxt.js
- リクルートライフスタイル
- note
- noteのフロントエンドをNuxt.jsへ刷新します
- https://note.mu/konpyu/n/n9b7bf4343514
- NoSchool
- 【実録】WordPressサイトをAWS+Laravel+Nuxtにフルリプレイスした話(技術選定編)
- https://qiita.com/mejileben/items/f68a50ec9164b261b9cd
- TERIYAKI
- Nuxtのプロダクション事例
- https://speakerdeck.com/tameto/nuxtfalsepurodakusiyonshi-li
- LINE
- Vue Fes Japan 2018 LINE株式会社 LunchスポンサーLT
- LINEとNuxtの話
- RoomClip
- 投稿日:2020-02-17T13:42:14+09:00
Reactで改行コード\nの入っているテキストを改行したまま表示する方法
テキストエリア等で改行されたテキストを改行したまま表示したい時がある。
よく見るのはテキスト中の改行コード
\n
を<br />
に置換する方法だと思う。class Message extends React.Component { constructor(props) { super(props) this.state = { message: 'これは、\nテキスト\nです。', } } render() { return ( <div dangerouslySetInnerHTML={{ __html: this.state.message.replace(/\n/g, '<br />') }} /> ) } }しかし、この方法だとXSSの脆弱性を生み出す可能性があるためできるだけ避けたい。
よって、改行タグ
<br />
をHTMLとして置換するのではなく、JSXの要素として置換できないか考えた。結果として、以下のような構成になった。
class Message extends React.Component { // ... render() { return ( <div>{ this.state.message.split('\n').map((str, index) => ( <React.Fragment key={index}>{str}<br /></React.Fragment> )) }</div> ) } }テキスト中の改行コードで分割して作られた配列でmap関数で繰り返し処理をして、JSX要素を戻り値とした。
この方法であれば、悪意のあるユーザーがテキストにhtml要素を入力した場合にも文字列として扱われるため、XSSの脆弱性は抑えられる。なお、map関数内の
<React.Fragment />
とは、戻り値が1つの要素しか指定でいないReactの制約を外してくれる便利な構文である。
- 投稿日:2020-02-17T11:42:28+09:00
Reactで画像をlazyloadしつつpreloadするコンポーネント
FirstViewにはいらんけど、スクロールして登場するまでにはなるべく存在してほしいみたいな画像があったので作ってみました。
正直こんな雑なやり方で合ってるのか分からんのでもっといい方法知ってる人がいたら教えてください頼みます。import * as React from 'react'; type Props = { imgUrl: string; alt: string; }; export default function LazyAndPreloadImage(props: Props) { const [loaded, setLoaded] = React.useState(false); React.useEffect(() => { // preload しておく const img = new Image(); img.src = props.imgUrl; // 読み込めたら img タグ描画する img.onload = () => { setLoaded(true); }; }, []); if (loaded) { return <img src={props.path} alt={props.alt} />; } // 雑に空divにしてるけど思い思いのplaceholder入れてください return <div></div>; }
- 投稿日:2020-02-17T08:27:52+09:00
TypeScriptを使ってReactのFunctionalComponentに型安全にpropsを渡す
import React, { FunctionComponent } from 'react'; type Props = { title: string } const Title: FunctionComponent<Props> = props => { return <> <h1>{props.title}</h1> </> }または、以下のようにtypeの定義省略することも出来る。
import React, { FunctionComponent } from 'react'; const Title: FunctionComponent<{ title: string }> = props => { return <> <h1>{props.title}</h1> </> }使用する時はこのように使う。
function App() { return <> <Title title="Hello World" /> </> }関連記事
- 投稿日:2020-02-17T01:07:43+09:00
react-fetching-library で Suspense 使ってみた。
最近ReactでAPIからデータフェッチする際に
useEffect
を使っていましたが、
これを読んだあと、これからはSuspense
を積極的に使っていこうと思い立ちました。
https://overreacted.io/ja/a-complete-guide-to-useeffect/いい感じに使えるパッケージはないものかと探していると、
4日前(投稿日現在)に出来立てホヤホヤのreact-fetching-library
を発見。
https://www.npmjs.com/package/react-fetching-library
Doc: https://marcin-piela.github.io/react-fetching-library/#/?id=usesuspensequery
早速使ってみよう。Suspense使用前
ポートフォリオに投稿したQiitaの記事を載せるために、こんな感じで実装していました。
レンダリングしている内容は省略します?♂️App.jsimport React from 'react' import Writing from 'sections/Writing' export default () => { return ( <div> <Writing /> </div> ) }sections/Writing.jsimport React, { useState, useEffect } from 'react' import { fetchQiitaArticles } from 'lib/api' export default () => { const [articles, setArticles] = useState([]) useEffect(() => { const handleGetArticles = response => { setArticles(response) } fetchQiitaArticles({ handleGetArticles }) }, []) return ( // fetchしたarticlesを使ってレンダリング ) }lib/api.jsexport const fetchQiitaArticles = async props => { const { handleGetArticles } = props await fetchGet({ auth: process.env.REACT_APP_QIITA_AUTH, url: 'https://qiita.com/api/v2/authenticated_user/items', successAction: handleGetArticles, }) } const fetchGet = async props => { const { auth, url, successAction, failureAction } = props return await fetch(url, { headers: { Authorization: `Bearer ${auth}`, }, }) .then(response => response.json()) .then(responseJson => { // console.log(responseJson) if (successAction) { successAction(responseJson) } }) .catch(error => { // console.error(error) if (failureAction) { failureAction() } }) }セットアップ
まずは
react-fetching-library
をインストール。$ yarn add react-fetching-library
続いてClientの作成。とりあえず今回はオプションなし。
api/Client.jsimport { createClient } from 'react-fetching-library' export const Client = createClient()そして
<ClientContextProvider>
でAppを囲う。App.jsimport React from 'react' import { ClientContextProvider } from 'react-fetching-library' import { Client } from 'api/Client' import Writing from 'sections/Writing' export default () => { return ( <ClientContextProvider client={Client}> <Writing /> </ClientContextProvider> ) }これで、
react-fetching-library
の機能が使用可能になりました!fetchの用意
api/fetchMyQiitaArticles.jsexport const fetchMyQiitaArticles = { method: 'GET', endpoint: 'https://qiita.com/api/v2/authenticated_user/items', headers: { Authorization: `Bearer ${process.env.REACT_APP_QIITA_AUTH}`, }, }コンポーネントのラッパー
useEffect
を廃止してWritingContainer
でラップします。
その際、useSuspenseQuery
を使ってデータフェッチを行うようにします。container/WiritingContainer.jsimport React from 'react' import { useSuspenseQuery } from 'react-fetching-library' import { fetchMyQiitaArticles } from 'api/fetchMyQiitaArticles' import Writing from 'sections/Writing' export default () => { const { payload, error, query } = useSuspenseQuery(fetchMyQiitaArticles) return <Writing error={error} articles={payload} /> }sections/Writing.jsimport React, { useState, useEffect } from 'react' import { fetchQiitaArticles } from 'lib/api' ++ export default props => { ++ const { articles, error } = props -- export default () => { -- const [articles, setArticles] = useState([]) -- useEffect(() => { -- const handleGetArticles = response => { -- setArticles(response) -- } -- fetchQiitaArticles({ handleGetArticles }) -- }, []) return ( // fetchしたarticlesを使ってレンダリング ) }Suspenseを使う!
App.js++ import React, { Suspense } from 'react' -- import React from 'react' import { ClientContextProvider } from 'react-fetching-library' import { Client } from 'api/Client' ++ import WritingContainer from 'container/WritingContainer' -- import Writing from 'sections/Writing' export default () => { return ( <ClientContextProvider client={Client}> ++ <Suspense fallback={<p>Loading ...</p>}> ++ <WritingContainer /> ++ </Suspense> -- <Writing /> </ClientContextProvider> ) }かなりアバウトになりましたが、
これでSuspenseを使ったデータフェッチが可能になりました。おわりに
とりあえず自分は
useEffect
を使わずにデータフェッチができるようになりました。
Suspense界隈のリリース情報はしっかり追っていきたい。あと
react-fetching-library
の公式Docがtsで説明されてた。
これからちゃんとtsも勉強したいと思います!参考
https://marcin-piela.github.io/react-fetching-library/#/?id=usage