ElectronでHello world

久々に技術ブログを書いていきます。 (最近は書評ブログとかが多かったので、、、、)

babelやwebpack

「Electron 環境構築」とか「Electron Hello wold」とかで検索すると大体は単に画面にテキストを表示するのみで終わってしまっているのもが多いので今回はbabelwebpackなどもいれてトランスパイルできるようにしていきます。

自分は下記の本を購入して勉強しました。おすすめの本です。

ElectronでHello world

まずはnode.jsをインストールしてnpmコマンドが使えるようにしてください。 ググればいろいろでてきます。

その後、プロジェクトファイルを作成してnpm initを実行する

mkdir Helloworld
cd Helloworld
npm init -y

すとJsonファイルが作成されます。

{
  "name": "Helloworld",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

各種パッケージファイルのインストール

  • Electronをインストール
npm install electron
  • webpackのインストール
npm install webpack
  • reactのインストール(今回はreactを使用します)
npm install react react-dom
  • 画面描写に必要なファイルのインストール
npm install babel-core babel-loader babel-preset-es2015 babel-preset-react
  • Windowを作成するファイル(最初に呼ばれるファイル)

src/main/index.jsを作成する(src と mainは各自ディレクトリを作成してください)

import { app } from "electron";
import createWindow from "./createWindow";

app.on("ready",() => {
  // Windowを生成する
  createWindow();
});

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", (_e, hasVisibleWindows) => {
  if (!hasVisibleWindows) {
    createWindow();
  }
});
  • createWindow.jsを作成

electronでwindowを作成するクラスを作成する

src/main/createWindow.js

import { BrowserWindow } from 'electron';

let win;

function createWindow() {
  win = new BrowserWindow();
  win.loadURL(`file://${__dirname}/../../index.html`);
  win.on("close", () => {
    win = null;
  });
}

export default createWindow;

  • index.htmlを作成

createwindowしたファイルの上に表示するhtmlファイルです (これはプロジェクトディレクトリの直下においてください)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ブログみてね!</title>
  </head>
  <body>
    <div class="window">
      <div id="app" class="window-content"></div>
    </div>
    <script>require("./dist/renderer/app.js")</script>
  </body>
</html>
  • app.jsxの作成

ここでHello worldします

src/renderer/app.jsx

import React from "react";
import { render } from "react-dom";


render(<div> Hello World </div>, document.getElementById("app"));
  • webpackの設定ファイルを作成する

プロジェクトディレクトリの直下に下記のファイルを作成する

webpack.config.js

module.exports = {
    target: "electron",
    node:{
        __dirname:false,
        __filename:false
    },
    resolve:{
        extensions: [".js",".jsx"]
    },
    module:{
    rules:[
        {
        test: /\.jsx?$/,
        exclude:/node_modules/,
        loader: "babel-loader"
        },
        {
        test: /.css$/,
            loaders:["style-loader","css-loader?modules"]
        }
        ]
    },
    entry: {
        "main/index":"./src/main/index.js",
        "renderer/app": "./src/renderer/app.jsx"
    },
    output: {
        filename: "dist/[name].js"
    },
    devtool:"source-map"
};
  • .babelrcを作成

このアプリで使用するEcmascriptのバージョンを指定しています。

.babelrc

{
  "presets":["es2015","react"]
}

## ビルドする

ここまでで完了です。 ビルドしてみましょう。

./node_modules/.bin/webpack

package.jsを編集する

ここでビルド後のファイルを最初に読み込ませるように修正する必要があるのでpackage.jsを編集します

main~の箇所を下記のように編集してください

"main": "dist/main/index.js",

起動する

./node_modules/.bin/electron .

すると表示されます。

f:id:jesus9387:20171107222252p:plain

今回参考にした本

下記になります。 すごくおすすめ

よければ

よければツイッターの方フォローお願いします。

twitter.com

自分の時間を取り戻そう-書評

一冊読み終わったので書評として残しておきます。

感想

この本は一言で言うと、

「自分の時間は重要度のかなり高いもので、それを手に入れるために生産性を追求しましょう」

と言うことだと思います。

自分のいつもできるだけ仕事を終わらして帰るようにしているがどうしても忙しいときは残業してしまっている。

そんな自分の働き方を見直す必要があるな〜と考えさせられました。

長い時間を投入するのは本当にいいことなのか

この本の中で

「長い時間を投入することは本当に最良の問題解決方法なのか」

ということが述べられていました。

身につけたいことがあったら もっと最短で身につけることはできないか、 生産性高く学習することはできないか、

ということを常に考え続ける必要があります。

例えば自分の子供が 「長い時間をかけて勉強して成績があがった」 より、 「勉強時間はたいして変わってないけど成績がぐんとあがった」 のほうがよっぽど価値のあることだとも述べられていました。

