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のすごさに感動。

参考サイト

babelでトランスパイル

今回はいつも以上に基本的なこと。
webpackを使って複数のjsを単一ファイルにトランスパイルしてきたが、トランスパイルはbabelの仕事。
だからbabel単体を利用して、トランスパイルするにはどんな環境構築作業が必要なのかを備忘録として残しておく。

ただ、jsでリファクタリング学習するためにトランスパイルしたファイルをnodejsで実行したかっただけなんだけど...

インストール

# babelをインストール
npm install -D babel-cli

# ES6対応presetをインストール
npm install -D babel-preset-env

babel設定

.babelrcを作成して、以下を記入する

// .babelrc
{
  "presets": ["env"]
}

buildコマンド設定

npm run buildと入力することでトランスパイルさせたいので、npm scriptsを設定する。今回は、srcフォルダ内をすべてトランスパイルさせて、結果をdistに出力させる。

// package.json
{
  ...
  scripts: {
    "build": "babel ./src --out-dir ./dist"
  }
  ...
}

これでnpm run buildと入力すると、distフォルダにトランスパイルした結果を得られる。

他のプラグインを追加したい時

クラスプロパティを利用したい場合や、トランスパイル時にflowの記述を取り除きたい場合は、別途プラグインのインストールと設定が必要

// クラスプロパティプラグイン
npm install -D transform-class-properties

// flow除去プラグイン
npm install -D transform-flow-strip-types
// .babelrc
{
  "presets": ["env"],
+  "plugins": [
+   "transform-class-properties",
+    "transform-flow-strip-types"
+  ]
}

jsでリファクタリングを学びたい(Nullオブジェクト編)

リファクタリングについて学びたく、「Java言語で学ぶリファクタリング」を読んでる。タイトル通り、Javaで記述されているが、js(ES6) + flowで書いてみて、リファクタリングの技術とflowの理解を深めようと思う.

今回取り上げるリファクタリング内容は、4章のNullオブジェクトについてである。

書籍にはNullオブジェクトの良いたとえ話が記載されている。一日おきに飲まなければならない薬があるとき、本日飲むべき日かどうか考えないといけない。その判断を避けるために、本来の薬とダミーの薬を交互に飲み続ければよい。これをプログラミングでも行おうというものが、Nullオブジェクトの目的である。

では、実際null判定の入った次のソースをリファクタリングしていこう。

リファクタリング

// Person.js
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    // nullチェック
    if (this._name !== null) {
      this._name.display();
    }

    // nullチェック
    if (this._mail !== null) {
      this._mail.display();
    }
  }

  toString(): string {
    let result: string = '[ Person:';
    result += ' name=';

    // nullチェック
    if (this._name === null) {
      result += '(none)';
    } else {
      result += this._name;
    }
    result += " mail=";
    if (this._mail === null) {
      result += '(none)';
    } else {
      result += this._mail;
    }
    result += ' ]';
    return result;
  }
}

nullかどうかでdisplay処理を行うかどうかを分けてるが、Nullオブジェクトを導入すると、nullかどうかを意識せずメソッドを呼び出せばよいのだ。

リファクタリング

// Person.js 
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  // Nullオブジェクトの導入
  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    this._name.display();
    this._mail.display();
  }

  toString(): string {
    return `[Person: name=${this._name.toString()} mail=${this._mail.toString()}]`;
  }
}

コンストラクタの第二引数に、mailアドレスが与えれなかった時、Label.newNull()によりNullオブジェクトが代わりに与えられる。
このNullオブジェクトは、Label型のサブクラスとして実装されるため、型一致している。 そして、NullオブジェクトはLabelオブジェクト同様displayメソッドをもつが、何も影響与えないよう設計されている。

以上の要件からNullオブジェクトに要求されるものが、
- 本来のクラスを継承する
- 本来のクラスのもつメソッドを無効化する
という2点にある。

実装は次の通り。

Nullオブジェクト

// Label.js
// @flow
export default class Label {
  _label: string;

  constructor(label: string) {
    this._label = label;
  }

  // 本来行いたい処理
  display() {
    console.log(`display: ${this._label}`);
  }

  toString(): string {
    return this._label;
  }

