20211224のReactに関する記事は13件です。

Canvas APIでの描画をする時の落とし穴と便利メソッド

2021年、Canvas APIで複雑なものを比較的たくさん描いてきました。 Canvas API で何か描画しようと思った時に、予め知っておくと良いのではないかと思うことについてまとめてます。 CanvasのDOMに上書きされていく Canvas API は、呼び出された順番にCanvasのDOMが上書きされていきます。 ゆえに、「どの順番で、何を描画させていくか」が結構重要になってきます。 ※「リセットをする、という上書き」をしない限り、最初に描いた内容の上に次の描画が塗り重ねられていきます。 Canvas APIを用いて、ポケモンのコダックを描いてみたことがあるのですが、各パーツの描画順番を間違えると、完全なコダックの描画に失敗します。 (正しいケースと間違えたケース) 正しいケース export const kDuck = (ctx: CanvasRenderingContext2D) => { comb(ctx, "#383838"); // トサカ rightArm(ctx, "#F2C86B"); // 右腕 head(ctx, "#F2C86B"); // 頭 eyes(ctx, "#eee"); // 両目       ※「頭」パーツの後に描画することが正しい tail(ctx, "#F2C86B"); // 尻尾 rightFoot(ctx, "#C4C2BB"); // 右足 bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕 beak(ctx, "#C4C2BB"); // くちばし leftFoot(ctx, "#C4C2BB"); // 左足 }; 間違えたケース export const kDuck = (ctx: CanvasRenderingContext2D) => { comb(ctx, "#383838"); // トサカ rightArm(ctx, "#F2C86B"); // 右腕 eyes(ctx, "#eee"); // 両目       ※「頭」パーツの前に描画しているので正しくない head(ctx, "#F2C86B"); // 頭 tail(ctx, "#F2C86B"); // 尻尾 rightFoot(ctx, "#C4C2BB"); // 右足 bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕 beak(ctx, "#C4C2BB"); // くちばし leftFoot(ctx, "#C4C2BB"); // 左足 }; 実際の描画物を見れば一目瞭然ですが、後者のコダックは、目がありません。 これは実際には描画されているけれど、「目」パーツの描画の順番が、「頭」パーツよりも先に実行されてしまっているため、あとから描画された「頭」パーツに隠れて見えなくなってしまっています。 描画する順番を誤るとこのような悲惨なコダックを描いてしまうことになるので気をつけましょう。 その時の状態に応じて、描き分けたいケースの対処 例えば Reactで Canvas APIを使うケースなどでは、state(状態)によって描画したいものを変えたいといったことが発生すると思います。 そんな時は、描画メソッド等のroot(根本的)な部分にリセット用の描画を施しましょう Canvas DOMに width(x方向), height(y方向) を指定し、そのサイズに応じて座標(x,y)方向を特定の色で埋めることで、上書きされることなく、都度描きたいものだけ表示させられるようになります。 以下の例は、widthが880px、heightが680pxです。 ctx.beginPath() ctx.fillStyle = '#EEEEEE' // デフォルトの <Canvas/> 空間の背景色 ctx.moveTo(0, 0) // <Canvas/> 空間の座標始点(デフォルト左上) ctx.lineTo(0, 680) // 左下末端 ctx.lineTo(880, 680) // 右下末端 ctx.lineTo(880, 0) // 右上末端 ctx.fill() ctx.closePath() stroke()とfill() メソッドの順番に注意 描画の際、輪郭線を表現したい場合は、必ずfill() メソッドよりもstroke()メソッドを後に実行する必要があります。 以下に、コダックの頭パーツを描画している関数があります。 // コダックの頭 const head = (ctx: CanvasRenderingContext2D, fillColor: string) => { ctx.beginPath(); ctx.strokeStyle = "#383838"; ctx.fillStyle = fillColor; ctx.moveTo(399, 147); // ctx.quadraticCurveTo(298, 142, 262, 212); ctx.quadraticCurveTo(235, 275, 323, 316); ctx.quadraticCurveTo(399, 345, 472, 306); ctx.quadraticCurveTo(533, 269, 486, 189); ctx.quadraticCurveTo(452, 153, 399, 147); // ctx.fill(); ctx.stroke(); ctx.closePath(); }; 頭の輪郭線を描きたいと思った場合、当然stroke() メソッドを実行することで描画可能です。 しかし、下記のコードのように、 stroke() メソッドの後にfill()メソッドが呼び出されると、輪郭線がなくなってしまいます。 // const head = (ctx: CanvasRenderingContext2D, fillColor: string) => { // ... ctx.quadraticCurveTo(533, 269, 486, 189); ctx.quadraticCurveTo(452, 153, 399, 147); // ctx.stroke(); ctx.fill(); // stroke() の後に実行している ctx.closePath(); stroke()の実行で輪郭線が描画されはしたものの、fill()メソッドの実行によって、fillStyleカラーに塗りつぶされることになります。 「輪郭線を描きたいのに、描画されない...」と思った時は、描画したい対象のコードのstroke() メソッドとfill()メソッドの呼び出し順に誤りがないか確認してみましょう。 save()メソッド と restore()メソッドを使いこなす 例えば特定のパーツだけサイズを変えたり、角度を変えたくなるケースが発生するとします。 サイズを変えるにはscale()メソッドを、 https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/scale 角度を変えるには rotate()メソッドをそれぞれ使うことで変更できます。 https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate しかし、先にも述べた「その時の状態によって描画したいもの」が変わるケースなど(角度やサイズの変更も含みます)では、scale()メソッドやrotate()メソッドを使うと、状態が変化するたびに、複利的に角度やサイズも変わっていってしまいます。 もちろん、意図的に、複利的なサイズ変更を望まれているのであればこれでも問題ありません。 一方で、描きたい対象は、あくまでも1つで良いケース(コダックが1体だけ描かれることを意図している時)の場合は、 save()メソッドと restore()メソッドを使いこなすことが重要になってきます。 https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/save https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/restore 少し例を変更し、「コダックのくちばしパーツだけ、サイズ変更する」ケースで考えています。 ① くちばしパーツの前で、 save()メソッドを実行 ② scale()メソッドを実行 ③ くちばしパーツを構成する関数で描画を実行 ④ restore()メソッドを実行 export const kDuck = (ctx: CanvasRenderingContext2D) => { comb(ctx, "#383838"); // トサカ rightArm(ctx, "#F2C86B"); // 右腕 head(ctx, "#F2C86B"); // 頭 eyes(ctx, "#eee"); // 両目 tail(ctx, "#F2C86B"); // 尻尾 rightFoot(ctx, "#C4C2BB"); // 右足 bodyAndLeftArm(ctx, "#F2C86B"); // 胴体と左腕 ctx.save(); // scale() メソッドの実行前に、bodyAndLeftArmまでの描画を一時保存 ctx.scale(1.5, 1.5); // 以降の描画対象のサイズが1.5倍になるscale()を実行 beak(ctx, "#C4C2BB"); // くちばしパーツを描画 ctx.restore(); //bodyAndLeftArmまでの描画を元の情報のまま復活 leftFoot(ctx, "#C4C2BB"); // 左足 パーツは、1.5倍にならず、元のサイズのまま描画される }; 「コダックとしての描画」は崩れてしまっていますが、くちばしパーツだけサイズが大きくなり、他のパーツは元のサイズのままであることがお分かりいただけるかと思います。 特定のパーツや、特定の描画のみ、サイズや角度を変更したいケースでは、save()メソッドとrestore()メソッドを上手く使うことで、再利用性の高いパーツに切り出すことも可能です。 変更したいサイズや角度の具体的な数値を引数に取るような関数に切り出すことで、 Reactなどのstateの変更に応じて、同じ関数で描画を変えていくことが可能になります。 なかなか scale()メソッドや rotate()メソッドまで使うケースは多くないかと思いますが、使う機会があれば、ぜひ一緒にsave()メソッドとrestore()メソッドも使ってみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

未来の構成を先取りする ~ React サーバーコンポーネント × GraphQL ~

GraphQLアドベントカレンダー24日目の投稿です! メリークリスマス! と心の中で思いつつ、記事を書いたので見てください はじめに Next.js Conf 2021とReact Conf 2021 皆さんはご覧になりましたか? とてもワクワクする未来の話が多かったように思います。 とりわけ、僕がワクワクしたのはサーバーコンポーネントです。 コンポーネントごとに、サーバー側でデータフェッチを行い、サーバー側でレンダリングを行う。 端末に依存されず、高速にレスポンスを返すことができる世界線を少し早く体験したいなーと思い、今回のテーマを選びました。 この記事で何をするか サーバーコンポーネントからGraphQLでデータをフェッチし、レンダリングをします。 GraphQLサーバーを立てるのは面倒だったので、GithubのGraphQLを使わせてもらいます 下準備 2021年12月24日現在、Next.jsにおいてサーバーコンポーネントはAlphaバージョンのため、少し下準備が必要です あと、TypeScriptだとうまく動かないとどこかで聞いたので、jsでやります。 1. Next.jsアプリケーションの作成 yarn create next-app 2. reactのバージョンをrcへ変更 yarn add react@rc react-dom@rc 3. 実験モードをONにする next.config.js module.exports = { ... experimental: { concurrentFeatures: true, serverComponents: true, }, } concurrentFeatures レンダリングをサスペンスしたコンポーネントに対し、 Promiseをthrowすることで通信が終わったことを伝える機能です。(後で使います) 実装1: サーバーコンポーネントでGraphQLがフェッチできるようにする GithubからGraphQLでフェッチできるようにする関数を作っておきます GraphQLは、実際はただのPOSTなので、そんなに難しくなくできます。 libs/fetchGraphQLFromGithub.js export const fetchGraphQLFromGithub = async (query, variables) => { return ( await fetch('https://api.github.com/graphql', { method: 'POST', headers: { Authorization: `bearer ${process.env.GRAPHQL_API_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables, }), }) ).json() } 裏話 本当はReact Relayを用いてやれないかなぁと画策していたのですが、 エラーのオンパレードでできなかったので、おとなしくサーバーコンポーネント対応済みのfetchを使いました 実装2. Pageコンポーネントをサーバーコンポーネントに変更 サーバー側でレンダリングするコンポーネントは*.server.jsという形にする必要があります IndexPageをこの拡張子に変更し、同じくサーバーコンポーネントであるViewerコンポーネントを呼び出します pages/index.server.js import Viewer from '../components/viewer.server' export default function Index() { return ( <div> <h1>Server Components with GraphQL Example</h1> <Viewer /> </div> ) } 実装3. Viewerコンポーネントを作成 Viewerコンポーネントでは、GraphQLの通信を行い、コンポーネントをレンダリングします。 components/viewer.server.js import { Suspense } from 'react' import { useData } from '../libs/use-data' import { fetchGraphQLFromGithub } from '../libs/fetch-from-github' export default function Viewer() { const query = ` query { viewer { login } } ` const data = useData('viewer', () => fetchGraphQLFromGithub(query, {})) return <Suspense fallback={'Loading...'}>{JSON.stringify(data)}</Suspense> } useDataでは、Suspenseにデータフェッチが終わったことを伝えるため、promiseをthrowするようにしています。 libs/use-data.js const cache = {} export const useData = (key, fetcher) => { if (!cache[key]) { let data let promise cache[key] = () => { if (data !== undefined) return data if (!promise) promise = fetcher().then((r) => (data = r)) throw promise } } return cache[key]() } エラー以外をthrowするというのが気持ち悪いような気がしたのですが、 非同期処理を隠蔽化するためにpromiseをthrowし、Suspenseでそれを解釈するというのはいかにもReactっぽいなぁと思いました まとめ サーバーコンポーネント と GraphQLを用いて未来を先取りしてみました。 サーバーコンポーネントはまだまだα状態なので、hooksも使えず、APIクライアントなども限られるため、かなり不自由な世界です。 ただ、コンポーネントごとにAPIリクエストを送り、クライアント側に大きな仕事をさせない、というのはかなり未来が明るいなぁと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactの再描画を減らそう。ついでにステートも管理しよう。

1. Reactにもモデルが必要 Reactにはモデルはありません。しかしデータをuseStateなどに格納していると、一つのデータが更新されるたびに再描画がかかってしまいます。小さいプロジェクトであれば問題ありませんが、大きなプロジェクトでは再描画はサーバやクライアントにとって大きな負担となり得ます。ユーザデータは以下のようなモデルクラスを定義してそこに格納し、可能な限りuseStateの呼び出しを減らしましょう。 model.ts class AppData { public data : string = "" } export const appData = new AppData() export const AppContext = React.createContext(appData) Contextを使っています。Contextの使用方法については公式ドキュメントなどを参照ください。この記事ではContextを利用した簡単な再描画通知・ステート管理の仕組みを紹介します。 2. API呼び出しはモデルの内部に書く コンポーネント内部で直接APIを呼んでデータを更新するのはやめ、モデルに更新関数を定義しましょう。これにより一連の更新処理を一つの更新関数にまとめ、コードが見やすくなるとともに、コンポーネント内でのuseEffectを減らし、依存関係を簡単にすることができます。APIを複数呼ぶ場合、それぞれのデータに対してuseStateを使用するとその都度再描画がかかってしまいますが、モデル内のデータであればそのようなことは起こりません。 model.ts class AppData { public data : string = "" public async update() { //API呼び出し&データ更新 } } 3. ビューの更新 さて、データをユーザ定義のモデルクラスに格納したため、ビューが自動で更新されなくなりました。Contextを利用すると、その内部のデータが更新されるたびに再描画が走ると誤解している人がいますがそんなことはありません。Reactによるデータ変更検知はシャローチェックです。ここから適切なタイミングで再描画をビューに通知してあげる仕組みを作ります。まずは単純な通知機能です。 useModel.tsx function useSignal() : () => void { const [, set] = useState(new class{}()) return () =>{ set(new class{}()) } } これをコンポーネント内で呼び出せば、内部的にuseStateを呼び出し、再描画通知関数を返します。これが便利なのはフォームです。公式ドキュメントではフォームデータをuseStateに格納していますが、モデル内の他の関数や他のコンポーネントから簡単に参照できるのでフォームデータもモデル内に格納すべきです。データは一カ所に集めることを心がけましょう。それにより余計なAPI呼び出し・ローカルストレージ・クエリパラメタ・クッキーの操作も減らせるかもしれません。データをファイルに書き出したりするときも扱いやすいですね。 component.tsx const Component () => { const a = useContext(AppData) const redraw = useSignal() const handleChange = (e : any) => { // aのデータを更新して再描画 a.data = e.target.value redraw() } return( <textarea value={a.data} onChange={handleChange}></textarea> ) } 4. 通知機能付きモデルクラス モデルというからにはモデル内のデータが変更されたことをビューに通知する機能が必要です。以下のようなクラスを作成しました。 useModel.tsx class Model { private notify : (n : any) => void = (n : any) => {} public changed(): void { this.notify(new class{}()) } public setNotifier(n : any) : void { this.notify = n } public resetNotifier() : void { this.notify = (n : any) => {} } } またModelクラスを受け取ってModelにuseStateの更新関数をセットするuseModelを定義します。 useModel.tsx function useModel(data: Model) : void { const [, set] = useState(null) data.setNotifier(set) } 先程のユーザモデルクラスをModelクラスを継承するように修正します。更新関数では更新が完了したタイミングでchangedという再描画を通知する関数を呼び出しています。 model.ts class AppData extends Model { public data : string = "" constructor() { super() } public async update() { //API呼び出し&データ更新 super.changed() } } コンポーネントではuseSignalの代わりにuseModelを呼び出します。changedが呼び出されると、このコンポーネントが再描画されます。useSignalと違うのは、更新通知関数をモデル側やモデルを利用する別のコンポーネントからも呼び出すことができることです。タイマを利用した定期更新や、このコンポーネントの外側で発生したイベントによる再描画指示などに便利です。 component.tsx const Component () => { const a = useContext(AppData) useModel(a) // 生成・消滅を繰り返すコンポーネントの場合は // 消滅時に通知機能をリセットする useEffect(()=>{ return () => a.resetNotifier() },[]) const handleChange = (e : any) => { // aのデータを更新して再描画 a.data = e.target.value a.changed() } return( <textarea value={a.data} onChange={handleChange}></textarea> ) } 5. ステート管理機能付きモデルクラス さらにステート管理を追加しましょう。以下のようなクラスを作成しました。ここではステート遷移の整合性をチェックしていませんが、必要に応じて実装してもよいでしょう。 useModel.tsx class StateModel<StateType> extends Model { private state : StateType public constructor(initialState : StateType, bRedrawNow=false) { super() this.state = initialState if(bRedrawNow){ super.changed() } } public setState(state : StateType, bRedrawNow=true) : void { if(this.state !== state){ this.state = state if(bRedrawNow){ super.changed() } } } public getState() : StateType { return this.state } } このクラスを継承するように、ユーザモデルクラスを再修正します。更新関数が開始したタイミングでsetState('loading')、完了したタイミングでsetState('complete')を呼び出します。 model.ts class AppData extends StateModel<'idle'|'loading'|'complete'> { public data : string = "" constructor() { super('idle') } public async update() { super.setState('loading') //API呼び出し&データ更新 fetch() .then(result : any){ this.data = result super.setState('complete') } } } コンポーネントではsetStateが呼び出されるたびに再描画が発生し、getStateの返り値に応じた描画が行われます。 component.tsx const Component () => { const a = useContext(AppData) useModel(a) switch(a.getState()){ case 'idle': return null case 'loading': return <div>お待ちください...</div> case 'complete': return <div>完了</div> } } 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで使えるCSS-in-JSのライブラリ比較

? 始めに KWCのフロントエンドエンジニアの@eri2490です。 React(Preact)で利用できるスタイリングの比較・導入の検討を行いました。 ⛑ CSS in JSとは? 特定のフレームワークやライブラリを指すものでは無く、「JavaScriptの中でCSSを書く」という概念のことです。JS実行時にスタイルを処理し、HTMLのheadにスクリプトを追加します。 他にはCSS-Modulesや、直接cssファイルを読み込む手法があります。今回はJSファイルに直接CSSも書く方法を取りたかったので、CSS in JSのライブラリの比較も行いました。 ?? npm trendsでの比較 候補にあがったライブラリ 1. styled-component 2. emotion 3. JSS 4. goober 5. Picostyle styled-component のダウンロード数の多さとemotion の一定の人気・パッケージサイズの軽量さが特徴的でした。 一方でPicostyleはダウンロード数・スター数の少なさから、検討することを止めました。 ? インポートや書き方の比較 emotion は複数の書き方が提供されており、styled-component や goober はStyled Componentを作成します。 JSS はCSSのクラスを作成し、classNameで指定します。 styled-component import styled from 'styled-components'; const TitleComponent = () => { return <Title>title component</Title>; }; const Title = styled.h1` color: 'red'; `; emotion // styled-componentっぽく import styled from '@emotion/styled'; const Title = () => <Text>title component</Text>; const Text = styled.h1` color: red; `; // Object Styles import { css } from '@emotion/css'; const Title = () => <h1 className={Text}>title component</h1>; const Text = css({ color: 'red', }); // Object Stylesを変形してJSSっぽく import { css } from '@emotion/css'; const Title = () => <h1 className={styles.title}>title component</h1>; const styles = { title: css({ color: 'red', }), }; JSS import { createUseStyles } from 'react-jss'; const Title = () => { const classes = styles(); return ( <h1 className={classes.text}>title component</h1> ); }; const styles = createUseStyles({ text: { color: 'blue', } }); goober import { styled } from 'goober'; const TitleComponent = () => <Title>title component</Title>; const Title = styled('h1')` font-size: 3rem; margin-bottom: 0.8rem; `; ? まとめ Styled Componentを作成すると生のCSSを書けるという利点はあるものの、IDEでCSSの補完が効かない点や、一見して普通のコンポーネントかどうかわからないという点はデメリットに思えました。 逆にCSSクラスを作成する場合は上記の書きづらさは解消される一方で、定義されたプロパティのみしか使えないという点で、困難さが予想されます。 個人的にはパッケージサイズが小さく、IDEでCSSの補完が効く書き方ができるemotionの使い勝手が良さそうでした。 ? We're hiring! KWCでは、エンジニアを募集しています。 https://recruit.kddi-webcommunications.co.jp/recruitment/career/ ご興味を持たれた方はぜひご連絡をお待ちしております! それでは良いお年をお迎えください?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Create React Appで出力されるラインセンスファイル関連の挙動変更

概要 Create-React-Appで作成し、buildしたファイルのライセンスコメント関連の出力をいじりたい という内容です。 やりたかったこと フロントエンドで使用しているライブラリの条文をyarn licenses generate-disclaimerで自前出力して、公開したい ライセンス条文ファイルが無かったりしてyarn licenses generate-disclaimerで拾えないものは手動で追記したい buildされたjsファイルのコメントを自前ファイルに関する内容に変更したい といった感じです。 結論を先に言うと、 yarn build時にstatic/js以下に固定名のLICENSEファイルを自前で配置する buildタスクを、yarn buildが走る前に以下をするように変更 yarn licenses generate-disclaimerの結果をpublic/static/js以下に出力する 追記したいファイルの内容をライセンスファイルに追記する react-app-rewiredを利用して、minimizeしたファイルの先頭に自前配置したライセンスファイルへの言及させる といった感じで対応しました。 自前でライセンスファイルを作成する部分は/public以下にファイルを置けばいいだけなのですが、 ↓のようなビルドさせたファイルの先頭コメントを、ejectせずにどうやって変えるかの話になります。 ライセンス関連出力の振る舞いを変える デフォルトで出力されるライセンスファイル create-react-appしたものをyarn buildするとbuild/statis/js以下にこんな感じで出力されます main.xxxxx.LICENSE.txtの中身はこんな感じです /* object-assign (c) Sindre Sorhus @license MIT */ /** @license React v0.20.2 * scheduler.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ ... 加えて、main.xxxxxx.jsの先頭にこんな感じのコメントが入ります。 /*! For license information please see main.93b92443.js.LICENSE.txt */ @license/@preserve/@cc_onでソース内にコメントを入れると、上記ファイルに追記されます。 App.txtに適当にコメントを入れてみます。 // App.txt /** * @license これはライセンスコメントです。 */ /** * @preserve これも追記されます。 */ /** * @cc_on これも残されます。 */ /** * @comment これは残りません。 */ function App() { ... とソース内にコメントすると、ライセンスファイルには /* object-assign (c) Sindre Sorhus @license MIT */ /** * @cc_on これも残されます。 */ /** * @license これはライセンスコメントです。 */ /** * @preserve これも追記されます。 */ /** @license React v0.20.2 * scheduler.production.min.js * ... と出力されます。/^\**!|@preserve|@license|@cc_on/iで拾っているみたいです。 この振る舞いを変更するには? webpackのconfig設定をすればいいのですが、ejectしたくない場合はreact-app-rewiredやcracoといったライブラリを使用して、ejectせずに設定をいじれるようにします。   ここではreact-app-rewiredを使って試してみます。 react-app-rewiredをインストール yarn add -D react-app-rewired react-scriptsをreact-app-rewiredに変更 "scripts": { "start": "react-scripts start", - "build": "react-scripts build", + "build": "react-app-rewired build", "test": "react-scripts test", "eject": "react-scripts eject" }, config-override.jsをプロジェクトのトップに作成 @commentも拾うようにしてみます。 TerserPluginを使っていますが、Create-React-Appで入っているのでそのまま使えます。 // config-override.js const TerserPlugin = require("terser-webpack-plugin"); module.exports = function override(config) { return { ...config, optimization: { minimize: true, minimizer: [ new TerserPlugin({ extractComments: /^\**!|@preserve|@license|@cc_on|@comment/i, }), ], }, }; }; ビルドする yarn buildしてみます。 // LICENSE.txt /* object-assign (c) Sindre Sorhus @license MIT */ /** * @cc_on これも残されます。 */ /** * @comment これは残りません。 */ /** * @license これはライセンスコメントです。 */ /** * @preserve これも追記されます。 */ /** @license React v0.20.2 @commentも拾うようになりました。 あとはTerser Pluginのドキュメントを見て、好きにいじります。 ↓な感じで変えてやれば // config-override.js const TerserPlugin = require("terser-webpack-plugin"); module.exports = function override(config) { return { ...config, optimization: { minimize: true, minimizer: [ new TerserPlugin({ extractComments: { banner: () => { return `ライセンスはここを見て! -> LICENSE.txt`; }, }, }), ], }, }; }; こんな感じでコメントを変えられるので(開発者ツールのsourceです) 自前で用意したラインセスファイルに関するコメントを好きに設定できます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebpackとViteをプロジェクト導入して比較した話

みなさんこんにちは。suginokoです。 寒くなってきたので布団を着て仕事をしています。(弊社はフルリモートで仕事ができます。暖かくして仕事しましょう。) 前置き 今日は弊社Web開発における開発環境についての記事書いてみようかなと思います。 弊社の開発環境構築に普段はWebpackを使って環境構築しているのですが、 Webpackで別に開発する必要なくない?もっと簡単に導入できそうなのあるのではないかな 開発環境の立ち上がりが遅すぎてムカつく。もっとはやく動かないもんかね というのが根底にあり、調べてみることにしました。 Viteというのは、私自身が前からesbuildに注目していて、これWebpackの代わりになるのではないかなと思っていたところで、Viteが出てきた(詳しい話は後述します。中でesbuild使っている)ので比較してみようかなと思った次第です。 (ようやくテストではなくて実際のプロジェクトに導入できてうれしい。 速いってことはわかったけど、どれくらい速くなるとか、開発環境構築楽になるとかわからなかったので、比較して使いたいなと思った次第です。 普段のWebpackでの開発環境構築 プロジェクトでは普段Reactを使っているのでそれに合った環境構築をWebpackで行います。 そこにTypeScriptを入れる入れない、Redux使うなり他のモジュール入れるなりそれは開発環境作る人で様々ですが、おおよそは style系のloader、プラグイン devServer alias入れるとか入れないとか TypeScriptを入れるならts-loader入れるとか vendor.js分けるとか まあ細かいところはもう少しありますが、大体上記の導入して整えていきます。 これらをViteでも同じように行って比較していきます。 Viteでの環境構築 (TypeScript入れるほうの構築めっちゃ時間かかった・・・) Webpackと同じ構成にするので、React + TypeScript + Sassくらいか。Redux(toolkit)もいれちゃったかもしれないけど、だいたいこんな感じで構成 import { defineConfig } from 'vite' import path from 'path' import react from '@vitejs/plugin-react' const src = path.resolve(__dirname, 'src') // const dist = path.resolve(__dirname, 'dist') const ALIAS = {...} // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { console.log('** mode **', mode) return { base: './', server: { port: 11080, open: true }, plugins: [react()], publicDir: 'public', // 明示的に build: { sourcemap: mode == 'develop' ? true : false, minify: mode == 'production' ? 'terser' : false, // dev stg pro // outDir: dist, outDir: './dist', }, // root: './src', difine: { global: 'window', // global指定しないと取得不可 }, resolve: { alias: ALIAS }, } }) Loaderとか書かなくていい。これが最高すぎる。 そもそもViteの場合、取説にあるように、 # npm 6.x npm init vite@latest my-vue-app --template react # ts npm init vite@latest my-vue-app --template react-ts # npm 7+, extra double-dash is needed: npm init vite@latest my-vue-app -- --template react # ts npm init vite@latest my-vue-app -- --template react-ts # yarn yarn create vite my-vue-app --template vue するだけでReact(+TypeScript)の環境構築ができます。最高。 一応Webpack v5の構成 const webpack = require('webpack') const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') // webpack5ではデフォルトでTerserPlugin入ったらしいがまだ必要らしい const TerserPlugin = require('terser-webpack-plugin') // OptimizeCSSAssetsPluginが消えたのでcss-minimizer-webpack-pluginに切り替える const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const mode = process.env.NODE_ENV || 'production' const isDevserver = process.env.IS_DEVSERVER == 1 const port = process.env.PORT || '1111' const src = path.resolve(__dirname, 'src') const dist = path.resolve(__dirname, 'dist') console.log('** mode **', mode) module.exports = { // mode 必須 mode: mode === 'production' ? mode : 'development', cache: { type: 'filesystem', buildDependencies: { config: [__filename] } }, devtool: mode === 'production' ? false : 'eval-cheap-module-source-map' // : 'eval-cheap-module-source-map' , entry: isDevserver ? [ 'react-hot-loader/patch', `webpack-dev-server/client?http://localhost:${ port }`, 'webpack/hot/only-dev-server', `${ src }/main.dev.tsx`, ] : { 'bundle': `${ src }/main.tsx` }, output: { path: dist, filename: isDevserver ? '[name].js' : '[name].js?[hash]', // filename: isDevserver ? 'bundle.js' : '[name].js?[hash]', // bundle.jsにするとエラーになるv4ではいけたが。 publicPath: '/', clean: true }, module: { rules: [ { test: /\.ts(x?)$/, exclude: [/node_modules/], use: { loader: 'ts-loader' } }, { test: /\.scss|.css$/, use: [ isDevserver ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { url: false, sourceMap: true } }, { loader: 'postcss-loader', options: { sourceMap: true } }, { loader: 'sass-loader', options: { sourceMap: true } } ] } ] }, plugins: [ new HtmlWebpackPlugin({ template: `${ src }/index.html`, inject: 'body', filename: `index.html` }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new MiniCssExtractPlugin({ filename: '[name].css?[contenthash]' }) ], optimization: { splitChunks: { name: 'vendor', chunks: 'all', }, minimize: true, minimizer: [ new CssMinimizerPlugin(), new TerserPlugin({ terserOptions: { compress: { drop_console: true, }, output: { comments: false, } } }) ] }, devServer: { static: { directory: dist, }, historyApiFallback: true, host: 'localhost', compress: true, port: port, hot: true, open: true }, resolve: { alias: {...}, extensions: ['.ts', '.tsx', '.js'], }, performance: { hints: false }, // ES5(IE11等)向けの指定(webpack 5以上で必要) target: isDevserver ? ["web"] : ["web", "es5"], } 。。。長いな~~ この構成するだけでViteの倍くらい体感かかったかもです。 実験のディレクトリ構成(それぞれおおよそ同じ感じで) node_modules package.json postcs![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696960/b29f9ca4-48ba-92e7-03f3-e42c32f847f3.png) s.config.js tsconfig.json webpack.config.js(Viteではvite.config.js) (+viteではenvファイルとか) src ┗components(ここでは共通してApp.tsxのみ) ┗index.html ┗main.tsx 開発環境の起動について時間を比較してみる とりあえずは現時点でのプロジェクトに使っているWebpackでの開発環境ではどれくらいで起動するのか計ってみました。 まあまあ大きい案件だからなのか、Webpack v4 だからなのかわからないけど、13954msやら20871msは遅すぎやしませんかね。 既に動いているプロジェクトだとあんまり意味がなかったので、実験的にViteと同じ構成を作ってWebpack v5も試してみます。(React + TypeScript + Sass) とりあえず、テスト的にWebpack v5の開発環境(React+TypeScript+scssくらいでいいかな)作って、現状のほぼまっさらなViteと同じような構成で開発環境を起動してみましょう。 Webpack v5 6226ms Vite 1370ms Webpack v5もだいぶ速くなったけど、Vite優勢ですね!(なんならViteのほうの構成のほうが少し多いのにこの速度。すばら。 ビルド時の時間の比較 Webpack v5 12.44s Vite 8.9s 少しViteの方がはやそう React(+TypeScript)の案件ならViteでもいいかもしれないですね。 Viteは何故速いのか Webpackはbuildするときにモジュール全体をクロールしてビルドする必要があるので、全部の依存関係が解消されないとビルドされない。 アプリケーションが大きくなるほど遅くなるということでしょう。 (だから大きめの案件だと開発環境の起動が遅かったのですね) 公式にもあるのですが、Viteはこの問題を解消するために依存関係とソースコードのカテゴリで分割することで開発サーバーの起動時間を解消したようです。 モジュールとかは頻繁に変更されることはないので、事前バンドルしておいてページの読み込みを解消したらしいです。(これをキャッシュして使うっぽい。node_modules/.viteにキャッシュするらしい)その事前バンドルをGo製のesbuildを使っているとのこと。(これが爆速) ソースコードはネイティブのESMを使ってブラウザの要求に応じてバンドルせずそのまま提供しているようです。 本番buildではrollupを使っているそうです。(バージョン違うとbuild時にrollupのエラーも出たんで、ちゃんと最新バージョン使ったほうがいです) …rollupはお試しなのかしら。esbuildを本番buildで使いたいけど、特定のコード分割とCSSの取り扱いの関係でまだ使ってないそうなので、将来的には本番buildもesbuildを使うのかもしれないです。 詳しいことは日本語ドキュメントもあるのでみてみてください。 Vite TypeScript使うときだけやや面倒くさい じゃあもうWebpackではなくてViteでいいじゃん!って思うんですけど、ややTypeScriptの扱いが面倒で、 TypeScriptはサポートされているんですけど、型チェックをしてくれません。 なので自分は2窓使って監視するようにしました。 tsc --watch で一応型でエラーしたらわかるので、見つつ対応しています。 また、ブラウザで型チェックしてくれるvite-plugin-checkerも入れてみました。 一応ブラウザでもエラー出るようになってくれたのですが、tsc --watchでどうもエラーが出る箇所が違うみたいで、微妙な感じです。 無いよりはいいかな・・・という感じ。 まあでもTypeScript入れないなら圧倒的にViteのほうがよさげに見えました。 (CSSのbuild設定もこれでは足りないっぽくて実験しながらまだいじくってます。ぶつぶつ) 最後に 年末最後に投稿できてよかったです。 しばらくVite使ってみようかなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(自称) 日本一わかりやすいemotionのドキュメント

※ あくまで自称です。笑 ※ 今後もどんどん拡張していく予定です。 ※ emotion/styled に関しては割愛する予定です。 Emotionとは? フレームワークにとらわれず使うことができるCSS in JSのフレームワーク。 (styled-componentsと比較されがちだが、emotionの方が後発であるためstyled-componentsができることは基本できます。) 初期設定 利用したいEmotionのパッケージをインストールする Emotion関連のパッケージ @emotion/css フレームワークにとらわれず、Emotionを使用するためのパッケージ。 @emotion/react React用のEmotionパッケージ。 @emotion/styled styled-component っぽい記法で書くことができるEmotionパッケージ。(筆者はstyled-componentから乗り換える人用のパッケージだと思っています) @emotion/core @emotion/core はEmotion 11で @emotion/react に改名したため、忘れ去ってください。 参考:https://emotion.sh/docs/emotion-11#package-renaming @emotion/babel-preset-css-prop css propsを使用する場合、本来はすべてのファイルに/** @jsx jsx */を書く必要がある。 このJSX Pragmaの問題を解決するためのプラグインが@emotion/babel-preset-css-prop。 @emotion/babel-plugin Emotionのコンパイルを最適化するためのプラグイン。 version 8 以上は不要 @emotion/server サーバー側レンダリングするAPI 基本文法 基本的には2種類の書き方がある。 (厳密には3種類?@emotion/styledをインストールすればstyled-componentsの書き方もできます!!) オブジェクトスタイル 厳密な型定義がされるため予測変換しやすい CSSプロパティがキャメルケースになる ストリングスタイル そのままのcssを文字列に展開するだけ 型定義がないため、デフォだと予測変換がないしエラー検知しづらい vscodeであれば、vscode-styled-componentsを入れると予測変換が使える // オブジェクトスタイル const objectStyle = css({ maxWidth: '400px', margin: '0 auto', padding: '10px' }); // ストリングスタイル const stringStyle = css` max-width: 400px; margin: 0 auto; padding: 10px ` // css propは css({}) もしくは css`` を代入するだけ render( <div css={stringSty le}> {/* ... */} </div> ) 擬似要素やメディアクエリー 両方とも、scssの記述方法で入れ子にすればokそうです!! ストリングスタイル const styles = { main: css` padding: 12px; &:hover { opacity: 0.5; } ` } オブジェクトスタイル const styles = { main: css({ padding: '12px' '&:hover': { opacity: 0.5, }, }) } Propsで渡す css propに記載されたスタイルが評価され、計算されたクラス名がclassNameに渡されます。 そのため、cssを親から受け取りたいComponentはpropsにcss(SerializedStyles型)ではなくclassName(string型)を受け取れるようにすればよいです。(もちろんcssとして受け取ることもできます。) とても賢いが正直わかりづらい。 const Button = ({className, children}) => ( <button className={className}>{children}</button> ) const buttonWrapperStyle = css` background-color: #ddd; ` // cssで渡しても、classNameに変換される const ButtonWrapper = () => ( <Button css={buttonWrapperStyle}>ボタン</Button> ) 結合する EmotionにおけるCSSの結合は、Specificityも解決します。 基本、配列で渡せば結合されます。 const marginBottom = css` margin-bottom: 16px ` const style = css` padding: 16px; ` render ( <div css={[marginBottom, style]}>{/* */}</div> ) ストリングスタイル そのまま変数代入展開することができる。 const marginBottom = css` margin-bottom: 16px ` const style = css` ${marginBottom} padding: 16px; ` オブジェクトスタイル スプレッド演算子で展開することができる。 const marginBottom = css({ marginBottom: '16px' }) const style = css({ ...marginBottom, padding: '16px' }) 入れ子 ストリングスタイル 子要素に対してスタイルを当てる const listStyle = css` li { //... } ` return ( <ul css={listStyle}> <li css={item}>{/* */}</li> </ul> ); 親のタグを指定する 親のタグに囲まれているときのみスタイルを当てることもできる const item = css` ul & { //... } ` return ( <> <ul> <li css={item}>{/* あたる */}</li> </ul> <div> <span css={item}>{/* あたらない */}</span> </div> </> ); 設計 Media Query ※ マサカリ大歓迎です type breakpointsType = { id: 'tablet' | 'pc'; breakpoint: number; }[]; const breakpoints: breakpointsType = [ { id: 'tablet', breakpoint: 960, }, { id: 'pc', breakpoint: 560, }, ]; // min-widthのみしか設定できてないからもう少し工夫は必要かも? export const MediaQuery = breakpoints.reduce((acc, { id, breakpoint }) => { return { ...acc, [id]: `@media (min-width: ${breakpoint}px)`, }; }, {}); // inline style const style = css` background-color: '#fff'; ${MediaQuery['pc']} { color: gray; } ` // object style const style = css({ backgroundColor: '#fff', [MediaQuery['pc']]: { padding: 20, }, });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React製サービスにおけるGTMとの付き合い方

モダンフロント技術におけるGTMの問題 ここでいうモダンフロント技術は、VueやReactなど仮想DOMを用いられた技術を指します。 仮想DOMなので「DOMが存在しているとは限らない」 要素の表示や要素のクリックなどのトリガーに関して、class名やらid名などDOMの情報に対して条件を絞りイベントを制御すると思いますが、必ずしもGTMがその仮想DOMが正しく検知できるとは限らないです。 仮想は消失したり入れ替わったりするものなので。 そのため、GTMの既存の要素の表示や要素のクリックに依存しない設計を理想とします。 GTMのページビュートリガーが使えない モダンフロント系の技術では、React Routerやらvue-routerなどの仕組みによって、 サーバーサイドを介さずにクライアントサイドのみでページの変更を行なっているケースが多いです。 つまり、History APIで更新しているだけなので、ページビュートリガーが機能しないです。 仮に既存のGTMのトリガーを使うとしても、初期ロード時は「ページビュー」、遷移時は「履歴の変更」が発火するため、 2つのトリガーを1つのタグに対して付与する必要があります。 ※ 正しく発火する保証はしきれないかなとおもいます...重複するケースがありそうな気もしてます。 履歴の変更は、URLの一部が変更されたとき、またはpushState APIときを検知するトリガーです。 モダンフロント技術におけるGTMとの付き合い方 既存のトリガーがすこぶる使えなさそうで「じゃあどうすればいい?GTM使わなければいい?」って思った人もいると思いますが、 ここでカスタムイベント トリガーの登場です! カスタムイベント トリガーとは? カスタムイベントトリガーは、キー名event / 値イベント名でdataLayer.pushされたものを検知するトリガーです。 下記のように設定します。 dataLayer.push({ event: "page_view" // eventパラメータにイベント名をいれる }) ▼ ドキュメント つまり、ページビューやら要素のクリックなどをコード側で検知して、 このデータレイヤーをpushしてあげれば良いわけです!! 設計 注意 ※ Next.jsを使ってるため、React routerを使ってる人はよしなにメソッドを置き換えてください... ※ 処理をシンプルにしてます。サービスで運用しているものはもう少し複雑だったりします。特にデータレイヤーで送るものに関しては、拡張してください。 事前準備: dataLayer.pushの処理を別関数でわける 型定義したかったり、初期化処理を閉じ込めたかったりしたので、 dataLayer.pushの処理を分けて使ってます。 type PageViewEvent = { event: 'page_view'; pagePath: string; }; type InViewEvent = { event: 'inView'; label: string; }; type ClickEvent = { event: 'click'; label: string; }; export type DataLayerType = | PageViewEvent | InViewEvent | ClickEvent export const pushDataLayer = (data: DataLayerType): void => { window.dataLayer = window.dataLayer || []; window.dataLayer.push(data); }; この処理を、'@/libs/analytics'からimportできるようにしています。 ページビュー 下記Componentをpages/_appに読み込みます。 (Componentじゃなくても、hooksでも良いと思います。) ちょっと複雑に見えますが、やってることは至ってシンプルで、 初期ロード時は1回のみなのでuseRefを使って制御 なんでこれで1回のみ発火できるん?って思った人は【React】useRefの基本的な使い方・活用術・注意点 参考にしてみてください(告知) 履歴の変更はrouteChangeCompleteを検知する だけです。 import { VFC, useRef, useEffect, useCallback } from 'react'; import { useRouter } from 'next/router'; import { pushDataLayer } from '@/libs/analytics'; export const TrackPageView: VFC = () => { const once = useRef(false); const { asPath, basePath, events } = useRouter(); // page_viewイベントを送信 // pagePathだけじゃなく、pageのタイトルとかも送っていいかもね // pageLocationとかの方がGAのパラメータと合ってよかったかも^^ const pushPageView = useCallback((url) => { pushDataLayer({ event: 'page_view', pagePath: url, }); }, []); // 初期ロード時の処理 useEffect(() => { // routeChangeCompleteが返すurlと同じ形になるよう整形 const url = `${basePath}${asPath}`.replace(/\/$|\/\?/, (matched) => matched === '/?' ? '?' : '' ); !once.current && pushPageView(url); once.current = true; }, [pushPageView, prepared, basePath, asPath]); // URLに変更があった時の処理 useEffect(() => { if (!prepared) return; events.on('routeChangeComplete', pushPageView); return () => { events.off('routeChangeComplete', pushPageView); }; }, [events, pushPageView, prepared]); return null; }; 仮想ページビュー 通常の履歴の変更以外にもページビューを送りたい(仮想ページビュー)ケースもあると思います。 基本仮想ページビューを送りたいのは、要素の表示 / 非表示のケースだとは思うので、このように実装しています。 (正直、ページビューじゃなくて、別のイベントでもいいとは思いますがね...!) import { VFC, useRef, useEffect } from 'react'; import { useRouter } from 'next/router'; import { pushDataLayer } from '@/libs/analytics'; type Props = { onMount?: boolean; onUnMount?: boolean; suffix: string; }; export const TrackVirtualPageView: VFC<Props> = ({ onMount, onUnMount, suffix, }) => { const { basePath } = useRouter(); const mount = useRef(onMount); const unMount = useRef(onUnMount); const path = useRef(`${basePath}/${suffix}`); useEffect(() => { if (!prepared) return; mount.current && pushDataLayer({ event: 'page_view', pagePath: path.current, }); const unmounted = unMount.current; const _path = path.current; return () => { unmounted && pushDataLayer({ event: 'page_view', pagePath: _path, }); }; }, [prepared]); return null; }; 要素が表示されたタイミングで仮想ページビューを送る <TrackVirtualPageView suffix="hoge" onMount /> 要素が非表示されたタイミングで仮想ページビューを送る <TrackVirtualPageView suffix="hoge" onUnMount /> これを、検知したいComponetのどっかに追加すればokです。 クリックの検知 めっちゃシンプルですが、クリックを検知してデータレイヤーを送るだけの処理をもったComponentを作りました。 import React, { VFC, ReactNode, useCallback } from 'react'; import { pushDataLayer } from '@/libs/analytics'; type Props = { children: ReactNode; label: string; }; export const TrackOnClick: VFC<Props> = ({ children, label }) => { const handleClick = useCallback(() => { pushDataLayer({ event: 'click', label, }); }, [label]); return <div onClick={handleClick}>{children}</div>; }; これを囲ってあげるだけです。 <TrackOnClick label="hoge"> <Button /> </TrackOnClick> 要素の表示 要素の表示は、react-intersection-observerを使って検知しています。 このライブラリはとても便利で、何秒表示されたから発火する、何pxスクロールされたら発火するなどの制御ができ、 GTMの要素の表示トリガーができる設定を網羅できます。 ▼ 参考 import React, { VFC, ReactNode, useEffect } from 'react'; import { pushDataLayer } from '@/libs/analytics'; import { useInView, IntersectionOptions } from 'react-intersection-observer'; /** * continue - イベントの発火を継続させるか(デフォルトは一回のみ) * options * - rootMargin: 上からの距離 (中央は'50%') * - delay: 何秒遅らせるか */ type Props = { label: string; children: ReactNode; continuous?: boolean; options?: Omit<IntersectionOptions, 'triggerOnce'>; }; export const TrackInView: VFC<Props> = ({ children, label, continuous = false, options, }) => { const { ref, inView } = useInView({ ...options, triggerOnce: !continuous, }); useEffect(() => { if (!inView) return; pushDataLayer({ event: 'inView', label, }); }, [inView, label]); return <div ref={ref}>{children}</div>; }; こんな感じに使います。 <TrackInView label="voice_5s" options={{ delay: 5000 }} continuous> <SomethingComponent /> </TrackInView> もし、もっと良い方法があったら教えていただきたいです!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material UIのMenuを使おうとするとコンパイルエラーが起きる現象の解決

解決策 npm install @emotion/react @emotion/styled yarn add @emotion/react @emotion/styled で解決。 MUI(Material-UI)の利用時には、emotionあるいはstyled-componentsのインストールが必須なケースがあるようです。 参考サイト https://ufirst.jp/memo/2021/09/28/mui-%E3%81%AB%E3%81%A6%E3%82%A8%E3%83%A9%E3%83%BC%E3%80%8Cmodule-not-found-cant-resolve-emotion-react-in-node_modules-mui-styine%E3%80%8D/ https://cloudpack.media/59677
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部ライブラリを利用時にonChangeが発火しなくて困った話

Qiita株式会社 Advent Calendar 2021の25日目の担当は、プロダクト開発グループの @kiitan です! はじめに Qiita Jobsではプロフィール入力を楽にするためWantedlyのフォーム自動入力ボタンを利用しています Wantedlyのフォーム自動入力ボタン の機能に軽く触れておくと転職活動を行う際に必ず入力が必要な名前や年齢などを Wantedly上にプロフィールからワンクリックでフォームにコピーすることができる便利なボタンです この記事は Wantedlyのフォーム自動入力ボタン を React で書かれたフォームに導入しようとして困った話です 入力されるがStateに反映されない 説明するより触ってもらったほうが早いので早速みていきましょう See the Pen react hooks useState by kiitan128 (@kiitan) on CodePen. 一見しっかり入力されているようですが <p>あなたの年齢は{age}歳です</p> の部分で年齢が表示されていないことで state へ反映されていないことがわかります 原因は Wantedly のフォーム自動入力ボタンで input の value を書き換えても onChange が発火しないことでした これは Wantedly のフォーム自動入力ボタンが特別 onChange が発火しないのではなく value を javaScript で直接書き換えを行なった場合に発生します See the Pen react hooks useState by kiitan128 (@kiitan) on CodePen. 上記の例では document.querySelector(".inputStyle").value = では onChange が発火しないことがわかります これは通常であれば document.querySelector(".inputStyle").onchange() を入れることで解決できる問題です しかし外部ライブラリを利用する今回の場合ではこの対策をすることができません 解決編 最終的なコードは泥臭いものとなりました 下記のコードのように state と value の値を setInterval で監視し差分があれば反映する方法を使いました React.useEffect(() => setInterval(() => { if(age !== ageRef.current.value) { setAge(ageRef.current.value) } }, 1000) , []) See the Pen react hooks useState by kiitan128 (@kiitan) on CodePen. これで無事stateに反映することができました もっといいやり方やアイデアがあればコメント欄で教えてください 終わりに 明日の投稿を担当するのはこの記事を見た皆さんです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今度こそ Redux を完全に理解する(初心者向け)

はじめに こんにちは。 DMM WEBCAMP Advent Calendar 2021 24日目 を担当させていただきます FarStep です。 プログラミングの学習、ポートフォリオの作成に役立つ動画を配信していますので、よろしければご覧ください。 Farstep【プログラミング講座】 本記事の目標 本記事の目標は、Redux のフロー(流れ)を理解し、Redux が公開している example の一つ「todos」アプリ(簡易版)の動作原理を説明できるようになる です。 本記事の構成 本記事は全部で3章から構成されています。以下が各章の内容です。 第1章:Redux のフローを理解しよう(例え話やアニメーションを用いて詳しく説明していきます) 第2章:React & Redux を用いて Todos アプリを作ろう(第1章で学んだ Redux を使って Redux が公式サイトで公開しているアプリの簡易版を作成します) 第3章:まとめ(本記事の内容を総括します) 本記事の対象者 React を触ったことのある方 Redux のことを聞いたことがある方 Redux を一度学んで挫折してしまった方 絶対に Redux を理解したい方 本記事は初学者の方を対象にしているため、同じことを繰り返し述べている箇所があります。学習内容を定着させるための繰り返し表現ですのでご了承ください。 第1章 Redux のフローを理解しよう 本章では、Redux のフローを説明していきます。本章を読み終わったころには、Redux を使ったデータの更新方法が理解できているはずです。 Redux とは Redux を一言で表すと、アプリケーションの状態を管理および更新するためのライブラリです。Redux を使うことで、アプリケーション全体の状態(State といいます)を一元管理(複数の種類のデータや情報を一か所にまとめ、出し入れしやすいような管理)することができます。 では状態とはなんでしょうか。 アプリケーションにおける状態の例として、ユーザのログイン状態・カート内商品の個数などがあげられます。みなさんが普段使っているアプリケーションでは様々な状態が管理されています。 この状態を管理しやすくしてくれるのが、「Redux」というわけです。 Redux を使うと何が嬉しいのか さて、React で開発する全てのアプリケーションは、「コンポーネント」と呼ばれる部品で構成されています。コンポーネントを使うことで、UI を独立した再利用可能な部分に分解し、各部分を個別に考えることができます。下記はある画面の例です。 まず、すべてのコンポーネント(部品)を囲む「App」コンポーネントがあります。 そして、ナビゲーションバーには「Navbar」というコンポーネントの中に「NavItem」というコンポーネントが複数あります。 また、ユーザのプロフィールを表示する「Profile」コンポーネントの中に、「Avatar」コンポーネント・「Name」コンポーネントがあり、記事などの投稿を表示するために、「Content」コンポーネントの中に、「Post」コンポーネントが複数あります。 このように、React のアプリは「コンポーネント」と呼ばれる部品で構成されているのです。 もう少し抽象的な話をしましょう。 React アプリのコンポーネント間の関係を模式的に表すと下図のようになります。 図に示すように、コンポーネント間は Tree 構造で結ばれています。つまり、App コンポーネントという親は、A コンポーネントと B コンポーネントという子を持っています。さらに、A コンポーネントは C コンポーネントと D コンポーネントという子を持ち、C コンポーネントは G コンポーネントという子を持ちます。 App コンポーネントからすると、G コンポーネントはひ孫になります。 では、この Tree 構造を形成しているコンポーネント間においてデータを受け渡すにはどうすればいいでしょうか。 今、D コンポーネントと I コンポーネントでユーザがログインしているか否かを知りたいという状況だとしましょう。ユーザのログイン状態によって表示を切り替えるというのはよくあることです。 アプリケーションの状態は最上位に位置する App コンポーネントで保持されています。したがって、D コンポーネントと I コンポーネントに状態を受け渡すためには、バケツリレーのように他のコンポーネントを介してデータを渡す必要があります(下図)。これは非常に面倒です。コンポーネントが少ないうちはいいですが、コンポーネントが増えるにしたがって、このバケツリレーは大変になってしまいます。大変ということはバグの温床になってしまうということです。これはよろしくありません。 そこで登場したのが Redux です。Reduxを利用すればどのコンポーネントからも同じ方法で共有したデータ(Storeといいます)にアクセスすることが可能になります。(下図) このように Redux を使うことで、全てのコンポーネントは Store という場所からダイレクトに状態を取得することができます。 Redux のフローのイメージ Redux はデータを一元管理することにより、全コンポーネントからデータをダイレクトに取得することを可能にします。このような大きなメリットを有する Redux ですが、デメリットもあります。それは、Redux は少々難しいということです。 Redux を使ったデータ更新のフローを理解し、使えるようになるには、分かりやすい説明が不可欠です。本記事が分かりやすい説明の一つになることを祈っています(笑)。それでは、いよいよ Redux のフローを説明していきます。 突然ですが、ここからしばらくあなたは執事を持つほどのお坊ちゃまです。そして、今銀行にお金を預けようとしています。銀行の残高は簡単に見ることができますが、お金の預入・引き出しは簡単ではありません。(今やこれらは ATM やスマホなどで簡単に行うことができますが、この記事の中だけそのような文明はないものとします。) まず、あなたは銀行の口座の残高が 0 ドルであることを知ります。そこで、「10 ドル預金したい」というと、執事はその命を受けて 取引:預入 金額:$10 という札(ふだ)を作ります。 次に執事が口笛を吹くと、フクロウが飛んできて先ほど作った札を銀行まで持っていってくれます。 銀行に札が届くと、銀行員は札の内容にしたがって 10 ドルを口座に預け入れます。すると、銀行の残高が 0 ドルから 10 ドルになります。 こうして無事、あなたは銀行の残高を増やすことができました。 上記を図解すると下記のようになります。 以上のようなフローが理解できましたら、後はフローの中で登場したものに名前をつけるだけです。 銀行は Redux の世界では「Store」と呼びます。この Store(銀行) に、残高が記録されています。残高は Redux の世界で「State」と呼びます。 「State」とは「状態」を意味します。 アプリケーションの中で Redux を使うと Store が、アプリケーションの状態(State)を管理します。 執事が作った札のことを Redux の世界では、「Action」とよびます。「Action」は、データをどのように変更するのかという情報を持つオブジェクトです。そのため、Action には何をするのかという情報が必須となります。 オブジェクトというのは以下のようなものです。キーと値がセットになっていますね。 { type: 'DEPOSIT', payload: 10 } Action はオブジェクトという単なるデータですので、Action が何かの処理を行うことはありません。(これは重要です。) 次に、データをどのように変更するかという情報を持つ Action を銀行に届けなくてはなりません。先ほどはフクロウに Action を運んでもらいました。このように Action を運ぶことを「Dispatch」とよびます。Dispatch は Action を銀行へ送るので、単なる情報ではありません。Action というオブジェクトに対する操作ですので、Dispatch はメソッドとなります。 メソッドとは「オブジェクトに対する操作」と定義されています。 いよいよ銀行に Action が Dispatch されてきます。すると、銀行にいる銀行員が Action という札を受け取ります。Action には、データをどのように変更するかという情報が書かれているので、それをもとに現在の残高(State)を変更します。ここで、必ず現在の残高(State)をもとに変更していることに注意してください。現在の State を参照しないと、最終的な残高を導くことができません。 こうした流れの後、もう一度 Store の中の State を見ると、State が更新されているというわけです。 この銀行にいる銀行員を Redux を「Reducer」と呼びます。Reducer は唯一銀行口座の残高(State)を変更できるすごい人です。つまり、Reducer は State を変更できる唯一の存在です。(非常に重要です) ではここで一旦整理しましょう。銀行にお金を預けるという例え話と Redux の対応関係は以下のようになります。 例え話の世界 Redux の世界 銀行 Store 残高 State 執事が作った札 Action フクロウが銀行に札を銀行に届けること Dispatch  銀行員 Reducer ちなみに、お坊ちゃんは銀行を利用する人ですので、ITの世界ではユーザとなります。ユーザという言葉は Redux では登場しませんので、除外しました。 ここまでくればもう Redux の世界のことばで Redux のフローを説明することができるはずです! ユーザが何かしらの決定を下すと、 1. Action が作成される。 2. 作成された Action が Dispatch される。 3. Reducer によって、Dispatch された Action と現在の State をもとに State を更新する。 4. 画面が切り替わる という流れでデータが更新されます。 いかがでしょうか。 これで「Redux」のデータの流れの理解は完了です。 この章の最後に、Redux の公式サイトに掲載されている Redux のフローを紹介します。やっていることは、全く同じです。Redux の図解のなかでこれが最もわかりやすいと思います。さすが公式ドキュメントですね。 ここまで読んでいただければ、上記の図が何を意味しているかが理解できると思います。わからないようでしたら、もう一度この章を読んでみてください。必ず理解できるようになります。 第2章 React & Redux を用いて Todo アプリを作ろう 本章では、第1章で学んだ Redux と、JavaScript のライブラリである React を用いて Todo アプリを作成します。これから作成するアプリは、Redux が公開している「todos アプリ」の一部機能を削除したものになります。完全版のアプリの挙動およびコードを見たい方は下記のリンクよりご覧ください。 また、今回作成するアプリのコードは下記からご覧いただけます。 では、早速アプリを作成していきましょう。まずは、下記のコマンドを打ってください。 $ npx create-react-app todo_app_with_redux $ cd todo_app_with_redux $ npm run start そうしましたら、最初に不要なファイルの削除を行います。 App.css、App.test.js、index.css、logo.svg、reportWebVitals.js、setupTests.jsを削除します。 それに伴い、App.js、index.jsを次のように編集します。 App.js function App() { return ( <div> Hello World </div> ); } export default App; index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); http://localhost:3000 にアクセスして「Hello World」が表示されればOKです。 では次に redux と react-redux をインストールしましょう。 redux と react-redux について redux ライブラリは、React アプリ以外でも使用できます。(例えば Vue、Angular、そしてバックエンドの Node/Express アプリでも動作します。) redux という名前の先頭2文字につられて、react のライブラリと勘違いしがちですが、redux は React のことを全く知りません。 したがって、React において redux を使用するときには、redux だけでなく react-redux をインストールする必要があります。 $ npm i redux react-redux インストールが完了したら、srcディレクトリに移動したのち下記コマンドを実行して、componentsフォルダを作成します。 $ mkdir components そのまま、srcディレクトリ上で下記コマンドを実行して、App.js を componentsフォルダ内に移動させます。 $ mv App.js components App.jsの位置が変更されたので、index.js内の記述も更新します。 - import App from './App'; + import App from './components/App'; Action の作成 今回は、2つの Action を作成します。このアプリ内では、どんなアクションを作成するのかが決まっているため、あらかじめ Action を作っておきます。 src/actions/index.js // Todoを追加するときに使うAction let nextTodoId = 0 export const addTodo = text => ({ type: 'ADD_TODO', // Todoを追加するたびにidは1ずつ増やすようにします。 id: nextTodoId++, text }) // Todoの完了・未完了を操作するときに使うAction export const toggleTodo = id => ({ type: 'TOGGLE_TODO', id }) 先ほどお伝えしたように、Action には、typeというキーが必ず存在します。これは、「Action」は、データをどのように変更するのかという情報を持つオブジェクトだからです。そのため、Action には何をするのかという情報、つまり type が必須となります。 これで、Action の定義は終了です。ユーザの操作によってこのいずれかの Action を Dispatch して Reducer に渡し、状態(=State)を変更します。 Reducerの作成 では次に、Dispatch される Action の情報をもとにデータを変更する Reducer を作成していきます。 src/reducers/index.js // まず、Reducer を定義します。 // 引数には、state の初期値と Dispatch される Action を設定します。 const todosReducer = (state = [], action) => { // そして、Action の type によってデータをどのように変更するのかを switch 文を用いて記述します。 switch (action.type) { // Action の type が ADD_TODO のとき、 // 現在の todo に追加する形で state を更新します。 // completed はデフォルトではfalse(未完了)としておく。 case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, completed: false } ] // Action の type が TOGGLE_TODO のとき、 // ある一つの todo の completed を反転させて上書きします。 // ある一つの todo は id をもとにして探します。 case 'TOGGLE_TODO': return state.map(todo => (todo.id === action.id) ? {...todo, completed: !todo.completed} : todo ) default: return state } } export default todosReducer 上記のコードに登場する ...state や ...todo は、スプレッド構文と呼ばれています。スプレッド構文を用いることで、配列やオブジェクトを展開したり、配列同士・オブジェクト同士を結合することができます。(オブジェクト同士の結合の際、同じ名前のプロパティが存在していた場合は上書きが実行されます。) はい、これで Reducer の定義が完了しました。 Store の作成 ではいよいよ、どのコンポーネントからもダイレクトにアクセス可能な Store を作成していきましょう。 index.jsを編集していきます。 まず以下の3つを新たに import してください。 index.js // Storeを作成するもの import { createStore } from 'redux' // Storeを提供するもの import { Provider } from 'react-redux' // 定義したReducer import todosReducer from './reducers' そして、「createStore」を使って store を作成します。 index.js // 引数には Reducer と、Redux DevTools を利用可能にするためのものを渡します。 const store = createStore( todosReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); 最後に、App コンポーネントを Provider で囲ってあげましょう。store を渡すことに注意してください。 index.js ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); 最終的に、index.jsは以下のようになります。 index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; import { createStore } from 'redux' import { Provider } from 'react-redux' import todosReducer from './reducers'; const store = createStore( todosReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); Redux Devtools について 先ほど、Redux DevTools が登場しました。Redux DevTools とは Redux アプリの開発ツールの一つで、ブラウザの拡張機能から Redux の状態管理を可視化してくれます。Redux を使ってアプリ開発しているときに必須のデバックツールです。 現在の State の状態、Action の実行履歴などを見ることができます。詳細は下記URLからご覧ください。 それでは、 $ npm run start を実行して、http://localhost:3000にアクセスしてみてください。デベロッパーツールを開き、Redux DevToolsが起動していればOKです。 Todo 投稿機能の作成 Action・Reducer・Store が作成し終わったので、次に Todo の投稿機能を作成していきます。 Todo を投稿するときのフローは以下の通りです。 1. ユーザが投稿ボタンを押す。 2. addTodo というアクションを Dispatch の引数に渡す。 3. Dispatch メソッドを実行する。 4. Reducer に実行したい Action が伝わる。 5. Store の中 State が更新される。 6. State が更新されると再描画され、表示が切り替わる。 では上記のフローを意識しながら、AddTodo.js を作成していきます。 srcフォルダの中に、containerフォルダを作って、containerフォルダ内にAddTodo.jsを作成してください。 AddTodo.jsの中身を書いていきます。 src/containers/AddTodo.js import React from 'react' // 「connect」は、component 内で dispatch を使えるようにするために必要です。 import { connect } from 'react-redux' import { addTodo } from '../actions' const AddTodo = ({ dispatch }) => { let input return ( <div> <form onSubmit={e => { // ページのリロードを防ぎます。 e.preventDefault() // 入力された文字列が空だった場合にはこれより先のコードは実行されません。 if (!input.value.trim()) { return } // dispatch メソッド実行します。 dispatch(addTodo(input.value)) // 投稿ボタンを押した後に、テキストボックスの中身を空にします。 input.value = '' }}> <input ref={node => input = node} /> <button type="submit"> Add Todo </button> </form> </div> ) } // ここで、connectを使用します。 export default connect()(AddTodo) 【補足:connect() について】 コンポーネント内で Redux の Store や Dispatch を使用するためには、connect()を使って、コンポーネントと Redux を接続(connect)してあげる必要があります。後ほど説明しますが、TodoList コンポーネントでは、現在の状態(=State)を参照する・Dispatch メソッドを実行するということが必要になります。そこで、TodoList containerを作成し、TodoList component 内で State の参照・dispatch メソッドの実行ができるようにします。 connect() は難しいと思いますので、とりあえず「コンポーネント内で State の参照・Dispatch メソッドの実行ができるようにするために connect() が必要」ということだけ押さえておいてください。 AddTodo.jsが完成したので、App.jsに import していきましょう。 src/components/App.js import AddTodo from '../containers/AddTodo' function App() { return ( <div> // Appコンポーネント内にAddTodoコンポーネントを描画します。 <AddTodo /> </div> ); } export default App; では、http://localhost:3000にアクセスしてみてください。テキストフィールドと「Add Todo」というボタンが出現すればOKです。 実は、もう Redux を使った Todo の投稿機能は完成しています。試しに、テキスフィールドに文字列を入力して、「Add Todo」ボタンを押してみてください。すると、State が更新されます。 テキストフィールドに「Hello」・「Apple」と入力すると、State は下記のようになります。 id・text・completed というキーがありますね。 completed は、デフォルトで false が入るようになっています。 あとは、これらのデータを描画してあげるだけです。 もう一息なので頑張りましょう! Todo 一覧の表示 最後に Todo の一覧表示を実装していきます。 Todo を一覧表示させる際には、 1. state からのデータ取得 2. Todo の完了・未完了を更新するメソッド(=Dispatch) が必要となります。 先ほどの補足でも説明しましたが、コンポーネント内で state や Dispatch を使えるようにするために、connect() を利用しましょう。 それでは、TodoList Container と TodoList Component を作成していきます。 containersフォルダ内にTodoList.jsを、componentsフォルダ内にTodoList.jsをそれぞれ作成してください。 まず、TodoList Container から記述していきます。 src/containers/TodoList.js import { connect } from 'react-redux' // Actionとして定義した、toggleTodoをimportします。 import { toggleTodo } from '../actions' // stateとdispatchメソッドを渡す先であるコンポーネントをimportします。 import TodoList from '../components/TodoList' // これは、stateをコンポーネントに渡すための準備です。 // 「todos」という名前でstateを渡します。 const mapStateToProps = state => ({ todos: state }) // これは、Dispatchメソッドをコンポーネントに渡すための準備です。 // toggleTodo という関数を渡します。 // この関数は、Todo の id のみの情報を必要としています。 const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) }) // 上で定義した todos と toggleTodo を TodoList コンポーネントに渡しています。 export default connect( mapStateToProps, mapDispatchToProps )(TodoList) これで、TodoList コンポーネントに state と Dispatch メソッドを渡す準備が整いました。では、TodoList コンポーネントを作成していきます。 src/components/TodoList.js // 型定義機能を提供する PropTypes を import します。 import PropTypes from 'prop-types' // TodoList Component には、TodoList Container で定義した // todos と toggleTodo が渡されています。 const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => <li>{todo.text}</li> )} </ul> ) // 下記では、TodoList Component に渡される引数(propsといいます)について // 「データ型」・「必須か否か」を定義しています。 // データ型は記述の通りで、全ての値を必須としています。 TodoList.propTypes = { todos: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired).isRequired, toggleTodo: PropTypes.func.isRequired } export default TodoList TodoList.js が完成したので、App.jsに import していきましょう。 src/components/App.js import AddTodo from '../containers/AddTodo' // 下記を追加 import TodoList from '../containers/TodoList' function App() { return ( <div> <AddTodo /> // 下記を追加 <TodoList /> </div> ); } export default App; ここまで記述できましたら、http://localhost:3000にアクセスしてみてください。 テキストフィールドに文字列を入力して「Add Todo」ボタンを押すと、Todo が表示されたらOKです。 ついに最後のファイルとなりました。 Todo Component を作成して Todo アプリが完成となります。 componentsフォルダ内にTodo.jsを作成してください。このファイルでは、Todo 一つ一つを描画する Todo Component を定義していきます。 src/components/Todo.js import PropTypes from 'prop-types' // Todo Component では props として、「onClick」・「completed」・「text」が渡されます。 const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} // completed が True だった場合、取り消し線を適用します。 style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) // TodoList Component と同様にデータ型の定義をしました。 Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default Todo Todo.js が完成したので、components/TodoList.jsに import しましょう。 src/components/TodoList.js import PropTypes from 'prop-types' // 下記を追加 import Todo from './Todo' const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => // ここで Todo コンポーネントを描画します。 // onClick には、toggleTodo(引数は todo の id)を渡します。 <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> )} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired).isRequired, toggleTodo: PropTypes.func.isRequired } export default TodoList ここまで記述できましたら、http://localhost:3000にアクセスしてみてください。 テキストフィールドに文字列を入力して「Add Todo」ボタンを押すと Todo が表示され、さらに Todo をクリックすると取り消し線が出現しましたら OK です。 Todo をクリックすると completed の値が true や false に切り替わります。 また、どんな Action が Dispatch されたのかが時系列順で並んでいます。 これでめでたく Todo アプリが完成となります。 第3章 まとめ お疲れ様でした。最後にもう一度、Redux のフローを復習しておきます。 ユーザが何かしらの決定を下すと、 1. Action が作成される。 2. 作成された Action が Dispatch される。 3. Reducer によって、Dispatch された Action と現在の State をもとに State を更新する。 4. 画面が切り替わる という流れでデータが更新されます。 Action は単なる情報であり、State を更新するのはあくまでも Reducer であることが非常に重要です。 もしもフローが分からなくなった場合には、お坊ちゃまになって銀行にお金を預けてみてください。 最後まで読んでいただき有難うございます。 ここまで読んで頂ければ、Redux の基礎は完成しています。 もしも誤植等ありましたらコメントしていただけると幸いです。 それでは、Merry Christmas and a Happy New Year
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Docker】Rails6 API + React(TypeScript)のSPA開発環境を構築する

はじめに 自分用のメモとして記録しておきます。 1. ディレクトリ構成の確認と必要ファイルの用意 以下のディレクトリ構成になるようにファイルを用意する。 $ mkdir backend $ mkdir frontend $ touch docker-compose.yml $ touch backend/Gemfile $ touch backend/Gemfile.lock $ touch backend/entrypoint.sh $ touch backend/Dockerfile $ touch frontend/Dockerfile 2. docker-compose.ymlの編集 docker-compose.yml version: "3" services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password command: --default-authentication-plugin=mysql_native_password volumes: - mysql-data:/var/lib/mysql - /tmp/dockerdir:/etc/mysql/conf.d/ ports: - 3306:3306 api: build: context: ./backend/ dockerfile: Dockerfile command: /bin/sh -c "rm -f /myapp/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" image: rails:dev volumes: - ./backend:/myapp - ./backend/vendor/bundle:/myapp/vendor/bundle environment: TZ: Asia/Tokyo RAILS_ENV: development ports: - 3001:3000 depends_on: - db volumes: mysql-data: 3. API側の環境構築 3-1. 各ファイルの編集 backend/Dockerfile FROM ruby:3.0 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs ENV APP_PATH /myapp RUN mkdir $APP_PATH WORKDIR $APP_PATH COPY Gemfile $APP_PATH/Gemfile COPY Gemfile.lock $APP_PATH/Gemfile.lock RUN bundle install COPY . $APP_PATH # Add a script to be executed every time the container starts. COPY entrypoint.sh /usr/bin/ RUN chmod +x /usr/bin/entrypoint.sh ENTRYPOINT ["entrypoint.sh"] EXPOSE 3000 # Start the main process. CMD ["rails", "server", "-b", "0.0.0.0"] backend/Gemfile source 'https://rubygems.org' gem 'rails', '>= 6.1.4.1' backend/Gemfile.lock # 空のまま backend/entrypoint.sh #!/bin/bash set -e # Remove a potentially pre-existing server.pid for Rails. rm -f /myapp/tmp/pids/server.pid # Then exec the container's main process (what's set as CMD in the Dockerfile). exec "$@" 3-2. Railsアプリケーションの作成 $ docker-compose run --no-deps api rails new . --force -d mysql --api オプションについて docker-compose run に対するオプション --no-deps: リンクしたサービス(今回であればdepends_onで指定したdb)を起動しない rails new に対するオプション --force(-f): ファイルが既に存在している場合は上書きする -d(--database): データベースの種類 --api: apiモードでアプリケーションを作成 3-3. build rails newでGemfileが更新されたのでdocker-compose buildを実行する $ docker-compose build 3-4. database.ymlの編集 backend/config/database.yml default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # デフォルトだと空欄になっているので変更 host: db # デフォルトだとlocalhostになっているので変更 development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: <<: *default database: <%= ENV["DATABASE_NAME"] %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOST"] %> 3-5. データベースの作成 データベースを作成しないと以下のようにデータベースがないと怒られます。 $ docker-compose run api rails db:create ー API側の環境構築は以上になります。 4. Front側の環境構築 4-1. docker-compose.ymlにfront側の処理を追加 docker-compose.yml version: "3" services: db: ... api: ... # ↓ 追加 front: build: context: ./frontend/ dockerfile: Dockerfile volumes: - ./frontend:/usr/src/app command: sh -c "cd app && yarn && yarn start" ports: - "4000:3000" volumes: mysql-data: 4-2. Dockerfileの編集 frontend/Dockerfile FROM node:14.18-alpine WORKDIR /usr/src/app 4-3. build $ docker-compose build 4-4. アプリケーション作成 アプリケーション名はappとする。 TypeScriptを使いたので最後に--template typescript を追加する。 nodeのバージョンによってはESlint等をインストールする際にエラーが発生する可能性があるので、適宜、nodeイメージのバージョンを変更する。 参考: https://hub.docker.com/_/node npmで管理する場合 $ docker-compose run --rm front sh -c "npx create-react-app app --template typescript" yarnで管理する場合(<= 今回はこちら) $ docker-compose run --rm front sh -c "yarn create react-app app --template typescript" ※ yarnはnodeのイメージ(node:14.18-alpine)にデフォルトで入っているらしいのでインストールは不要。 参考: https://github.com/nodejs/docker-node/blob/b695e030ea98f272d843feb98ee1ab62943071b3/14/alpine3.14/Dockerfile 4-5. ディレクトリ構成の確認 以下のように指定したappディレクトリ配下にnode_modules, public, srcディレクトリ、pacage.json、tsconfig.json, yarn.lockファイル等が作成されていることが確認できます。 5. 各コンテナを立ち上げる $ docker-compose up -d 6. 確認 API側 http://localhost:3001 にアクセス Front側 http://localhost:4000 にアクセス ー 無事に初期画面が表示されました。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タスクマネージャーをアップデートした!

前回の記事 作成したタスクマネージャーbot 追加した機能 ・WebViewでタスクの登録 ・通知機能 ・リッチメニュー 使用した技術 フロントエンド ・LINE バックエンド ・Node.js(Express) ・firebase(cloud functions, firestore) ・Messaging API ・dayJs WebView ・LIFF(LINE SDK) ・ReactHooks ・Material-UI ・firebase(Hosting, firestore) システム構成 Cloud Functions + LIFF 1.main関数 : チャットボットとのやりとりを行う 2.register関数 : タスクが登録できたことを知らせる 3.reminder関数 : 指定された時間に通知をする + 4.LIFF : タスクの登録を行う それぞれの関数について main関数 main関数の主な役割は登録したタスクの表示です。 下記シーケンス図はタスク一覧表示の処理の流れです。 main関数はwebhookURL1として設定しています。 つまりユーザーがメッセージを送信すると、main関数にpost(webhookイベントオブジェクト)が送られます。 webhookイベントオブジェクトの中身はこちら 1.webhookイベントオブジェクトを受け取ったmain関数は、メッセージ内容を確認します。    メッセージ内容が「@show」であれば2へ 2.ユーザIDとfirebaseに登録してあるIDと照合し一致するタスク取り出します。 3.取り出したタスクをボタンテンプレート形式でユーザに送信するよう処理を行なっています。 ざっくり説明するとmain関数は1~3の処理を行なっています。 1webhookとはユーザーがメッセージを送信したり、友達追加したりなどイベントが発生すると、指定したURLにpostを投げる仕組み・技術のことです register関数 register関数はタスクが登録された時に登録できたことを通知するプログラムです。 トリガーをonCreateにすることでfirestoreにタスクが登録された時に、処理が走るようになっています。 Node.js exports.register = functions.region("asia-northeast1") .firestore .document('todos/{todoId}') .onCreate((snap, context) => { const userId = snap.data().userId; const todoName = snap.data().name; return Handle.registerMessage(userId, todoName); }); firestoreにタスクが登録されるとそのタスクからユーザIDとタスク名を取得し、ユーザID宛にpushメッセージを送る処理をHandle.registerMessage()で書いています。 remider関数 reminder関数は通知設定されたタスクを時間通りに通知するプログラムです。 トリガーは1分毎に実行されるように書いていて、現在時刻と通知時間が一致するタスクがあるかどうかを毎分照合しています。 一致するタスクがあればそのタスクデータを取り出し、登録したユーザーにpushメッセージを送るコードをHandle.reminder()で書いています。 Node.js exports.reminder = functions.region('asia-northeast1') .pubsub.schedule('every 1 minutes') .timeZone('Asia/Tokyo') .onRun((context) => { return Handle.reminder(); }) LIFF タスクの登録はwebアプリケーションで行なっています。 LIFFとはwebアプリケーションをLINEの中で起動させる仕組みのことで、LINEから離れずに操作することができます。 こんな感じ↓↓↓↓ React.js useEffect(() => { liff.init({liffId: LIFF_ID}) .then(() => { getProfile(); }) .catch((err) => { console.log(err.code, err.message); }) }, []) const getProfile = () => { liff.ready.then( async() => { const context = await liff.getContext() setUid(context.userId); }) } webアプリケーションの中でLINESDKを用いて、liff.init()→liff.getContext()することで ユーザー情報を取得できます。公式ドキュメント 本アプリではタスクにユーザIDをタグ付けすることで、誰のタスクかを管理しています。 まとめ コードによるプログラム解説ではなく、設計段階の解説をしてみました。 こちらのLINEBotはまだまだ改良していくつもりなので、追加機能の案やバグがあればドシドシコメントお願いします!! また、今回の記事で分かりにくいことがあればコメント頂ければ幸いです! 実装予定機能 毎週リマインド機能 スヌーズ機能 参考文献 https://developers.line.biz/ja/docs/messaging-api/ https://dev.classmethod.jp/articles/try-cloud-functions-scheduler-pubsub/ https://qiita.com/hirothings/items/37430b2408a5a7a85972 https://ginpen.com/2019/06/01/firestore-indexes-json/ https://github.com/iamkun/dayjs/blob/dev/docs/en/API-reference.md#list-of-all-available-formats
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む