やりたくないことこそダラダラ続けてしまう

やりたくないはダラダラやるとなんとかなってしまう。 でも生産性に目を向けた時にやりたくないことに我慢できなくなり本当にやりたいことが見つかるとも書かれていました。

これにはすごく共感しました。

最低賃金はもっとあげたほうがいい

このように書かれてた理由がすごく興味深かったです。

最低賃金の引き上げに反対し、海外からの労働者の受け入れに積極的な企業は安い労働力が豊富に入手できないとビジネスが成り立たない生産性の低い企業なのです。」

これには軽く衝撃でした。 たしかに短時間でかなりの利益をあげていればブラック企業は生まれない。 安い賃金で長時間働かせてなんとかしようとする企業はもうすでに生産性の低い企業としてレッテルを貼られているのと同じということと解釈しています。

なにか新しい技術や、ビジネスを目にした時に生産性という判断する

今ではITの力でいろんなことが自動化され、世の中をより便利にしてきました。

世の中で流行っているサービスのほとんどが「使うことで生産性があがるもの」です。

なので新しくビジネスを目にした時に「これは生産性があがるものなのか」という視点はとても大事だ思いました。

生産性が低いと思われる事例

生産性が低いと考えられる業務や、世の中の仕組みについていくつか書かれておりました。

例えば

  • アパレル店員
  • 選挙
  • 学校制度 etc

この辺りは理由もしっかり書かれていて面白かったのでよければ読んでみてください。

商品リンクはこちら

よければ

ツイッターの方フォローよろしくお願いします

twitter.com

Electron+React.jsで作るチャットアプリ

Electron + React.jsで作るチャットアプリケーション

自分はElectronの初心者です。 Electronで本を購入しチャットアプリを写経して自分の中で理解を深めるためにここに残しておきます。

  • 作成物

チャットアプリケーション

f:id:jesus9387:20171024182312g:plain

Github

github.com

  • 購入した本
  • 感想

Electronについて概要からしっかり学べた

## Electron本体をインストール

mkdir electron_chat

cd electron_chat

  • プロジェクト作成

npm init -y

(npmが使えなかった場合はインストールしてくる)

  • Electronをインストール
npm install electron@1.6.1 --save-dev
  • photonKitのインストール

photonKitとはmacOSネイティブアプリケーションのようなUIを簡単に実装できるFrameworkです。

npm install connors/photon --save-dev
  • Reactのインストール
npm instal react@15.4.2 react-dom@15.4.2 react-router@3.0.0 --save
  • babel関連モジュールのインストール
npm install babel-cli@6.18.0 babel-preset-es2015@6.18.0 babel-preset-react@6.16.0 --save-dev

実装

  • index.js
import { app } from "electron";
import createWindow from "./createWindow";
import setAppMenu from "./setAppMenu"

app.on("ready",() => {
  //アプリが起動したとき
  // 上でimportしたcreateWindowクラスのcreateWindow()メソッドを呼ぶ
  createWindow();
});

app.on("window-all-closed", () => {
  //mac以外の場合はウインドウを閉じた時にアプリを終了する
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", (_e, hasVisibleWindows) => {
  if (!hasVisibleWindows) {
    createWindow();
  }
});

createWindow.js

import { BrowserWindow } from 'electron';

let win;

function createWindow() {
  win = new BrowserWindow();
  //BrowserWindowクラスを使ってHTMLファイルを読み込み画面に表示
  win.loadURL(`file://${__dirname}/../../index.html`);
  win.on("close", () => {
    win = null;
  });
}

export default createWindow;
  • index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Electron Chat</title>
    <link rel="stylesheet" href="node_modules/photon/dist/css/photon.css"/>
  </head>
  <body>
    <div class="window">
      <div id="app" class="window-content"></div>
    </div>
    <script>require("./.tmp/renderer/app.js")</script>
  </body>
</html>
  • app.jsx

Railsでいうrouteみたいなファイル。この中にFirebaseの設定も書き込む

import React from "react";
import {render} from "react-dom";
import { Router, Route, hashHistory } from "react-router";
import Login from "./Login";
import Signup from "./Signup";
import Rooms from "./Rooms";
import Room from "./Room";

import firebase from "firebase/firebase-browser"

// Routingの定義
const appRouting = (
  <Router history={hashHistory}>
    //親のpathから繋げていく
    <Route path="/">
     //例えば "/login"を設定するとLoginクラスが呼ばれる
      <Route path="login" component={Login} />
      <Route path="signup" component={Signup} />
      <Route path="rooms" component={Rooms} >
        <Route path=":roomId" component={Room} />
      </Route>
    </Route>
  </Router>
);


