マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

React + Reduxをさっくりと覚えた

こんなのつくりました

ytnk531.hatenablog.com

(さっくりと)覚えたこと

動機

  • フロントエンドのコードを読みたい
    • 障害の原因調査が捗りそう

問題

  • 読めない
    • [...array1, ...array2]
    • { key1 } = object
    • {...object1, ...object2}
  • なにやってるかわからない
    • 階層化されたコンポーネント
    • イベントを処理する流れが追いかけられない
    • reducerとかいう謎の単語

さっくりと覚えていく

React

動的なUIを宣言的に書ける。

やったこと

  • 公式サイトのチュートリアル
  • create-my-react-appで作ったテンプレートをいじくる
  • ソートするやつを作る

メモ

  • DOMコンポーネント
    • HTMLの塊を部品として定義して再利用可能にする
    • props
    • state
      • コンポーネントの内部で保持している値(フィールド)
      • stateの更新はめんどくさい
    • propsやstateが変化すると、DOM要素が再描画される(儀式が必要)

Redux

stateを一か所に集めて、stateの更新と反映を行えるようにする。
Observerパターンのちょっとマッチョなやつ。
Reducerというコンポーネントにロジックをまとめられる。

やったこと

メモ - dispatcherで生成したアクションをreducerで受け取って新しい状態を計算する - stateをsubscribeして画面を再描画する

Recat-Redux

propsにstateとdispacherを渡せるようにする。
状態の更新⇒再描画のための儀式がゴッソリ消える。
ReactDOMコンポーネントはビューを返すためだけに使えるようになる⇒スッキリする

やったこと

  • Reduxの流れで一緒に学習
  • ソートするやつをリファクタ

新しい発見

  • コンパイルエラー相当の静的解析はしてくれる
    • トランスパイルするから当たり前なんだけど盲点だった
  • 環境構築めっちゃラク
  • ライブラリごとに使うメソッドの数が少なかったので、あんまり覚えなくてよかった
  • 画面で動くの楽しい

つまづいたところ

  • 当初、Reduxを使うことにあんまりメリットを感じなかった
    • とりあえずReactコンポーネントで状態管理してみた
    • ⇒カオスになった(Reduxを使うまではそこまでカオスだとも思ってなかった)
  • Presentational ComponentとContainer Componentの構成
    • どうやってState Treeを構成すべきか悩ましい
  • Presentational Componentにどれだけロジックを入れるか悩ましい
  • SVGのアニメーションむずい
  • sleep()ないんですか

つまづかなかったところ

  • npm
    • gradleとだいたい同じ
  • Reduxの概念
  • 純粋関数

今後

  • テストしたい
  • reduxで副作用したい⇒redux-saga
  • アニメーションしたい

reactのstateがごちゃつき始めるくらいまで使ってみた。

stateがごちゃついてなんかつらいなこれ…ってなり始めるくらいまでreact.jsを使ってみました。 初心者なりにつらかったところを記録しておきます。 react.jsの環境構築や使い方については、初心者向けの記事は良い記事がたくさんあるので、そちらにお任せします。

つくったもの

ソートするやつをつくりました。 0~99までの100個の数値を、バブルソートでソートします。

f:id:ytnk531:20180617203631p:plain
ソートするやつ
もっとちゃんとしたやつをどこかで見たことがるって?はい、私も見たことがあります。いいのです。学習用なので、これでいいのです。
herokuにデプロイしてみました。 https://evening-brushlands-42735.herokuapp.com/ https://github.com/ytnk531/react-tutor ソースコードも公開しています。ただしフロントエンドド素人が作ったものであるため、くれぐれもお手本にはしないように( ´∀` )

github.com

reactのコンポーネントで状態管理するのはつらい?

