katekichiのゆるブログ

普段の作業メモや日常の出来事とか

docker for mac + nginxのログをrsyslogコンテナで出力してみた

docker for mac + nginx + オレオレ証明書でローカルSSL環境を作ったメモ - katekichiのゆるブログ

前回の続きで、ログの集中管理する的なことをやってみたかったので rsyslogコンテナに吐き出してみることにしました。

ログの集中管理というと、「fluentd」(http://www.fluentd.org/)が主流だと思いますが 今回は、基本的なところを抑えたかったのでrsyslogにしました。


rsyslogサーバーを立てる

http://d.hatena.ne.jp/tmatsuu/20140603/1401811893 の記事を参考に立てました。

Dockerfile作成

FROM centos:latest
RUN yum install -y rsyslog
ADD remote.conf /etc/rsyslog.d/
EXPOSE 514
CMD ["/sbin/rsyslogd", "-n"]

remote.conf作成(rsyslog.conf)

$ModLoad imudp
$UDPServerRun 514
$AllowedSender UDP, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

local1.* /var/log/nginx_access_log
local2.* /var/log/nginx_error_log

イメージビルド

$ docker build --tag simple-rsyslog .

コンテナ起動

$ docker run -d -p 514:514/udp -h "logserver" simple-rsyslog

テストしてみる

loggerコマンドを使用して、以前構築したnginxコンテナからログを送信してみます。

rsyslogコンテナのIPは、以下で調べました。

$ docker inspect -format="{{ .NetworkSettings.IPAddress }}"  コンテナID

nginxコンテナからログ送信

$ docker exec -it [nginxのコンテナID] /bin/bash  # コンテナに入る

# logger -n[コンテナのIP] -p local1.info testtesttest
# logger -n[コンテナのIP] -p local2.error errorerrorerrorerror

rsyslogコンテナでログの確認

$ docker exec -it [rsyslogのコンテナID] /bin/bash # コンテナに入る

# tail -f /var/log/nginx_access_log 
Jun 27 10:07:16 172.17.0.3 <someone>: testtesttest

# tail -f /var/log/nginx_error_log  
Jun 27 10:12:01 172.17.0.3 <someone>: errorerror12345678

ログフォーマット等の調整は必要そうですが、出力することはできました。

nginxイメージの修正

nginx.confのログ設定を変更します。

  access_log    /var/log/nginx/access.log;
  error_log     /var/log/nginx/error.log;
  ↓
  access_log syslog:server=logserver,facility=local1 main;
  error_log syslog:server=logserver,facility=local2 notice;

イメージビルドと起動(--add-hostで「logserver」を指定)

$ docker build --tag webapp-syslog .
$ docker run -d --name webapp-syslogp -p 443:443 -p 80:80 --add-host logserver:[rsyslogのIP] webapp-syslog

rsyslogコンテナでログ確認

$ docker exec -it [rsyslogのコンテナID] /bin/bash # コンテナに入る

# tail -f /var/log/nginx_access_log 
Jun 27 11:08:12 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:12 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"
Jun 27 11:08:22 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:22 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"
Jun 27 11:08:24 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:24 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"

まとめ

思いつきでやってみましたが、rsyslogの知見がそもそも無いのに、Docker上で構築するってのが 少し敷居が高かったです(そもそもDockerのオペレーションも怪しい・・)。

何事もステップ・バイ・ステップですね。良い勉強になりました。

記事読まれている方で、この辺り詳しい方、是非アドバイス頂けると嬉しいです。


rsyslog 実践ログ管理入門

rsyslog 実践ログ管理入門

docker for mac + nginx + オレオレ証明書でローカルSSL環境を作ったメモ

nginxのローカルSSL環境が必要になり、docker for macオレオレ証明書で作ったので備忘録としてまとめました。

以下の記事を参考に構築してみました。

docker の nginx イメージの設定ファイルを眺めながら、独自ページを表示します。 - Qiita


nginxのdokcerイメージを取得

Docker Hubから最新のnginxイメージを取得する

詳細は、以下の記事を参照

$ docker pull nginx:latest

docker コマンド 基本のキ(nginx のコンテナを実行してみる) - Qiita

環境構築

$ mkdir nginx; cd $ _
$ touch Dockerfile
$ mkdir html; cd $_
$ touch index.html

$ tree
.
├── Dockerfile
└── html
    └── index.html

index.html

<html>
  <head>
     <meta charset="UTF-8">
     <title>SSL Test</title>
  </head>
  <body>
    <h1>SSL Test</h1>
  </body>
</html>

DockerFile作成

FROM nginx
COPY ./html /usr/share/nginx/html

Dockerイメージの作成とコンテナ立ち上げ

$ docker build --tag ssl-nginx .
Sending build context to Docker daemon 4.096 kB
Step 1/2 : FROM nginx
 ---> cc1b61406712
Step 2/2 : COPY ./html /usr/share/nginx/html
 ---> Using cache
 ---> 2cc22a506a12
Successfully built 2cc22a506a12

$ docker run -d --name ssl-nginx-container -p 80:80 ssl-nginx
651eb9b5642d13274aed51f535d7014754bb001bdcfdf90d7e194bcd5c188b74

ブラウザで確認(http://localhost/


SSL対応するためにオレオレ証明書の作成をします。 この辺りは、こちらを参考にしました。 DockerでNginxのコンテナを作成し、https化してWebページやサービスを公開する方法 | Black Everyday Company

$ brew list openssl 

$ mkdir keys; cd $ _
### 秘密鍵を暗号化してしまうと、コンテナ起動時(Nginx起動時)に
「Enter PEM pass phrase」とパスワードが求められ起動できないようなので暗号化はしない。###
$ openssl genrsa 2048 > server.key

### XXXXは適当に入力 ###
$ openssl req -new -key server.key > server.csr

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:XXXX
Locality Name (eg, city) []:XXXX
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XXXX
Organizational Unit Name (eg, section) []:XXXX
Common Name (e.g. server FQDN or YOUR name) []:example.com
Email Address []:hoge@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: <--- パスワードは空白でOK
An optional company name []:

# 有効期限はざっくり100日で
$ openssl x509 -in server.csr -days 100 -req -signkey server.key > server.crt

# 現状の構成
$ tree
.
├── Dockerfile
├── html
│   └── index.html
└── keys
    ├── server.crt
    ├── server.csr
    └── server.key

nginx.confの設定

HTTP(ポート80)で受けたリクエストはHTTPS(ポート443)にリダイレクトする設定にしておきます。

server {
  listen      80;
  server_name localhost;

  return 301 https://$host$request_uri;
}

server {
  server_tokens off;

  access_log    /var/logs/access.log;
  error_log     /var/logs/error.log;

  listen      443;
  server_name localhost;

  ssl                  on;
  ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
  ssl_certificate      /etc/nginx/server.crt;
  ssl_certificate_key  /etc/nginx/server.key;

  location / {
    alias /var/www/;
  }
}

DockerFileの変更

FROM nginx

ADD ./html /var/www/html
ADD ./app.conf /etc/nginx/conf.d/app.conf
ADD ./keys/server.crt /etc/nginx/server.crt
ADD ./keys/server.key /etc/nginx/server.key

RUN chmod 755 -R /var
RUN chmod 400 /etc/nginx/server.key

EXPOSE 443
CMD ["nginx", "-g", "daemon off;"]

Dockerイメージの作成とコンテナ立ち上げ

$ docker build --tag webapp .
Sending build context to Docker daemon 12.29 kB
Step 1/9 : FROM nginx
 ---> cc1b61406712
Step 2/9 : ADD ./html /var/www/html
 ---> Using cache
 ---> 102243bc267d
Step 3/9 : ADD ./app.conf /etc/nginx/conf.d/app.conf
 ---> Using cache
 ---> 00668c5641b7
Step 4/9 : ADD ./keys/server.crt /etc/nginx/server.crt
 ---> Using cache
 ---> 85ee3c0da89a
Step 5/9 : ADD ./keys/server.key /etc/nginx/server.key
 ---> Using cache
 ---> f3095cadf8a3
Step 6/9 : RUN chmod 755 -R /var
 ---> Running in 8eab56aefa1c
 ---> 565d1f1e9f07
Removing intermediate container 8eab56aefa1c
Step 7/9 : RUN chmod 400 /etc/nginx/server.key
 ---> Running in fe5c00f05409
 ---> 02c65246caec
Removing intermediate container fe5c00f05409
Step 8/9 : EXPOSE 443
 ---> Running in 1e67a2a92994
 ---> d95cc7a21aa0
Removing intermediate container 1e67a2a92994
Step 9/9 : CMD nginx -g daemon off;
 ---> Running in 0efbccec8be8
 ---> 437f35b60db0
Removing intermediate container 0efbccec8be8
Successfully built 437f35b60db0

$ docker run -d --name webapp -p 443:443 -p 80:80 webapp
2e389797c84eb2162c0f67ab13eb865966ea70ecead1077a61055eadd869e952

ブラウザで確認(https://localhost/

f:id:katekichi:20170614162035p:plain

とりあえずできました。


最後にログは、コンテナ外に出したいのでその辺を試します。

ホストのディレクトリにマウントする

以下のように-v でホストのディレクトリにマウントするのが まずは、手っ取り早い方法かなと思いした。

$ docker run -d -p 443:443 -p 80:80 -v /tmp/sslnginx-log:/var/log/nginx webapp

Syslogで吐き出す方法もありますが、こちらは別の機会に試そうと思います。 nginx-1.7.1でsyslogにログを飛ばせるようになったので試してみた - Dマイナー志向


まとめ

最近、手を動かすことが少なくなってきているように感じたのでローカルの SSLテスト環境を作ろうと思ってやってみましたが、Dockerの理解がまだ浅かったので 良い勉強になりました。

あと普段の業務では、SSL証明書AWSのELBに配置してしまっているため、直接nginxに 設定する良い機会になりました。

今回作成した環境は、githubにあります。 GitHub - y-nakazawa/docker-sslnginx

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

nginx実践ガイド impress top gearシリーズ

nginx実践ガイド impress top gearシリーズ

ハイパフォーマンスHTTPサーバ Nginx入門

ハイパフォーマンスHTTPサーバ Nginx入門

マスタリングNginx

マスタリングNginx

Nginx ポケットリファレンス

Nginx ポケットリファレンス

「Electronではじめるアプリ開発」を写経してみた ⑥

またかなり間が空いてしまったのと、写経したものの期待通りの動きではなく調査しましたが 以下の理由からかなりハマってしまっていました。

  1. Electron上でのReactのDebug方法を確立できていなかった。

  2. ES2016と、Promiseの理解不足から原因の特定に時間が掛かった。

  3. そもそもFirebaseのお作法が分かってなく、原因の特定に時間が掛かった。

※1.に関しては、以下を参考にしてDebug環境の構築をしたいと思います。 qiita.com

※2. 3.の課題については別途調査してまとめようと思います。

今回は、チャットルーム一覧の実装から

src/renderer/Rooms.jsx

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: []
        };
        this.db = firebase.database();
        this.handleOnChangeRootName = this.handleOnChangeRootName.bind(this);
        this.handleOnSubmit = this.handleOnSubmit.bind(this);        
    }

    componentDidMount() {
        this.fetchRooms();
    }


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

    handleOnSubmit(e) {
        const { roomName } = this.state;
        e.preventDefault();

        if (!roomName.length) {
            return;
        }

        // Make new Chatroom for a Firebase 
        const newRoomRef = this.db.ref("/chatrooms").push();
        const newRoom = {
            description: roomName
        };

        newRoomRef.update(newRoom).then(() => {
            this.setState({ roomName: "" });
            return this.fetchRooms().then(() => {
                hashHistory.push(`/rooms/${newRoomRef.key}`);            
            });
        });
    }

    fetchRooms() {
        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()));
            }); 

            this.setState({ rooms });  
        });
    }

    // Drawing the left pane 
    renderRoomList() {
        const { roomId } = this.props.params;
        const { rooms, roomName } = this.state;
        return (
            <div className="list-group">
                { rooms.map(r => <RoomItem room={r} key={r.key} selected={r.key == roomId} />)}
                <div className="list-group-header">
                    <form style={FORM_STYLE} onSubmit={this.handleOnSubmit} >

                        <input 
                            type="test"
                            className="form-control"
                            placeholder="New room"
                            value={roomName}
                            onChange={this.handleOnChangeRootName}
                        />

                        <button className="btn btn-default" style={BUTTON_STYLE}>
                            <span className="icon icon-plus" />
                        </button>
                    </form>
                </div>
            </div>
        );
    }

    // Drawing the right pane 
    renderRoom() {
        if (this.props.children) {
            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 slider">{this.renderRoomList()}</div>
                <div className="pane">{this.renderRoom()}</div>                
            </div>
        );
    }
}