//Routingの初期化
if (!location.hash.length) {
  location.hash = "#/login";
}

  // Firebaseの初期化
  //ここはFirebaseからこぴぺ
  const config = {
    apiKey: "*******************",
    authDomain: "*******************",
    databaseURL: "*******************",
    projectId: "*******************",
    storageBucket: "",
    messagingSenderId: "*******************"
  };
  firebase.initializeApp(config);


// Applicationの描画
render(appRouting, document.getElementById("app"));
  • .babellc

Reactをjavascriptに変換するためのファイル

{
  "preset": ["es2015,"react]
}
  • Login.js

ログイン画面の実装

import React from "react";
import { Link, hashHistory } from "react-router";
import Errors from "./Errors";
import firebase from "firebase/firebase-browser";

const FORM_STYLE = {
  margin:"0 auto",
  padding:30
};

const SIGNUP_LINK_STYLE = {
  display:"inline-block",
  marginLeft: 10
};

export default class Login extends React.Component {

  constructor(props) {
    super(props);

    //props:親から渡される情報
    //state:自Componentの状態
    this.state = {
      email: localStorage.userEmail || "",
      password: localStorage.userPassword || "",
      errors:[],
    };

    //それぞれのメソッドをbindする
    //bindすることでフロントの状態変化を検知できる
    this.handleOnChangeEmail = this.handleOnChangeEmail.bind(this);
    this.handleOnChangePassword = this.handleOnChangePassword.bind(this);
    this.handleOnSubmit = this.handleOnSubmit.bind(this);
  }

  // 下のinputの状態が変化したら呼ばれる
  handleOnChangeEmail(e){
    this.setState({ email: e.target.value});
  }

  // 下のinputの状態が変化したら呼ばれる
  handleOnChangePassword(e){
    this.setState({ password: e.target.value });
  }

  //ログイン処理
  handleOnSubmit(e){
    // 二つのinputタグの状態を取得する
    const { email, password } = this.state;
    const errors = [];
    let isValid = true;
    e.preventDefault();

    if (!email.length) {
      //emailに何もデータが入っていない場合
      isValid = false;
      errors.push("Email cannot blank.");
    }

    if (!password.length) {
      //passwordに何もデータが入っていない場合
      isValid = false;
      errors.push("Password cannot be blank.");
    }

    if (!isValid) {
      //必須入力チェックに該当した場合はエラーを表示する
      this.setState({ errors });
      return;
    }

    //Firebaseのログイン処理
    //この辺はリファレンスを見る
    firebase.auth().signInWithEmailAndPassword(email,password).then(() => {
      //次回ログインし簡略化のため、localStorageに値を保存
      localStorage.userEmail = email;
      localStorage.userPassword = password;
      //チャットルーム一覧画面へ遷移
      hashHistory.push("/rooms");
    }).catch(() => {
      // Firebaseでログインエラーになった場合
      this.setState({errors: ["Incorrect email or password."] })
    });
  }

  //フロントのレンダリング
  render(){
    return (
      <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}>
        <Errors errorMessages={this.state.errors} />
        <div className="form-group">
          <label>Email address</label>
          <input
            type="email"
            className="form-control"
            placeholder="email"
            onChange={this.handleOnChangeEmail}
            value={this.state.email}
          />
        </div>
        <div className="form-group">
          <label>Password</label>
          <input
            type="password"
            className="form-control"
            placeholder="password"
            onChange={this.handleOnChangePassword}
            value={this.state.password}
          />
        </div>
        <div className="form-group">
          <button className="btn btn-large btn-default">Login</button>
          <div style={SIGNUP_LINK_STYLE}>
            <Link to="/signup">create new account</Link>
          </div>
        </div>
      </form>
    );
  }
}

  • signup.js

サインアップページ

import React from "react";
import { Link, hashHistory } from "react-router";
import Errors from "./Errors";
import firebase from "firebase/firebase-browser";

const SIGNUP_FORM_STYLE = {
  margin: "0 auto",
  padding: 30
};

const CANCEL_BUTTON_STYLE = {
  marginLeft: 10
};

export default class Signup extends React.Component{

  constructor(props){
    super(props);
    //状態の初期化
    this.state = {
      email:"",
      password:"",
      name:"",
      photoURL:"",
      errors:[]
    };

    //各種bindする
    this.handleOnChangeEmail = this.handleOnChangeEmail.bind(this);
    this.handleOnChangePassword = this.handleOnChangePassword.bind(this);
    this.handleOnChangeName = this.handleOnChangeName.bind(this);
    this.handleOnChangePhotoURL = this.handleOnChangePhotoURL.bind(this);
    this.handleOnChangeSubmit = this.handleOnChangeSubmit.bind(this);
  }