下記の記事では、reactのコンポーネントの状態管理はreduxに任せてしまおう、と言っています。 qiita.com reactのコンポーネントはstateと呼ばれるプロパティを持っていて、通常はこのstateプロパティを変化させてコンポーネントの状態を変更します。
reduxは、コンポーネントごとに状態を持つのはやめて、reduxを使うことで一か所で管理してしまおう、というライブラリです。こういったライブラリが推奨されるということは、コンポーネントで状態管理することに辛さがある、ということが示唆されているわけですが、いまいちそのつらみが理解できませんでした。

つらくなるまでやってみた

reactを使うならreduxと一緒に使いなさい!と教えていただきましたが、いまいち嬉しさがわかりませんでした。reduxで状態管理する嬉しさがわからないというよりは、reactで提供されているstateを使って状態管理する困難さがわからなかったのです。
ということで、ある程度つらくなるまで使ってみました。 フロントエンドは経験も知識ありませんでしたが、react自体は難しい物じゃないのでわりとすんなりと使えるようになりました。

作るのに必要だった知識

子要素は筆者の事前の理解度や利用経験。

stateで状態管理するつらさ

stateの更新がめんどくさい

stateを操作するときは、以下のように記述します。

// value1を1増やす
this.setState(prevState => ({
  value1: prevState.value1 + 1
});

状態を変えるたびにこれをやるのはしんどいってもんですね。

stateの反映されるタイミングが謎

setState()は非同期で呼ばれます。いつstateが更新されるかは定かではないのです。
例えば、下記のコードは無限ループします。 '''javascript // finishの初期状態はfalse while(this.state.finish) { this.setState({finish: true}); } '''

やっぱり散らばるのはよくない

コンポーネント間で共有したい値があったりします。 stateを外部から参照する方法があるにはあるのですが、とてもやりづらいうえに非推奨だそうです。

minotaur.badwitch.io

コンポーネントの状態が、変更されているかわかりにくくなってしまうのは、確かに悪いことに感じます。 状態がいつ変更されているのかを追跡する手段が用意されていなければ、バグを見つけにくくなってしまうでしょう。 一つのクラスの中にstateが留められていたとしても、それはやはり同じでしょう。

結局つらかったか

すみません、stateを持ったコンポーネントを一つしか作らなかったので、そんなにつらくなかったです。 というか、stateもったコンポーネント同士で親子関係を作ると値の変更がしにくいので、子コンポーネントは状態を持たずにpropsで渡した値が状態になるような作りにするほかなかったです。
つまるところ、規模が大きくなってくると、一つのコンポーネントでたくさんの状態を管理したり、コンポーネント間で値が伝搬していくように作らなければならないように思います。
これがコンポーネントで状態を管理するつらさ…で…いいのか…な…。

つまづいたところ

コンポーネントをループで生成する h3poteto.hatenablog.com

Docker ToolBoxのDocker環境をWindows 10 HomeのBash On Windowsから使う

結論

docker-machine.exe start default
docker.exe $(docker-machine.exe config default) run hello-world

はじめに

Windows 10 HomeでDocker Toolboxで入れたDockerをBash On Windowsを使う方法を説明します。 Docker for Windows + Bash on Windowsだったり、Dockerを入れたVM + Bash on Windowsとはちょっと違う環境の話です。 Docker for Windows使えればよかったんですけどねぇ。

注意

本ページに登場するコマンドの.exeは省略しないでください。 省略すると、コマンドが見つからずエラーになるか、Linux Subsystemの方にインストールされたコマンドが実行されてしまいます。

環境

Windows 10 HomeではDocker for Windowsが利用できないので、Docker Toolboxを使います。Docker ToolboxとBash on Windowsはインストール済みという前提で説明していきます。

BoWからdocker.exeはうまくいかない

BoWからDocker Toolboxでインストールされたdocker.exeを実行すると次のようなエラーになります。

$ docker.exe ps
error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.31/containers/json: open //./pipe/docker_engine: The system cannot find the file specified. In the default daemon configuration on Windows, the docker client must be run elevated to connect. This error may also indicate that the docker daemon is not running.

docker.exeはDockerのクライアントですから、docker daemonというdockerのコンテナを実際に動作させるサーバと接続しなければなりません。 BoWからdocker.exeを起動した場合、接続先の設定がされていないためエラーになります。 Docker Quick Start Terminalでは起動時にこの設定を行ってくれるようです。以下では、Docker Quick Start Terminalに頼らず接続設定を行う方法を説明していきます。

docker-machine.exeでdocker-daemonを起動する

Docker Toolboxで使えるようになるDocker daemonは、実はdocker-machineで作られたVM上にあるので、docker-machine.exeを使って確認できます。

$ docker-machine.exe ls
NAME      ACTIVE   DRIVER       STATE     URL   SWARM   DOCKER    ERRORS
default   -        virtualbox   Stopped                 Unknown

defaultという名前のdocker machine(docker daemonを動作させるVM)があるのがわかりますね。Docker Quick Start Terminalではこのdocker machineを利用しています。STATEがStoppedなので、まずは起動しましょう。私の環境では起動に3分くらいかかりました。

$ docker-machine.exe start default
Starting "default"...
(default) Check network to re-create if needed...
(default) Waiting for an IP...
Machine "default" was started.
Waiting for SSH to be available...
Detecting the provisioner...
Started machines may have new IP addresses. You may need to re-run the `docker-machine env` command.

Docker Quick Start Terminalは、起動時このdocker machineの立ち上げも自動で行っていたというわけです。

Dockerを使う

起動したdocker machineを介してdockerを操作するには、下記のコマンドを使います。

docker.exe $(docker-machine.exe config default) [dockerコマンド]

[dockerコマンド]にはrun mysqlpsなど、docker操作用のコマンドを入れてください。docker-machine.exe config defaultでdefault(Docker Machine)に接続するためのdocker.exeのためのオプションが生成されるので、これを評価したものを引数にしています。

では実行してみます。

$ docker.exe $(docker-machine.exe config default) run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
9bb5a5d4561a: Pulling fs layer
9bb5a5d4561a: Verifying Checksum
9bb5a5d4561a: Download complete
9bb5a5d4561a: Pull complete
Digest: sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

BoWからDocker ToolboxでインストールしたDockerを操作することができました。

おまけ: もうちょっと楽にする

docker machineの起動と、docker操作用のコマンドにaliasを張っておきます。

alias dmsd='docker-machine start default'
alias docker='docker.exe $(docker-machine.exe config default)'

docker machineの起動は/etc/rc.localに書いておけばうまくいくでしょうか? すみません、試してないのでわかる方いらっしゃいましたらコメントか何かで教えてください😷

余談

Docker Toolboxのほうでは、おそらくdocker-machine.exe env default --shellで生成される設定スクリプトを起動時に実行して環境変数を設定しているのですが、これと同じ方法はBoWでは使えません。というのも、docker.exeはBoWで設定された環境変数を読んでくれないからです。 ならば、Windowsにインストールしたdocker.exeでなく、BoWにインストールしたdockerクライアントを実行すれば環境変数を読めるのでうまく動きそうです。結果は、これもダメでした。docker-machine.exe env default --shellで設定した環境変数では、pemファイルのパスがWindowsの形式(C:\foo\bar\的な)で設定されてしまい、BoWからはそのパスをたどれないからです。

【メモ】DDD実践ドメイン駆動設計 第1章と第2章

実践ドメイン駆動設計の第1章と第2章を読んだメモ。

DDDへの誘い

  • DDDとは何か
  • ドメイン
    • 組織が行う事業やそれを取り巻く世界
  • ユビキタス言語
    • 開発チームとドメインエキスパートが全員合意の上で決定された、ドメインを取り巻く言葉。
    • ユビキタス言語は、「境界付けられたコンテキスト」内で閉じている
      • 同じ言葉でも、コンテキストが違えば意味が異なる。業務で使う言葉にはそういう性質がある。
  • ドメインエキスパート
    • システム化する業務に精通した人。
    • DDDの大きな目的は、ドメインエキスパートの持つ知見をシステムに取り入れることで、実際の業務を忠実に再現したモデルの上にシステムを作り出すこと。
  • ドメインモデル

  • DDDの利点

    • ドメインエキスパートと開発チームで業務知見を共有できる。
      • 開発を通して業務を改善する方法を見いだせる。
      • 業務上の戦略の変化に対応できるようになる。
    • ドメインエキスパートと認識の祖語が起きにくくなる。
      • ドメインオブジェクトの仕様を確認してもらえば、ドメインエキスパートがロジックに誤りがないことを確認できる。
    • ドメインの変化、後から手に入れた業務知見をコードに取り入れられる。
      • 最初から完璧なものを作る必要が無い。
  • DDDの課題

  • サブドメイン

    • ドメインの一部分
    • 基本的に業務やモノ単位で分割する。ただし、ドメイン分割に明確なルールは無い。チームで合意をとって決める。
  • コアドメイン
  • 境界付けられたコンテキスト
    • 特定のモデルを定義・適用する境界を明示的に示したもの。
    • 代表的な境界の例は、サブシステムやチームなど。

Angularプロジェクトをコピーしたらビルドできなかった

Angular CLIで作ったプロジェクトをコピーしたらビルドがコケました。

隠しファイル(ドットファイル)をコピーし損ねてたただけでした(汗)

 

> cp -r angular-app/* .

> ng build
Cannot read property 'config' of null
TypeError: Cannot read property 'config' of null
at Class.run (/home/tanaka/scala/play/play-scala-seed/src/node_modules/@angular/cli/tasks/build.js:15:56)
at Class.run (/home/tanaka/scala/play/play-scala-seed/src/node_modules/@angular/cli/commands/build.js:216:26)
at resolve (/home/tanaka/scala/play/play-scala-seed/src/node_modules/@angular/cli/ember-cli/lib/models/command.js:261:20)
at new Promise (<anonymous>)
at Class.validateAndRun (/home/tanaka/scala/play/play-scala-seed/src/node_modules/@angular/cli/ember-cli/lib/models/command.js:240:12)
at Promise.resolve.then.then (/home/tanaka/scala/play/play-scala-seed/src/node_modules/@angular/cli/ember-cli/lib/cli/cli.js:140:24)
at <anonymous>

エラーメッセージで検索すると、Angular-CLIのissueにヒントがありました。

github.com

ardiadrianadri commented on Feb 17 I know why it is happening but I don't know how to solved. The problem is that the transpiled JS code remove the "." at the beginning of the string ".angular-cli.json", so angular-cli cannot find the configuration file because it is looking for a file that doesn't exist. Let me show you:

This is the content of packages/@angular/cli/models/config.ts:

(...) import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path';

export const CLI_CONFIG_FILE_NAME = '.angular-cli.json'; const CLI_CONFIG_FILE_NAME_ALT = 'angular-cli.json'; (..)

以前はトランスパイルするとファイル名の頭のドットが取れてしまうバグがあったようですね。 リンク先の問題とは違いますが、私の環境では".angular-cli.json"がコピーできていなかったのでコケていました。

cp -r ~/myapp/.angular-cli.json ~/myapp/.editorconfig .

 これでビルドが成功するようになりました。

ワイルドカードで指定しても、ドットファイルは拾ってくれないんですね。 今まで意識していませんでした。

Spring + JSP + JQueryでポチポチしてフォームを増やしたい。

ポチポチしたい

フォームをネストして、サブフォームをいっぱい持つParentForm

package myspring.web;

import java.io.Serializable;
import java.util.List;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class ParentForm implements Serializable{
    private static final long serialVersionUID = 1L;
    
    private List<ChildForm> children;
    
    @NotNull
    @Size(min = 1, max = 20)
    private String name;    
    /**
     * 
     */
    public ParentForm() {
	super();
    }
    /**
     * @return the children
     */
    public List<ChildForm> getChildren() {
        return this.children;
    }
    /**
     * @param children the children to set
     */
    public void setChildren(List<ChildForm> children) {
        this.children = children;
    }
    /**
     * @return the name
     */
    public String getName() {
        return this.name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
}

いっぱい持たれるChildForm

package myspring.web;

public class ChildForm {
    private String name;
    private String group;
    /**
     * 
     */
    public ChildForm() {
	super();
    }
    /**
     * @return the name
     */
    public String getName() {
        return this.name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
    /**
     * @return the group
     */
    public String getGroup() {
        return this.group;
    }
    /**
     * @param group the group to set
     */
    public void setGroup(String group) {
        this.group = group;
    }
}

Controller

package myspring.web;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class WelcomeController {
    @RequestMapping("/")
    public String fuck(Model model) {
	return "welcome";
    }
    
    @RequestMapping("/me")
    public String me(ParentForm form, Model model) {
	form.setChildren(Arrays.asList(new ChildForm(), new ChildForm()));
	model.addAttribute("form", form);
	return "fuckYou";
    }
    
    @RequestMapping("/new")
    public String rec(@Valid ParentForm form, BindingResult result, Model model) {
	if (form.getChildren() != null) {
	form.getChildren().stream()
		.filter(Objects::nonNull)
		.forEach(child -> System.out.println("forEach" + child.getName()));
	
	List<ChildForm> childforms = form.getChildren().stream()
		.filter(Objects::nonNull)
		.filter(child -> child.getName() != null)
		.collect(Collectors.toList());
	
	form.setChildren(childforms);
	}
	model.addAttribute("form", form);
	if (result.hasErrors()) {
	    return "fuckYou";
	}
	return "fuckMe";
    }    
}

JSPは、JQueryを使ったけど、たいしたことしてないので使わないほうがいいかもしれない。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Sub Form Sample</title>
<script src="/webjars/jquery/2.2.4/jquery.min.js"></script>
</head>
<body>
<c:set var="count" value="0"/>
<h2>Sub Form Sample</h2>
  <form:form modelAttribute="form" action="/new">
    <table>
      <tr>
        <td colspan="4">
          <button type="button" id="add">Add</button>          
        </td>
      </tr>
      <c:forEach items="${form.children}" varStatus="rowS" var="item">
        <tr id="subform${rowS.index}">
          <td>Name</td>
          <td><form:input path="children[${rowS.index}].name" /></td>
          <td>Group</td>
          <td><form:input path="children[${rowS.index}].group" /></td>
          <td><button type="button" class="remove" onclick="removeForm(${rowS.index})">Remove</button></td>
        </tr>
        <c:set var="count" value="${rowS.index + 1}" />
      </c:forEach>
      <tr id="subformanker"><td></td></tr>
      <tr>
        <td colspan="4"><input type="submit" value="Save Changes" /></td>
      </tr>
    </table>
  </form:form>

  <script>
      var count = ${count};
      function removeForm(index) {
        $("#subform" + index).remove();
        console.log(count);
      }
      $(document).ready(function () {
        $("#add").click(function (event) {
          $('<tr id=subform' + count + '>').append("<td>Name</td>")
                .append('<td><input id="children' + count + '.name" name="children[' + count + '].name" type="text" value=""/></td>')
                .append('<td>Group</td>')
                .append('<td><input id="children' + count + '.group" name="children[' + count + '].group" type="text" value=""/></td>')
                .append('<td><button type="button" class="remove" onClick="removeForm(' + count + ')">Remove</button></td>')
                .insertBefore("#subformanker");        
          console.log(count);
          count += 1;
        });      
      });

    </script>
</body>
</html>