以下は、チャットルーム一覧の個々のアイテムの実装

src/renderer/RoomItem.jsx

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

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

export default function RoomItem(props) {
    const { selected } = props;
    const { description, key } = props.room;
            
    return(
        <div className={selected ? "list-group-item selected" : "list-group-item"}>
            <Link to={`/rooms/${key}`} style={LINK_STYLE}>
                <div className="media-body">
                    <strong>{description}</strong>
                </div>
            </Link>
        </div>
    );
}

ルームを追加すると一覧に登録されることが確認できました。 f:id:katekichi:20170614001823p:plain f:id:katekichi:20170614001827p:plain

Firebaseのchatroomsにも登録されました。 f:id:katekichi:20170614001831p:plain f:id:katekichi:20170614001834p:plain

今回はここまで!!

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

JavaScript フレームワーク入門

JavaScript フレームワーク入門

「Electronではじめるアプリ開発」を写経してみた ⑤

諸々立て込んでいて停滞していた写経再開しました。今回は、Firebaseの設定から実装します。

firebase.google.com

目次:

Firebaseのセットアップ

上記のサイトでアカウント生成等をして取得した接続情報 を src/render

er/app.jsに貼ります。

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";

const appRouting = (
    <Router history={hashHistory}>
        <Route path="/">
            <Route path="login" component={Login} />
            <Route path="signup" component={Signup} />
            <Route path="rooms" component={Rooms}>
                <Route path=":roomId" component={Room} />
            </Route>            
        </Route>
    </Router>
);