  handleOnChangeEmail(e){
    this.setState({ email: e.target.value});
  }

  handleOnChangePassword(e){
    this.setState({ password:e.target.value });
  }

  handleOnChangeName(e){
    this.setState({ name: e.target.value});
  }

  handleOnChangePhotoURL(e){
    this.setState({photoURL: e.target.value });
  }

  handleOnChangeSubmit(e){
    const {email, password, name,photoURL } = this.state;
    const errors = [];
    let isValid = true;
    e.preventDefault();

    if (!email.length) {
      isValid = false;
      errors.push("Email address cannt be blank.");
    }

    if (!password.length) {
      isValid = false;
      errors.push("Password cannot be blank");
    }

    if (!name.length) {
      isValid = false;
      errors.push("Name cannot be blank.");
    }

    if (!isValid) {
      this.setState({errors});
      return;
    }

    // Firebaseの新規アカウント作成処理
    firebase.auth().createUserWithEmailAndPassword(email,password).then(newUser => {
      //ユーザー情報を更新する
      return newUser.updateProfile({
        displayName:name,photoURL
      });
    }).then(() => {
      //チャットルーム一覧画面へ遷移
      hashHistory.push("/rooms");
    }).catch(err => {
      // Firebaseでエラーになった場合
      this.setState({errors: [err.message] })
    });
  }

  render(){
    return (
      <form style={SIGNUP_FORM_STYLE} onSubmit={this.handleOnChangeSubmit}>
        <Errors errorMessages={this.state.errors} />
        <div className="form-group">
          <label>Email address *</label>
          <input
            type="email"
            className="form-control"
            placeholder="email"
            value={this.state.email}
            onChange={this.handleOnChangeEmail}
          />
        </div>
        <div className="form-group">
          <label>Password *</label>
          <input
            type="password"
            className="form-control"
            placeholder="password"
            value={this.state.password}
            onChange={this.handleOnChangePassword}
          />
        </div>
        <div className="form-group">
          <label>User name *</label>
          <input
            type="text"
            className="form-control"
            placeholder="user_name"
            value={this.state.name}
            onChange={this.handleOnChangeName}
          />
        </div>
        <div className="form-group">
          <label>Photo URL</label>
          <input
            type="text"
            className="form-control"
            placeholder="photo url"
            value={this.state.photoURL}
            onChange={this.handleOnChangePhotoURL}
          />
        </div>
        <div className="form-group">
          <button className="btn btn-large btn-primary">Create new account</button>
          <Link to="/login">
            <button
              type="button"
              style={CANCEL_BUTTON_STYLE}
              className="btn btn-large btn-default"
              >
              cencel
              </button>
          </Link>
        </div>
      </form>
    );
  }
}

  • Rooms.jsx

左ペインにRoom一覧を表示し、右ペインにはチャットルームを表示する

import React from "react";
import { hashHistory } from "react-router";
import RoomItem from "./RoomItem";
import firebase from "firebase/firebase-browser";

const ICON_CHAT_STYLE = {
  fontSize: 120,
  color:"#DDD"
};

const FORM_STYLE = {
  display: "flex"
};

const BUTTON_STYLE = {
  marginLeft: 10
};

