- 投稿日:2019-12-18T21:17:01+09:00
React + chart.jsで「動く」グラフサンプル
はじめに
こちらのようなリアルタイムに動いていくグラフを作りたかったのですが、Reactのドキュメントがあまり充実していなかったので色々と調べて動くコードを作ったのでメモしておきます。
(そのままでも動きますがリファクタリングしたほうが良いと思います。)※blueCount, redCount, whiteCountの値をなにかのタイミングで変えればグラフも変わります。
(Stateを使用して変数の変化を監視する必要がありません)demo.jsimport React from 'react' import { Bar, Chart } from 'react-chartjs-2' import 'chartjs-plugin-streaming' let blueCount = 5 let redCount = 5 let whiteCount = 5 let RealTimeChart = (props) => { function onRefresh(chart) { chart.config.data.datasets.map((dataset) => { switch (dataset.label) { case 'blue': dataset.data.push({ x: Date.now() - 1, y: blueCount, }) return null case 'white': dataset.data.push({ x: Date.now(), y: whiteCount, }) return null case 'red': dataset.data.push({ x: Date.now() + 1, y: redCount, }) return null default: console.log('default') return null } }) } var color = Chart.helpers.color return ( <div> <Bar height={100} data={{ datasets: [ { label: 'blue', stack: 'blue', backgroundColor: color('#516897') .alpha(1) .rgbString(), data: [], }, { label: 'white', stack: 'white', backgroundColor: color('#B9B9B9') .alpha(1) .rgbString(), data: [], }, { label: 'red', stack: 'red', backgroundColor: color('#AC3A38') .alpha(1) .rgbString(), data: [], }, ], }} barSize={100} options={{ responsive: true, legend: { display: false, }, scales: { xAxes: [ { stacked: true, type: 'realtime', realtime: { duration: 15000, refresh: 3000, delay: 1000, onRefresh: onRefresh, }, gridLines: { color: '#4d4d4d', }, ticks: { display: false, }, }, ], yAxes: [ { stacked: true, gridLines: { color: '#4d4d4d', }, ticks: { min: 0, beginAtZero: true, callback: function(value) { if (value % 1 === 0) { return value } }, }, }, ], }, }} /> </div> ) } export default RealTimeChart
- 投稿日:2019-12-18T20:28:33+09:00
React Context APIを使った非同期通信のハンドリング
本稿はReact Advent Calendar 2019 18日目の記事です!
はじめに
Reactにおける非同期通信のハンドリングどうしていますか?
通信中のローディングアイコンの表示や、エラーハンドリング・・・
正解がわからない?そこで今回はReactのContext APIを使ってハンドリングしてみました!
これが正解だとは思いませんが、一例として共有させていただきますTL;DR
- Redux使わないよ
- Context APIでエラーハンドリングとダイアログコンポーネントの表示やってみたよ
Reduxを使うパターン
よくありがちな、リクエスト毎に成功時と失敗時のアクションを用意するパターン。
Storeにエラー内容を突っ込んで、エラー表示のためのコンポーネントを作ってよしなにやるイメージ。const GET_ITEMS_REQUEST = 'GET_ITEMS_REQUEST'; const GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS'; const GET_ITEMS_FAILURE = 'GET_ITEMS_FAILURE'; const getItemsRequest = () => { ... } const getItemsSuccess = () => { ... } const getItemsFailure = () => { ... } const getItems = () => { return (dispatch) => { dispatch(getItemsRequest); return axios.get(`http://localhost/api/items`) .then(res => dispatch(getItemsSuccess(res.data)) ).catch(err => dispatch(getItemsFailure(err)) ); } } const initialState = { isFetching: false, error: null, ...}; const reducer = (state = initialState, action) { switch (action.type) { case GET_ITEMS_FAILURE: return { ...state, isFetching: true, }; case GET_ITEMS_FAILURE: return { ...state, isFetching: false, error: action.error, }; ... } } // jsx {error && <Dialog>Error!!</Dialog>}なんか冗長でしんどい?
Context APIで実装してみる
Context APIに関しては公式リファレンスをご参照くださいませ?♂️
コンテクスト – React以下、作ったものです!
https://codesandbox.io/s/loving-wiles-tvdjy?fontsize=14&hidenavigation=1&theme=darkindex.js
APIのサンプルとして、QiitaのAPI叩かせてもらっています。
failureRequest内のpostは、tokenがなくて認証エラーになる形です。import React, { useContext } from "react"; import ReactDOM from "react-dom"; import axios from "axios"; import { ApiRequestHandleContext, ApiRequestHandleContextProvider } from "./apiRequestHandleContext"; const successRequest = async params => { return axios.get("https://qiita.com/api/v2/items", { params }); }; const failureRequest = async () => { return axios.post("https://qiita.com/api/v2/items"); }; const App = () => { const { execRequest, isRequesting } = useContext(ApiRequestHandleContext); const handleOnSuccessClick = () => { // APIのレスポンスが返ってくる execRequest(successRequest, { page: 2 }).then(console.log); }; const handleOnFailureClick = () => { execRequest(failureRequest); }; return ( <div className="App"> <button onClick={handleOnSuccessClick}>Success Button</button> <button onClick={handleOnFailureClick}>Failure Button</button> {isRequesting && <div>Now Requesting!!</div>} </div> ); }; const rootElement = document.getElementById("root"); ReactDOM.render( <ApiRequestHandleContextProvider> <App /> </ApiRequestHandleContextProvider>, rootElement );apiRequestHandleContext.js
import React, { useState, createContext } from "react"; import Dialog from "./Dialog"; export const ApiRequestHandleContext = createContext({ execRequest: () => {}, isRequesting: false }); export const ApiRequestHandleContextProvider = props => { const [isRequesting, setIsRequesting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const handleError = error => { setErrorMessage(error.response.data.message); }; const execRequest = async (requestFn, ...args) => { setIsRequesting(true); const res = await requestFn(...args).catch(handleError); setIsRequesting(false); return res; }; return ( <ApiRequestHandleContext.Provider value={{ isRequesting, execRequest }} > {errorMessage && <Dialog>{errorMessage}</Dialog>} {props.children} </ApiRequestHandleContext.Provider> ); };解説
apiRequestHandleContextのexecRequestがポイント
const execRequest = async (requestFn, ...args) => { setIsRequesting(true); const res = await requestFn(...args).catch(handleError); setIsRequesting(false); return res; };
requestFn
が実行する非同期通信処理で、可変長引数...args
をパラメータとして渡します。非同期通信でエラーがあった場合、catchに渡されている
handleError
が実行され、エラーレスポンス内のmessageをDialogとして表示という仕組みになっています。また、リクエストの前後でuseStateを利用してリクエスト中かのフラグ
isRequesting
をハンドリング。
このisRequestingはcontextとして提供されているので、リクエスト中はindex.js側で「Now Requesting!!」というテキストを表示しています。あとは
ApiRequestHandleContextProvider
でラップしてあげれば?♂️const { execRequest, isRequesting } = useContext(ApiRequestHandleContext); const handleOnSuccessClick = () => { // execute someAsyncFunction(param1, param2, param3); execRequest(someAsyncFunction, param1, param2, param3}).then(res => { console.log(res)}); };注意
今回の実装だと、並列で複数のリクエストが呼ばれた際にリクエスト状態は1つのisRequestingを参照しているので、実際はまだ終了していないリクエストがある場合もisRequestingはfalseになってしまいます。
コードが複雑になるのを避けたかったので今回は実装していませんが、リクエスト毎にユニークキーを振って、それぞれのリクエストの状態を1つずつ管理するような実装もしたりしました
![]()
おわりに
- ロジックをview側に寄せる形になるので抵抗ある人はあるかも・・・
- でもReducerを肥大化させるのも辛い?
- たぶん色々なハンドリングパターンがあると思うので、もっと色々調べてみたい
- Hooksは偉い!?
- 投稿日:2019-12-18T20:01:13+09:00
Hover.cssをReact上で使って、要素をブルンブルン動かしたい!
はじめに
タイトル通り、Reactで生成した要素にホバーアクションを実装してブルンブルン動かしたくなったのでやってみました。
開発環境
VsCode
npm 6.12.1セットアップ
Hover.cssのインストール
何パターンか方法があるらしいですが、今回はGithubからソースファイルを引っ張ってきてローカルに保存する方法でやってきます。
https://github.com/IanLunn/Hover
上記サイトのcssフォルダ配下にあるhover-min.css
をダウンロードして、ローカルの適当なフォルダに配置します。hover-min.cssを上位のスタイルファイルでインポート
プロジェクトの上位のスタイルファイルで、保存した
hover-min.css
をimportします。@import url("hover-min.css");classをつける
これでHover.cssを使える状態になったので、ブルンブルンさせたい要素に対してクラスをつけてあげます。
<div className="hvr-grow">動作確認
終わりに
楽ちんですね。
- 投稿日:2019-12-18T17:38:18+09:00
<day2>Webアプリ完成するまで続ける開発日誌
こんにちは!山形大学のもえとです!
day1と同じ日ですがday2をリアルタイムで勉強しながら書いていきます。
day1:https://qiita.com/se_n_pu_u_ki/items/e92bfa9bdadc1316d2f1
今日から早速Reactの基本的な概念とか使い方を学んでいきます。
今日学ぶのは
・JSX = Reactの文法
・コンポーネント = Reactの基本
・state = Webサイトの動きを作る
・props = JSXのパラメータ
・input要素 =入力テキストボックスざっくりこんな感じ。
盛りだくさんだけどReactの基本なので頑張ろう!ReactのJSXについて
JSXとは、HTMLとJacascriptごちゃ混ぜにできる文法です。
< day1 >で書いた
client.js
の文法でこんなのがありました。src/js/client.jsrender() { return ( <h1>It works!</h1> ); }return まではJavascriptの文法ですが < h1 > ってHTMLですよね。
webpackでコンパイルすると
client.js
はsrc/js/client.jsreturn React.createElement( "h1", null, "Welcome!" );このように変換される(HTML→Javascript)ことで読み込んでいるんだそうです。
コンパイルが優秀なおかげで楽に書くことができるのがJSX。
ちなみにHTML→Javascript変換してくれてるのは「Babel」と呼ばれるツール。
JSXで変数
JSXの中にJavascriptで定義した関数を使いたい時は
{変数名}
のように記載すると埋め込むことができます。
やってみましょうsrc/js/clinent.jsimport React from "react"; import ReactDOM from "react-dom"; class Layout extends React.Component { render() { let name = "Moeto"; return ( <h1> It's {name}!</h1> ); } } const app = document.getElementById('app'); ReactDOM.render(<Layout/>,app);name変数は"Moeto"が入っていたため画面には
It's Moeto
と表示されます
Reactのコンポーネントについて
Reactはコンポーネントというものを定義していき、それを組み合わせることで画面を作っていきます。いくつかのファイルに分けることができるため保守性、再利用性が高いのです。
実際に前のclient.jsにある
Layoutクラス
をsrc/js/componetnts.Layout.js`に移動してみましょう。まずはファイルとディレクトリ作成
$ mkdir -p ./src/js/components $ touch src/js/components/Layout.jsそして
Layout.js
を以下のように書き込み。src/js/components/Layout.jsimport React from "react"; export default class Layout extends React.Component{ constructor() { super(); this.name = "Moeto"; } render() { return ( <h1>It's {this.name}!</h1> ); } }export defaultはJavascriptの文法で他のファイルに読み込ませるためのもので、Layoutクラスを外部アクセス可能にします。
そして
client.js
にも変更を加えます。さっきのLayoutクラス
は消し、import構文をつかってLayout.js
にあるLayoutクラス
を読み込ませます。
次のように変更してください。src/js/client.jsimport React from "react"; import ReactDOM from "react-dom"; import Layout from "./components/Layout"; const app = document.getElementById('app'); ReactDOM.render(<Layout/>,app);行数がかなり減りましたね。class Layoutと書いていたところをimport Layoutにすることで分かりやすく変更を加えることができます。
実際にここで
http://localhost:8080
をみてみてもIt's Moeto
などと表示されているかと思います。Header、Footerコンポーネント
ページのHeader、Footerも別コンポーネントにしておくと便利ですね。
作成しましょう$ touch ./src/js/components/Header.js $ touch ./src/js/components/Feader.js作成した
Header.js
はこのように書き込み。src/js/components/Header.jsimport React form "react"; export default class Header extends React.Component{ render() { return ( <div>header</div> ); } }
Footer.js
にも書き込み。src/js/components/Footer.jsimport React from "react"; export default class Footer extends React.Component { render() { return ( <footer>footer</footer> ); } }キーボードで打っている方はなんとなく似た文法がわかってきたかもしれません。コピペを使わずにキーボードで打っているだけでも少しずつReact文法が把握できます。
Header.jsとFooter.jsができたのでLayout.jsに読み込ませましょう。
src/js/components/Layout.jsimport React from "react"; import Header from "./Header"; export default class Layout extends React.Component { render() { return( <div> <Header /> <Footer /> <div> ); } }これでブラウザを確認すると、
header
footerと表示されていれば成功です!
コンポーネントのお話終了。
stateについて
stateはアプリケーションの状態を保持するもので、コンポーネントをどのようにレンダリング(表示)するのかという情報を格納する場所のこと。
まぁまずは使ってみましょう。
これやると動きをつけることができます!
Layout.js
を以下のように変更。src/js/components/Layout.jsimport React from "react"; import Header from "./Header" import Footer from "./Footer"; export default class Layout extends React.Component{ constructor(){ super(); this.state = {name:"Moeto"}; } render(){ setTimeout( () => { this.setState({name:"Hello"});} ,1000); return( <div> {this.state.name} <Header /> <Footer /> </div> ); } }ブラウザを再読み込みしてみましょう。
一番上の行が最初は"Moeto"などです。
5秒待ってみると"Hello"に変わりましたね!setTimeoutの下にある数字を変えてみると変更までの待機時間が変わることが分かります。
なるほど、このように動きを付け加えられるのがstateなんですね。
Propsについて
ひとつのコンポーネントに対し、一部だけ別な色にしたい、などを可能にするのがProps。HTMLではタグ要素に対してパラメータ(classなど)を持たせてそのパラメータをCSSで変更することで一部の色を変えたりイベントを登録したりできます。JSXでもPropsをつかうことで同じようにパラメータを渡すことができます。
Propsをつかって書き換えてみましょう。
まずLayout.jsを以下のように書き換えます。
src/js/components/Layout.jsimport React from "react"; import Header from "./Header" import Footer from "./Footer"; export default class Layout extends React.Component{ render(){ const title = "Welcome Moeto!"; return( <div> <Header title={title}/> <Footer /> </div> ); } }
title={title}
という部分の書き方がPropsですね。
続いてHeader.js
を書き換えていきます。src/js/components/Header.jsimport React from "react"; import Title from "./Header/Title" export default class Header extends React.Component{ render(){ console.log(this.props); return ( <div> <Title /> </div> ); } }Layout.js殻渡されたPropsは
this.props
でアクセスすることができます。ブラウザの開発者ウィンドウのコンソールに
{title: "Welcome Moeto!"}
などど書かれていたら成功!
※開発者ウィンドウは、chromeの場合はF12で開くことができます。
また、Headerコンポーネントを複数作成して異なるPropsを渡すことで異なるパラメータを持ったHeaderを呼び出すことができます。
src/js/components/Layout.jsimport React from "react"; import Header from "./Header" import Footer from "./Footer"; export default class Layout extends React.Component{ render(){ const title = "Welcome Moeto!"; return( <div> <Header title={title}/> <Header title={"Thank You!"}/> <Footer /> </div> ); } }HeaderコンポーネントからTitleコンポーネントへPropsを渡してみましょう。
src/js/components/Header.jsimport React from "react"; import Title from "./Header/Title" export default class Header extends React.Component{ render(){ console.log(this.props); return ( <div> <Title title={this.props.title}/> </div> ); } }src/js/components/Title.jsimport React from "react"; export default class Title extends React.Component{ render(){ return ( <h1>{this.props.title}</h1> ) } }これでブラウザを見てみると
Welcome Moeto!
Thank you!
footerと書かれていますね!
input要素
input要素を追加するとブラウザで表示した人にデータ入力を要求し、データを受け取ります。
Header.js
にinput要素を追加。src/js/components/Header.jsimport React from "react"; import Title from "./Header/Title" export default class Header extends React.Component{ render(){ console.log(this.props); return ( <div> <Title title={this.props.title}/> <input /> </div> ); } }はい、これでブラウザ見てみると
なんか空白のテキストボックスができており、文字を入力できるのが分かります。
今はinput要素を付けただけなので入力しても何も起きません。ではこれから入力フォームに入ったテキストを取得してタイトルを表示する処理をだんだんと追加していきます。
inputの中身を取得
Layout.js
にchageTitleメソッド
を作成し、changeTitleメソッド
をHeaderコンポーネント
へ渡すようにしてみましょう、
と書かれていますね。カタカナアレルギーの人はここで挫折しそう笑
一旦ここでは写経することにしましょう!
こういうのは成果物ができた後に意味がわかるってもんですsrc/js/components/Layout.jsimport React from "react"; import Header from "./Header" import Footer from "./Footer"; export default class Layout extends React.Component{ constructor() { super(); this.state = {title:"Welcome"}; } changeTitle(title){ this.setState({title}); } render(){ return( <div> <Header changeTitle={this.changeTitle.bind(this)} title={this.state.title} /> <Footer /> </div> ); } }さっき説明を飛ばしましたが、ReactのPropsに関数(=メソッド)を指定することも可能です。
今回のchangeTitle Props
にはchangeTitleメソッド
を呼び出していますね。
ん、よくみてみると
this.changeTitle.bind(this)
bind?誰?これは関数のスコープの問題らしいです。
これってLayout.jsからHeader.jsに渡してるじゃないですか。
これbindなしでthis.changeTitle(this)
でもHeader.js内で呼び出せるんだけどLayout.jsでのchangeTitleとはまた別な関数になってしまうんだそうです。
そーすると
changeTitle(title){
this.setState({title});
}
のthisがLayoutではなくなってしまい予想外の動作をする可能性がある。
bindを使うことで確実にLayoutからHeaderに渡すことができているんですね!bindの謎が解けたところで
Header.js
の編集。src/js/components/Header.jsimport React from "react"; import Title from "./Header/Title" export default class Header extends React.Component{ handleChange(e){ const title = e.target.value; this.props.changeTitle(title); } render(){ console.log(this.props); return ( <div> <Title title={this.props.title}/> <input value={this.props.title} onChange={this.handleChange.bind(this)}/> </div> ); } }ここまで書いたらテキストボックスになにか入力してみましょう!
入力したその場でタイトルが書き変わりますね!
(2019/12/18作成。)
以降の日誌
day3作成中...12/18
- 投稿日:2019-12-18T15:51:33+09:00
tailwndcss+React でカードデザインのポートレートサイトを作ってみる
はじめに
Reactでカードを並べたポートレートサイトを作ってみたかったので、tailwindcssを用いて実装してみます。
イメージこんな感じ。
https://scrapbox.io/help-jp/開発環境
VsCode
npm 6.12.1tailwindcss
react-router-domセットアップ
必要なライブラリがインストールされている前提で進めていきます。
コンポーネント設計
index.js --App.js ----HomePage.js ------ContentCard.js //カードコンポーネント --------SNSSample.js //カードから遷移する先のコンポーネント --------ShopSample.js //カードから遷移する先のコンポーネントカードから遷移する先のコンポーネントを作成
先に遷移先のコンポーネントを作っておきましょう。
ShopPage.jsimport React from "react"; export function ShopPage() { return ( <div className="ShopPage"> ShopPage is working! </div> ); }SNSPage.jsimport React from "react"; export function SNSPage() { return ( <div className="SNSPage"> SNSPage is working! </div> ); }App.jsでのルーティング
次にApp.jsで、作成したコンポーネントに対して、ルーティングを行ってやります。
App.jsimport React from "react"; import { BrowserRouter as Router, Route } from "react-router-dom"; import { HomePage } from "./HomePage"; import { SNSPage } from './SNSPage'; import { ShopPage } from './ShopPage'; import { Header } from './Header'; function App() { return ( <Router> <div className="App"> <Header/> <Route exact path="/" component={HomePage} /> <Route path="/sns" component={SNSPage} /> <Route path="/shop" component={ShopPage} /> </div> </Router> ); } export default App;
BrowserRouter
タグでRoute
タグを囲うことで、タグ内でのルーティングを指定できます。カードコンポーネントを作成する。
次に、並べる用のカードコンポーネントを作成していきます。
ContentCard.jsimport React from "react"; export function ContentCard(props) { return ( <div className="ContentCard p-4"> <div class="max-w-sm rounded overflow-hidden shadow-lg text-center"> <img class="w-full" src="https://source.unsplash.com/random/1600x900/" alt="Sunset in the mountains" ></img> <div class="px-6 py-4"> <div class="font-bold text-xl mb-2">{props.pageName}</div> <p class="text-gray-700 text-base"> {props.description} </p> </div> </div> </div> ); }こいつは汎用的に使いたかったので、親コンポーネントからpropsを受け取り、そいつを表示するだけという作りになってます。
HomePage.jsからカードコンポーネントを呼び出す。
次にHomePage.jsから、作成したCardContents.jsにpropsを渡しつつ呼び出しましょう。
HomePage.jsimport React from "react"; import { ContentCard } from "./ContentCard"; import { Link } from "react-router-dom"; export function HomePage() { return ( <div className="HomePage flex mb-4"> <Link to="/sns" className="w-1/3"> <ContentCard pageName="SNS" pageUrl="" cmpName="" imgSrc="" description="SNSSample page!" /> </Link> <Link to="/shop" className="w-1/3"> <ContentCard pageName="Shop" pageUrl="" cmpName="" imgSrc="" description="ShopSample page!" /> </Link> </div> ); }呼び出す際に
Link to
を用いて、クリック時遷移するようにしてます。動作確認
ここまでできれば完了です。
サーバを起動して確認してみましょう。npm startこんな感じになってると思います。
2つだけだと寂しいので、カード増やしてヘッダーつけるといい感じになります。
(画像が全部一緒なのは大目に見て。。)終わりに
githubにコード挙げてあるので、こうしたほうがいいよ!ってあったら教えてください!
https://github.com/Anno328/Dev/tree/master/portrait/src
- 投稿日:2019-12-18T14:47:29+09:00
フルスタックエンジニアへの道(CakePHP/React)
はじめに
こんにちは、 @IZUMIRU0313 です。
ランサーズ Advent Calendar 2019 23日目の記事です。法人向けの社外人材活用サービス「Lancers Enterprise」のフルスタックエンジニアです。
まだよわよわなので恐縮ですが、api blueprintでAPI仕様書、CakePHPでAPI、ReactでUIを実装しています?想定する読者は、サーバーサイドエンジニアでフロントエンド(React)も学習していこうとしている方です。
エンジニア経歴
学生時代は、主にRails、Swift、AWS(EC2、S3)、Heroku、WordPressを利用して、サービス開発やインターンに取り組んでいました。特に以下2つのサービスは、すべての設計および開発をやっていたため、努力は報われると今日でも思える貴重な経験になっています。
フロントは、SassとjQueryが多少書けるレベルでした?ランサーズには、SREとしてジョインしました。当時、ターミナルはgitと多少のコマンドを知っているレベルであり、@yakitori009さんに、何から何まで教えていただきながら取り組んでいました?♂️
LPICでインプットしながら取り組んでいたため、座学と実務の両輪が上手く回せていました。
- 踏み台サーバーの移行
- Let's Encryptワイルドカード証明書の導入
- AutoScaling
- AutoScaling中ではデプロイ不可
- docker-compose対応
- MySQLコンテナ、WordPressコンテナの構築
- MySQLのバージョンアップ5.6->5.7
- LambdaでGitHubとChatworkの連携
- LambdaでAthenaのload partitionを自動実行
その後、サーバーサイドエンジニアとしてCakePHPでプラットフォームの開発をすることにしました。まともにチーム開発とCakePHPを書くのは初めてだったため、@waldo0515さんや@numanomanuさん、井上さんにシステム設計からプロジェクトマネジメント、コーディングに渡るまで大変お世話になりました?♂️
インプットは、オブジェクト指向やドメイン駆動設計、クリーンアーキテクチャ、リーダブルコード等に努めました。
- ランサーランク制度
- アナリティクス
- 顔写真判定
- ヤフースコア
- Linkedinログイン
- 提案見積書
- 業種
- 提案追加オプション
- Freelance Basics移行
- Lancers Pro
- BigQuery/Redashで全社データ出し・モニタリング構築
JavaScriptの習得
正直まだまだ未熟であり器用貧乏になる可能性も大いにあるのですが、自分が目指したいエンジニア像のために本格的にJavaScriptに力を入れることにしました?
まずは、半年後業務でReactを書けるレベルになることを目標に、GASでの個人開発から始めました。SREの際にLambdaでnode.jsを書いていたこともあり、特に詰まることなく開発できました。
インプットは、改訂新版JavaScript本格入門を読んでいました。
- [GAS/Twilio]広瀬すずさんがSlackのリマインダー機能でモーニングコールしてくれるサービス
- [GAS/GoogleCloudNaturalLanguageAPI]宇垣美里さんが時には厳しく、時には優しくしてくれるアメムチbot
- IZUMIRU/kenkahadamewan
- [Nuxt/WebSpeechAPI]騒がしい居酒屋でもワンタップで店員さんを呼ぶサービス「親指ですみません」
Reactの習得
ES6のお作法や非同期通信の変遷等も一通り理解することができたので、本格的にReactの学習を始めました。
元々副業や個人開発でVueやNuxtを触る機会があったのですが、個人的にはReactの方が学習ハードルが高かった印象です。
特にJSX(TSX)、TypeScript、Redux、redux-sagaは業務で開発するまで理解できませんでした。半年ほど、@intrudercl14さんと@takepo0928さんにキャリアやJavaScript、Reactのアドバイスをいただき、なんとか「Lancers Enterprise」の開発にジョインすることができました?♂️
- チュートリアル
- React.Component
- 一人React.js Advent Calendar 2014
- りあクト!
- React開発 現場の教科書
- WEB+DB PRESS Vol.112
- Atomic Design
- Atomic Design by Brad Frost
- Redux. From twitter hype to production
特にりあクト!は、対話形式で先輩エンジニアが後輩エンジニアに教えるというストーリーなので、非常に読みやすくオススメです。
またVue、React、React(Redux)で同じアプリケーションを実装することは、共通点と相違点を把握でき学習促進に繋がったのでオススメです。
Reactの学習と合わせて、APIの学習にも努めました。APIは学生時代のサービスでRailsでAPIを生やし、Swiftでキャッチするという経験等はありましたが、なんちゃってAPIレベルだったので1から学習しました。
- Web API: The Good Parts
- IZUMIRU/youtube-manager-nuxt
- IZUMIRU/youtube-manager-go
- GraphQLはRESTの置き換えではない
- 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ
- vvakame/graphql-with-go-book
- IZUMIRU/laravel-vue-with-graphql
- IZUMIRU/amplify-react
- よくわかるgRPC
- スターティングgRPC
- IZUMIRU/hello-grpc
展望
ReactやTypeScriptの学習は継続していますが、ReactNativeやFlutterの学習もし始めたため、@sayanetさんと@terukuraさんとともに「Lancers Enterprise」をより良くした後は、アプリの改善にコミットできたらと考えています。
またモチベーション高く学習するには、自分の性格を理解することが大事だなと非常に思いました。家だと怠惰なので仕事終わり必ずカフェに行く、まずは簡単なアプリケーションを開発した後に体系だった書籍で質を上げていく等。長くなりそうなので、個人開発のすゝめ的な記事は別途書けたら良いなと思います。
QiitaいいねやTwitterフォローは励みになります?
- 投稿日:2019-12-18T14:40:17+09:00
WordPress ブロックエディター(Gutenberg)ブロック一覧
WordPress ブロックエディター(Gutenberg)のコアブロック一覧です。
WordPress 5.3 時点。一般ブロック(common)
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/paragraph 段落 ○ ○ core/heading 見出し ○ ○(文字色のみ) ○ core/image 画像 ○ ○ ○ core/gallery ギャラリー ○ ○ ○ core/list リスト core/quote 引用 core/audio 音声 ○ ○ core/cover カバー ○ ○ core/file ファイル ○ ○ core/video 動画 ○ ○ フォーマット(formatting)
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/code コード core/freeform クラシック core/html カスタムHTML core/preformatted 整形済み core/pullquote プルクオート ○ ○ ○ core/table 表 ○ ○ core/verse 詩 ○ カラム
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/columns カラム ○ core/button ボタン ○ ○ core/group グループ ○ ○(背景色のみ) ○ core/media-text メディアとテキスト ○ ○ ○(背景色のみ) core/more 続きを読む core/nextpage 改ページ core/separator 区切り ○(色のみ) core/spacer スペーサー ウィジェット
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/shortcode ショートコード core/archives アーカイブ ○ ○ core/calendar カレンダー ○ ○ core/categories カテゴリー ○ ○ core/latest-comments 最近のコメント ○ ○ core/latest-posts 最近の記事 ○ ○ core/rss RSS ○ ○ core/search 検索 ○ ○ core/tag-cloud タグクラウド ○ ○ 埋め込み
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/embed 埋め込み ○ ○ core-embed/twitter ○ ○ core-embed/youtube YouTube ○ ○ core-embed/facebook ○ ○ core-embed/instagram ○ ○ core-embed/wordpress WordPress ○ ○ core-embed/soundcloud SoundCloud ○ ○ core-embed/spotify Spotify ○ ○ core-embed/flickr Flickr ○ ○ core-embed/vimeo Vimeo ○ ○ core-embed/animoto Animoto ○ ○ core-embed/cloudup Cloudup ○ ○ core-embed/collegehumor CollegeHumor ○ ○ core-embed/dailymotion Dailymotion ○ ○ core-embed/funnyordie Funny or Die ○ ○ core-embed/hulu Hulu ○ ○ core-embed/imgur Imgur ○ ○ core-embed/issuu Issuu ○ ○ core-embed/kickstarter Kickstarter ○ ○ core-embed/meetup-com Meetup.com ○ ○ core-embed/mixcloud Mixcloud ○ ○ core-embed/photobucket Photobucket ○ ○ core-embed/polldaddy Polldaddy ○ ○ core-embed/reddit ○ ○ core-embed/reverbnation ReverbNation ○ ○ core-embed/screencast Screencast ○ ○ core-embed/scribd Scribd ○ ○ core-embed/slideshare Slideshare ○ ○ core-embed/smugmug SmugMug ○ ○ core-embed/speaker-deck Speaker Deck ○ ○ core-embed/ted TED ○ ○ core-embed/tumblr Tumblr ○ ○ core-embed/videopress VideoPress ○ ○ core-embed/wordpress-tv WordPress.tv ○ ○ 変更等ありましたら編集リクエストください!
- 投稿日:2019-12-18T14:40:17+09:00
WordPress ブロックエディター(Gutenberg)ブロック一覧表
WordPress ブロックエディター(Gutenberg)のコアブロック一覧です。
WordPress 5.3 時点。一般ブロック(common)
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/paragraph 段落 ○ ○ core/heading 見出し ○ ○(文字色のみ) ○ core/image 画像 ○ ○ ○ core/gallery ギャラリー ○ ○ ○ core/list リスト core/quote 引用 core/audio 音声 ○ ○ core/cover カバー ○ ○ core/file ファイル ○ ○ core/video 動画 ○ ○ フォーマット(formatting)
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/code コード core/freeform クラシック core/html カスタムHTML core/preformatted 整形済み core/pullquote プルクオート ○ ○ ○ core/table 表 ○ ○ core/verse 詩 ○ カラム
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/columns カラム ○ core/button ボタン ○ ○ core/group グループ ○ ○(背景色のみ) ○ core/media-text メディアとテキスト ○ ○ ○(背景色のみ) core/more 続きを読む core/nextpage 改ページ core/separator 区切り ○(色のみ) core/spacer スペーサー ウィジェット
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/shortcode ショートコード core/archives アーカイブ ○ ○ core/calendar カレンダー ○ ○ core/categories カテゴリー ○ ○ core/latest-comments 最近のコメント ○ ○ core/latest-posts 最近の記事 ○ ○ core/rss RSS ○ ○ core/search 検索 ○ ○ core/tag-cloud タグクラウド ○ ○ 埋め込み
名前 配置揃え 幅広/全幅 色設定 HTML アンカー core/embed 埋め込み ○ ○ core-embed/twitter ○ ○ core-embed/youtube YouTube ○ ○ core-embed/facebook ○ ○ core-embed/instagram ○ ○ core-embed/wordpress WordPress ○ ○ core-embed/soundcloud SoundCloud ○ ○ core-embed/spotify Spotify ○ ○ core-embed/flickr Flickr ○ ○ core-embed/vimeo Vimeo ○ ○ core-embed/animoto Animoto ○ ○ core-embed/cloudup Cloudup ○ ○ core-embed/collegehumor CollegeHumor ○ ○ core-embed/dailymotion Dailymotion ○ ○ core-embed/funnyordie Funny or Die ○ ○ core-embed/hulu Hulu ○ ○ core-embed/imgur Imgur ○ ○ core-embed/issuu Issuu ○ ○ core-embed/kickstarter Kickstarter ○ ○ core-embed/meetup-com Meetup.com ○ ○ core-embed/mixcloud Mixcloud ○ ○ core-embed/photobucket Photobucket ○ ○ core-embed/polldaddy Polldaddy ○ ○ core-embed/reddit ○ ○ core-embed/reverbnation ReverbNation ○ ○ core-embed/screencast Screencast ○ ○ core-embed/scribd Scribd ○ ○ core-embed/slideshare Slideshare ○ ○ core-embed/smugmug SmugMug ○ ○ core-embed/speaker-deck Speaker Deck ○ ○ core-embed/ted TED ○ ○ core-embed/tumblr Tumblr ○ ○ core-embed/videopress VideoPress ○ ○ core-embed/wordpress-tv WordPress.tv ○ ○ 変更等ありましたら編集リクエストください!
- 投稿日:2019-12-18T13:42:15+09:00
Reactで日付入力にカレンダー(DatePicker)を使う ※年をプルダウンで指定して元号も表示する
概要
Reactで日付入力にカレンダー(DatePicker)を使う。
実装
React Datepickerを使うと楽。
サンプル
※
年:プルダウン指定(元号付き)
月:プルダウン指定
<InputeDate />
で。import getMonth from 'date-fns/getMonth'; import getYear from 'date-fns/getYear'; import ja from 'date-fns/locale/ja'; import React from "react"; import DatePicker, { registerLocale } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; // jaのロケールの設定が週頭が月曜始まりになっているので日曜始まりにする ja.options.weekStartsOn = 0; // ReactDatepickerのロケール登録 registerLocale('ja', ja); class InputDate extends React.Component { state = { startDate: null }; handleChange = date => { this.setState({ startDate: date }); }; eraHandler = yearNow => { const generate = (era, startYear) => { let yearDsp = yearNow - startYear + 1; if (yearDsp === 1) { yearDsp = "元"; } else { yearDsp = ('00' + yearDsp).slice(-2); } return `${era}${yearDsp}年`; }; if (yearNow >= 2019) { return generate('令和', 2019); } if (yearNow >= 1989) { return generate('平成', 1989); } if (yearNow >= 1926) { return generate('昭和', 1926); } if (yearNow >= 1912) { return generate('大正', 1912); } } render() { var startYear = 1912; // カレンダーに表示する最初の西暦(大正元年となる1912を指定) var futureListUp = 5; // カレンダーに表示する未来の年数 var years = Array.from({ length: getYear(new Date()) - startYear + futureListUp }, (v, k) => k + startYear).reverse(); const months = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; return ( <React.Fragment> <DatePicker locale='ja' selected={this.state.startDate} onChange={this.handleChange} placeholderText="日付を選択してください" dateFormat="yyyy/MM/dd" isClearable showMonthDropdown showYearDropdown todayButton="今日" dropdownMode="select" // カレンダーのヘッダ部分をカスタマイズする renderCustomHeader={({ date, changeYear, changeMonth, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => { return ( <div style={{ margin: 10, display: "flex", justifyContent: "center" }}> {/* 前月ボタン */} <button onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>{"<"}</button> {/* 年の部分 */} <select value={getYear(date)} onChange={({ target: { value } }) => changeYear(value)} > {years.map((option) => ( // eraHandler()で年のプルダウンに元号を付ける <option key={option} value={option}>{option}年({this.eraHandler(option)})</option> ))} </select> {/* 月の部分 */} <select value={months[getMonth(date)]} onChange={({ target: { value } }) => changeMonth(months.indexOf(value))} > {months.map(option => ( <option key={option} value={option}> {option}月 </option> ))} </select> {/* 次月ボタン */} <button onClick={increaseMonth} disabled={nextMonthButtonDisabled}>{">"}</button> </div> ); } } /> </React.Fragment> ); } }参考
- 投稿日:2019-12-18T13:28:40+09:00
React(Hooks)で認証付きリアルタイムチャットアプリを作ってみた。
はじめに
Reactの勉強を始めて、Hooksを使ってchatアプリを作成してみました。
一度作ってからこの記事を作成したので、説明する順序がおかしいと感じる点があるかもしれません。
特に注意して作成した箇所をまとめてみたので全体のコードを確認したい場合は
https://github.com/m-shichida/chat_app_with_react_hooks
ここにコード置いています。作成途中ですが、最低限の
- ログインユーザーの管理
- メッセージの送受信、表示
はできています。
firebaseの設定等は割愛します。
- React(Hooks)
- firebase
- ちょこっとRedux(複数のstate管理のためcombineReducers)
使いました。
修正点あれば教えてください。下準備
まずこのチャットアプリでは
- ①ログインユーザーの情報管理
- ②送受信されたメッセージの管理
この2つのstate/dispatch(stateの状態を変更させる動き)を管理させるため、複数のstate/dispatch管理と、全ての階層でstate/dispatchが使えるようにしなくてはなりません。
そのため、
components/App.js
(components
のルートファイル)に
- ①
state
の初期値- ②
reducers
内のstateを変更させるdispatch
を使用するとの宣言- ③全ての階層でstate/dispatchが使用できるようにする。
この3つを記述しました。
components/App.jsfunction App() { // ①stateの初期値の設定 const initialState = { currentUserInfos: '', messages: [] } // ②reducers内のstateの状態を更新する関数を呼び込む。 const [state, dispatch] = useReducer(reducers, initialState); // ここはあとで出てきます。 useEffect(() => { firebaseDb.ref('messages/').on('child_added', (snapshot) => { const messages = snapshot.val() dispatch({ type: SET_MESSAGES, messages }) }); }, []) // ページリロードしてもログインしているユーザーの情報をlocalStorageから持ってくる。 useEffect(() => { dispatch({ type: SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE }) }, []); return ( // ③AppContextに囲まれたコンポーネントでuseReducerで定義したstate, dispatchが使えるよう宣言する。 <AppContext.Provider value={ { state, dispatch } }> { state.currentUserInfos ? (<MainContent />) : (<Login />) } </AppContext.Provider> ); } export default App;そして複数のstate/dispatchを管理できるようにするためにReduxの
combineReducers
を使用します。reducers/index.jsimport { combineReducers } from 'redux'; import currentUserInfos from './currentUserInfos'; import messages from './messages'; // 複数のstateとdispatchを管理することが可能になる。 export default combineReducers({ messages, currentUserInfos })ログイン状態を保持する
components/login.jsimport React, { useContext } from 'react'; import AppContext from '../contexts/AppContext'; import { Button, Card, CardActions, CardContent, Grid } from '@material-ui/core'; import firebase from 'firebase'; import { firebaseApp } from '../firebase/index'; import GoogleLoginImage from '../images/btn_google_signin.png'; import { ADD_CURRENT_USER_INFO } from '../actions'; const Login = () => { // AppContextからstateの状態を変更させるdispatchを呼び出す。 const { dispatch } = useContext(AppContext); // 匿名ユーザーとしてログインする。 const loginAsAnonymousUser = e => { e.preventDefault(); firebaseApp.auth().signInAnonymously().catch(function(error) { const errorCode = error.code; const errorMessage = error.message; alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`) }); firebaseApp.auth().onAuthStateChanged(function(user) { if (user) { const uid = user.uid; const name = `ゲストユーザー${ uid }` dispatch({ type: ADD_CURRENT_USER_INFO, uid, name }); alert(`${ name }としてログインしました。`) } }); } // Googleアカウントを利用してログインする。 const loginAsGoogleAccount = () => { let provider = new firebase.auth.GoogleAuthProvider(); firebaseApp.auth().signInWithPopup(provider).then(function(result) { const user = result.user; const uid = user.uid const name = user.displayName; const image = user.photoURL dispatch({ type: ADD_CURRENT_USER_INFO, uid, name, image }) }).catch(function(error) { const errorCode = error.code; const errorMessage = error.message; alert(`エラーが発生しました。エラーコード${ errorCode }:${ errorMessage }`) }); } return ( <Grid container justify='center' style={ { position: 'fixed', top: '35%' } } > <Card style={ { width: '300px' } }> <CardContent> <h3>ログインして利用する</h3> </CardContent> <CardActions style={ { display: 'flex', flexDirection: 'column' } }> <Button className='loginBtn' variant='contained' color='primary' onClick={ loginAsAnonymousUser } style={ { marginBottom: '8px' } } > 匿名ログイン </Button> <img className='loginBtn' alt='GoogleLoginImage' style={ { cursor: 'pointer' } } src={ GoogleLoginImage } onClick={ loginAsGoogleAccount } /> </CardActions> </Card> </Grid> ) }; export default Login;匿名ログイン、グーグルアカウントを利用してのログイン両方とも、ログインに成功してもstate管理しなければ、ログイン状態を保持することができないため、
dispatch
を使ってstate管理します。またログイン後にページをリロードしてしまうとこれもまたログイン情報を管理したstateが全てなくなってしまうので、stateの更新とともにlocalstorageに保存します。
login.jsdispatch({ type: ADD_CURRENT_USER_INFO, uid, name, image // 匿名ログインのときはいらないです。 })このdispatchが以下の
currentUserInfos
に渡ります。state/dispatchが一つだけの管理で済むのであれば、格納場所はreducers/index.js
で大丈夫です。reducers/currentUserInfos.jsimport { ADD_CURRENT_USER_INFO, DELETE_CURRENT_USER_INFO, SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE } from '../actions'; import { APP_KEY } from '../shared'; const currentUserInfos = (state = [], action) => { switch(action.type) { case ADD_CURRENT_USER_INFO: const image = action.image ? action.image : ''; const params = { uid: action.uid, name: action.name, image } localStorage.setItem(APP_KEY, JSON.stringify(params)) // localstorageにログイン情報を保存する。 return params case DELETE_CURRENT_USER_INFO: localStorage.removeItem(APP_KEY) return '' // localStorageに保存したログイン情報を削除し、stateを空で返す。 case SET_CURRENT_USER_INFO_FROM_LOCALSTORAGE: const currentUserInfo = JSON.parse(localStorage.getItem(APP_KEY, JSON.stringify(state))) return currentUserInfo default: return state } } export default currentUserInfos;localStorageに使ったこの
APP_KEY
の中身は別のフォルダ(shared
)に格納しています。
ディレクトリはshared
ではなくhelpers
の方が多いかな?shared/index.jsexport const APP_KEY = 'currentUserInfo';メッセージを送信する
次に
messages.js
。
dispatch内でstate管理をするとともに、firebaseへメッセージの情報を格納しました。reducers/messages.jsimport { ADD_MESSAGE, SET_MESSAGES } from '../actions' import currentDate from '../shared'; import { firebaseDb } from '../firebase'; const messages = (state = [], action) => { switch(action.type) { case ADD_MESSAGE: // state管理する const message = { uid: action.uid, userImage: action.image, content: action.content, createdAt: action.createdAt ? action.createdAt : currentDate() } // firebaseにメッセージ情報を保存する。 firebaseDb.ref('messages/').push(message); return [...state, { ...message }] case SET_MESSAGES: return [...state, action.messages] default: return state } } export default messages;データに変更があった時
firebaseではデータに変更があった場合に
firebaseDb.ref('messages/').on('child_added', (snapshot)でリアルタイムでデータを取ってくることができます。
components/App.js// あとで出てくると書いてあったとこ。 // メッセージのデータに変更があった場合、'初回の一回だけ'stateを更新する。 useEffect(() => { firebaseDb.ref('messages/').on('child_added', (snapshot) => { const messages = snapshot.val() dispatch({ type: SET_MESSAGES, messages }) }); }, [])
components/Messages.js
(複数メッセージを表示させるコンポーネント)にこれを書くのかな、と思ったのですが、ボトムナビゲーションなどを使用している時にページ切り替えのたびにMessages
コンポーネントが呼ばれることになり、何度も重なってメッセージが呼ばれることになります。
そのため、一回だけ呼び出させるコンポーネント、components/App.js
にこれを定義しました。終わり
- 投稿日:2019-12-18T11:35:49+09:00
Webアプリ完成するまで続ける開発日誌<day1>
こんにちは!山形大学のもえとです!
とあるGLSで日本人のコミュニケーションストレスを少しでも解消したくITサービスを作っています。しいたけ占いで天秤座の仕事運は発信力に連動とのことでしたのでQiitaでどんどん発信していきます
しいたけ占い2020年上半期:https://voguegirl.jp/horoscope/shiitake2020-h1/noteも是非に。
https://note.com/se_n_pu_u_ki対象レベル(自己紹介)
ProgateのHTML、CSS、Javascriptあたり2周とかしたことある
授業でC言語、自分でPythonなどやったこと
Reactの勉強からはじめます。
この記事、とっても丁寧に書いてくださっています。
https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048
これを勉強していてエラーの対策などを書いたのが僕の記事です前準備
だいたいエラーで苦労するのって前準備の環境構築のところですね。
まずはエラーコードをそのままググってみる癖をつけようって誰かが言ってました。ワークスペースを作りましょう。
Macの方はターミナル.app
を起動し以下をひとつひとつ入力してください。$ mkdir react-tutorial $ cd react-tutorial $ mkdir -p src/js
mkdir
は「make directory」の命令で、これを実行するとreact-tutorialという名前のファイルが作成されます。Finderでみてみてもちゃんとファイルが作られているのが分かります。
cd
は「change directory」の命令で、これを実行するとカレントディレクトリ(ファイル構造の中の自分の現在地)を移動することができます。
mkdir
のあとについている-p
は、階層構造を1度に複数まで作ることができるオプションです。react-tutorial$ npm init
このコマンドを実行するといろいろ初期設定みたいなのがはじまります。
...... package name: (react-tutorial) version: (1.0.0) description: entry point: (index.js) webpack.config.js test command: git repository: keywords: author: Your Name license: (ISC) ......おそらくここまでは問題なく進む。
この次の工程で僕はエラー祭りでした。
webpackのパッケージをインストールしているようです$ npm install --save-dev webpack webpack-cli webpack-dev-server $ npm install -g webpack webpack-cli $ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader $ npm install --save-dev react react-domこれの1つ目を実行したところでエラーが大量に出てきました。
まずはエラーコードを見てみると前半の方に
No Xcode or CLT version detected!
と書かれていましたのでこのままコピペしてググってみました。→たどり着いたサイト:https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488
実行したコマンドを載せます。
CommandLineToolsの設定変更$ nodebrew install 8 $ sudo xcode-select --switch /Applications/Xcode.app $ npm insatall node-gypおそらく2つ目のやつが必要だったっぽいです。
Command Line Tools
とは...
Macのコマンドを実行するためのコマンドツール群らしく、homebrew等のプログラムのインストールにも使うそうです。んでここの
$ sudo xcode-select --switch /Applications/Xcode.app
で
Command Line Tools
をXcode同梱版に設定した、ということのようです。これによって無事エラーなく実行することができました。
あと、無視していいエラーコードnpm WARN react-tutorial@1.0.0 No description npm WARN react-tutorial@1.0.0 No repository field.
こんなエラーコードが出るかと思いますがこれは無視して大丈夫だそうです。
では気を取り直して次!
webpack.config.jsファイルを作る$ touch webpack.config.js
touch
コマンドはファイルを作ることができるコマンド。webpack.confing.jsのファイルに「バンドリングルール」を書いていくそうです。
バンドリングルールとは、おそらくweb読み込むときに「ここにこれがあるよ!」とかを教えてあげるためのモノな気がします()バンドリングルール書いていきましょう
webpack.config.jsvar debug = process.env.NODE_ENV !== "production"; var webpack = require('webpack'); var path = require('path'); module.exports = { context: path.join(__dirname, "src"), entry: "./js/client.js", module: { rules: [{ test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, use: [{ loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-env'] } }] }] }, output: { path: __dirname + "/src/", filename: "client.min.js" }, plugins: debug ? [] : [ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }), ] };これの書き方などは別途後で調べます。
メモ:https://original-game.com/how-to-use-webpack-config-js/続いて、src/index.htmlを作成
$ touch ./src/index.htmlそしてhtmlファイルに次のように書き込みましょう。
src/index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React Tutorials</title> <!-- change this up! http://www.bootstrapcdn.com/bootswatch/ --> <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css" rel="stylesheet"/> </head> <body> <div id="app"></div> <script src="client.min.js"></script> </body> </html>htmlファイルの中で
client.min.js
を読み込むように指定しています。webpackによって生成される、必要最低限のコードになったclient.js
ファイルのようです。
client.js
を書いてあげればそれをhtmlが読みこんで表示してくれるのですね。
では早速client.js
を書いていきましょう。やっとReact初登場
src/js/client.jsimport React from "react"; import ReactDOM from "react-dom"; class Layout extends React.Component { render() { return ( <h1>Welcome!</h1> ); } } const app = document.getElementById('app'); ReactDOM.render(<Layout/>, app);このファイル含めReactの理解は後でするとして、ここまででとりあえず準備完了のため
webpackコマンドからclient.min.jsファイルを作成したあとにindex.htmlファイルを開きましょう。$ webpack --mode developmentこれでclient.jsなどをコンパイルしました。
じゃあ早速chromeに表示しよう!
開発用webサーバを起動していきます$ ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --content-base src --mode developmentこれを実行すると開発用webサーバが起動し、htmlファイルなどに変更を加えるたびに画面が更新されます
chromeなどで
http://localhost:8080
とURLにうってみてください!It works!
と表示されていたら成功です!
また
./src/js/index.js
に変更を加えてみましょうsrc/js/index.jsimport React from "react"; import ReactDOM from "react-dom"; class Layout extends React.Component { render() { return ( <h1>Welcome!</h1> ); } } const app = document.getElementById('app'); ReactDOM.render(<Layout/>, app);It works!をWelcome!にしました
こうして保存すると
Welcome!
と変更されました!
前準備終了!
やっとReactの文法などについて始まります。
以降の日誌
day2作成中...(12/18)
- 投稿日:2019-12-18T11:35:49+09:00
<day1>Webアプリ完成するまで続ける開発日誌
こんにちは!山形大学のもえとです!
とあるGLSで日本人のコミュニケーションストレスを少しでも解消したくITサービスを作っています。しいたけ占いで天秤座の仕事運は発信力に連動とのことでしたのでQiitaでどんどん発信していきます
しいたけ占い2020年上半期:https://voguegirl.jp/horoscope/shiitake2020-h1/noteも是非に。
https://note.com/se_n_pu_u_ki対象レベル(自己紹介)
ProgateのHTML、CSS、Javascriptあたり2周とかしたことある
授業でC言語、自分でPythonなどやったこと
Reactの勉強からはじめます。
この記事、とっても丁寧に書いてくださっています。
https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048
これを勉強していてエラーの対策などを書いたのが僕の記事です前準備
だいたいエラーで苦労するのって前準備の環境構築のところですね。
まずはエラーコードをそのままググってみる癖をつけようって誰かが言ってました。ワークスペースを作りましょう。
Macの方はターミナル.app
を起動し以下をひとつひとつ入力してください。$ mkdir react-tutorial $ cd react-tutorial $ mkdir -p src/js
mkdir
は「make directory」の命令で、これを実行するとreact-tutorialという名前のファイルが作成されます。Finderでみてみてもちゃんとファイルが作られているのが分かります。
cd
は「change directory」の命令で、これを実行するとカレントディレクトリ(ファイル構造の中の自分の現在地)を移動することができます。
mkdir
のあとについている-p
は、階層構造を1度に複数まで作ることができるオプションです。react-tutorial$ npm init
このコマンドを実行するといろいろ初期設定みたいなのがはじまります。
...... package name: (react-tutorial) version: (1.0.0) description: entry point: (index.js) webpack.config.js test command: git repository: keywords: author: Your Name license: (ISC) ......おそらくここまでは問題なく進む。
この次の工程で僕はエラー祭りでした。
webpackのパッケージをインストールしているようです$ npm install --save-dev webpack webpack-cli webpack-dev-server $ npm install -g webpack webpack-cli $ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader $ npm install --save-dev react react-domこれの1つ目を実行したところでエラーが大量に出てきました。
まずはエラーコードを見てみると前半の方に
No Xcode or CLT version detected!
と書かれていましたのでこのままコピペしてググってみました。→たどり着いたサイト:https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488
実行したコマンドを載せます。
CommandLineToolsの設定変更$ nodebrew install 8 $ sudo xcode-select --switch /Applications/Xcode.app $ npm insatall node-gypおそらく2つ目のやつが必要だったっぽいです。
Command Line Tools
とは...
Macのコマンドを実行するためのコマンドツール群らしく、homebrew等のプログラムのインストールにも使うそうです。んでここの
$ sudo xcode-select --switch /Applications/Xcode.app
で
Command Line Tools
をXcode同梱版に設定した、ということのようです。これによって無事エラーなく実行することができました。
あと、無視していいエラーコードnpm WARN react-tutorial@1.0.0 No description npm WARN react-tutorial@1.0.0 No repository field.
こんなエラーコードが出るかと思いますがこれは無視して大丈夫だそうです。
では気を取り直して次!
webpack.config.jsファイルを作る$ touch webpack.config.js
touch
コマンドはファイルを作ることができるコマンド。webpack.confing.jsのファイルに「バンドリングルール」を書いていくそうです。
バンドリングルールとは、おそらくweb読み込むときに「ここにこれがあるよ!」とかを教えてあげるためのモノな気がします()バンドリングルール書いていきましょう
webpack.config.jsvar debug = process.env.NODE_ENV !== "production"; var webpack = require('webpack'); var path = require('path'); module.exports = { context: path.join(__dirname, "src"), entry: "./js/client.js", module: { rules: [{ test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, use: [{ loader: 'babel-loader', options: { presets: ['@babel/preset-react', '@babel/preset-env'] } }] }] }, output: { path: __dirname + "/src/", filename: "client.min.js" }, plugins: debug ? [] : [ new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }), ] };これの書き方などは別途後で調べます。
メモ:https://original-game.com/how-to-use-webpack-config-js/続いて、src/index.htmlを作成
$ touch ./src/index.htmlそしてhtmlファイルに次のように書き込みましょう。
src/index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>React Tutorials</title> <!-- change this up! http://www.bootstrapcdn.com/bootswatch/ --> <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css" rel="stylesheet"/> </head> <body> <div id="app"></div> <script src="client.min.js"></script> </body> </html>htmlファイルの中で
client.min.js
を読み込むように指定しています。webpackによって生成される、必要最低限のコードになったclient.js
ファイルのようです。
client.js
を書いてあげればそれをhtmlが読みこんで表示してくれるのですね。
では早速client.js
を書いていきましょう。やっとReact初登場
src/js/client.jsimport React from "react"; import ReactDOM from "react-dom"; class Layout extends React.Component { render() { return ( <h1>Welcome!</h1> ); } } const app = document.getElementById('app'); ReactDOM.render(<Layout/>, app);このファイル含めReactの理解は後でするとして、ここまででとりあえず準備完了のため
webpackコマンドからclient.min.jsファイルを作成したあとにindex.htmlファイルを開きましょう。$ webpack --mode developmentこれでclient.jsなどをコンパイルしました。
じゃあ早速chromeに表示しよう!
開発用webサーバを起動していきます$ ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --content-base src --mode developmentこれを実行すると開発用webサーバが起動し、htmlファイルなどに変更を加えるたびに画面が更新されます
chromeなどで
http://localhost:8080
とURLにうってみてください!It works!
と表示されていたら成功です!
また
./src/js/index.js
に変更を加えてみましょうsrc/js/index.jsimport React from "react"; import ReactDOM from "react-dom"; class Layout extends React.Component { render() { return ( <h1>Welcome!</h1> ); } } const app = document.getElementById('app'); ReactDOM.render(<Layout/>, app);It works!をWelcome!にしました
こうして保存すると
Welcome!
と変更されました!
前準備終了!
やっとReactの文法などについて始まります。
(2019/12/18作成。)以降の日誌
【day2】同日更新
https://qiita.com/se_n_pu_u_ki/items/50a64ffc3f643e9fd15f
- 投稿日:2019-12-18T04:20:02+09:00
Redux公式のBasics TutorialでToDoリストを作ってみた
Reactで、状態管理ライブラリとしてよく使われるRedux。
使い方を調べようと、公式サイトにあるBasics TutorialでToDoリストを作りにチャレンジしてみた。
ただ、このチュートリアル、ステップ・バイ・ステップになっていないので、なんだかわかりにくい。そこで、どんな内容なのか、簡単にメモを残します。
内容
チュートリアルは、こちら(英語)。
- Basic Tutorial: Intro · Redux
- https://redux.js.org/basics/basic-tutorial
Redux単独のチュートリアルのように見えるけど、じつはReactと組み合わせてToDo Listを作っている。
以下は、(ほぼ)同じ内容を日本語でなぞっている。
- React + Redux の基本的な使い方 - KDE BLOG
- https://kde.hateblo.jp/entry/2019/02/12/023325
こちらで、完成版のデモを試すことができる。
- todos - CodeSandbox
- https://codesandbox.io/s/github/reduxjs/redux/tree/master/examples/todos
はじめ方
ReactとReduxで作るので、create-react-appする。
$ create-react-app basic-tutorial-todo $ cd basic-tutorial-todo $ npm install --save redux react-redux redux-loggerソースコード
ToDoリストのソースコードは、こちらで入手できる。
チュートリアルだけ読んでいても、全体像がわかりにくい。
デモ版をみるか、create-react-appしたところにソースコードを流し込んだ方がいいと思う。Example: Todo List · Redux
https://redux.js.org/basics/exampleGithub - reduxjs/redux
https://github.com/reduxjs/redux/tree/master/examples/todosディレクトリ構成
完成すると、こんな構成になる。意外と大きい。
/basic-tutorial-todo │ index.js │ serviceWorker.js ├─actions │ index.js ├─components │ App.js │ Footer.js │ Link.js │ Todo.js │ TodoList.js ├─containers │ AddTodo.js │ FilterLink.js │ VisibleTodoList.js └─reducers index.js todos.js visibilityFilter.js実行する
$ npm start参考になるページ
Basic exampleは、ちょっと大きいので、こちらにもう少し小さいサンプルがある。
- React+Redux で Todoアプリを作ってみる │ Web備忘録
- https://webbibouroku.com/Blog/Article/react-redux-todo
そもそも、Reduxとは何なのか、これが分かりやすかった。
- Vanilla JSで学ぶRedux - Qiita
- https://qiita.com/ryota-murakami/items/2ed6b12943214ecfeeaf
ReactとReduxの連携は、これが参考になった。
- たぶんこれが一番分かりやすいと思います React + Redux のフロー図解 - Qiita
- https://qiita.com/mpyw/items/a816c6380219b1d5a3bf
- 投稿日:2019-12-18T00:59:08+09:00
GraphQLでタスク管理アプリを作る -フロントエンド編- [React+Apollo Client+Typescript]
まえがき
バックエンド編とフロントエンド編の2つに分けて、GraphQL を使ったタスク管理アプリを作っていきます。
このバックエンド編では、React / Apollo Client / typescript によるGraphQLを使ったTODOアプリの実装をご紹介していきます。この記事は@ebknさんの記事の後編です。
まだ読んでないというそこのあなた、ぜひご一読ください!(そして願わくばこの記事に戻ってきてくださいな)このアプリのコードは公開しています。
こんな感じのTODOアプリを作ります。
主な技術要素
- React
- Typescript(v3.7.2以上、Optional ChainingやNullish Coalescingを使えます)
- React apollo(及び @apollo/react-hooks)
- GraphQL Code Generator
- webpack
- eslint
Apolloとは
Apollo は、フロントエンド/バックエンド両方に対応したGraphQLフレームワークです。
TECHNOLOGY RADAR (イマドキの技術を半年に一回まとめて紹介しているガイド) の 2019年 4月版 でもADOPT
、つまり実際の開発現場投入に適している、と紹介されています。今回はその中のフロントエンド用のフレームワーク、Apollo client を使った開発を紹介します。
これを使うことで、非常に簡単にGraphQL APIを使うことができます。今回はReactを使いますが、他にもAngular、vueでも似たようなものが存在します。
注意:
@apollo/react-hooks
とreact-apollo-hooks
Apollo clientでHooksを使いたい!となって検索すると、たいていこの2つが出てきます。
結論、@apollo/react-hooks
を使ってください。
react-apollo-hooks
はまだ公式からhooksサポートが出される前に、有志の方が作ってくださっていたものです。
今は公式がサポートしているので、deprecatedとなっています。
1年くらい前の解説サイトだと、react-apollo-hooksをつかっているサイトもちらほらあるので、混同しないようにお気をつけください。実現される開発体験
GraphQLの強みである型システムの恩恵を全面に受け、型安全なReactアプリケーションをシュッっと作る。
手動でTypescriptの型定義することを可能な限り減らすことで、ヒューマンエラーの予防にもなる。ディレクトリ構成
なるべく単純化するために、コンポーネントもそんなに分けていません。
実開発ではもう少し分けたほうがいいと思います。$ tree -I node_modules . ├── README.md ├── codegen.yml ├── package-lock.json ├── package.json ├── query.graphql ├── src │ ├── components │ │ ├── CompletedIcon.tsx │ │ ├── CreateTaskModal.tsx │ │ ├── Tasks.tsx │ │ └── UpdateTaskModal.tsx │ ├── generated │ │ └── graphql.ts │ ├── hooks │ │ └── formHooks.ts │ ├── html │ │ └── index.html │ ├── index.tsx │ ├── lib │ │ └── sleep.ts │ ├── styles │ │ └── main.css │ └── types │ └── index.d.ts ├── tsconfig.json └── webpack.config.js開発の流れ
- 1: 必要パッケージのインストール / ビルドツール等の設定
- 2: GraphQLのスキーマファイルから、GraphQLを使うためのHooks、型定義を自動生成する
- 3: 自動生成されたコードを使ってReactコンポーネントを作っていく
では早速やっていきましょう!
1: 必要パッケージのインストール / ビルドツール等の設定
必要パッケージをインストールする(後述するpackage.jsonをコピペして
npm i
でも可)npm i -D @babel/core @babel/preset-env @babel/preset-react @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @types/react @types/react-datepicker @types/react-dom @types/react-infinite-scroller @types/react-router @types/react-router-dom" @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-eslint babel-loader babel-polyfill css-loader dotenv-webpack eslint eslint-config-prettier eslint-loader eslint-plugin-graphql eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks html-loader html-webpack-plugin prettier style-loader ts-loader typescript webpack webpack-cli webpack-dev-server && npm i -S @apollo/react-hooks apollo-boost core-js date-fns graphql graphql-tag react react-app-polyfill react-datepicker react-dom react-infinite-scroller react-router react-router-dom semantic-ui-reactたくさんインストールします...ざっくり説明すると以下です。
- webpack / babel / prettier / eslint / webpackのloader群
- TypescriptのコードをJSにトランスパイルしたり、css moduleを使ったり、lint/auto-formatしたりするやつ
- webpack-dev-server
- hot reload
- react / react-router等
- react本体とルーターなど
- react-infinite-scroller
- 無限スクロールを簡単にするコンポーネント
- semantic-ui-react
- いい感じのフロントを作れるフレームワークの一つ、semantic-uiのreact版
- apollo-boost / @apollo/react-hooks
- Apollo client
余談: tslintがdeprecatedになる話
typescriptのlinterとしてtslintをお使いの方は結構いるのではないでしょうか。
実はこのtslint、公式から2019年に非推奨になる
ことがアナウンスされています。⚠️ TSLint will be deprecated some time in 2019. See this issue for more details: Roadmap: TSLint → ESLint. If you're interested in helping with the TSLint/ESLint migration, please check out our OSS Fellowship program.
このアナウンスにもあるよう、今後はeslintを使っていきましょう。
もうすでにtslintで動いているプロジェクトがある場合は、「どのように移行すべきか」に関して記事を書いてくださっている方がいるので、そちら等を参照ください。
脱TSLintして、ESLint TypeScript Plugin に移行する筆者もtslintからeslintに移行しましたが、移行するにあって大した手間や不具合は感じなかったです。
codegen.yml (graphql code generatorの設定ファイル)
codegen.ymlschema: # GraphQL APIサーバーのエンドポイント # この配列に@restや@localを使うクエリファイルを列挙することで、それらに関してもhooksを生成してくれる - http://localhost:3000/graphql # GraphQLのクエリを書いたファイル(詳しくは後述) documents: ["query.graphql"] generates: # generatorで作成したいファイル名 ./src/generated/graphql.ts: plugins: - typescript - typescript-operations - typescript-react-apollo config: # hooksを生成するための設定 withHOC: false withComponent: false withHooks: true # gqlgenのcustom scalarをstringとして扱う scalars: Time: string hooks: # ファイルが生成されたあとに、eslintのauto-fixを自動で走らせる afterOneFileWrite: - npx eslint --fixその他諸々の設定ファイル
package.json
package.json{ "name": "graphql-app-advent-calendar-2019", "version": "1.0.0", "description": "", "main": "src/index.tsx", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "serve-dev": "npx webpack-dev-server --config webpack.config.js --inline --hot --port=8081 --content-base dist --open-page ." }, "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.7.5", "@babel/preset-env": "^7.7.6", "@babel/preset-react": "^7.7.4", "@graphql-codegen/cli": "^1.9.1", "@graphql-codegen/typescript": "^1.9.1", "@graphql-codegen/typescript-operations": "^1.9.1", "@graphql-codegen/typescript-react-apollo": "^1.9.1", "@types/react": "^16.9.16", "@types/react-datepicker": "^2.9.5", "@types/react-dom": "^16.9.4", "@types/react-infinite-scroller": "^1.2.1", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", "@typescript-eslint/eslint-plugin": "^2.11.0", "@typescript-eslint/parser": "^2.11.0", "babel-eslint": "^10.0.3", "babel-loader": "^8.0.6", "babel-polyfill": "^6.26.0", "css-loader": "^3.3.2", "dotenv-webpack": "^1.7.0", "eslint": "^6.7.2", "eslint-config-prettier": "^6.7.0", "eslint-loader": "^3.0.3", "eslint-plugin-graphql": "^3.1.0", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.17.0", "eslint-plugin-react-hooks": "^2.3.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "prettier": "^1.19.1", "style-loader": "^1.0.1", "ts-loader": "^6.2.1", "typescript": "^3.7.3", "webpack": "^4.41.3", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.9.0" }, "dependencies": { "@apollo/react-hooks": "^3.1.3", "apollo-boost": "^0.4.7", "core-js": "^3.5.0", "date-fns": "^2.8.1", "graphql": "^14.5.8", "graphql-tag": "^2.10.1", "react": "^16.12.0", "react-app-polyfill": "^1.0.5", "react-datepicker": "^2.10.1", "react-dom": "^16.12.0", "react-infinite-scroller": "^1.2.4", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "semantic-ui-react": "^0.88.2" } }
.eslintrc.js
eslintrc.jsmodule.exports = { extends: [ "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "react-hooks", "plugin:prettier/recommended", "prettier/@typescript-eslint" ], plugins: ["@typescript-eslint", "react-hooks"], overrides: [ { files: ["**/*.tsx"], rules: { "react/prop-types": "off" } } ], parser: "@typescript-eslint/parser", env: { browser: true, node: true, es6: true }, parserOptions: { sourceType: "module" }, rules: { "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-explicit-any": 0, "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "react/display-name": 0, } };
tsconfig.json
tsconfig.json{ "compilerOptions": { /* Basic Options */ "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, "module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "lib": [ "es2018", "esnext", "dom" ] /* Specify library files to be included in the compilation. */, // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./frontend/dist" /* Redirect output structure to the directory. */, // "rootDir": "./frontend/src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "strictNullChecks": true /* Enable strict null checks. */, "strictFunctionTypes": true /* Enable strict checking of function types. */, "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, // "baseUrl": "./frontend" /* Base directory to resolve non-absolute module names. */, // "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": [ "node_modules/@types", ] /* List of folders to include type definitions from. */, "types": [ "node" ] /* Type declaration files to be included in compilation. */, // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ } // "include": ["frontend/src/**/*"], // "exclude": ["node_modules", "frontend/dist"] }
webpack.config.js
webpack.config.js/* eslint-disable @typescript-eslint/no-var-requires */ const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.tsx", output: { path: path.join(__dirname, "/dist"), publicPath: "/", filename: "[hash].js" }, plugins: [ new HtmlWebpackPlugin({ template: "src/html/index.html" }) ], resolve: { extensions: [".ts", ".tsx", ".js", ".jsx"], modules: [path.join(__dirname, "src"), path.join(__dirname, "node_modules")] }, module: { rules: [ { test: /\.(ts|tsx)$/, enforce: "pre", exclude: /node_modules/, use: ["eslint-loader"] }, { test: /\.(ts|tsx)?$/, exclude: /node_modules/, use: [ { loader: "babel-loader", options: { presets: [ ["@babel/preset-react"], [ "@babel/preset-env", { useBuiltIns: "usage", targets: ">0.25%", corejs: 3 } ] ] } }, { loader: "ts-loader", options: { configFile: "tsconfig.json", experimentalWatchApi: true } } ] }, { test: /\.css$/, loaders: ["style-loader", "css-loader?modules"] } ] } };
.node-version
(nodeのバージョンが8系とかだとGraphql code generatorでエラーが出ました)
12.13.1
2: GraphQLのスキーマファイルから、GraphQLを使うためのHooks、型定義を自動生成する
次に、GraphQL Code Generatorを使ってHooks、型定義を自動生成していきましょう。
これが一連の開発のなかで一番GraphQL!!!神!!!となる瞬間です。GraphQLのクエリを書いていく
この記事の前編で@ebknさんが作ってくれたスキーマを元に、実際に使うクエリを書いていきましょう。
ざっとこんな感じです。query.graphqlfragment taskFields on Task { id title notes completed due } query fetchTasks( $completed: Boolean $order: TaskOrderFields! $first: Int $after: String ) { tasks( input: { completed: $completed } orderBy: $order page: { first: $first, after: $after } ) { pageInfo { endCursor hasNextPage } edges { cursor node { ...taskFields } } } } mutation createTask( $title: String! $notes: String $completed: Boolean $due: Time ) { createTask( input: { title: $title, notes: $notes, completed: $completed, due: $due } ) { ...taskFields } } mutation updateTask( $taskID: ID! $title: String $notes: String $completed: Boolean $due: Time ) { updateTask( input: { taskID: $taskID title: $title notes: $notes completed: $completed due: $due } ) { ...taskFields } }GraphQL Code Generatorでコードを生成
以下のコマンドでコードを生成します
// APIサーバーを起動 $ cd backend && make start && cd ../frontend $ npx graphql-codegen ✔ Parse configuration ✔ Generate outputs生成されたコードがこちらです!と言いたいところなのですが、300行を超えるファイルなので、一部だけご紹介します。
このアプリのコードは公開しているので、気になる方は見てみてください。graphql.ts// ~~~省略~~~ export type Task = Node & { __typename?: "Task"; id: Scalars["ID"]; title: Scalars["String"]; notes: Scalars["String"]; completed: Scalars["Boolean"]; due?: Maybe<Scalars["Time"]>; }; // ~~~省略~~~ /** * __useFetchTasksQuery__ * * To run a query within a React component, call `useFetchTasksQuery` and pass it any options that fit your needs. * When your component renders, `useFetchTasksQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useFetchTasksQuery({ * variables: { * completed: // value for 'completed' * order: // value for 'order' * first: // value for 'first' * after: // value for 'after' * }, * }); */ export function useFetchTasksQuery( baseOptions?: ApolloReactHooks.QueryHookOptions< FetchTasksQuery, FetchTasksQueryVariables > ) { return ApolloReactHooks.useQuery<FetchTasksQuery, FetchTasksQueryVariables>( FetchTasksDocument, baseOptions ); } // ~~~省略~~~最高ですね、Taskの型定義や、fetchTasksのhooksが型付きで生成されています。
ちなみに、
query.graphql
を書くときに存在しないフィールドを書いていたり、必須フィールドを飛ばしていたりすると、コード生成のタイミングで以下のように怒ってくれます。$ npx graphql-codegen ✔ Parse configuration ❯ Generate outputs ❯ Generate ./src/generated/graphql.ts ✔ Load GraphQL schemas ✔ Load GraphQL documents ✖ Generate → at query.graphql:15:3 Found 1 error ✖ ./src/generated/graphql.ts AggregateError: GraphQLDocumentError: Field "tasks" argument "input" of type "TasksInput!" is required, but it was not provided. at query.graphql:15:3 at Object.checkValidationErrors (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-toolkit/commo n/index.cjs.js:295:15) at Object.codegen (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/core/index.cjs.js:1 01:16) at processTicksAndRejections (internal/process/task_queues.js:93:5) at async process (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:770:56) at async Promise.all (index 0) at async /Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:777:37 at async Task.task (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:570:17) AggregateError: GraphQLDocumentError: Field "tasks" argument "input" of type "TasksInput!" is required, but it was not provided. at query.graphql:15:3 at Object.checkValidationErrors (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-toolkit/commo n/index.cjs.js:295:15) at Object.codegen (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/core/index.cjs.js:1 01:16) at processTicksAndRejections (internal/process/task_queues.js:93:5) at async process (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:770:56) at async Promise.all (index 0) at async /Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:777:37 at async Task.task (/Users/shoheiihaya/myworks/graphql-app-advent-calendar-2019/frontend/node_modules/@graphql-codegen/cli/bin.js:570:17) Something went wrong良い...これでググっとヒューマンエラーが減らせそうです。
いよいよ次の章からコンポーネントを組み立てていきます。3: 自動生成されたコードを使ってReactコンポーネントを作っていく
3-0: 今回説明しないファイルに関して
この記事ではApollo Clientがメインのため、スタイルやhtmlに関しては説明しません。
リポジトリには上げていますので、必要に応じてご確認ください。3-1: Apollo Clientの初期化
まずはApollo Clientの初期化です。ついでにReact Routerも初期化します。尚今回使うパスの数は1つです。笑
// src/index.tsx import React from "react"; import ReactDom from "react-dom"; import { Router } from "react-router"; import ApolloClient from "apollo-boost"; import { ApolloProvider } from "@apollo/react-hooks"; import { createBrowserHistory } from "history"; const history = createBrowserHistory(); // ここでApollo Clientの初期化 const client = new ApolloClient({ uri: "http://localhost:3000/graphql" }); export default function App() { return ( <Router history={history}> <ApolloProvider client={client}> <div>something</div> </ApolloProvider> </Router> ); } ReactDom.render(<App />, document.getElementById("app"));GraphQLサーバーのエンドポイントを指定するだけです。
非常にシンプルですね。
もう少し色々書くと、Rest APIをGraphQLのクエリの中で呼び出せるapollo-link-rest用のRest APIエンドポイントもまとめて登録したり、websocketエンドポイントとつなぐこともできます。
もちろん、Authorization ヘッダを指定したり、401が帰ってきたときにjwtトークンを再発行するAPIを叩いてリトライ...なんてことも可能です。
公式のページ①, 公式のページ②等が参考になると思います。3-2: taskの一覧ページを作る
最初にコンポーネントの全体を貼ります。以下に要所要所で説明していきます。
src/components/Tasks.tsx
// src/components/Tasks.tsx import React, { useCallback, useState, useEffect, useMemo } from "react"; import { Header, Icon, List, Dimmer, Loader, Dropdown, DropdownProps } from "semantic-ui-react"; import InfiniteScroll from "react-infinite-scroller"; import CreateTaskModal from "./CreateTaskModal"; import UpdateTaskModal from "./UpdateTaskModal"; import CompletedIcon from "./CompletedIcon"; import { formatRelative } from "date-fns"; import ja from "date-fns/locale/ja"; import { useFetchTasksQuery, TaskOrderFields, Task } from "../generated/graphql"; import styles from "../styles/main.css"; type TaskFilterType = "all" | "completed" | "notCompleted"; const Tasks = () => { const [selectedTask, setSelectedTask] = useState<Task>(); const [fetchMoreLoading, setFetchMoreLoading] = useState(false); const [taskFilterType, setTaskFilterType] = useState<TaskFilterType>("all"); const [orderType, setOrderType] = useState<TaskOrderFields>( TaskOrderFields.Latest ); const handleTaskFilterTypeChange = useCallback( (_: React.SyntheticEvent<HTMLElement, Event>, data: DropdownProps) => { setTaskFilterType(data.value as TaskFilterType); }, [] ); const completedInput = useMemo(() => { switch (taskFilterType) { case "all": return null; case "completed": return true; case "notCompleted": return false; } }, [taskFilterType]); const handleOrderTypeChange = useCallback( (_: React.SyntheticEvent<HTMLElement, Event>, data: DropdownProps) => { setOrderType(data.value as TaskOrderFields); }, [] ); const { data, error, fetchMore, refetch } = useFetchTasksQuery({ variables: { order: TaskOrderFields.Latest, first: 5 }, fetchPolicy: "cache-and-network" }); useEffect(() => { refetch({ order: orderType, completed: completedInput, first: 5 }); }, [completedInput, orderType, refetch]); const refetchAfterAdd = useCallback(() => { refetch({ order: orderType, completed: completedInput, first: 5 }); }, [completedInput, orderType, refetch]); const handleLoadMore = useCallback(async () => { if (data && !fetchMoreLoading) { setFetchMoreLoading(true); await fetchMore({ variables: { after: data.tasks.pageInfo.endCursor, order: orderType, completed: completedInput, first: 5 }, updateQuery: (previousResult, { fetchMoreResult }) => { if (!fetchMoreResult) { return previousResult; } const newEdges = fetchMoreResult.tasks.edges; const pageInfo = fetchMoreResult.tasks.pageInfo; return { tasks: { ...previousResult.tasks, pageInfo, edges: [...previousResult.tasks.edges, ...newEdges] } }; } }); setFetchMoreLoading(false); } }, [completedInput, data, fetchMore, fetchMoreLoading, orderType]); const handleListItemClick = useCallback( (subscriber: Task) => () => { setSelectedTask(subscriber); }, [] ); const handleModalClose = useCallback(() => { setSelectedTask(undefined); }, []); if (!data) { return ( <Dimmer active={true}> <Loader>ロード中...</Loader> </Dimmer> ); } if (error) { return <div>エラー</div>; } return ( <div className={styles.main_content_box}> <Header color="teal" icon={true} textAlign="center"> <Icon name="tasks" /> <Header.Content>TODOs</Header.Content> </Header> <Dropdown options={[ { value: "all", text: "すべて" }, { value: "notCompleted", text: "未完了" }, { value: "completed", text: "完了済み" } ]} value={taskFilterType} onChange={handleTaskFilterTypeChange} fluid={true} selection={true} /> <div className={styles.order_dropdown}> <Dropdown options={[ { value: TaskOrderFields.Due, text: "期限順" }, { value: TaskOrderFields.Latest, text: "作成順" } ]} icon="sort amount up" value={orderType} onChange={handleOrderTypeChange} /> </div> <InfiniteScroll loadMore={handleLoadMore} hasMore={data.tasks.pageInfo.hasNextPage} loader={ <p style={{ textAlign: "center" }} key={0}> <Icon loading={true} name="spinner" /> </p> } > <List selection={true} divided={true}> {data.tasks.edges.map(task => task ? ( <List.Item key={task.node.id}> <CompletedIcon task={task.node} /> <List.Content onClick={handleListItemClick(task.node)}> <List.Header>{task.node.title}</List.Header> {task.node.due ? ( <List.Description> <Icon name="time" /> {formatRelative(new Date(task.node.due), new Date(), { locale: ja })}{" "} まで </List.Description> ) : null} </List.Content> </List.Item> ) : null )} </List> </InfiniteScroll> <CreateTaskModal refetch={refetchAfterAdd} /> {selectedTask !== undefined ? ( <UpdateTaskModal task={selectedTask} handleModalClose={handleModalClose} /> ) : null} </div> ); }; export default Tasks;
いきなり長いですね〜、少しずつ紐解いていきましょう。
3-2-1: taskのシンプルな取得
まずはTasksを取得する部分です。
const { data, error, fetchMore, refetch } = useFetchTasksQuery({ variables: { order: TaskOrderFields.Latest, first: 5 }, fetchPolicy: "cache-and-network" });
useFetchTasksQuery
が前のステップで自動生成してくれたHooksですね。
この引数として、queryの変数やfetchPolicy(キャッシュを優先するか、postするかを決める)を指定しています。
queryの変数というのは、query.graphql
で言うところのこれ↓です。
今回はpaginationのための変数です。
graphQLの仕様として、!
がついてないフィールドはnullable
ということで、省略しても大丈夫です。# ~~ 省略 ~~ query fetchTasks( $completed: Boolean <--- これ! $order: TaskOrderFields! <--- これ! $first: Int <--- これ! $after: String <--- これ! ) { tasks(orderBy: $order, page: { first: $first, after: $after }) { # ~~ 省略 ~~このHooksはコンポーネントが初期化するタイミングや、variablesの中身が変わったタイミングでfetchします。
fetchした結果やエラーの有無、今回は取っていませんがloadingなどが返り値としてゲットできます。(fetchMore, refetchに関しては後述)
ほしいtasksのデータはdata
の中です。次に、ロード状態の表示や、エラーの表示を記述します。
本来はloadingをhooksからもらうのが一般的ですが、諸々の都合で今回はdataが空の間はロード中とみなします。// ~~ 省略 ~~ if (!data) { return ( <Dimmer active={true}> <Loader>ロード中...</Loader> </Dimmer> ); } if (error) { return <div>エラー</div>; } // ~~ 省略 ~~それができたら次は、取得したtasksをリストとして表示しましょう。
配列として入っているので、シンプルにmapすれば完了です。
CompletedIconという謎のコンポーネントや、List.Contentに渡しているハンドラに関しては後述します。// ~~ 省略 ~~ <List selection={true} divided={true}> {data.tasks.edges.map(task => task ? ( <List.Item key={task.node.id}> <CompletedIcon task={task.node} /> <List.Content onClick={handleListItemClick(task.node)}> <List.Header>{task.node.title}</List.Header> {task.node.due ? ( <List.Description> <Icon name="time" /> {/* date-fnsのformatRelative関数は 「あとOO日」や「OO日前に投稿」のような相対的な時間をいい感じに表示してくれる。localオプションによるローカライズも可能。 */} {formatRelative(new Date(task.node.due), new Date(), { locale: ja })}{" "} まで </List.Description> ) : null} </List.Content> </List.Item> ) : null )} </List> // ~~ 省略 ~~ここまでで、シンプルな一覧の取得・表示はできました!
3-2-2: 無限スクロールでタスクを表示する
先程のHooksが返してくれた値の中に、
fetchMore
という関数があります。
これはPaginationのための関数で、これを呼び出すことで更にタスクを取得してきてくれます。
実際に使っている部分はここ↓です。再取得時の変数(variables)と、取得した際にどのように既存データとマージするかを定義した関数(updateQuery)を渡します。// ~~ 省略 ~~ await fetchMore({ variables: { after: data.tasks.pageInfo.endCursor, order: orderType, completed: completedInput, first: 5 }, updateQuery: (previousResult, { fetchMoreResult }) => { // previousResultがもともとのデータ、fetchMoreResultは再取得結果 if (!fetchMoreResult) { return previousResult; } const newEdges = fetchMoreResult.tasks.edges; const pageInfo = fetchMoreResult.tasks.pageInfo; // 新しいデータを組み立てて返す return { tasks: { ...previousResult.tasks, pageInfo, edges: [...previousResult.tasks.edges, ...newEdges] } }; } // ~~ 省略 ~~一旦これを定義すれば、あとはこの関数を再取得したいタイミングで呼び出すだけです。
今回はreact-infinite-scrollerを使っています。// ~~ 省略 ~~ <InfiniteScroll // loadMoreが再取得用の関数 loadMore={handleLoadMore} hasMore={data.tasks.pageInfo.hasNextPage} loader={ <p style={{ textAlign: "center" }} key={0}> <Icon loading={true} name="spinner" /> </p> } > // ~~ 省略 ~~無限スクロールとかそれっぽくていいですね〜
次はフィルタリングと並び替えをやっていきます!3-2-3: タスクのフィルタリングと並び替え
これも考え方は同じで、フィルタするための変数と並び替え用の変数をgraphQLに渡してfetchすればOKです。
ここでは、Hooksが返してくれるrefetch
を使っていきます。その名の通り、読んだタイミングで再取得する関数です。
useEffectを使って、変数が変わったときにrefetchを呼んでいます。// ~~ 省略 ~~ useEffect(() => { refetch({ order: orderType, completed: completedInput, first: 5 }); }, [completedInput, orderType, refetch]); // ~~ 省略 ~~いい感じに一覧を実装することができました!続いてタスクの作製用コンポーネントを作りましょう。
3-3: タスクの作成
CreateTaskModal.tsx
import React, { useCallback, useState } from "react"; import { Form, Modal, Button, Icon, Message, Checkbox } from "semantic-ui-react"; import { useCreateTaskMutation } from "../generated/graphql"; import { useTaskFields } from "../hooks/formHooks"; import sleep from "../lib/sleep"; import styles from "../styles/main.css"; import "react-datepicker/dist/react-datepicker-cssmodules.css"; import DatePicker, { registerLocale } from "react-datepicker"; import ja from "date-fns/locale/ja"; registerLocale("ja", ja); interface Props { refetch: () => void; } const CreateTaskModal = ({ refetch }: Props) => { const { titleProps, notesProps, completedProps, dueProps, clearValue } = useTaskFields(); const [success, setSuccess] = useState(false); const [open, setOpen] = useState(false); const handleMutationCompleted = useCallback(async () => { setSuccess(true); refetch(); await sleep(1500); clearValue(); setOpen(false); setSuccess(false); }, [clearValue, refetch]); const [createTask, { loading, error }] = useCreateTaskMutation({ variables: { title: titleProps.value, notes: notesProps.value, completed: completedProps.checked, due: dueProps.selected?.toISOString() }, onCompleted: handleMutationCompleted }); const handleButtonClick = useCallback(() => { createTask(); }, [createTask]); const handleOpen = useCallback(() => { setOpen(true); }, []); const handleClose = useCallback(() => { setOpen(false); }, []); return ( <Modal open={open} closeIcon={true} onClose={handleClose} onOpen={handleOpen} trigger={ <div className={styles.add_button}> <Button icon={true} size="tiny" basic={true} circular={true} positive={true} > <Icon name="plus" /> </Button> </div> } > <Modal.Header>タスクを追加</Modal.Header> <Modal.Content> <Form loading={loading} success={success} error={!!error}> <Message error={true}>追加中にエラーが発生しました</Message> <Message success={true}>タスクを追加しました</Message> <Form.Field required={true}> <label>タスク名</label> <Form.Input placeholder="ピーマンを買いに行く" type="text" required={true} {...titleProps} /> </Form.Field> <Form.Field> <label>メモ</label> <Form.Input placeholder="駅前のOKストアがマジで安い" type="text" {...notesProps} /> </Form.Field> <Form.Field> <label>完了</label> <Checkbox {...completedProps} /> </Form.Field> <Form.Field> <label>期限</label> <DatePicker {...dueProps} locale="ja" dateFormat="yyyy/MM/dd" /> </Form.Field> </Form> </Modal.Content> <Modal.Actions> <Button icon={true} onClick={handleButtonClick} positive={true} disabled={titleProps.value === ""} > <Icon name="plus" /> 追加する </Button> </Modal.Actions> </Modal> ); }; export default CreateTaskModal;
作成するときは、
useCreateTaskMutation
を使います。
useFetchTasksQuery
の同じように、variableにqueryの変数を渡します。
今回はonCompetedに関数を指定し、mutation終了後(作成完了後)にする処理を決めています。
handleMutationCompletedの中ではrefetch、formの値のクリアなどを行っています。// ~~ 省略 ~~ const handleMutationCompleted = useCallback(async () => { setSuccess(true); refetch(); await sleep(1500); clearValue(); setOpen(false); setSuccess(false); }, [clearValue, refetch]); // ~~ 省略 ~~ const [createTask, { loading, error }] = useCreateTaskMutation({ variables: { title: titleProps.value, notes: notesProps.value, completed: completedProps.checked, due: dueProps.selected?.toISOString() }, onCompleted: handleMutationCompleted }); // ~~ 省略 ~~なぜrefetchをするのか? / mutation実行時のキャッシュの更新に関して
Apollo Clientは、更新のmutationを走らせたときは自動で新しい値にキャッシュを更新してくれます。(一覧に表示されている更新したタスクが新しい値に書き換わる)
ですが、新規作成・削除した場合は自動で更新されません。もちろん画面をreloadすればそのタイミングでfetchが走るので、新しい値に書き換わります。ですが、UX的にはmutationを走らせたときにキャッシュが更新されたほうが良いでしょう。
それを実現するために、mutationのHooksはupdate
関数を引数として受け取ります。
その関数の中で手動でキャッシュを書き換えます。イメージとしては、先程実装したfetchMoreと同じような感じです。以下のupdate関数は正しく動かないので、コードの雰囲気を感じるだけにしてください。
update: (cache, { data }) => { if (!data) return; // 新しく作られたタスク const createdTask = data.createTask; // readQuery関数で既存のキャッシュを取ってくる const tasksQuery = cache.readQuery< FetchTasksQuery, FetchTasksQueryVariables >({ query: FetchTasksDocument, variables: { ...fetchTaskParam } }); if (!tasksQuery) return; // writeQuery関数で既存のキャッシュに新しいデータをマージして書き込む cache.writeQuery<FetchTasksQuery, FetchTasksQueryVariables>({ query: FetchTasksDocument, variables: { ...fetchTaskParam }, data: { ...tasksQuery, tasks: { ...tasksQuery.tasks, edges: [ ...tasksQuery.tasks.edges, { node: createTask } ] } } }); }一見、単純なように見えますが、これがなかなか難しい問題を抱えています。
- connectionの形式でキャッシュに入れないといけないが、cursorとかはサーバーから貰っていない
- ソートや絞り込みがかかった状態で、どこに新しいタスクを追加すべきか問題がある(ただ末尾につけるだけでいいのか?)
1に関しては必要な情報をサーバー側に返してもらう、というのが解決策の一つです。このサイトなどで紹介されています。
2は同じようなことを議論しているフォーラムがありますが、特に結論は出ていないようです。。。
ソートとかをフロント側でやるのも違う。。。シンプルな一覧表示においてはupdate関数を使うのがベストですが、このような微妙な場合は多少のオーバーヘッドを犠牲にrefetchするのもありかな、ということで今回はシンプルにrefetchしています。
3-4: タスクの更新
UpdateTaskModal.tsx
import React, { useCallback, useState } from "react"; import { Form, Modal, Button, Icon, Message, Checkbox } from "semantic-ui-react"; import { useUpdateTaskMutation, Task } from "../generated/graphql"; import { useTaskFields } from "../hooks/formHooks"; import sleep from "../lib/sleep"; import "react-datepicker/dist/react-datepicker-cssmodules.css"; import DatePicker, { registerLocale } from "react-datepicker"; import ja from "date-fns/locale/ja"; registerLocale("ja", ja); interface Props { task: Task; handleModalClose: () => void; } const CreateTaskModal = ({ task, handleModalClose }: Props) => { const { titleProps, notesProps, completedProps, dueProps, clearValue } = useTaskFields(task); const [success, setSuccess] = useState(false); const handleMutationCompleted = useCallback(async () => { setSuccess(true); await sleep(1500); clearValue(); handleModalClose(); }, [clearValue, handleModalClose]); const [updateTask, { loading, error }] = useUpdateTaskMutation({ variables: { taskID: task.id, title: titleProps.value, notes: notesProps.value, completed: completedProps.checked, due: dueProps.selected?.toISOString() }, onCompleted: handleMutationCompleted }); const handleButtonClick = useCallback(() => { updateTask(); }, [updateTask]); return ( <Modal open={!!task} closeIcon={true} onClose={handleModalClose}> <Modal.Header>タスクを編集</Modal.Header> <Modal.Content> <Form loading={loading} success={success} error={!!error}> <Message error={true}>保存中にエラーが発生しました</Message> <Message success={true}>タスクを編集しました</Message> <Form.Field required={true}> <label>タスク名</label> <Form.Input placeholder="ピーマンを買いに行く" type="text" required={true} {...titleProps} /> </Form.Field> <Form.Field> <label>メモ</label> <Form.Input placeholder="駅前のOKストアがマジで安い" type="text" {...notesProps} /> </Form.Field> <Form.Field> <label>完了</label> <Checkbox {...completedProps} /> </Form.Field> <Form.Field> <label>期限</label> <DatePicker {...dueProps} locale="ja" dateFormat="yyyy/MM/dd" /> </Form.Field> </Form> </Modal.Content> <Modal.Actions> <Button icon={true} onClick={handleButtonClick} positive={true} disabled={titleProps.value === ""} > <Icon name="plus" /> 保存する </Button> </Modal.Actions> </Modal> ); }; export default CreateTaskModal;
CompletedIcon.tsx
import React from "react"; import { Icon, Message } from "semantic-ui-react"; import { useUpdateTaskMutation, Task } from "../generated/graphql"; interface Props { task: Task; } const CreateTaskModal = ({ task }: Props) => { const [updateTask, { error }] = useUpdateTaskMutation({ variables: { taskID: task.id, completed: !task.completed } }); if (error) { return <Message error={true}>更新に失敗しました</Message>; } return task.completed ? ( <Icon name="check circle" color="green" size="big" onClick={updateTask} /> ) : ( <Icon name="check circle" size="big" onClick={updateTask} /> ); }; export default CreateTaskModal;
最後に、タスクの更新です。
今回は、一覧画面上のチェックアイコンをクリックすると完了・未完了のtoggleができる機能と、モーダルを出して諸項目をまとめて更新するものを作っています。更新は、
useUpdateTaskMutation
を使います。このHooksの使い方はcreateとだいたい同じです。// UpdateTaskModal.tsx const [updateTask, { loading, error }] = useUpdateTaskMutation({ variables: { taskID: task.id, title: titleProps.value, notes: notesProps.value, completed: completedProps.checked, due: dueProps.selected?.toISOString() }, onCompleted: handleMutationCompleted }); // CompletedIcon.tsx const [updateTask, { error }] = useUpdateTaskMutation({ variables: { taskID: task.id, completed: !task.completed } });更新の場合はキャッシュが自動で書き換わるので、update等を気にする必要はありません!これで一通り完成です!
ここまでで、React/Apollo Client/Typescriptを使ったフロントエンド実装ができました。
型定義を自分でする必要なく、Hooksで取得できる値に型が付いていることでエディタの補完もはかどり、書き心地は抜群です。
まだ発展途上の技術ではありますが、どんどんキャッチアップして、素敵なGraphQLライフを送っていきましょう。
- 投稿日:2019-12-18T00:35:06+09:00
React-VisでReact-Friendlyなデータビジュアライズ
はじめに
この記事はReact#2 Advent Calendar 2019 18日目の記事です。
タイトル通り、Reactでのデータ可視化に関する内容になっています。
ダッシュボードを初めとしたデータビジュアライズの絡む開発をReactでするなら、React-Visってライブラリもなかなか良いよ!ということで書きました。個人的な背景としては、Reactベースでのプロダクト開発において、Reactのライフサイクルやコンポーネント設計に合わせて作られた可視化ライブラリはないのかな?、と思い探していたところ見つけたのがReact-Visなので、似たような思いをお持ちの方がいたら参考になるかもしれません?(元々は似た理由でrechartsを使っていました)
React-Visとは?
Uber社のOpen Source Projectの1つで、githubに公開されています。Star 6.6k(2019/12/18時点)でなかなかと思うのですが、日本語記事が探しても見つからなかったので、使っている方がいたら教えて欲しいですね。
github: https://github.com/uber/react-vis
公式HP: https://uber.github.io/react-vis/なぜReact-Vis?
データを可視化するライブラリならD3やChartJS, ThreeJSなどの有名どころがあって、それらの表現力はReact-Vis以上の部分も多く、事足りてはいます。それならわざわざReact特化のライブラリを使う必要などないケースも多いでしょうが、個人的には
1.React Componentとして記述し構成できる点
2.パフォーマンスに魅力を感じています。
1つ目は、グラフの構成要素(ラベルや軸の範囲やアニメーションなど)にComponentと同様でStateを仕込めば、Stateの更新によってグラフの描画もインタラクティブに更新できます。React Componentと同じ感覚で設計して記述できるのがありがたいですね。
2つ目は、感覚的な話ですが、SPAで要素が1,000や10,000~とかのデータポイント多めのグラフを複数描画していく際のパフォーマンスに違いを感じます。ただこの辺はバックエンドでの処理も重要ですし、比較検証テストをちゃんとした訳でもないので、違いは追々書きたいと思います。また、Reactベースでプロダクト開発をしている身としては、React-Visが掲げる下記の原則にも魅力を感じました。
(意訳)
[React-friendly]
React-VisはReact Componentと同様に機能する設計となっており、properties, children, callbacksをもって構成できる[High-level and customizable]
React-Visはシンプルなコードとデフォルトの設定でも複雑なチャートを作成することができるが、個々のパーツをあなたが好きな様にカスタマイズすることもできる[Industry-strong]
React-VisはUberの様々な内部ツールをサポートする目的で開発されている全て自分がライブラリに求めていたものですが、3番目の[Industory strong]が自分にとっては結構重要でした。
Uberのプロダクトが特徴として持つ、地理空間情報や機械学習というビッグデータの可視化と隣合わせの状況で、それらに対するパフォーマンスを重視して開発されている(であろう)ライブラリというのは魅力的です。
(個人事情ですが、少なからず類似性のあるプロダクトを扱っているため)逆にデメリットというか、不安な点としては、まだTypescriptに対応していないことですかね。。有志で
react-vis.d.ts
を提供してくれてる方がいますが、@typesはなしなので、時々自分で型を付けています。どんな感じで書けるの?
前置きが長くなりましたが、最小構成で代表的なグラフを書いてみます。
LineSeries(折れ線グラフ)
import React from "react"; import { XYPlot, LineSeries } from "react-vis"; interface SamplePropsTypes { width: number; height: number; } interface DataTypes { x: number; y: number; } const SampleLine = (props: SamplePropsTypes) => { const data: DataTypes[] = [ { x: 0, y: 18 }, { x: 1, y: 19 }, { x: 2, y: 20 }, { x: 3, y: 21 }, { x: 4, y: 22 }, { x: 5, y: 23 }, { x: 6, y: 24 }, { x: 7, y: 25 }, ]; return ( <div> React Advent2 18th <XYPlot width={props.width} height={props.height}> <LineSeries data={data} /> </XYPlot> </div> ); };VerticalBarSeries(棒グラフ 縦ver)
// 上記コードに追加・変更 import { VerticalBarSeries } from "react-vis"; return ( <div> React Advent2 18th <XYPlot width={props.width} height={props.height}> <VerticalBarSeries data={data} /> </XYPlot> </div> )HorizontalBarSeries(棒グラフ 横ver)
// 上記コードに追加・変更 import { HorizontalBarSeries } from "react-vis"; const dataHorizontal: DataTypes[] = [ { y: 0, x: 18 }, { y: 1, x: 19 }, { y: 2, x: 20 }, { y: 3, x: 21 }, { y: 4, x: 22 }, { y: 5, x: 23 }, { y: 6, x: 24 }, { y: 7, x: 25 }, ]; return ( <div> React Advent2 18th <XYPlot width={props.width} height={props.height}> <HorizontalBarSeries data={dataHorizontal} /> </XYPlot> </div> );Horizontalな棒グラフは軸が入れ替わるので、入力するデータのx, yも入れ替わるのが少しややこしいですね。
各パーツがReact Componentとしてexportされているので、リファレンスラインの挿入なんかも
LineSeries
を使って組み込める部分など、直感的でよいなーと思います。リファレンスラインの挿入
return ( <div> React Advent2 18th <XYPlot width={props.width} height={props.height}> <VerticalBarSeries data={data} /> <LineSeries data={data} /> </XYPlot> </div> );ちょっと描画をリッチにするとこんな感じです。
import React, { useState, useEffect } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Button from "@material-ui/core/Button"; import { HorizontalBarSeries, XYPlot, XAxis, YAxis, Crosshair, } from "react-vis"; import { descSort, ascSort } from "./utils"; import { sample1, sample2, sampleLabel } from "./sampleData"; const useStyles = makeStyles(theme => ({ crosshair: { color: "white", backgroundColor: "black", width: 20, opacity: 0.7, paddingLeft: 10, }, })); interface SamplePropsTypes { width: number; height: number; } interface DataTypes { x: number; y: number; } const SampleVis = (props: SamplePropsTypes) => { const classes = useStyles(); const sampleLabel = ["R", "e", "a", "c", "t", "-", "A", "C"]; const [crosshairValues, setCrosshairValues] = useState<any>({}); const [data, setData] = useState<DataTypes[]>([]); const [isLabel, setLabel] = useState<boolean>(false); const [isSort, setSort] = useState<boolean>(false); const [labelList, setLabelList] = useState<string[]>([]); const [top, setTop] = useState<number>(0); const [yLabel, setYLabel] = useState<number | null>(null); const getData = (direction: string) => { if (direction === "vertical") { setData(sample1); } else if (direction === "horizontal") { setData(sample2); } }; useEffect(() => { getData("horizontal"); }, []); useEffect(() => { if (isLabel) { setLabelList(sampleLabel); } else { setLabelList([]); } }, [isLabel]); useEffect(() => { const tmpLabel = labelList.slice().reverse(); if (isSort) { data.sort((a: any, b: any) => ascSort(a.x, b.x)); data.map((val: any, index: number) => (val.y = index)); setLabelList(tmpLabel); } else { data.sort((a: any, b: any) => descSort(a.x, b.x)); data.map((val: any, index: number) => (val.y = index)); setLabelList(tmpLabel); } }, [isSort]); const onMouseLeave = () => { setCrosshairValues({}); setYLabel(null); setTop(0); }; const onNearestX = (_value: any, { event, innerX, innerY, index }: any) => { console.log(`${innerY} | ${innerX} | ${index}`); setYLabel(index); setTop(event.offsetY); setCrosshairValues(data[index]); }; return ( <div> <Button onClick={() => setLabel(!isLabel)}>Change Label</Button> <Button onClick={() => setSort(!isSort)}>Change Sort</Button> {data.length > 0 ? ( <XYPlot width={props.width} height={props.height} onMouseLeave={onMouseLeave} > <XAxis title="React Advent2" /> <YAxis title="18th" tickFormat={v => { if (v > labelList.length) { return null; } return labelList[v]; }} /> <HorizontalBarSeries data={data} color="skyblue" onNearestXY={onNearestX} /> {yLabel != null ? ( <Crosshair values={[crosshairValues]}> <div className={classes.crosshair} style={{ position: "absolute", top: top - 30 }} > <p>{labelList[yLabel]}</p> <p>{data[yLabel].x}</p> </div> </Crosshair> ) : ( <div /> )} </XYPlot> ) : ( "" )} </div> ); };一気にパーツを増やしてしまいましたが、React Componentに慣れている方にはなかなか書きやすそうではないでしょうか?
これらの基本的なグラフ以外にも、様々なグラフ(散布図、面積図、ツリーマップ、ネットワーク、、)が用意されているので、描画に困ることはなさそうです。今回は省略していますが、フィルターなどのインタラクティブな操作ロジックを組む部分は、Hooksなどのおかげでより書きやすいのではなかろうかと思っています。
おわりに
React-Visによるデータビジュアライズの簡単な紹介をさせて頂きました。
ライブラリの原則通り各パーツがReact Componentとしてexportされているので、JSX内のXyPlot
に色々なコンポーネントを差し込んでいくことで、任意のパーツを組み込んだグラフが作れるのはとてもよいです。また、1つ1つのパーツに余計なものがついておらず、分離されており自由度が高いのもよいですね。逆にコードの記述量が増えがちというのはあるかも?そこはうまく汎化させていきたいところです。
ただ、ドキュメントでカバーされていない部分もたまにあり、カスタマイズする際にソースを読みにいかなければよく分からないこともあるのが玉に瑕ですが、React Componentとして設計されているので、把握しやすいといえば把握しやすいです。
Reactでデータの可視化を扱う際は、是非使ってみてください。
- 投稿日:2019-12-18T00:03:57+09:00
React-Bootstrapを使えるようにする
初めにreactのプロジェクトを作る
npx create-react-app my-react-project
プロジェクトのroot直下に移動してcd my-react-project
インストールしてnpm install react-bootstrap bootstrap
index.htmlに以下を記載して<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
使いたい要素をインポートして使う
App.jsimport React, { Component } from 'react'; import Button from 'react-bootstrap/Button'; class App extends Component { render() { return ( <div> <Button> ボタンだよ </Button> </div> ); } } export default App;macで`の打ち方初めて知った。[ option + Shift + _ ]
参考サイト
本家
- 投稿日:2019-12-18T00:01:52+09:00
jQuery/React/Vue.js/Svelte/StimulusのTodoアプリをつくる
※この記事はMisoca+弥生 AdventCalendar2019の18日目のエントリーです。
そうだ!フロントエンドを勉強しよう!
昨今のフロントエンドは難しいですよね。私も何もわかりません。
特にフレームワーク・ライブラリが多すぎます。
jQueryが良いって聞いてたのに、ReactやらVue.js使って当然みたいな雰囲気ですし、最近だとSvelteやら、Basecampが作ったStimulusというのも登場してるらしいですよ。
とはいえ、嘆いていても始まりません。
まずは簡単なアプリをつくってそれぞれの実装を勉強してみましょう!!
Todoアプリをつくってみよう!
フロントエンドのFWを学びたければ、息をするより先にTodoアプリを作れって誰かが言ってました。
Todoアプリは動的な要素の追加や削除、さらにコレクションの描画が必要となるので入門にはぴったりですね!!
よーし、がんばるぞ〜!
できたもの
そういうわけで、jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました!!
まずは、jQueryで動くTodoアプリです!!
→ https://jumble-todo.netlify.com/
しっかりタスクの追加もできますね!
いかがでしたか?
このようにjQueryを駆使することで簡単にTodoアプリを作ることができました。
jQueryはお手軽に使えて便利ですね!
さて、私は
jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました
とお伝えしていますので、残りのTodoアプリもご紹介したいと思います。
...おや?
なんだか、このjQueryのTodoアプリのページ、下の方にまだ何かスクロールできますね...
(不穏な音)
おもむろにこのコードをコピーして、ChromeDevToolsのコンソールで実行してみましょう。
すると...
Vue.jsのTodoアプリになりました
Vue DevToolsでも、ちゃんとVue.jsで動作していることが確認できます。
そして、この画面も全体を表示すると...
はい。もうここまで来たらこの後の流れはあなたの予想している通りです。
このコードをコピーして同じようにDevToolsのコンソールで実行してみると...
ReactのTodoアプリになりました。
React Developer ToolsのComponentsタブでも確認できますね。
そしてこのページのコードも実行してみると..
SvelteのTodoアプリになりました。
(※DevToolsとかを動かせてないので、Svelteっぽさが表現できてないのはスイマセン)
さらにコードを実行してみます。
StimulusjsのTodoアプリになりました。
data-actionなどの属性で動作してるのがそれっぽいですね。
そして最後に、StimulusjsのTodoアプリのページのコードを実行すると...
jQueryのTodoアプリに戻ってきます。
このページのコードを実行すると再度Vue.jsのTodoアプリとなり、以降はループします。
また、最後に実行したコードは、ページ自体を初期表示した際に実行されるコードと完全に一致するようになっています。
いかがでしたか?
jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを(縛りとして、単一の
index.html
のみで完結し、かつコードが循環することを条件に)作ってみたというエントリでした!
いやー、Todoアプリは入門にぴったりですね。勉強になりました。
コードについて
はい、すいませんここからが本編です。
半分ネタみたいな内容にお付き合いいただきありがとうございました。
実際の動作ページやコードは以下で確認できます。
- https://jumble-todo.netlify.com/
- mugi-uno/jumble-todo: jQuery/Vue/React/Stimulus Todo App on single source file
以前RubyKaigiでTRICKという超絶技巧プログラミングを見て感動して、同じレベルのものは無理でも、何か自分でも作ってみたいな〜と思ってやってみた、というのがこのエントリの本質です。
今回作成したのは、いわゆるQuineと呼ばれるものの一種です。
クワイン(英: Quine)は、コンピュータプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムである。娯楽として、プログラマが任意のプログラミング言語での最短クワインを書くことがある。プログラムを出力するプログラムだと見れば、クワインのプログラミングはメタプログラミングの一種である。
クワイン (プログラミング) - Wikipedia
自身のソースコードと完全に同じ文字列を出力する
という記述があったので、一応コードを実行するたびにHTMLのみだけではなく、console.logでもソースコードを出力しています。動作の仕組み
実際に動いているコードは若干のMinifyをしています。
圧縮前のコードはこちらです↓
https://github.com/mugi-uno/jumble-todo/blob/master/index.full.html長々書いてありますが、結局のところ次のような構造です。
const num = 0; // 実行するFW const html = ["jQuery用のHTML", "Vue用のHTML", ...]; const scripts = ["jQuery用のJS", "Vue用のJS", ...]; const nextScriptTemplate = "次に実行するJSのテンプレート"; // 〜このあたりの処理で「自分自身と同じ構造のコード」をnextScriptTemplateをもとに構築〜 const script = scripts[num].replace("_NEXTHTML_", nextHtml); // 完成したscriptを実行 window.eval(script);「自分自身と同じ構造のスクリプトを文字列上で構築して、それをevalで実行する」を繰り返しているのみで、実はさほど難しいことはやっていません。
なお、html
とscripts
の配列の内容を順に実行する仕組みで、自由に増やしたり減らしたりできます。(AngularやElmのコードを実装してくれてもいいんですよ!)実はどこでも動く
見た目をある程度整えるためにデモページを用意しましたが、スクリプトはデモページじゃなくても動きます。(すでに読み込まれているスクリプトやheadタグの内容によっては動作しない可能性もあります)
では、実際に試してみましょう。
(※本エントリ内では紹介のために私自身が管理している別ページを利用しています。もし同様に試す場合は迷惑のかからないページで自己責任にて実行してください。)
たとえば、私はToyama.rbというコミュニティを主催しており、公式ページは次のURLです。
こちらのページでDevToolsを開いてコードを実行してみます。
ソリャー!!
はい、動きましたね。bodyタグのみを書き換えるので、スタイルシートはもとのページのhead内に定義されているものを引き継いでいます。
ハマったところ
せっかくなので今回のコードを作るにあたってハマった点をいくつか紹介します。
React動かない問題
「別にJSX使わなくてもReact使ったと言っても良いのでは..?」と一瞬思いましたが、負けた気がしたのでテンプレート部は絶対にJSXで書くことにしました。
幸い、standalone版のbabelが存在するので、そちらを利用すればいけるかな〜と思ってました。
https://reactjs.org/docs/add-react-to-a-website.html#quickly-try-jsxが、今回のコードに適用した場合、サンプルのままでは動作しませんでした。
結果として、
「動的に挿入するstandalone版のbabelを使った場合に、同じく動的に挿入されるscript(type=text/jsx)タグの内容が動きません!!」
という、恐らくこの世で誰も困ってないし解決しても二度と役に立たない問題に激突しました。同じ質問されても「いますぐその方法をやめてwebpackでビルドしてください」と答えると思います。
結果的には、自力で
Babel.transformScriptTags()
を呼ぶだけで良かったのですが、やってることが日常と違いすぎて、辿り着くまでに結構ハマりました。エスケープ地獄
「スクリプトを文字列化してevalする」という前提があるので、何らかのエスケープはほぼ必須です。
最悪のケースでは、「evalで実行可能な文字列から出力されるscriptタグ内のJSXの中に表示する文字列の中のevalで実行可能な文字列の中の
\
」 のエスケープが必要でした。自分でも何を言ってるのかわかりません。さらに、実行対象のFWによって必要なエスケープ回数も異なるため、頭がクラッシュします。
最終的には、スクリプトを一旦Base64化した上で、eval実行されるスクリプト内でそれをDecodeする方法を取ることで回避しました。
妥協した点
完璧に作れたわけではなく、いくつか妥協しています。
- Vue.jsのSFCをブラウザ上でコンパイルして実行したかった
- Svelteは諦めてビルドしたものを直接埋め込んでる
- コードがでかい
Quine...奥が深いぜ...!
まとめ
というわけで、JSを駆使してQuineを書いてみた、というエントリでした。
仕事でコードを書くのも楽しいですが、たまにはこういうトリッキーなことをしてみるのも新鮮で面白いので、興味が湧いた方はぜひ遊んでみてください!
Misoca+弥生 Advent Calendar 2019の次回19日目は、@issiによる業務効率化の話とのことです。
私のエントリからガラリとかわり、現実で役に立ちそうな内容で楽しみですね!
- 投稿日:2019-12-18T00:01:52+09:00
(縛りプレイ)jQuery/React/Vue.js/Svelte/StimulusのTodoアプリをつくる
※この記事はMisoca+弥生 AdventCalendar2019の18日目のエントリーです。
そうだ!フロントエンドを勉強しよう!
昨今のフロントエンドは難しいですよね。私も何もわかりません。
特にフレームワーク・ライブラリが多すぎます。
jQueryが良いって聞いてたのに、ReactやらVue.js使って当然みたいな雰囲気ですし、最近だとSvelteやら、Basecampが作ったStimulusというのも登場してるらしいですよ。
とはいえ、嘆いていても始まりません。
まずは簡単なアプリをつくってそれぞれの実装を勉強してみましょう!!
Todoアプリをつくってみよう!
フロントエンドのFWを学びたければ、息をするより先にTodoアプリを作れって誰かが言ってました。
Todoアプリは動的な要素の追加や削除、さらにコレクションの描画が必要となるので入門にはぴったりですね!!
よーし、がんばるぞ〜!
できたもの
そういうわけで、jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました!!
まずは、jQueryで動くTodoアプリです!!
→ https://jumble-todo.netlify.com/
しっかりタスクの追加もできますね!
いかがでしたか?
このようにjQueryを駆使することで簡単にTodoアプリを作ることができました。
jQueryはお手軽に使えて便利ですね!
さて、私は
jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを作ってみました
とお伝えしていますので、残りのTodoアプリもご紹介したいと思います。
...おや?
なんだか、このjQueryのTodoアプリのページ、下の方にまだ何かスクロールできますね...
(不穏な音)
おもむろにこのコードをコピーして、ChromeDevToolsのコンソールで実行してみましょう。
すると...
Vue.jsのTodoアプリになりました
Vue DevToolsでも、ちゃんとVue.jsで動作していることが確認できます。
そして、この画面も全体を表示すると...
はい。もうここまで来たらこの後の流れはあなたの予想している通りです。
このコードをコピーして同じようにDevToolsのコンソールで実行してみると...
ReactのTodoアプリになりました。
React Developer ToolsのComponentsタブでも確認できますね。
そしてこのページのコードも実行してみると..
SvelteのTodoアプリになりました。
(※DevToolsとかを動かせてないので、Svelteっぽさが表現できてないのはスイマセン)
さらにコードを実行してみます。
StimulusjsのTodoアプリになりました。
data-actionなどの属性で動作してるのがそれっぽいですね。
そして最後に、StimulusjsのTodoアプリのページのコードを実行すると...
jQueryのTodoアプリに戻ってきます。
このページのコードを実行すると再度Vue.jsのTodoアプリとなり、以降はループします。
また、最後に実行したコードは、ページ自体を初期表示した際に実行されるコードと完全に一致するようになっています。
いかがでしたか?
jQuery/React/Vue.js/Svelte/Stimulusそれぞれを利用してTodoアプリを(縛りとして、単一の
index.html
のみで完結し、かつコードが循環することを条件に)作ってみたというエントリでした!
いやー、Todoアプリは入門にぴったりですね。勉強になりました。
コードについて
はい、すいませんここからが本編です。
半分ネタみたいな内容にお付き合いいただきありがとうございました。
実際の動作ページやコードは以下で確認できます。
- https://jumble-todo.netlify.com/
- mugi-uno/jumble-todo: jQuery/Vue/React/Stimulus Todo App on single source file
以前RubyKaigiでTRICKという超絶技巧プログラミングを見て感動して、同じレベルのものは無理でも、何か自分でも作ってみたいな〜と思ってやってみた、というのがこのエントリの本質です。
今回作成したのは、いわゆるQuineと呼ばれるものの一種です。
クワイン(英: Quine)は、コンピュータプログラムの一種で、自身のソースコードと完全に同じ文字列を出力するプログラムである。娯楽として、プログラマが任意のプログラミング言語での最短クワインを書くことがある。プログラムを出力するプログラムだと見れば、クワインのプログラミングはメタプログラミングの一種である。
クワイン (プログラミング) - Wikipedia
自身のソースコードと完全に同じ文字列を出力する
という記述があったので、一応コードを実行するたびにHTMLのみだけではなく、console.logでもソースコードを出力しています。動作の仕組み
実際に動いているコードは若干のMinifyをしています。
圧縮前のコードはこちらです↓
https://github.com/mugi-uno/jumble-todo/blob/master/index.full.html長々書いてありますが、結局のところ次のような構造です。
const num = 0; // 実行するFW const html = ["jQuery用のHTML", "Vue用のHTML", ...]; const scripts = ["jQuery用のJS", "Vue用のJS", ...]; const nextScriptTemplate = "次に実行するJSのテンプレート"; // 〜このあたりの処理で「自分自身と同じ構造のコード」をnextScriptTemplateをもとに構築〜 const script = scripts[num].replace("_NEXTHTML_", nextHtml); // 完成したscriptを実行 window.eval(script);「自分自身と同じ構造のスクリプトを文字列上で構築して、それをevalで実行する」を繰り返しているのみで、実はさほど難しいことはやっていません。
なお、html
とscripts
の配列の内容を順に実行する仕組みで、自由に増やしたり減らしたりできます。(AngularやElmのコードを実装してくれてもいいんですよ!)実はどこでも動く
見た目をある程度整えるためにデモページを用意しましたが、スクリプトはデモページじゃなくても動きます。(すでに読み込まれているスクリプトやheadタグの内容によっては動作しない可能性もあります)
では、実際に試してみましょう。
(※本エントリ内では紹介のために私自身が管理している別ページを利用しています。もし同様に試す場合は迷惑のかからないページで自己責任にて実行してください。)
たとえば、私はToyama.rbというコミュニティを主催しており、公式ページは次のURLです。
こちらのページでDevToolsを開いてコードを実行してみます。
ソリャー!!
はい、動きましたね。bodyタグのみを書き換えるので、スタイルシートはもとのページのhead内に定義されているものを引き継いでいます。
ハマったところ
せっかくなので今回のコードを作るにあたってハマった点をいくつか紹介します。
React動かない問題
「別にJSX使わなくてもReact使ったと言っても良いのでは..?」と一瞬思いましたが、負けた気がしたのでテンプレート部は絶対にJSXで書くことにしました。
幸い、standalone版のbabelが存在するので、そちらを利用すればいけるかな〜と思ってました。
https://reactjs.org/docs/add-react-to-a-website.html#quickly-try-jsxが、今回のコードに適用した場合、サンプルのままでは動作しませんでした。
結果として、
「動的に挿入するstandalone版のbabelを使った場合に、同じく動的に挿入されるscript(type=text/jsx)タグの内容が動きません!!」
という、恐らくこの世で誰も困ってないし解決しても二度と役に立たない問題に激突しました。同じ質問されても「いますぐその方法をやめてwebpackでビルドしてください」と答えると思います。
結果的には、自力で
Babel.transformScriptTags()
を呼ぶだけで良かったのですが、やってることが日常と違いすぎて、辿り着くまでに結構ハマりました。エスケープ地獄
「スクリプトを文字列化してevalする」という前提があるので、何らかのエスケープはほぼ必須です。
最悪のケースでは、「evalで実行可能な文字列から出力されるscriptタグ内のJSXの中に表示する文字列の中のevalで実行可能な文字列の中の
\
」 のエスケープが必要でした。自分でも何を言ってるのかわかりません。さらに、実行対象のFWによって必要なエスケープ回数も異なるため、頭がクラッシュします。
最終的には、スクリプトを一旦Base64化した上で、eval実行されるスクリプト内でそれをDecodeする方法を取ることで回避しました。
妥協した点
完璧に作れたわけではなく、いくつか妥協しています。
- Vue.jsのSFCをブラウザ上でコンパイルして実行したかった
- Svelteは諦めてビルドしたものを直接埋め込んでる
- コードがでかい
Quine...奥が深いぜ...!
まとめ
というわけで、JSを駆使してQuineを書いてみた、というエントリでした。
仕事でコードを書くのも楽しいですが、たまにはこういうトリッキーなことをしてみるのも新鮮で面白いので、興味が湧いた方はぜひ遊んでみてください!
Misoca+弥生 Advent Calendar 2019の次回19日目は、@issiによる業務効率化の話とのことです。
私のエントリからガラリとかわり、現実で役に立ちそうな内容で楽しみですね!