Prettierについて調査
最近エディタをsublimeからvscodeに変更しました。
その中でPrettierも適用することになったので、こいつのことを理解しようとおもうようになりました。
はじめはimport内のタブサイズが4になってたりと思うどおりに動かなくて断念かと思ったが、再インストールしてようやく想定したとおりに動いてくれたので、安心。
目次
- Prettierとはなにか?
- eslintと併用する方法
- なぜeslint --fixではダメなのか? わざわざprettierを利用する意味はなんだ?
- vscodeでprettierを利用する
まずは、npm scripts経由でprettierの挙動を確かめ、その後vscodeで設定する方法を紹介していきたい。
Prettierとはなにか?
Prettierは、ソースをいい感じに整形してくれるツールである。
linterと非常によく似たポジションだと思える。
これを使えばプロジェクト内のコーディングスタイルを統一的にできるわけだ。
自分であっても先週と今週の書き方が異なったりするのでその差をなくしてくれる。
eslintと併用する
早速Prettierをつかってみよう
1. インストール
何はともあれ、必要なものをインストール。
eslintのインストールと設定は、以前の記事をみてほしい。
npm i -D prettier npm install eslint-config-prettier eslint-plugin-prettier -D
2. eslintの設定にprettierの設定を追加する
.eslintrc.json
{ "extends": [ "airbnb", "plugin:prettier/recommended" // <- prettierを使うことを宣言 ], "rules": { "prettier/prettier": [ "always", // prettierのオプションをここに入れる { "singleQuote": true, "trailingComma": "es5", "semi": true, "bracketSpacing": true } ] } }
3. 動作確認
npm scriptsにprettierを登録して、npm run fmt
で実行してみる。
修正前
// flow const obj = { key1: 'singleQuote', // 問題なくシングルウォートを利用してる key2: "doubleQuote", // 誤ってダブルクォートを利用してる key3: "tabSize4", // タブのサイズが4と異なる key4: "notComma" // 最後に,がない }; const {key1, key2} = obj; // 中括弧内にスペースがない // 無駄な改行がある // 1行が長すぎる関数 function doSomething(argument1: Array<Object>, argument2: boolean, argument3: boolean): string { return 'result' // 最後にセミコロンなし }
修正後
// flow const obj = { key1: 'singleQuote', key2: 'doubleQuote', key3: 'tabSize4', key4: 'notComma', }; const { key1, key2 } = obj; function doSomething( argument1: Array<Object>, argument2: boolean, argument3: boolean ): string { return 'result'; }
以上のようにコード上に問題はないが、みやすさ的な保証を担保してくれるわけだ。
なぜeslint --fixではダメなのか? 何が違うのか?
How does it compare to ESLint/TSLint/stylelint, etc.?にあるように、
linterが適用するルールには2つのタイプがあって、
prettierはその一方を楽にするもの。
2タイプというのは、
1. Formatting rules(eg: max-len, keyword-spacing, ...)
2. Code-quality rules(eg: no-unused-vars, no-extra-bind, ...)
のことであり、prettierが扱うのは前者のほうで、
コードの品質的に問題はないが、チーム内でのスタイルルールが異なる問題を自動的に解決してくれるものといわけだ。
たとえば公式の説明をつかえば、次のコードはコード上問題ない。
foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());
だからlinterで修正はされないが、読みにくさというものはのこる。 これをprettierならば、次のように整形してくれるわけだ。
foo( reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne() );
vscodeのprettierの設定
1. 拡張機能のインストール
ext install esbenp.prettier-vscode
2. prettier設定反映
設定方法は、以下の通り複数存在するが、今回は3の設定を紹介する.
エディタに依存することなく、npm scriptsで確実に動作をするのを確認してから進められ、さらにeslintrcの設定をそのまま活かしたいからである。
3. 保存時にprettier適用
最後に、ファイル保存時にprettierが自動的に実行されるように設定。
settings.json
... "prettier.eslintIntegration": true, "[javascript]": { "editor.formatOnSave": true }, ...
以上で完了。あとはいつもどおりに書けばよい。 prettierのoptionについてはまた調べよう。
参考サイト
- Prettier 入門 ~ESLintとの違いを理解して併用する~
- 公式サイト
WebpackのdevServerを使ってもサーバと通信できるフロント開発がしたい
はじめに
自分はReactやReduxを使ってWebアプリを開発するときにWebpackのdevServerにお世話になる。devServerを使えば、ソース変更時に自動でブラウザをリロードしてくれて便利だからである。
しかし、devServerを使った場合、localhostで動かしてるwebサーバと通信できない。たとえば、devServerは、http://localhost:8080で起動する。
そこから、http://localhost:8000で動作するwebサーバとHTTP通信することは異なるオリジンにアクセスすることになってしまい通信は失敗する。
localhost:8080とlocalhost:8000は、異なるオリジンなので前者から後者に対して通信することは普通できないのである。
ローカルでwebサーバを動かす一方でフロント開発でdevServerを使ってるとき、フロントがサーバからデータ取得できるようにするにはどうすればよいのだろうかと壁にぶつかった。異なるオリジンであっても通信できるようにするにはどうすればよいのだろうかということである。
なので今回は、異なるオリジンであってもサーバと通信できる設定について備忘録を残そうと思う。
目次
- expressで簡単なwebサーバの準備
- 同一オリジンから通信できることの確認
- 別オリジンから通信の失敗確認
- expressのCORS設定
- SessionありCORS
1. expressで簡単なwebサーバの準備
nodejsのexpressを使用する。
expressでのwebサーバ構築方法は、割愛させていただきます。
npm i -S express
でexpressをインストール
次のapp.js
を用意する。
app.js
const express = require('express'); const app = express(); // "/"にアクセスしたときにres.jsonの引数値を返す app.get('/', (req, res) => { res.json({ result: 'success', message: 'hello express', }); }); // 8000ポートで起動 const server = app.listen(8000, () => { console.log(`Node.js is listening to PORT: ${server.address().port}`); });
あとは、node app.js
(あるいはnpm scriptsに記述してnpm start
)でサーバを起動させる。すると、http://localhost:8000にアクセスしたときにres.json()の引数に指定したjsonを受け取ることができる。
これでまずサーバサイドの実装は終了。
2. 同一オリジンから通信できることの確認
ブラウザを起動させ、urlにhttp://localhost:8000と入力すると、 先程のjsonが画面に表示される。
さらに、fetchメソッドも使って取得できることも確認しよう。
console画面に次を入力して取得できることを確認する。
fetch('/', { method: 'GET', headers: { 'content-type': 'appliaction/json' } }).then(r => r.json()).then(r => { console.log(r); }) // コンソール出力値 // {result: "success", message: "hello express"}
3. 別オリジンから通信の失敗確認
グーグルトップページ(https://wwww.google.co.jp)を開いて、同じくコンソール画面に次を入力する
// ドメインが違うので、URLを省略せずに書く fetch('http://localhost:8000', { method: 'GET', headers: { 'content-type': 'appliaction/json'} }).then(r => r.json()).then(r => { console.log(r); })
fetchの第一引数は、http://localhost:8000になる。
"/"は、https://www.google.co.jpを意味するからだ。
その結果は次のとおりである。
やはり、ドメインが異なるため通信失敗する。 エラー内容にある通り、HTTPリクエストのヘッダーに'Access-Control-Allow-Origin'が存在しないからダメだよと言われるている。(他にもヘッダーに足りない情報がある)
4. expressのCORS設定
異なるオリジンからの通信も許可するために、サーバサイドに変更を加える。
app.js
const express = require('express'); const app = express(); // ------------- ここから -------------- app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', "*"); // (1) res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // (2) res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); // (3) next(); }); // ------------- ここまで -------------- app.get('/', function(req, res) { res.json({ result: 'success', message: 'hello express', }); }); const server = app.listen(8000, () => { console.log(`Node.js is listening to PORT: ${server.address().port}`); });
追加した内容については、
(1): どんなオリジンからの通信を許可("*"はすべてを意味する)
(2): 指定するheader情報の通信を許可
今回fetchメソッドでは、headersに'content-type': 'appliaction/json'を指定してるため
(3): GETからOPTIONSまでのHTTPメソッドを使って通信すること許可
ということになる。
これでもういちど、fetchメソッドを実行すると取得できるようになる(やったね)。
5. SessionありCORS(おまけ)
session情報をもたせるときは、もう少し改良が必要になる。 変更箇所のみ記述する
app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', req.headers.origin); // (1) res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); res.header('Access-Control-Allow-Credentials', true); // (2) next(); });
(1): "*"ではなく、リクエストからオリジン情報を取得し、そのからの通信を許可
(2): sessionを使うときはこれを追加してあげる
これでsessionありの通信も可能となるが、最後にフロントでのfetchメソッドも確認しておく。
fetch('http://localhost:8000', { method: 'GET', headers: { 'content-type': 'application/json' }, credentials: 'include', // (3) }).then(r => r.json()).then(r => { console.log(r); })
(3)のとおり、credentailをfetchメソッドのオプションに追加する。
最後に
これでサーバと通信できるフロント開発ができるように助けになってくれれば幸いである。非常に駆け足で説明してきたので、かなり読みにくい文章になってしまった。申し訳ない(あとでもう少し読みやすくしようと思います。)
参考サイト
http://var.blog.jp/archives/60055220.html https://qiita.com/MuuKojima/items/2b2e7bc0db8d5e97ada9 https://blog.kazu69.net/2017/03/23/http-request-using-cors/
Higher Order Componentについて調査
はじめに
Higher Order Componentについて学習。
atomic Designと呼ばれるものを実現するよい手法なのだろうと感じ、
使い方を少し学ぼうと思ったので、その記録を。。。
ゴール
- MyButtonを拡張して、MyCustomButtonコンポーネントをつくる。
ポイントは、styleをどうやって拡張させることができるのかということである。
MyButtonについて
MyButtonは、赤色の単純なボタンコンポーネントである。
MyButton.jsx
// @flow import * as React from 'react'; type PropsType = { children: React.Element<any>, style: Object, }; // デフォルトstyle情報 const defaultStyle = { backgroundColor: 'red', }; export default function MyButton(props: PropsType): React.Node { const { children, style } = props; // デフォルトstyleとpropsで指定されたstyle情報を結合 const Style = { ...defaultStyle, ...style, }; return ( <button type="button" style={Style} {...props} > {children} </button> ); }
defaultスタイルとpropsから渡ってきたsytleを結合するのがポイント。
MyCustomButton
MyCustomButtonは、MyButtonの特徴を受け継ぎながら独自のスタイルをもったボタン。 背景色が青色になり、フォントサイズや文字色のstyle情報が新たに追加される。
MyCustomButton.jsx
// @flow import * as React from 'react'; import MyButton from './MyButton'; // Enhance関数は、Componentを引数にとり、コンポーネントを返す関数を返す function Enhance(Component): (Object) => React.Node { return props => ( <Component {...props} style={{ backgroundColor: 'blue', fontSize: '50px', color: 'white', }} /> ); } // HOC const MyCustomButton = Enhance(MyButton); export default MyCustomButton;
Enhance関数のように、関数を返す関数は、高階関数(Higher Order function)と呼ばれる。
このHigher OrderであるEnhance関数によりできた新たなコンポーネントこそ
Higer Order Componentということになる。
childrenがpropsに存在するが、HTML要素にchildren属性はつかなく、またMyCustomButton内では、propsをそのまま伝達するだけでよいので感心する。
App.js
ReactDOM.render( <div> <MyButton>PUSH!!</MyButton> <MyCustomButton onClick={() => {console.log('CUSTOM')}} > CUSTOM </MyCustomButton> </div>, document.getElementById('root'), );
各ボタンの表示結果は以下の通り。 MyCustomButtonで追加されているonClickも問題なく動作する。
気になること
class属性が付与されると、どちらのスタイルが優先されるのだろうか? これについても調べていこう。
React DnD tutorial翻訳してもっと理解を!!
はじめに
React DnDは、Reactアプリでドラッグ&ドロップを楽に実装できるライブラリである。
React DnDのOverviewの翻訳を前回記述した。
そこで、Overviewを呼んだだけでは実装イメージがつかめなく、使い方がわからないということを指摘をした。
なので、今回は、公式Tutorialの肝となる部分を翻訳して実装助けになるものがかけたらと思う。Tutorialでは、チェスアプリをつくる。ナイトピースのみドラッグ&ドロップできるようにするものだ(Tutorialなので、実装するのは1ピースのみ)。
Adding the Drag and Drop Interactionというセクションに入るまでは、Reactの説明なのでその翻訳なし。それまでのソースにどうやってReact DnDを注入するのかがポイントなので、本セクション以降を例の通り自分がわかるように翻訳してみた。
ただし、セクションまでに作成されたソースは下に記します。
翻訳
Adding the Drag and Drop Interaction
npm install -S react-dnd react-dnd-html5-backend
はじめにセットアップすることは、DragDropContext
である。アプリでHTML5 backendを使うことを特定させるのに必要なのである。
Board
はアプリ内の最上位コンポーネントなので、これにDragDropContext
を付与する。
Board.jsx
import React, { Component } from 'react'; import { DragDropContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; @DragDropContext(HTML5Backend) // HTML5Backendを使用することを引数で指定 export default class Board extends Component { /* ... */ }
次に、ドラッグできる固定のItemTypeを作成する。今回のアプリではKNIGHT
というただ一つのItemTypeをもたせるだけだ。Constants
モジュールとしてしようできるようにExportさせている。
Constants.jsx
export const ItemTypes = { KHNIGHT: 'knight' };
これで準備は整った。Knight
をドラッグさせよう!
HOCであるDragSource
は3つのパラメータを持つ。type
,spec
とcollect
である。
typeは先程定義したものなので、ドラッグ対象仕様とcollect関数をコーディングする必要がある。Knight
のドラッグ対象の仕様はあまりにも簡単である。
Knight.jsx
const knightSource = { beginDrag(props) { return {}; } };
というのは、何も記述することがないからである。全くもって、アプリ内でドラッグできるものはただ一つなのだから。複数のチェスピースを利用するなら、props
や{ pieceID: props.id }
のような返り値を利用するのは良いアイデアであろうが、今回の場合空であっても十分なのである。
次に、collect関数をしあげよう。Knight
に必要なpropsは何だろうか? ドラッグ対象ノードを特定する方法が必要なのはわかるだろう。
さらに、ドラッグ中にKnight
の透明度がわずかにぼやけるようにするのもよいだろう。だから、ドラッグ状態にあるかかどうかを知ることも必要になる。
そこで、collect関数は以下のようになる。
Knight.jsx
function collect(connect, monitor) { return { connectDragSource: connect.dragSource(), isDragging: monitor.isDragging() } }
それではKnightコンポーネント全体をみて、DragSource
が呼ばれていたり、render
関数が変更されているのを確認しよう。
Knight.jsx
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ItemTypes } from './Constants'; import { DragSource } from 'react-dnd'; const knightSource = { beginDrag(props) { return {}; } }; function collect(connect, monitor) { return { connectDragSource: connect.dragSource(), isDragging: monitor.isDragging() } } @DragSource(ItemTypes.KNIGHT, knightSource, collect) export default class Knight extends Component { static propTypes = { connectDragSource: PropTypes.func.isRequired, isDragging: PropTypes.bool.isRequired }; render() { const { connectDragSource, isDragging } = this.props; return connectDragSource( <div style={{ opacity: isDragging ? 0.5 : 1, fontSize: 25, fontWeight: 'bold', cursor: 'move' }}> ♘ </div> ); } }
これでKnight
はドラッグ対象となったが、まだドロップイベントに反応してくれるドロップ先は存在しない。なので、Square
をドロップ先にしよう。
ここで、Square
コンポーネントに位置情報を持たせないようにすることはできない。というのも、Square
コンポーネントは、自分の場所を知らずして、ドラッグされてるナイトをどこに移動すべきかにわかるはずがないからだ。しかしその一方で、Square
コンポーネント自体、アプリ内で変化することはないし、シンプルに使用されてるのだから、なぜ複雑にするのかと歯がゆさも依然として感じてしまう。このジレンマに直面したときは、smart and dumb components
の考えを利用するときだ。
BoardSquare
という新しいコンポーネントを紹介しよう。それは、前のSquare
コンポーネントをその通りにrenderするが、位置情報も意識されて作られている。実際、BoardSquare
コンポーネントは、Board
コンポーネント内のrenderSquare
メソッドで先程行った幾分のロジックを包み込んでいる。
それではBoardSquare
コンポーネントを見てみよう。
BoardSquare.jsx
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Square from './Square'; export default class BoardSquare extends Component { static propTypes = { x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }; render() { const { x, y } = this.props; const black = (x + y) % 2 === 1; return ( <Square black={black}> {this.props.children} </Square> ); } }
続いて、Board
コンポーネントも変更する
Board.jsx
renderSquare(i) { const x = i % 8; const y = Math.floor(i / 8); return ( <div key={i} style={{ width: '12.5%', height: '12.5%' }}> <BoardSquare x={x} y={y}> {this.renderPiece(x, y)} </BoardSquare> </div> ); } renderPiece(x, y) { const [knightX, knightY] = this.props.knightPosition; if (x === knightX && y === knightY) { return <Knight />; } }
BoardSquare
をDropTarget
で包もう。drop
イベントだけを処理するようにドロップ先の仕様を書こう。
BoardSquare.jsx
const squareTarget = { drop(props, monitor) { moveKnight(props.x, props.y); } };
わかるだろうか? drop
メソッドは、BoardSquare
のprops
を受け取るので、ドロップされたときにナイトをどこに移動させるかわかるのである。本番のアプリ開発では、monitor.getItem()
も利用して、ドラッグ対象のbeginDrag
メソッドが返すdrag itemを受け取れるが、このアプリではドラッグ対象がひとつだけなので不要である。
collect関数では、ドロップ先のノードと結びつける関数を第1引数に与え、第2引数にもmonitorを渡して、現在カーソルがBoardSquare
上にあるかどうかでハイライトさせるのに利用する。
BoardSquare.jsx
function collect(connect, monitor) { return { connectDropTarget: connect.dropTarget(), isOver: monitor.isOver() }; }
ドロップ先を結びつけ、カーソルが上に置かれたときにハイライトするようにrender
関数を変更すると、BoardSquare
は次のようになる。
BoardSquare.jsx
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Square from './Square'; import { canMoveKnight, moveKnight } from './Game'; import { ItemTypes } from './Constants'; import { DropTarget } from 'react-dnd'; const squareTarget = { drop(props) { moveKnight(props.x, props.y); } }; function collect(connect, monitor) { return { connectDropTarget: connect.dropTarget(), isOver: monitor.isOver() }; } @DropTarget(ItemTypes.KNIGHT, squareTarget, collect) export default class BoardSquare extends Component { static propTypes = { x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, connectDropTarget: PropTypes.func.isRequired, isOver: PropTypes.bool.isRequired }; render() { const { x, y, connectDropTarget, isOver } = this.props; const black = (x + y) % 2 === 1; return connectDropTarget( <div style={{ position: 'relative', width: '100%', height: '100%' }}> <Square black={black}> {this.props.children} </Square> {isOver && <div style={{ position: 'absolute', top: 0, left: 0, height: '100%', width: '100%', zIndex: 1, opacity: 0.5, backgroundColor: 'yellow', }} /> } </div> ); } }
これで順調なスタートである! このチュートリアルを完成させるためにはあともう一つ変更がある。移動可能な場所におかれたときにBoardSquare
をハイライトさせ、またそのときにのみドロップを処理させたい。
ありがたいことに、React DnDを使えば楽勝である。ドロップ先を特定するオブジェクトにcanDrop
メソッドを定義するだけである。
BoardSquare.jsx
canDrop(props) { return canMoveKnight(props.x, props.y); }
collect関数に、monitor.canDrop()
も追加する。
BoardSquare.jsx
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Square from './Square'; import { canMoveKnight, moveKnight } from './Game'; import { ItemTypes } from './Constants'; import { DropTarget } from 'react-dnd'; const squareTarget = { canDrop(props) { return canMoveKnight(props.x, props.y); }, drop(props) { moveKnight(props.x, props.y); } }; function collect(connect, monitor) { return { connectDropTarget: connect.dropTarget(), isOver: monitor.isOver(), canDrop: monitor.canDrop() }; } @DropTarget(ItemTypes.KNIGHT, squareTarget, collect) export default class BoardSquare extends Component { static propTypes = { x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, connectDropTarget: PropTypes.func.isRequired, isOver: PropTypes.bool.isRequired, canDrop: PropTypes.bool.isRequired }; renderOverlay(color) { return ( <div style={{ position: 'absolute', top: 0, left: 0, height: '100%', width: '100%', zIndex: 1, opacity: 0.5, backgroundColor: color, }} /> ); } render() { const { x, y, connectDropTarget, isOver, canDrop } = this.props; const black = (x + y) % 2 === 1; return connectDropTarget( <div style={{ position: 'relative', width: '100%', height: '100%' }}> <Square black={black}> {this.props.children} </Square> {isOver && !canDrop && this.renderOverlay('red')} {!isOver && canDrop && this.renderOverlay('yellow')} {isOver && canDrop && this.renderOverlay('green')} </div> ); } }
Final Touches
このチュートリアルで、Reactコンポーネントを作成したり、コンポーネントのアプリのデータ層の仕様を決めたり、さらにドラッグ&ドロップ作用を追加できるようになった。我々がこれらを通じて狙っていることは、React DnDがReact哲学に非常にフィットしていることや、複雑なドラッグ&ドロップ作用の実装に入る前にアプリのアーキテクチャについてまず考えるようになることを示すことである。
最後のデモとして,ドラッグ中のプレビューカスタマイズを紹介しよう。ブラウザは、DOMノードのスクリーンショットをプレビューとして利用するのだが、別のものを表示したい場合どうすればよいのだろうか?
ラッキーなことに、React DnDを使えば簡単である。connect.dragPreview()
をKnight
のcollect関数に追加するだけで十分である。
Knight.jsx
function collect(connect, monitor) { return { connectDragSource: connect.dragSource(), connectDragPreview: connect.dragPreview(), isDragging: monitor.isDragging() } }
これでconnectDragSource
と同じように、render関数内でconnectDragPreview
が使えたり、またcomponentDidMount
内であってもカスタムイメージを使えるようになる。
Knight.jsx
componentDidMount() { const img = new Image(); img.src = 'data:image/png:base64,xxxxxxxxx'; img.onload = () => this.props.connectDragPreview(img); }
それでは、ドラッグ&ドロップを楽しみあれ〜
最後に
Overviewで紹介されたTypeやcollect関数、connector, monitorがどのように実装されていくのかが気づけたらいいなと思う。今後はチェスアプリにピースを追加するやり方や、他のExampleに挑戦しもっと実装方法について理解し説明できたらと思う。
最後まで読んでくれてありがとうございます!
React DnD注入までのソース全体
ディレクトリ構成は次の通り
. ├── dist │ └── index.html ├── package.json ├── src │ ├── App.js │ ├── Board.jsx │ ├── Game.js │ ├── Knight.jsx │ └── Square.jsx └── webpack.config.js
各ファイルは下記の通り。flowを導入したので、 公式のソースとは若干異なる。
App.js
// @flow import React from 'react'; import ReactDOM from 'react-dom'; import Board from './Board'; import { observe } from './Game'; const rootEl = document.getElementById('root'); observe(knightPosition => ReactDOM.render( <Board knightPosition={knightPosition} />, rootEl ) );
Board.jsx
// @flow import * as React from 'react'; import Square from './Square'; import Knight from './Knight'; import { canMoveKnight, moveKnight } from './Game'; type PropsType = { knightPosition: Array<number>, }; export default class Board extends React.Component<PropsType> { handleSquareClick(toX: number, toY: number) { if (canMoveKnight(toX, toY)) { moveKnight(toX, toY); } } renderSquare(i: number): React.Node { const { knightPosition } = this.props; const x = i % 8; const y = Math.floor(i / 8); const black = (x + y) % 2 === 1; const [knightX, knightY] = knightPosition; const piece = (x === knightX && y === knightY) ? <Knight /> : null; return ( <div key={i} style={{ width: '12.5%', height: '12.5%' }} role="presentation" onClick={() => this.handleSquareClick(x, y)} > <Square black={black}> {piece} </Square> </div> ); } render(): React.Node { const squares = []; for (let i = 0; i < 64; i += 1) { squares.push(this.renderSquare(i)); } return ( <div style={{ width: '100%', height: '100%', display: 'flex', flexWrap: 'wrap', }} > {squares} </div> ); } }
Game.jsx
// @flow let knightPosition = [1, 7]; let observer: any = null; function emitChange() { observer(knightPosition); } export function observe(o: (any) => any) { if (observer) { throw new Error('Multiple observers not implemented.'); } observer = o; emitChange(); } export function moveKnight(toX: number, toY: number) { knightPosition = [toX, toY]; emitChange(); } export function canMoveKnight(toX: number, toY: number): boolean { const [x, y] = knightPosition; const dx = toX - x; const dy = toY - y; return (Math.abs(dx) === 2 && Math.abs(dy) === 1) || (Math.abs(dx) === 1 && Math.abs(dy) === 2); }
Knight.jsx
// @flow import * as React from 'react'; export default class Knight extends React.Component { render(): React.Node { return <span>♘</span>; } }
Square.jsx
// @flow import * as React from 'react'; type PropsType = { black: boolean, children: ?React.Element<any>, }; export default class Square extends React.Component<PropsType> { render(): React.Node { const { black, children } = this.props; const fill = black ? 'black' : 'white'; const stroke = black ? 'white' : 'black'; return ( <div style={{ backgroundColor: fill, color: stroke, width: '100%', height: '100%', }} > {children} </div> ); } }
Decorator機能をつかってるのでwebpackの設定も載せておきます。
webpack.config.js
const path = require('path'); const config = { mode: 'production', entry: './src/app.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'app.js', }, devServer: { contentBase: './dist', port: 3000, inline: true, }, devtool: 'source-map', module: { rules: [{ test: /\.jsx?$/, exclude: path.resolve(__dirname, 'node_modules'), loader: 'babel-loader', query: { presets: ['react', 'env'], plugins: ['transform-class-properties', 'transform-decorators-legacy'], }, }, { test: /\.css$/, exclude: path.resolve(__dirname, 'node_modules'), loader: ['style-loader', 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]'], }], }, resolve: { extensions: ['.js', '.jsx'], }, }; module.exports = config;
React DnDの公式Overviewを訳して理解を深めたい!
React環境で、ドラッグ&ドロップ操作をどうやって扱えばよいのだろうかと思ってると、 React DnDというライブラリを見つけた。便利そうではあるが、いくつかのブログを見てもよくわからないし、DecoratorやHigher Order Componentの考え方も登場し正直難しいと感じた。
だから、公式ページのOverviewを翻訳しながら、少しでも理解を深めていこうと思い、 その記録をブログに残そうと思う。翻訳は、適当である。自分が理解できるようにしか直訳していない。翻最終的に自分の頭で理解できる表現になっていれば多少ニュアンスが違っても翻訳と呼べるだろうと思ってる。
はじめに
翻訳してみて感じたことをはじめに記しておこうと思う。Overviewを呼んでも正直React DnDが使えるようになるぐらいになるとは思えなかった。Overviewで登場するいくつかの概念をぼんやりわかりながら、tutorialの実装をやってみることでOverviewの効果がでてくるし、React DnDの使い方もわかるようになった気がする。
だから、僕のOverviewを呼んでさっぱりわからなかったとしても絶望せず、tutorialまで手を進めてもらえばそこではじめて何かつかめるものがあるのではないかと思う。
後々紹介されるが、僕のようにせっかちな人向けに、
React DnDにおいて重要な概念を先に載せておこうと思う。
- drap source, drop target(ドラッグされたもの、ドラッグされたものがドロップされる先)
- ItemとType(識別子・分類子)
- monitor関数(ドラッグ&ドロップ状態を管理する)
- connect関数(イベントをどのDOMと結びつけるか決める)
- HOCとES7のデコレータアノテーション(既存コンポーネントとうまく結びつける)
注意: 公式では、ES6,7ごとの書き方が紹介されているが、ここではES7のみ記載。
翻訳
Overview
React DnDは、ちまたの多くのdrag&dropライブラリと異なり、今まで使用したことがなかったらきっと驚かせるものであろう。しかし、ひとたびReact DnDのいくつかのデザインコンセプトを味わえば、理解できるようになる。後述のドキュメントでこららのコンセプトを読むことをおすすめする。
これらのコンセプト中には、FluxやReduxの考え方と共通する部分がある。 これは偶然ではなく、React DnDは、Reduxを内部で使用してる。
Backends
React DnDは、HTML5 drag and drop APIをもとに作られている。このAPIを使えば、ドラッグ中のDOMノードのスクリーンショットが取れたり、それを"ドラッグプレビュー"として利用できるため、合理的な手抜きなのである。カーソルが動いてるときに何も描画する必要がないので便利である。またこのAPIは、ファイルドロップイベントを扱える唯一のものでもある。
残念なことに、HTML5 drag and drop APIには、いくつかの欠点もある。APIは、タッチスクリーンでは機能しないし、他のブラウザと比べると、IEではカスタマイズできることが少ない。
そういうわけで、React DnDでは、プラグイン的な方法で、HTML5 drag and drop機能を可能にしている。そのプラグインを使う必要はない。タッチイベントや、マウスイベント、その他のすべてを含んだ別の実装をすることができる。このようなプラグイン的な実装を、React DnDではbackendsと呼んでいる。HTML5 backendはライブラリにすぎず、今後もっと多く機能が追加されるかもだろう。
backendsの役割は、Reactの仮想的なイベントシステムとよく似ている: ブラウザの差異を抽象化しネイティブのDOMイベントを処理する。類似するといっても、React DnD backendsは、Reactやその仮想的なイベントシステムに依存していない。backendsがやっていることは、DOMイベントをReact DnDが扱えるように内部で使用してるReduxのactionに丁寧に変換してるだけなのである。
Items and Types
Flux (あるいはRedux)のように、React DnDは信頼できる情報としてデータを扱い、ビューを使用しない。何かをドラッグしたとき、コンポーネントやDOMがドラッグされていると表現しない。代わりに、特定のtypeのitemがドラッグされていると表現する。
itemとは何か? itemはJavascriptのプレーンオブジェクトで、何がドラッグされているかを記述したものである。たとえば、カンバンボードアプリの場合、カードをドラッグしてるとき、itemは{cardId: 42}
のように表現される。チェスアプリならば、あるピースを持ち上げたとき、itemは{fromCell: 'C5', piece: 'queen'}
のように表現される。プレーンオブジェクトとしてドラッグ情報を表現することでコンポーネントを分けて、ごっちゃにしてしまわないようにできるのである。
typeとは何か? typeは、アプリ内にある全てのitemクラスのどのクラスに分類されるか一意に特定してくれる文字列(あるいはシンボル)である。カンバンボードアプリの場合、ドラッグできるカードを示すものとしてcard
typeを持たせたり、ドラッグできるカードの一覧をlist
typeをもたせたりするであろう。チェスアプリならば、piece
typeだけしか持たないかもしれない。
Typeが役に立つのは、アプリが大きくなったとき、もっと多くのものをドラッグさせたいが、既存のドロップ先が新しいitemに反応してほしくないときである。Typeを使うことでどのドラッグ元とどのドロップ先が対応するのかを特定させられる。Reduxのaction typeの一覧をもつように、React DnDでもtypeの一覧をアプリ内でもつようになるであろう。
Monitors
ドラッグ&ドロップは、本質的にステートフルである。ドラッグ操作中は、進行中という状態であり、ドラッグ操作をやめると進行中でなくなる。TypeとItemも同じであり、どこかにstateをもたせなければならいない。
React DnDは、内部stateストレージを備えた少規模のラッパーを通じてコンポーネントにstateをもたせており、そのストレージをmonitorsと呼んでいる。monitorsによって、ドラッグ&ドロップのstateが変更されるたびに、コンポーネントのpropsを変更できるようになる。
コンポーネントがドラッグ&ドロップ状態を追跡できるように、monitorsから適切なstateを取り出せるcollect関数を定義できる。そしてReact DnDは、適切なタイミングでcollecting関数を呼び出したり、その返り値をコンポーネントのpropsにマージすることに力を注いでくれる。
たとえば、ピースがドラッグされているときに、チェスのある盤面をハイライトさせたいとする。そのとき、Cell
コンポーネントのcollect関数は、次のようになるだろう。
function collect(monitor) { return { highlighted: monitor.canDrop(), hovered: monitor.isOver() }; }
collect関数から指示をうけ、React DnDはすべてのCellコンポーネントに最新のハイライト
やホバー
状態をpropsとして伝達する。
Connectors
backendはDOMイベント扱うが、ReactコンポーネントがDOM描画するならば、どのDOMノードから発したイベントに反応すればよいのかをbackendが知る術はあるのだろうか。 そこでconnectorsである。connectorsによって、DOMノードはrender
関数内で、事前に決められた(ドラッグ元か、ドラッグプレビューか、ドロップ先か)役割に割り当てられる。
訳しても、ちょっと意味不明なので、もう少し自分の言葉で表現すると、 ドラッグされたり、ドロップされたときに、どのDOMからイベントが発生したのか、backendにはわからない。 だから、connectorを使ってどのDOMが対象なのかを決めようということ。
実装時、connectorは上述したcollect関数の第一引数となる。connectorによっていかにしてドロップ先を特定するのか見てみよう。
function collect(connect, monitor) { return { highlighted: monitor.canDrop(), hovered: monitor.isOver(), connectDropTarget: connect.dropTarget() // drop先であることを決める関数 }; }
コンポーネントのrenderメソッド内で、monitorから得られるデータ(1)や、connectorから得られる関数(2)の両方にアクセスできることがわかる。
1とはmonitor.canDrop()やmonitor.isOver()のことで、Drag&Dropの状態がわかる。 2とは、connect.dropTarge()のことであり、これは関数を返してる
render() { const { highlighted, hovered, connectDropTarget } = this.props; return connectDropTarget( <div className={classSet({ 'Cell': true, 'Cell--highlighted': highlighted, 'Cell--hovered': hovered })}> {this.props.children} </div> ); }
connectDropTarget
で包んであげることで、React DnDはコンポーネントのルートDOMノードがドロップ先だと理解でき、ホバーイベントやドロップイベントがbackendで処理されるだろうとわかる。そのメカニズムとして、Reactが提供してるのcallback refが利用されている。connector関数の戻り値関数はメモリに保存されるため、shouldComponentUpdate
による最適化の邪魔しない。
Drag Sources and Drop Targets
これまで、DOMイベントを扱うbackendsやItemやTypeで表現されるデータや、monitor関数やconnector関数のおかげで、React DnDがコンポーネントにどんなpropsを注入すべきかをcollect関数が表現できることを説明してきた。
しかし実際propsをコンポーネントに注入するにはどのように設定すればよいのだろうか? ドラッグ&ドロップイベントに反応する副作用をどのように扱えばよいのだろうか? そこで、React DnDの中の主要な抽象ユニットであるdrag sourcesやdrop targetsの登場である。これによって、type、item、副作用、collect関数がすべてがコンポーネントに結び付けられる。
ひとつあるいはその中の一部のコンポーネントをドラッグさせたいときは常に、drag source宣言でコンポーネントをラッピングさせる必要がある。あらゆるドラッグ元はある特定のtypeとして登録され、コンポーネントのpropsからitemを生成するメソッドを実装しなければならない。オプションとして、ドラッグやドロップイベントを処理するいくつかのメソッドを設定することもできる。その他に、drag source宣言時に、コンポーネントが内部で使用するcollect関数も設定する。
drop targetもdrag sourceと非常に似ている。唯一の違いは、ひとつのドロップ先に一度に複数のitem typeを登録したり、itemを生成するのかわりに、ホバーやドロップイベントを処理をするであろうということである。
ここでいうdrag source宣言とは、ソースでいうと@DragSourveのことであり、 その実現方法として次のHigher Order ComponentとDecoratorが登場するわけだ。
Higher-Order Component and ES7 decorators
コンポーネントをどうやってラッピングするのか? そもそもラッピングとはどういう意味だろうか? もし今までHigher-orderコンポーネントを使ったことがなかったら、お先にこの記事を読もう。この記事ではHOCの概念について詳細に説明されている。
higer-orderコンポーネントは、Reactコンポーネントクラスを受け取り、別のReactコンポーネントクラスを返すただの関数である。ライブラリによって提供されたラッピングコンポーネントは、render
メソッドでコンポーネントをレンダリングしたり、それにpropsを与えるが、さらにいくつの便利な振る舞いも追加する。
React DnDでは、DragSource
やDropTarget
は、他のいくつかのトップレベルの関数と同じように、実際HOCである。これらは、コンポーネントにドラッグ&ドロップの魔法をかける。
DragSourceやDropTargetを使うときの注意として、2つの関数を求められることである。たとえば、DragSource
でYourComponent
がどのようにラッピングされるかみてみよう。
ES6
import { DragSource } from 'react-dnd'; class YourComponent { /* ... */ } export default DragSource(/* ... */)(YourComponent);
1つ目の関数呼び出しでは、DragSource
パラメータを渡し、その後の二つ目の関数呼び出しで、ようやく自分のコンポーネントクラスを渡してることに気づくだろう。これはカリー化
、や部分適用
と呼ばれるものであり、ES7 decoratorシンタックスが機能するには必要である。
ES7
import { DragSource } from 'react-dnd'; @DragSource(/* ... */) export default class YourComponent { /* ... */ }
ES7シンタックスは必要ないが、もし好みならば、.babelrcファイルに{ "stage": 1 }
を設定し、Babelでトランスパイルすれば実現できる。
ES7を使うつもりがなくても、部分適用は簡単に実装できる。_.flow
のような合成ヘルパー関数をつかって、ES5, ES6のシンタックスの範囲でDragSource
やDropTarget
宣言と結合することができるからだ。ES7ならば、decoratorを付与するだけで、同じ効果を実現できる。
import { DragSource, DropTarget } from 'react-dnd'; @DragSource(/* ... */) @DropTarget(/* ... */) export default class YourComponent { render() { const { connectDragSource, connectDropTarget } = this.props return connectDragSource(connectDropTarget( /* ... */ )) } }
下に、Card
コンポーネントをドラッグ元としてラッピングした例を示そう。
Putting It All Together
import React from 'react'; import { DragSource } from 'react-dnd'; // ドラッグ元とドロップ先が相互作用するのは、 // 両者が同じtypeをもつ場合である。 // ファイルを分れば、他のファイルからも呼べる。 const Types = { CARD: 'card' }; /** * ドラッグ元として特定させる。 * `begin`関数だけが必要になる。 */ const cardSource = { beginDrag(props) { // ドラッグされてるitemを示すデータを返す const item = { id: props.id }; return item; }, endDrag(props, monitor, component) { if (!monitor.didDrop()) { return; } // 然るべき先でドロップされたときの処理 const item = monitor.getItem(); const dropResult = monitor.getDropResult(); CardActions.moveCardToList(item.id, dropResult.listId); } }; // decoratorシンタックスを利用 @DragSource(Types.CARD, cardSource, (connect, monitor) => ({ // render関数内でこれを呼べば、 // React DnDはドラッグイベントを処理できる connectDragSource: connect.dragSource(), // monitorから現在のドラッグstateを尋ねることができる。 isDragging: monitor.isDragging() })) export default class Card extends React.Component { render() { // いつものように自身のpropsを受け取れる const { id } = this.props; // この2つのpropsは、上で書いたcollect関数で定義したように // React DnDによって注入されたものである const { isDragging, connectDragSource } = this.props; return connectDragSource( <div> I am a draggable card number {id} {isDragging && ' (and I am being dragged now)'} </div> ); } }
感想
やっぱり訳しても、理解は難しいだろうなと思う。なぜならば、dragSourceの説明がなかったり、それらがdecorator関数の引数に与えられているのがなぜか説明がないからである。 このあたりは、tutorialやAPIドキュメントを見てまた、説明できたらいいな。 疲れた。
webpackで.babelrcっているんだっけ?
他人のwebpackの設定をみてると人によっては.babelrcがあったりなかったりする。
どっちが正解なんだと思うことがあったが、別にbabelrcがあってもなくてもよい。
webpack.config.jsでbabelの設定するか、babelrcで設定するかの違いということがわかったのでとりあえずメモしておく。
今回も基本的なこと。
webpack.config.jsでbabel設定する方法
webpack.config.js
const config = { ... module: { rules: [{ test: /\.jsx?$/, exclude: path.resolve(__dirname, 'node_modules'), loader: 'babel-loader', // ここがbabelの詳細設定 query: { presets: ['react', 'env'], plugins: ['transform-class-properties', 'transform-decorators-legacy'], }, }, ... }; module.exports = config;
webpack.config.jsにbabelの設定を記述してるので、.babelrc
は不要。
これでたとえば、npm run build
と実行するとトライスパイルできる
babelrcでbabel設定する
webpack.config.js
const config = { ... module: { rules: [{ test: /\.jsx?$/, exclude: path.resolve(__dirname, 'node_modules'), loader: 'babel-loader', // queryキーは書かない }, ... }; module.exports = config;
.babelrc
{ "presets": ['react', 'env'], "plugins": [ 'transform-class-properties', 'transform-decorators-legacy' ] }
webpack.confi.jsでbabel設定したときのqueryキーで指定した内容をそのままjson形式で.babelrcに記述するだけでよい。
これで同じくnpm run build
によりトランスパイルできる。
それだけだよね。。。
gulpを使ってReact Electron Webpack環境にLiveReloadを!
electron開発してると、毎回トランスパイルして起動というのが面倒だなと思ってくる。
webpackのHot Module Replacementを使えばもっと早くできるようだが、僕には理解できなかった。
だがしかし、gulpを使えばそれなりに実現できる!!
とわかり挑戦してみたので、備忘も込めて投稿しようと思う。
目的
React Electron Webpack環境で、LiveReloadできるようする。
下図は、Electron起動時にデベロッパーツールを起動させる設定にしていたのをコメントアウトして保存すると自動でElectronが再起動してるもの。
使用環境
以前の記事でReact x Electron開発で構築した以下の環境を利用する。
- node: 7.10.1
- React: 16.2.0
- electron: 1.8.4
- webpack: 4.2.0
webpackの詳細の設定は、前回の記事参照。
ディレクトリ構成
ディレクトリ構成は以下の通り
. ├── dist ├── gulpfile.js ├── package.json ├── node_modules ├── src │ ├── assets │ ├── main │ ├── renderer │ └── utils └── webpack.config.js
gulpと関連ツールのインストール
npm install -D gulp webpack-stream electron-connect
webpack-streamは、gulpとwebpackをつなぐために使用し、
electron-connectは、コンパイル後にelectronを再起動したり、再ロードするなどelectronを制御するために使用する。
gulpの設定
touch gulpfile.js
で設定ファイルを作成する。
main用のrenderer用のwebpackの設定を一度に読み込んで、
wepackStreamメソッドに渡すとエラーになってしまうので
、両方の設定を個別に読み込んでからmain用とrender用のタスクを定義するように記述した。
// gulpfile.js const gulp = require('gulp'); const webpackStream = require('webpack-stream'); const webpack = require('webpack'); const electron = require('electron-connect').server.create(); // main用とrenderer用の設定ファイルを格納 const [mainConfig, rendererConfig] = require('./webpack.config'); // main用のコンパイルタスクを定義 gulp.task('main', () => ( webpackStream(mainConfig, webpack) .pipe(gulp.dest('./dist/main')) )); // renderer用のコンパイルタスクを定義 gulp.task('renderer', () => ( webpackStream(rendererConfig, webpack) .pipe(gulp.dest('./dist/renderer')) )); // gulp起動時のタスクを定義 gulp.task('default', ['main', 'renderer'], () => { // electron開始 electron.start(); // main.jsファイルが変更されたら再コンパイル gulp.watch('src/main/*.{js,jsx}', ['main']); // rendererフォルダ配下のファイルが変更されたら、renderer用のコンパイルを実行 gulp.watch('src/{renderer,utils}/**/*.{js,jsx}', ['renderer']); // mainのコンパイルを終了すると,electronをRestart。 gulp.watch('dist/main/main.js', electron.restart); // rendererコンパイルが終了するとReload。 gulp.watch('dist/renderer/**/*.{html,js,css}', electron.reload); });
npm scriptsの修正
npm run gulpと打てば実行されるようにコマンド登録する
{ "scripts" : { "build": "gulp default" } }
npm run gulp
でgulpを起動させたときには、defaultタスクが実行される。
はじめにmain, rendererタスクが実行されコンパイルが走る。
コンパイル完了後electronを起動させ、ファイルが更新されるたびに再起動や再ロードが
実行されるように設定している。
index.htmlの修正
gulpで起動したelectron制御サーバと通信するためのクライアントをindex.html側に作成する。 これにより、ファイル変更時にelectronの再起動や再ロードが自動で実行される。
// index.html <html> ... <script>require('electron-connect').client.create()</script> ... </html>
以上で設定完了。これでmainファイルやrenderer用のファイルを更新すると、 その度に自動でelectronが再起動したり、最ロードされる。
さいごに
LiveReloadの設定は、gulpを使うとあっという間にできてしまう。
ただ自動とはいえ再ロードには結構時間かかるので微妙ではある。
やっぱりHMRが求められるのだろうな、electron docで紹介されてるelectron-react-boilerplate
を理解していきたい!!
ともあれgulpのすごさに感動。