if (!location.hash.length) {
    location.hash = "#/login";
}

// Firebaseの初期化 ←ココ追加
var config = {
    apiKey: "xxxxxxx",
    authDomain: "electron-chat-cyyyyyy.firebaseapp.com",
    databaseURL: "https://electron-chat-cyyyyyy.firebaseio.com",
    projectId: "electron-chat-cyyyyyy",
    storageBucket: "electron-chat-cyyyyyy.appspot.com",
    messagingSenderId: "xxxxxxxx"
};
firebase.initializeApp(config);

render(appRouting, document.getElementById("app"));

ログイン画面の実装

src/renderer/Login.jsxにゴリゴリ実装します。 Validationはもう少し後々、工夫できそうです

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);
        this.state = {
            email: localStorage.userEmail || "",
            password: localStorage.userPassword || "",
            errors: []
        };

        this.handleOnChangeEmail = this.handleOnChangeEmail.bind(this);
        this.handleOnChangePassword = this.handleOnChangePassword.bind(this);
        this.handleOnSubmit = this.handleOnSubmit.bind(this);        
    }

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

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

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

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

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

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

        firebase.auth().signInWithEmailAndPassword(email, password).then(() => {
            localStorage.userEmail = email;
            localStorage.userPassword = password;
            hashHistory.push("/rooms");            
        }).catch(() => {
            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"
                        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">
                    <button className="btn btn-large btn-default">Login</button>
                    <div style={SIGNUP_LINK_STYLE}>
                        <Link to="/signup">create new account</Link>
                    </div>
                </div>
            </form>
        );
    }
}