  // Nullオブジェクトを返すfactoryメソッド --- (a)
  static newNull(): Label {
    return NullLabel.getInstance();
  }
}

// Nullオブジェクトクラス
class NullLabel extends Label { // --- (1)
  // シングルトンの実装 --- (b)
  static singleton = new NullLabel();
  static getInstance(): NullLabel {
    return NullLabel.singleton;
  }

  constructor() {
    super('(none)');
  }

  // オーバーライドして何も影響を与えないようにする --- (2)
  // @override
  display() {

  }
}

本来のLabelクラスを継承するNullLabelサブクラスがある(1)
またサブクラス内でメソッドを無効化するためにオーバーライドを利用している(2)
そして、メモリ節約のためのシングルトン(b)や、new演算子をさけるためのFactoryメソッド(a)を利用して、LabelクラスからNullオブジェクトを呼び出している。

まとめ

今回は、リファクタリングとしてNullオブジェクトについて学んだことを残してみた。Nullオブジェクトを一言でいうと、本物と同じ動きをするが影響を及ぼさないダミーオブジェクトのことを指すのであろう。
それを、継承とオーバライドによる無効化によって実現する技術なんだなと理解できた。

気になったこと

javaオーバーロードが実装できるので、nullオブジェクトの検討がわかりやすいが、jsではオーバーロードが実装できない。
代わりに、デフォルト引数を利用すれば補えるのではないかと気づいた!

public class Person {
  private Label _name;
  private Label _mail;

  public Person(Label name, Label mail) {
    _name = name;
    _mail = mail;
  }

  public Person(Label name) {
    this(name, Label.newNull());
  }

  ...
}
export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

}

Java言語で学ぶリファクタリング入門

Java言語で学ぶリファクタリング入門

electronでキャプチャーソフトつくってみるpart2

キャプチャーソフトを作成中に、録画経過時間を表示する機能が必要になった。フラグのON/OFFでタイマーの開始・リセット出来るならば、他のアプリにも活かせるかもと思い、タイマーコンポーネントを作ってみた。

要件

  1. タイマーつくる
  2. propsであるisRecordを切り返ると、タイマーが開始・リセットされる

propsTypeの代わりに、flowで型チェックを行っている

// @flow
import * as React from 'react';

type PropType = {
  isRecord: boolean
};

type StateType = {
  time: number,
  timerId: number | null
};

export default class Timer extends React.Component<PropType, StateType> {
  constructor(props: PropType) {
    super(props);
    this.state = {
      time: 0,
      timerId: null,
    };
  }

  shouldComponentUpdate(nextProps: PropType): boolean {
    if (nextProps.isRecord !== this.props.isRecord) {
      (nextProps.isRecord) ? this.clearTimer() : this.setTimer();
    }
    return true;
  }

  setTimer() {
    const timerId = setInterval(() => {
      const { time } = this.state;
      this.setState({ time: time + 1 });
    }, 1000);
    this.setState({ timerId });
  }

  clearTimer() {
    const { timerId } = this.state;
    timerId && clearInterval(timerId);
    this.setState({ time: 0, timerId: null });
  }

  formatTime(time: number): string {
    const zeroPad = (num: number): string | number => {
      if (num < 10) { return `0${num}`; }
      return num;
    };

    const hour = zeroPad(Math.floor(time / 60 / 60));
    const minuite = zeroPad(Math.floor(time / 60));
    const second = zeroPad(time % 60);

    return `${hour}:${minuite}:${second}`;
  }

  render(): React.Node {
    const { time } = this.state;
    return (
        <span>
          {this.formatTime(time)}
        </span>
    );
  }
}

timerIdについて、eslint/flowでエラーが吐かれてしまう。 なんだこれ。あとで調べよう。。。

electronでキャプチャーソフトつくってみるpart1

electronに最近はまってます(笑)。キャプチャーソフトも作れるということなので、
reactとmaterial-uiを練習かねて、つくりました。

f:id:poppon555:20180415005255p:plain


electronのキャプチャー機能と動画保存機能を実装してみたかっただけなので、
ものすごく単純です。この調子で仕上げていきたい~

参考サイト

Electronでデスクトップを録画するアプリが簡単に作れました - なになれ
デスクトップを録画するアプリを書いた - Qiita