export default class Rooms extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      roomName: "",
      rooms:[]
    };

    // Firebaseデータベースを初期化
    this.db = firebase.database();
    this.handleOnChangeRoomName = this.handleOnChangeRoomName.bind(this);
    this.handleOnSubmit = this.handleOnSubmit.bind(this);
  }

  componentDidMount(){
    //コンポーメントの初期化時にチャットルーム一覧を取得する
    this.fetchRooms();
  }

  handleOnChangeRoomName(e){
    this.setState({
      roomName: e.target.value
    });
  }

  handleOnSubmit(e){
    const { roomName } = this.state;
    e.preventDefault();
    if (!roomName.length) {
      return;
    }

    //Firebaseデータベースに新規チャットルームのデータ作成
    const newRoomRef = this.db.ref("/chatrooms").push();
    const newRoom = {
      description: roomName
    };

    //作成したチャットルームのdescriptionを更新する
    newRoomRef.update(newRoom).then(() => {
      //状態を再初期化する
      this.setState({ roomName:"" })
      //チャットルーム一覧を再取得
      return this.fetchRooms().then(() => {
        //右ペインを作成した詳細画面に遷移させる
        hashHistory.push(`/rooms/${newRoomRef.key}`);
      });
    });
  }

  //チャットルーム一覧の取得
  fetchRooms(){
    //Firebaseデータベースからチャットルームを20件取得
    return this.db.ref("/chatrooms").limitToLast(20).once("value").then(snapshot => {
      const rooms = [];
      snapshot.forEach(item => {
        //データベースから取得したデータオブジェクトとして取り出す
        rooms.push(Object.assign({key:item.key}, item.val()));
      });
      //取得したオブジェクトの配列をコンポーメントのstateにセット
      this.setState({rooms});
    });
  }

  //左ペイン(チャットルーム一覧)の描写処理
  renderRoomList(){
    const { roomId } = this.props.params;
    const { rooms, roomName } = this.state;
    return (
      <div className="list-group">
        //ルーム一覧をfetchRoomsから取得したデータ分だけ表示する
        //selectしたらRoomItemにroomIdを設定する
        // RoomItemタグを使っているといことはRoomItemクラスを呼んでいる
        {rooms.map(r => <RoomItem room={r} key={r.key} selected={r.key === roomId} /> )}
        //新しいチャットルームを作成するためのinputエリア
        <div className="list-group-header">
          <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}>
            <input
             type="text"
             className="form-control"
             placeholder="New room"
             onChange={this.handleOnChangeRoomName}
             value={roomName}
             />
             <button className="btn btn-default" style={BUTTON_STYLE}>
                <span className="icon icon-plus" />
             </button>
          </form>
        </div>
      </div>
    );
  }

  //右ペイン(チャットルーム詳細)の描写処理
  renderRoom(){
    if (this.props.children) {
      // propsにデータが設定されている = Roomが設定されている
      return this.props.children;
    } else {
      return (
        <div className="text-center">
          <div style={ICON_CHAT_STYLE}>
            <span className="icon icon-chat" />
          </div>
          <p>
            Join a chat room from the sidebar or create your chat room.
          </p>
        </div>
      );
    }
  }

  render(){
    return (
      <div className="pane-group">
        //左ペイン
        <div className="pane-sm sidebar">{this.renderRoomList()}</div>
        //右ペイン
        <div className="pane">{this.renderRoom()}</div>
      </div>
    );
  }
}
  • RoomItem.jsx

Room一覧ののひとつひとつのアイテム

import React from "react"
import { Link } from "react-router"


const LINK_STYLE = {
  color:"inherit",
  textDecoration:"none"
};

export default function RoomItem(props) {
  //propsからアイテムひとつひとつを設定する
  const { selected } = props;
  const { description, key } = props.room;
  return (
    <div className={selected ? "list-group-item selected":'list-group-item'}>
      //`/rooms/${key}`の設定でroom.jsxが呼ばれて設定される
      <Link to={`/rooms/${key}`} style={LINK_STYLE}>
        <div className="media-body">
          <strong>{description}</strong>
        </div>
      </Link>
    </div>
  );
}
  • Room.jsx

選択されているチャットルームでチャット処理を実装する

import React from "react";
import Message from "./Message";
import NewMessage from "./NewMessage";
import firebase from "firebase/firebase-browser";

const ROOM_STYLE = {
  padding: "10px 30px"
};

export default class Room extends React.Component {

  constructor(props){
    super(props);
    this.state = {
      description: "",
      messages:[],
    };

    this.db = firebase.database();
    this.handleOnMessagePost = this.handleOnMessagePost.bind(this);
  }

  componentDidMount(){
    const { roomId } = this.props.params;
    //コンポーメントの初期化時にチャットルームの詳細情報を取得する
    this.fetchRoom(roomId);
  }

  componentWillReceiveProps(nextProps){
    const { roomId } = nextProps.params;
    if (roomId === this.props.params.roomId) {
      //チャットルームのIDいん変更がなければなにもしない
      return;
    }

    if (this.stream) {
      //メッセージの監視解除
      this.stream.off();
    }

    // stateの再初期化
    this.setState({ messages:[] });
    //チャットルーム詳細の再取得
    this.fetchRoom(roomId);
  }

  componentDidUpdate(){
    setTimeout(() => {
      //画面下端へスクロール
      this.room.parentNode.scrollTop = this.room.parentNode.scrollHight;
    }, 0);
  }

  componentWillUnmount(){
    if (this.stream) {
      //メッセージ監視を解除
      this.stream.off();
    }
  }

  //メッセージ投稿処理
  handleOnMessagePost(message){
    const newItemRef = this.fbChatRoomRef.child("messages").push();
    ///Firebaseにログインしているユーザーを投稿ユーザーとして利用
    this.user = this.user || firebase.auth().currentUser;
    return newItemRef.update({
      writtenBy:{
        uid:this.user.uid,
        displayName: this.user.displayName,
        photoURL:this.user.photoURL,
      },
      time:Date.now(),
      text:message,
    });
  }