実行するとこんな感じになりました。

f:id:katekichi:20170525162743p:plain

サインアップ画面の実装

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 Login extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            email: "",
            password: "",
            name: "",
            photoURL: "",
            errors: []
        };

        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.handleOnSubmit = this.handleOnSubmit.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 });
    }


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

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

        if (!password.length) {
            isValid = false;
            errors.push("Password can't be blank.");
        }
        
        if (!name.length) {
            isValid = false;
            errors.push("Name can't be blank.");
        }

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

        firebase.auth().createUserWithEmailAndPassword(email, password).then(newUser => {
            return newUser.updateProfile({
                displayName: name,
                photoURL
            });
        }).then(() => {
            hashHistory.push("/rooms");
        }).catch(err => {
            this.setState({ errors: [err.message] });
        });
    }

    render() {
        return (
            <form style={SIGNUP_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"
                        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">Cancel</button>                
                    </Link>                    
                </div>
            </form>

        );
    }
}

f:id:katekichi:20170525163230p:plain

ユーザ作成を実際に作成するとFirebase上にユーザが作られました。

f:id:katekichi:20170525163525p:plain

今回は、ここまで!!

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

「Electronではじめるアプリ開発」を写経してみた ④

G.W.は、何もしないで終わってしまったので、また写経を再開しました。チャットアプリ の続きをやります。

