Prettierについて調査

最近エディタをsublimeからvscodeに変更しました。
その中でPrettierも適用することになったので、こいつのことを理解しようとおもうようになりました。

はじめはimport内のタブサイズが4になってたりと思うどおりに動かなくて断念かと思ったが、再インストールしてようやく想定したとおりに動いてくれたので、安心。

目次

  1. Prettierとはなにか?
  2. eslintと併用する方法
  3. なぜeslint --fixではダメなのか? わざわざprettierを利用する意味はなんだ?
  4. 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の設定をそのまま活かしたいからである。

  1. 基本設定 > 設定のsetting.jsonに記述
  2. プロジェクトの.prettier.jsonに記述
  3. eslintrc内のprettier設定箇所に記述

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を使ってるとき、フロントがサーバからデータ取得できるようにするにはどうすればよいのだろうかと壁にぶつかった。異なるオリジンであっても通信できるようにするにはどうすればよいのだろうかということである。

なので今回は、異なるオリジンであってもサーバと通信できる設定について備忘録を残そうと思う。

目次

  1. expressで簡単なwebサーバの準備
  2. 同一オリジンから通信できることの確認
  3. 別オリジンから通信の失敗確認
  4. expressのCORS設定
  5. 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を意味するからだ。

その結果は次のとおりである。

f:id:poppon555:20181008204419p:plain

やはり、ドメインが異なるため通信失敗する。 エラー内容にある通り、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も問題なく動作する。

f:id:poppon555:20180917222359p:plain

気になること

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,speccollectである。 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 />;
  }
}

BoardSquareDropTargetで包もう。dropイベントだけを処理するようにドロップ先の仕様を書こう。

BoardSquare.jsx

const squareTarget = {
  drop(props, monitor) {
    moveKnight(props.x, props.y);
  }
};

わかるだろうか? dropメソッドは、BoardSquarepropsを受け取るので、ドロップされたときにナイトをどこに移動させるかわかるのである。本番のアプリ開発では、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がドラッグされていると表現しない。代わりに、特定のtypeitemがドラッグされていると表現する。

itemとは何か? itemJavascriptのプレーンオブジェクトで、何がドラッグされているかを記述したものである。たとえば、カンバンボードアプリの場合、カードをドラッグしてるとき、itemは{cardId: 42}のように表現される。チェスアプリならば、あるピースを持ち上げたとき、itemは{fromCell: 'C5', piece: 'queen'}のように表現される。プレーンオブジェクトとしてドラッグ情報を表現することでコンポーネントを分けて、ごっちゃにしてしまわないようにできるのである。

typeとは何か? typeは、アプリ内にある全てのitemクラスのどのクラスに分類されるか一意に特定してくれる文字列(あるいはシンボル)である。カンバンボードアプリの場合、ドラッグできるカードを示すものとしてcardtypeを持たせたり、ドラッグできるカードの一覧をlisttypeをもたせたりするであろう。チェスアプリならば、piecetypeだけしか持たないかもしれない。

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 sourcesdrop 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では、DragSourceDropTargetは、他のいくつかのトップレベルの関数と同じように、実際HOCである。これらは、コンポーネントドラッグ&ドロップの魔法をかける。

DragSourceやDropTargetを使うときの注意として、2つの関数を求められることである。たとえば、DragSourceYourComponentがどのようにラッピングされるかみてみよう。

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のシンタックスの範囲でDragSourceDropTarget宣言と結合することができるからだ。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が再起動してるもの。 f:id:poppon555:20180916115336g:plain

使用環境

以前の記事で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のすごさに感動。

参考サイト