  fetchRoom(roomId){
    //Firebaseデータベースからチャットルーム詳細データの参照を取得
    this.fbChatRoomRef = this.db.ref("/chatrooms/" + roomId);
    this.fbChatRoomRef.once("value").then(snapshot => {
      const { description } = snapshot.val();
      this.setState({ description });
      window.document.title = description;
    });

    this.stream = this.fbChatRoomRef.child("messages").limitToLast(10);
    //チャットルームのメッサージ追加を監視
    this.stream.on("child_added",item => {
      const { messages } = this.state || [];
      // 追加されたメッセージをstateにセット
      messages.push(Object.assign({key:item.key},item.val()));
      this.setState({ messages });
    });
  }

  render(){
    const { messages } = this.state;
    return (
      <div style={ROOM_STYLE} ref={room => this.room = room} >
        <div className="list-group">
          //fetchRoomで取得したデータ分だけ画面に表示
          {messages.map(m => <Message key={m.key} message={m} />)}
        </div>
        <NewMessage onMessagePost={this.handleOnMessagePost} />
      </div>
    );
  }
}
  • Message.jsx

1メッセージのオブジェクト

import React from "react";
import Avatar from "./Avatar";

const MEDIA_BODY_STYLE = {
  color: "#888",
  fontSize:".9em"
};

const TIME_STYLE = {
  marginLeft: 5
};

const TEXT_STYLE = {
  whiteSpace:"normal",
  wordBreak:"break-word"
};

export default function Message(props){
  const { text, time, writtenBy } = props.message;
  const localeString = new Date(time).toLocaleString();
  return (
    <div className="list-group-item">
      <div className="media-object pull-left">
        <Avatar user={writtenBy} />
      </div>
      <div className="media-body">
        <div style={MEDIA_BODY_STYLE}>
          <span>{writtenBy.displayName}</span>
          <span style={TIME_STYLE}>{localeString}</span>
        </div>
        <p style={TEXT_STYLE}> {text} </p>
      </div>
    </div>
  );
}
  • NewMessage.jsx

投稿するinputタグ

import React from "react"

const FORM_STYLE = {
  display: "flex"
};

const BUTTON_STYLE = {
  marginLeft: 10
};

export default class NewMessage extends React.Component{
  constructor(props){
    super(props);
    this.state = {message: ""};
    this.handleOnChange = this.handleOnChange.bind(this);
    this.handleOnSubmit = this.handleOnSubmit.bind(this);
  }

  handleOnChange(e){
    this.setState({message:e.target.value});
  }

  handleOnSubmit(e){
    const { onMessagePost } = this.props;
    if (!onMessagePost || !this.state.message.length) {
      return;
    }
    onMessagePost(this.state.message);
    this.state({ message: ""});
    e.preventDefault();
  }

  render(){
    return(
      <form style={FORM_STYLE} onSubmit={this.handleOnSubmit}>
        <input
          type="text"
          className="form-control"
          onChange={this.handleOnChange}
          value={this.state.message}
          />
          <button className="btn btn-large btn-primary" style={BUTTON_STYLE}>POST</button>
      </form>
    );
  }

}
import React from "react"

const AVATAR_STYLE = {
  width:32,
  textAlign:"center",
  fontSize:24
};


export default function Avatar(props){
  const { photoURL } = props.user;
  if(photoURL){
    // photoURLURLが設定されている場合img要素を表示
    return <img className="img-rounded" src={photoURL} style={AVATAR_STYLE} />;
  } else {
    //photoURLが設定されていない場合代替えとしてiconを表示
    return (
      <div style={AVATAR_STYLE}>
        <span className="icon icon-user" />
      </div>
    );
  }
}

実行

./node_modules/.bin/babel --out-dir tmp src
  • 実行
./node_modules/.bin/electron .

すると、実行できる

最後に

ただのメモ書きになってしましたが、 この本を勉強してかなり理解が深まったのでおすすめです。

本をブックオフに持って行く前にメルカリに出品したほうがいい!

普段から本をよく読みます。 今引越しすることを考えていて本をなんとか処分したいと考え、「メルカリ カウル」を使って出品してみました! 出品していろいろメリットがあったのでそれについて書いていきます。

mercarikauru.com

ブックオフにはいかないがメルカリはみる

普段から本をよく読みますが、ブックオフで本を買ったことは一回もありません。全て新品で購入していました。 そもそもわざわざブックオフに足を運んで本を買いに行くこと自体がめんどくさいからです。

そして、本を安く買って読書をはじめようとか思う人ってだいたいメルカリをみるような気がします。

本って大体タイトルを見て欲しいと思うし、試着とかそういうのもいらないのでネットで買うのがかなり効率的です。

本を売る側も出品や送付が簡単で、買う側も簡単。 双方にメリットがあるメルカリで売ることがベストだと思います。