目次:

npmスクリプトの登録

package.jsonにnpmスクリプト(scripts)を記述すると コマンドのエイリアス登録をできます。

  "scripts": {
    "watch": "babel --out-dir .tmp src --watch",
    "start": "electron .",
  },

$ npm start で electron . と同様となります。

ルーティングの実装

reactreact-router で各画面とルーティングを実装していきます。

src/main/Login.jsx(ログイン画面)

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

export default class Login extends React.Component {
    render() {
        return (
            <div>
                <h2>Login</h2>
                <Link to="/rooms">Login</Link> <br />
                <Link to="/signup">Create new account</Link>                
            </div>
        );
    }
}

src/main/SignUp.jsx(サインアップ画面)

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

export default class Login extends React.Component {
    render() {
        return (
            <div>
                <h2>Signup</h2>
                <Link to="/rooms">Login</Link> <br />
                <Link to="/login">cancel</Link>                
            </div>
        );
    }
}

src/main/Rooms.jsx(チャットルーム一覧画面)

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

export default class Login extends React.Component {
    render() {
        return (
            <div>
                <h2>Rooms</h2>
                <ul>
                    <li><Link to="/rooms/1">Room 1</Link></li>
                    <li><Link to="/rooms/2">Room 2</Link></li>                    
                </ul>
                <div>{this.props.children}</div>
            </div>
        );
    }
}

src/main/Room.jsx(チャットルーム詳細画面)

import React from "react";

export default class Login extends React.Component {
    render() {
        return (
            <div>
                <h3>Room</h3>
            </div>
        );
    }
}

src/renderer/app.jsx(ルーティングの実装)

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";

const appRouting = (
    <Router history={hashHistory}>
        <Route path="/">
            <Route path="login" component={Login} />
            <Route path="signup" component={Signup} />
            <Route path="rooms" component={Rooms}>
                <Route path=":roomId" component={Room} />
            </Route>            
        </Route>
    </Router>
);

if (!location.hash.length) {
    location.hash = "#/login";
}

render(appRouting, document.getElementById("app"));

メニューの作成

もろもろメニュー項目を追加します。 単純作業で地味にツラったです・・

src/main/setAppMenu.jsx(ルーティングの実装)

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

function setAppMenu() {

    const template = [
        {
            label: "File",
            submenu: [
                { label: "New Window", accelerator: "CmdOrCtrl+N", click: createWindow },
                { type: "separator" },
                { label: "Close", accelerator: "CmdOrCtrl+W", role: "close" },                
            ],                     
        },
        {
            label: "Edit",
            submenu: [
                { label: "Copy", accelerator: "CmdOrCtrl+C", role: "copy" },
                { label: "Paste", accelerator: "CmdOrCtrl+V", role: "paste" },
                { label: "Cut", accelerator: "CmdOrCtrl+X", role: "cut" },
                { label: "Select All", accelerator: "CmdOrCtrl+A", role: "selectall" }                
            ],                     
        },
        {
            label: "View",
            submenu: [
                { 
                    label: "Reload", 
                    accelerator: "CmdOrCtrl+R", 
                    click: (item, focusedWindow) => focusedWindow && focusedWindow.reload()
                },
            ],                     
        },
        {
            label: "Toggle DevTools",
            submenu: [
                { 
                    label: "Reload", 
                    accelerator: process.platform === "darwin" ? "Alt+Command+I" : "Ctrl+Shift+I",             
                    click: (item, focusedWindow) => focusedWindow && focusedWindow.toggleDevTools()
                },
            ],                     
        },
    ];

    if (process.platform === "darwin") {

        template.unshift({
            label: app.getName(),
            submenu: [
                { role: "about" },
                { type: "separator" },
                { role: "services", submenu:[] },
                { type: "separator" },
                { role: "hide" },            
                { role: "hideothers" },
                { role: "unhide" },
                { type: "separator" },
                { role: "quit" }
            ],        
        });
    }

    template.push({
        role: "window",
        submenu: [
            { role: "minimize" },
            { role: "zoom" },            
        ]
    });

    const appMenu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(appMenu);        
}

export default setAppMenu;

src/main/index.js(アプリケーションメニューを追加)

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

app.on("ready", () => {
    setAppMenu();
    createWindow();
});

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

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

こんな感じになりました。

f:id:katekichi:20170514001241p:plain

f:id:katekichi:20170514001252p:plain

f:id:katekichi:20170514001259p:plain

f:id:katekichi:20170514001301p:plain

今回は、メニュー作成するのが少々単純作業で辛かったです・・。 次回は、Firebaseを使用するので、テクニカルな面白味はありそうです。

「Electronではじめるアプリ開発」を写経してみた ③

少し間が空きましたが、ぼちぼち写経しました。チャットアプリの続きをやります。

目次:

3-3 開発プロジェクトの作成

package.jsonの作成と必要なモジュールをインストールする

 npm init
 npm install electron@1.6.1 --save-dev
 npm install connors/photon --save
 npm install react@15.4.2 react-dom@15.4.2 react-router@3.0,0 --save
 npm install babel-cli@6.18.0 babel-preset-es2015@6.18.0 babel-preset-react@6.16.0 --save-dev

src/main/index.js(mainプロセス実装)

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

app.on("ready", () => {
    createWindow();
});

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

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

src/main/createWindow.js(Window生成)

import  { BrowserWindow } from "electron";

let win;
function createWindow() {
    win = new BrowserWindow();
    win.loadURL(`file://${__dirname}/../../index.html`);
    win.on("closed",() => {
        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>

src/renderer/app.jsx(renderプロセス実装)

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

render(<div>Hello, Electron and React JSX</div>, document.getElementById("app"));

.babelrc(Babelの設定。ES2015、React JSX形式のファイルをトランスパイルする)

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

Babelの実行(srcディレクトリ下層の各jsとjsxを.tmp配下にトランスパイルしたjsとして配置) –watch 指定でファイル変更を自動検知する

$ ./node_modules/.bin/babel --out-dir .tmp src --watch

package.jsonのエンドポイントを変更

  "main": "main.js",
     ↓
  "main": ".tmp/main/index.js",

実行

$ ./node_modules/.bin/electron .

こんな感じ

f:id:katekichi:20170501235715p:plain

今回は、React JSXの初歩的な書き方と、Babelを使用したトランスパイルでした。 次回は、ルーティングの実装から

「Electronではじめるアプリ開発」を写経してみた ②

今日もぼちぼち写経しました。

目次:

前回の続きから・・。Renderプロセスにformの実装をするところから

2-4 最初のアプリケーションを作成する

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>My first Electron app</title>
    </head>
    <body>
        <h1>CommentBox</h1>
        <form id="comment-form">
            <input type="text" id="comment-input" placeholder="コメント">
            <input type="submit" value="投稿">            
        </form>
        <ul id="comments"></ul>
        <script src="render.js"></script>
    </body>
</html>

render.js

※ひさびさに生JSを書いた

document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("comment-form").onsubmit = () => {
        const commentInput = document.getElementById("comment-input");

        if (commentInput.value === "") {
            return false;
        }

        const newComment = document.createElement("li");
        newComment.innerText = commentInput.value;
        document.getElementById("comments").appendChild(newComment);

        commentInput.value = "";
        return false;
    };
});

f:id:katekichi:20170425185434p:plain

とりあえず、ここまでは普通にWeb開発やっているときとそんなに大差ない感じ。 Mainプロセスがクライアントアプリ感出ているくらい

3 チャットアプリを作ろう

Firebase使うぽいので、ちょっと楽しみ

3-1 Electronが使われているチャットアプリケーション

  • SlackRocker.chatの紹介
  • WebSocketやWebRTCのようなリアルタイムで双方向通信する仕組みが必要
  • Firebaseもその一つ

3-2 開発するチャットアプリケーション

React使ってSPAで作るよって話し。 機能としては、ログイン、サインアップ、メイン(チャットルーム一覧と詳細画面)

facebook.github.io

次回は、 3-3 開発プロジェクトの作成から