また、ブックオフに持ってくとせっかくいい本がブックオフの大量に並んだ本の中に埋もれてしまいます。 そうなると古本を欲しい人がいい本を見つけづらくなります。 せっかくいい本を売るんだから他の人たちにも読んで欲しいそれが私の願いでそれを実現できるのがメルカリです。

ブックオフより高く売れる

自分が大学生の頃本を十冊ほどブックオフに持って行ったら合計が200円とかだった記憶があります。 本を新品で買って1日で読んで次の日に売りに行った本が50円でした。

ですが、メルカリで本を7冊売って売り上げは2910円でした!!

f:id:jesus9387:20171022211223p:plain:w300:h600

ブックオフよりかなり高く売れています。 いい本が高く売れて買った人もいい本を読んでいただける。すごく幸せなことです。

メルカリで売るデメリット

  • 早く売れない

値下げ交渉をしてきたり、発送に手間がかかったりなどするので

「来週引越しするから今すぐ本を全部処分したい!」

みたいにすぐに大量に処分したい人には向かないでしょう

  • 梱包が必要

一冊一冊梱包して送る必要があります。 自分はその手間を省くために封筒の中に梱包材がすでに入ってるものをアマゾンで購入しています。

これを持っていたら本を中にいれてあとはファミマに持っていきメルカリラクラク便で送るだけなので楽です。

最後に

いかがだったでしょうか。 メルカリかなりおすすめです。

まだまだ、本も残っていますが、よければどうぞ

https://www.mercari.com/jp/u/228705595/

最後まで見ていただきありがとうございました。 よろしければTwitterのフォローよろしくお願い致します。

twitter.com

小さなチーム大きな仕事-書評

読んだので書評として残して起きます。

とにかく無駄なものは排除

この本では仕事の中にある無駄に削ぎ落とすと小さなチームで十分ではないかということを説いた本だと私は思います。

例えば

一時間かかる会議を設定し、
参加者を一〇人召集したとしよう。
これは実際には一時間の会議ではなく
一〇時間の会議である

会議を大人数ですることで時間が失われます。 その分生産性が失われて人が足りないと錯覚してしまいます。

なので会議はなくす、もしくは極力少なくすることが大事だと書かれていました。

誰かに話しかけること自体に相手の時間を奪うことと同じとも書かれておりました。

どうしても何か聞きたいことなどはチャットを使うことが私は有効だと思います。

モニターから目を離さずに質問などすることができるので集中力を切らさないことができると私は考えています。

すごい製品や、サービスを作る秘訣

これからビジネスをはじめようとか、はじめたい方へのアドバイスも書かれていました。

すごい製品やサービスを
生み出す最も単純な方法は、
あなたが使いたいものを作ることだ。

この考え方ってすごく大事だな〜と思っていて、自分が個人開発するときは全てこの基準でソースコードを書いています。

今この記事を書いているエディタも自分で開発したツールです。

近々リリース致しますのでよければダウンロードおねがい致します。(無料です)

f:id:jesus9387:20171016232123p:plain:w300:h700

顧客の言うことを聞いていたら、
もっと速く走る馬を彼らに与えていただろう

ヘンリー・フォード

顧客の言うことを聞きすぎるのはよくないとをいっていると私は解釈しました。

自分の尊敬するApple社はいつもこの考え方をしてると私は思っています。

MacにUSBを取り去ってUSB-Cに変えて批判を受けても、今一番売れているノートパソコンはMacbookproです。

自分の中の何かしら信念を持って製品を作り続けるということはかなり重要な考え方だと私は思います。

注文の多い少数の顧客を幸せにするために
多くの人に迷惑をかけ、
商品を台無しにしてしまうのは良くない。

注文の多い顧客に対してあれこれカスタマイズして疲弊するのはその製品自体をどんどん悪くするのと同じだと解釈しました。

業務でこういうことを経験したことがあってかなりぐさっときました。

## 商品リンク

よろしければ

ブログ更新したらツイートしたりしていますのでよろしければフォローお願い致します。

twitter.com

マネーフォワードのような横スワイプでタブを切り替えできるライブラリXLPagerTabStripを使ってみた

横スワイプで画面を切り替えれるかっこいいアニメーションを導入したくてライブラリを導入したのでメモとして残しておきます。

今回導入するのはこちらです。

github.com

f:id:jesus9387:20171010123242g:plain

かっこいいですね。

Install

Cocoapodsから導入できます。 Cocoapodsを入れてない場合は導入してください。 (導入方法はググればたくさんでてきます)

Podfileに下記を追加

pod 'XLPagerTabStrip'

追加したPodFileを保存し、下記を実行

pod install

管理元ViewControllerと切り替えるViewControllerを作成する

ルートのViewControllerと切り替え用のViewControllerを作成する

f:id:jesus9387:20171010124851p:plain

次に、切り替え用のViewControllerそれぞれにStoryboardIDを設定してください

f:id:jesus9387:20171011093245p:plain

(もうひとつのstoryboardには別のIDを設定してください)

ソースコード設定

下記のように設定する

管理するルートViewController

import UIKit
import XLPagerTabStrip


class ViewController: ButtonBarPagerTabStripViewController{

    override func viewDidLoad() {

        //バーの色
        settings.style.buttonBarBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1)
        //ボタンの色
        settings.style.buttonBarItemBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1)
        //セルの文字色
        settings.style.buttonBarItemTitleColor = UIColor.white
        //セレクトバーの色
        settings.style.selectedBarBackgroundColor = UIColor(red: 254/255, green: 0, blue: 124/255, alpha: 1)

        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
        //管理されるViewControllerを返す処理
        let firstVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "First")
        let secondVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second")
        let childViewControllers:[UIViewController] = [firstVC, secondVC ]
        return childViewControllers
    }


}

管理されるViewController 管理されるViewControllerの数だけViewControlerを下記のように編集してください。

import UIKit
import XLPagerTabStrip


class Chenge1ViewController: UIViewController, IndicatorInfoProvider {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

    //必須
    func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
        return "First"
    }

}

ルートのViewControllerにCollectionViewとScrollViewを追加する

管理するViewControllerにCollectionViewとScrollViewを追加してください。

f:id:jesus9387:20171011093439p:plain

CollectionView:切り替えタブ表示エリア ScrollView:表示するViewエリア

となります。

Storyboardの設定

CollectionView、ScrollViewそれぞれに下記の操作をおこなってください。

CollectionViewもしくはScrollViewを右クリック→Referencing outletのNew Reference outletをドラッグして 親のViewControllerにカーソルを当てる→containerViewを選択

f:id:jesus9387:20171011093650p:plain

実行してみる

f:id:jesus9387:20171011125624g:plain

カスタマイズ

下記の部分を変更することでタブをカスタマイズすることができます。

 override func viewDidLoad() {

        //バーの色
        settings.style.buttonBarBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1)
        //ボタンの色
        settings.style.buttonBarItemBackgroundColor = UIColor(red: 73/255, green: 72/255, blue: 62/255, alpha: 1)
        //セルの文字色
        settings.style.buttonBarItemTitleColor = UIColor.white
        //セレクトバーの色
        settings.style.selectedBarBackgroundColor = UIColor(red: 254/255, green: 0, blue: 124/255, alpha: 1)

        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

詳しくはGithubに書かれていますのでここをご覧ください。

github.com

サンプルは下記にありますのでよければどうぞ

github.com

よろしければツイッターもフォローお願いします

twitter.com

時間がないを言い訳にしないためにしていること

エンジニアたるもの勉強をし続けることが大事だとネット上で言われ続けています。

これについては賛否両論あるかと思いますが個人的にMarkdownでかけるメモアプリを開発しています。 (この記事も自分で作ったアプリでお風呂の中でで記事を書いています。)

f:id:jesus9387:20171002124757p:plain:w300:h500

仕事をしながら個人アプリを開発するにあたってやっていることを共有したいと思います。

迷うことを極力減らす

[自分を操る超集中]

この本の中で 「迷うことが疲れにつながる」とかかれていました。

仕事が終わって家に着き、自分のやりたいことができる時間ができたときに

「何から始めようかなー🤔」

と迷うことが疲れにつながり時間を無駄に消費してしまいます。

ですので自分は行きの電車や帰りの電車で帰ってからやることリストをトレロにまとめています。

f:id:jesus9387:20171002095648p:plain

このリストを作成するにあたって気をつけているのは優先順位の高い順で書くことです。 そうすることで、 パソコンを開いたら何も考えずリストの上から順に進めていけばいいだけというようにしています。

スキマ時間を有効活用

自分は基本的いつでも携帯を持っていて スキマ時間になったらやりたいことのためにアイデアを絞ったり、記事を書いたりしています。

電車の中や仕事の昼休みなど細切れの時間をまとめるとすごい時間なります。 これを有効活用するように考えています。

イデアはすぐにメモに書き出すこと。 頭の中のままにするとすぐ忘れます。

f:id:jesus9387:20171002125402p:plain:w300:h500

(メモの自分の作成しているアプリで記入しています。)

記事を書くときは画像など どうしてもパソコンのいる作業以外は携帯で済ませておくようにしています。

土日に大きな時間が取れるとは限らない

これは自分だけかもしれませんが 土日にたくさん時間が取れるとは限りません。

出かけたり、急な遊びの予定が入ったり、集中できなくてダラダラしてしまったりしてしまうかと思います。

ですので毎日コツコツ続けることが大事だと思います。

よければ

twitter.com