読者です 読者をやめる 読者になる 読者になる

undefined

bokuweb.me

Phoenix + ReactでChannelを使用した簡易チャットを作る(1)

勉強用の題材としてブログを作りはじめた。

blog.bokuweb.me

ブログの更新やコメントにも使用できるので、次はchannelを使用して簡単なチャットを作ってみる。Channelとは簡単にwebsocket通信ができる機能で、Node.jsにおけるSocket.io的なものと理解している。

動作環境

  • Erlang 17.5
  • Elixir 1.2.0
  • Phoenix 1.1.4
  • node.js 4.2.1

環境構築

前回同様。

blog.bokuweb.me

プロジェクト名はphoenix_channel_sandboxとする。

mix phoenix.new phoenix_channel_sandbox

また今回、package.jsonのdependenciesを削除する際以下の2つは削除しない。

  "dependencies": {
   "phoenix": "file:deps/phoenix",
   "phoenix_html": "file:deps/phoenix_html"
  },

Channelへのjoinまでを実装

バックエンド

lib/phoenix_channel_sandbox/endpoint.exsocket用のエンドポイントが用意されている。

  • lib/chat_phoenix/endpoint.ex
defmodule PhoenixChannelSandbox.Endpoint do
  use Phoenix.Endpoint, otp_app: :phoenix_channel_sandbox

  socket "/socket", PhoenixChannelSandbox.UserSocket

これにより、エンドポイント/socketにアクセスするとUserSocketモジュール に接続されることになる。 UserSocketweb/channels以下に雛形が生成されており、以下の行のコメントアウトを解除する。

  • web/channels/user_socket.ex
  channel "rooms:*", PhoenixChannelSandbox.RoomChannel

メッセージはPhoenix.Socket.Messageの形式でやりとりされる。上記の"rooms:*"はtopic名で、topic名はtopic:subtopic、またはtopicの形式で指定される。*はワイルドカードなのでトピックroomsに来たすべてのサブトピックをRoomChannelモジュールに送ることになる。

Phoenix.Socket.Message – Phoenix v1.1.4

メッセージの受け先となるモジュールroom_channelweb/channelsに作成する。

  • web/channels/room_channel.ex
defmodule PhoenixChannelSandbox.RoomChannel do
  use PhoenixChannelSandbox.Web, :channel

  def join("rooms:join", message, socket), do: {:ok, socket}
end

join/3channelへの参加を承認/拒否する関数で、{:ok, socket}{:ok, reply, socket}を返すことで、承認されchannelに参加できる。 拒否するには、{:error, reply} を返す。今回はtopic名rooms、subtopic名joinにアクセス接続が合った場合無条件に参加を承認している。

バックエンドはこれでOKで、JS側を実装する。

フロント

  • web/static/js/app.js
import "../../../deps/phoenix_html/web/static/js/phoenix_html";
import Socket from "./socket";
  • web/static/js/socket.js
import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()

const channel = socket.channel("rooms:join", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

余談だけど、サンプルのJSには;がついていない。;付けない一派なんだろうか。

joinの確認

$ mix phoenix.server
$ open http://localhost:4000/ 

問題なければ下記のようにjoin成功のメッセージが出力されているはず。

f:id:bokuweb:20160322223048p:plain

Reactを使った簡易チャットの実装

バックエンド

room_channel.exに以下の関数を追加する。

  • web/channels/room_channel.ex
  def handle_in("new:message", message, socket) do
    broadcast! socket, "new:reply", %{msg: message["msg"]}
    {:noreply, socket}
  end

handle_in/3は受信メッセージをハンドルする関数で、今回はnew:messageというイベントを受け取った場合、メッセージ内容をブロードキャスト配信している。

https://hexdocs.pm/phoenix/Phoenix.Channel.html#c:handle_in/3

次にReactでマウントできるようweb/template/layout/app.tml.eexを掃除しておく。

  • web/template/layout/app.tml.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello PhoenixChannelSandbox!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <body>
      <div id="root"></div>
      <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

フロント

  • Reactのインストール
$ npm i  -S react react-dom

送受信が行えるよう、socket.jsを変更しておく。

  • web/static/js/socket.js
import { Socket } from "phoenix";

export default class SocketTest {
  constructor(endpoint, token) {
    this.socket = new Socket(endpoint, { params: { token }});
    this.socket.connect();
  }

  connect(topic, msg = {}) {
    this.channel = this.socket.channel(topic, msg)
    this.channel.join()
      .receive("ok", resp => console.log("Joined successfully", resp))
      .receive("error", resp =>  console.log("Unable to join", resp));
  }

  send(msg) {
    this.channel.push("new:message", { msg }); // TODO: 
  }

  addListener(key, fn) {
    this.channel.on(key, fn);
  }
}

app.jsではフォームの描画とメッセージ受信時の更新、ボタン押下時のメッセージ送信を行う。 今回はnew:replyというイベントをListenして、コールバックでメッセージ内容をstateに詰めてる。

  • web/static/js/app.js
import "../../../deps/phoenix_html/web/static/js/phoenix_html";
import Socket from "./socket";
import React, { Component } from 'react';
import { render } from 'react-dom';

class Chat extends Component {
  constructor(props) {
    super(props);
    this.state = { messages: [] };
    this.socket = new Socket('/socket');
    this.socket.connect('rooms:join', {});
    this.socket.addListener("new:reply", messages => {
      this.setState({ messages: this.state.messages.concat(messages) });
    })
  }

  handleSubmit(e) {
    e.preventDefault();
    const message = this.refs.message.value.trim();
    if (!message) return;
    this.socket.send(message);
  }

  renderMessages() {
    return this.state.messages.map(message => <p>{message.msg}</p>);
  }

  render() {
    return (
      <div>
        <h1>Chat sample!!</h1>
        <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
          <input type="text" ref="message" placeholder="message" />
          <input type="submit" value="Post" />
        </form>
        {this.renderMessages()}
      </div>

    );
  }
}

render(<Chat />, document.querySelector('#root'));

ひとまずこれで最低限のチャットは可能となる。

f:id:bokuweb:20160322210259g:plain

今回の成果物

github.com

TODO

  • データの永続化
  • 認証
  • 見た目
  • スクロールとか 検索とか
  • Herokuへのデブロイ
    • かなり検索したがまだ失敗している

参考記事

2015/07/09/Phoenix FrameworkのChannelを使ってTwtter Streamingをbroadcastする - ヽ(´・肉・`)ノログ

ruby-rails.hatenadiary.com

所感

少しElixir書いた

Phoenix + React + Reduxでブログシステムを作る(1)

f:id:bokuweb:20160321192610p:plain

Elixir、Phoenixの勉強のための題材として、ブログシステムで作ってみることにした。飽きるまでのんびり改修していこうと思う。Elixir/Erlangの学習はすごいE本を並行して進める。

Elixir、Phoenixのインストールは完了しているものとする。

今回のゴール

  • 記事の投稿ができる
  • 記事の閲覧ができる

動作環境

  • Erlang 17.5
  • Elixir 1.2.0
  • Phoenix 1.1.4
  • node.js 4.2.1

プロジェクトの作成

プロジェクトを作成する。ただし、自分みたいなフロントな人間は真っ先に--no-brunchを付けたくなると思うんだけど--no-brunchを付けずにあとから手動で変更したほうが楽だという話しを聞くので参考にしてみる。

$ mix phoenix.new phoenix_redux_blog

参考にしたのは以下の記事。

qiita.com

以下、基本的には上記の記事に沿うが、差分としてはbabel-preset-stage-0stylusを入れている点。その他詳細は上記の記事を参照のこと。

  • node_modulesの削除
$ rm -rf node_modules
  • dependenciesの掃除
{
  "repository": {
  },
  "dependencies": {
  }
}
開発用パッケージのインストール
$ npm i -D watchify browserify babelify uglify-js
$ npm i -D babel-preset-es2015 babel-preset-react babel-preset-stage-0 stylus
  • .babelrc
{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ]
}
  • package.json
{
  "repository": {},
  "scripts": {
    "build-assets": "cp -r web/static/assets/* priv/static",
    "watch-assets": "watch-run -p 'web/static/assets/*' npm run build-assets",
    "build-js": "browserify -t babelify web/static/js/app.js | uglifyjs -mc > priv/static/js/app.js",
    "watch-js": "watchify -t babelify web/static/js/app.js -o priv/static/js/app.js -dv",
    "watch-stylus": "stylus web/static/stylus/ --watch  --out  priv/static/css/",
    "build-stylus": "stylus web/static/stylus/ --out priv/static/css/",
    "build": "npm run build-assets && npm run build-js && npm run build-stylus",
    "watch": "npm run watch-assets & npm run watch-js & npm run watch-stylus",
    "test": "echo \"Error: no test specified\" && exit 1",
    "compile": "npm run build" 
  },
  "dependencies": {
      ...

watchers: [npm: ["run", "watch"]]に変更して、npm scriptでwatchできるようにする。

  • config/dev.exs
config :goal_server, GoalServer.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  cache_static_lookup: false,
  check_origin: false,
  watchers: [npm: ["run", "watch"]] 

これでJS周りの環境構築は完了

API作成

JSON APIを作成。ひとまず、タイトルの記事本文のみ作成。gen.jsonではなくgen.htmlしてから、JSONを返すようにしたほうが、UIが自動生成されるため便利という情報もあったけど、作業増えそうだったので一旦ペンディング。気になる方は以下を参考にするとよいかも。

qiita.com

以降同じテーマの記事があったのでそちらを参考にさせていただく。

qiita.com

APIを生成する。

$ mix phoenix.gen.json Post posts title:string body:text

指示通り、web/router.exに追記する。

web/router.ex

scope "/api", PhoenixReduxBlog do
  pipe_through :api
  resources "/posts", PostController, except: [:new, :edit]
end
  • PostgreSQLを起動しておく
$ postgres -D /usr/local/var/postgres
  • ecto.createとecto.migrateを実行
$ mix ecto.create 
$ mix ecto.migrate

ここでAPIを確認してみる。以下のように一覧が確認できる。よさそう。

$ mix phoenix.routes

page_path  GET     /               PhoenixReduxBlog.PageController :index
post_path  GET     /api/posts      PhoenixReduxBlog.PostController :index
post_path  GET     /api/posts/:id  PhoenixReduxBlog.PostController :show
post_path  POST    /api/posts      PhoenixReduxBlog.PostController :create
post_path  PATCH   /api/posts/:id  PhoenixReduxBlog.PostController :update
           PUT     /api/posts/:id  PhoenixReduxBlog.PostController :update
post_path  DELETE  /api/posts/:id  PhoenixReduxBlog.PostController :delete

次に参考記事通り、データを追加してみる

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"post": {"title": "phoenixでAPIをつくってみる", "body": "APIのテストだよー" }}' http://localhost:4000/api/posts

取得。

$ curl -v -H "Accept: application/json" -H "Content-type: application/json" http://localhost:4000/api/posts
{"data":[{"title":"phoenixでAPIをつくってみる","id":1,"body":"APIのテストだよー"}]}

ちゃんと返ってきてる。

Reactを使えるようにする

ここでReact周りの準備をしておく。 余分なものを削除。web/templates/layout/app.html.eexを以下のように編集

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Hello PhoenixReduxBlog!</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
  </head>

  <body>
      <main id="root"></main>
      <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

Reactのインストール

npm i -S react react-dom

web/static/js/app.jsを以下のように編集。 設定が上手くいっていればJSの変更もリアルタイムに反映されるはず。 ひとまず、#root<h1>Hello, World!!</h1>をマウントする。

import "../../../deps/phoenix_html/web/static/js/phoenix_html"
import React from 'react';
import { render } from 'react-dom';

render(<h1>Hello, World!!</h1>, document.querySelector('#root'));

localhost:4000にアクセスして、Hello, World!!が表示されていればOK。

f:id:bokuweb:20160321223810p:plain

よさそう。

Reduxを入れて、最初に登録したデータを表示するところまでやってみる

パッケージの追加

npm i -S redux react-redux redux-api-middleware redux-logger redux-actions

Entry

web/static/js/app.jsを以下のように編集。なお以降のJSファイルは原則web/static/js/配下に配置しているものとする。

  • web/static/js/app.js
import "../../../deps/phoenix_html/web/static/js/phoenix_html";
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './stores/configure-store';
import App from './containers/app';

const store = configureStore();

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#root')
);

Store

middlewareにはapi-middlewareloggerを使用する。 middlewareを適用してストアを生成する。

  • stores/configure-store.js
import { createStore,  applyMiddleware } from 'redux';
import reducers from '../reducers'
import { apiMiddleware } from 'redux-api-middleware';
import createLogger from 'redux-logger';

export default () => {
  const logger = createLogger();
  return createStore(
      reducers,
      applyMiddleware(apiMiddleware, logger)
  );
};

Action

redux-api-middlewareを使用している。 fetchArticlesが呼ばれたら、エンドポイント/api/postsGETし、記事を取得してくる。 成功時には取得記事がpayloadに詰められたActionが生成され、reducerに配送される。

  • actions/blog.js
import { CALL_API } from 'redux-api-middleware';

export const fetchArticles = () => {
  return {
    [CALL_API]: {
      endpoint: '/api/posts',
      method: 'GET',
      types: [
        'REQUEST_FETCH_POSTS',
        {
          type: 'SUCCESS_FETCH_POSTS',
          payload: (action, state, res) => res.json().then(payload => payload),
        },
        'FAILURE_FETCH_POSTS',
      ],
    }
  }
}

Reducer

RootReducer。ひとまずblogにしとく。

  • reducers/index.js
import { combineReducers } from 'redux';
import blog from './blog';

const rootReducer = combineReducers({
  blog,
});

export default rootReducer;

blog rediucer。投稿記事のフェッチ成功時にpostsに内容を突っ込んでおく。

  • reducers/blog.js
import { handleActions } from 'redux-actions';

const defaultState = {
  posts: [],
};

export default handleActions({
  SUCCESS_FETCH_POSTS: (state, action) => {
    return { posts: action.payload.data };
  },
}, defaultState);

Container

  • containers/app.js
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Blog from '../components/blog';
import * as blog from '../actions/blog';

const mapStateToProps = state => state;

const mapDispatchToProps = dispatch => ({
  actions :{
    blog: bindActionCreators(blog, dispatch),
  }
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Blog);

Components

mount時に記事を取りに行って、先頭記事のタイトルのみ表示している。 記事がない場合はテキストloadingを表示する。

  • components/blog.js
import React, { Component } from 'react';

export default class Blog extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.props.actions.blog.fetchArticles();
  }

  render() {
    return (
      <h1>
        {
          this.props.blog.posts.length !== 0
            ? this.props.blog.posts[0].title
            : 'loading'
        }
      </h1>
    );
  }
}

f:id:bokuweb:20160321201830p:plain

表示された。 ここまで実装したら見た目とか、フォーム、actionをゴリゴリ書いてく。 ここまでのコードは以下。

github.com

記事の投稿・閲覧の実装

ここでスタイルの追加やコンポーネントの分割を行っている。

Components

Content

サイドメニューとコンテンツ部分に分割した。サイドメニューには今は何もないので省略。 取得した記事をひたすら表示していく。

import React, { Component, PropTypes } from 'react';
import PostForm from './post-form';

export default class Contents extends Component {
  renderPosts() {
    return this.props.posts.map(post => {
      return (
        <div>
          <h2 className="contents__title">{post.title}</h2>
          <p className="contents__body">{post.body}</p>
        </div>
      );
    })
  }

  render() {
    return (
      <div className="contents">
        <PostForm addPost={this.props.post} />
        {
          this.props.posts.length !== 0
            ? this.renderPosts()
            : 'loading'
        }
      </div>
    );
  }
}
PostForm

postボタンが押下されるとtitle, bodyを引数にアクションaddPostを呼ぶ。

import React, { Component, PropTypes } from 'react';

export default class PostForm extends Component {
  constructor(props) {
    super(props);
    this.state = { title: '', body: '' };
  }

  onTitleChange(e) {
    this.setState({ title: e.target.value });
  }

  onBodyChange(e) {
    this.setState({ body: e.target.value });
  }

  onSubmit(e) {
    e.preventDefault();
    const title = this.state.title.trim();
    const body = this.state.body.trim();
    if (!title || !body) return;
    this.props.addPost({ post: { title, body } });
  }

  render() {
    return (
      <div className="post-form">
        <input
           type="text"
           className="post-form__input"
           onChange={this.onTitleChange.bind(this)}
           placeholder="title"
        />
        <textarea
          className="post-form__textarea"
          onChange={this.onBodyChange.bind(this)}
        />
        <input
          className="post-form__button"
          type="submit"
          onClick={this.onSubmit.bind(this)}
          value="Post"
        />
      </div>
    );
  }
}

Action

Actionに投稿用のものを追加した。

export const addPost = (payload) => {
  return {
    [CALL_API]: {
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
      endpoint: '/api/posts',
      method: 'POST',
      body: JSON.stringify(payload),
      types: [
        'REQUEST_ADD_POST',
        {
          type: 'SUCCESS_ADD_POST',
          payload: (action, state, res) => res.json().then(payload => payload),
        },
        'FAILURE_ADD_POST',
      ],
    }
  }
}

前回のものから大きな差分はこんなところ。 ひとまず記事の投稿・閲覧ができるようになった。

f:id:bokuweb:20160321202929g:plain

その他詳細は以下を参照ください。

github.com

TODO

  • [ ] HerokuへのDeploy
    • 試してみたが、今はなぜかstartに失敗してクラッシュしている.更のプロジェクトで試してみる
  • [ ] 認証、ログイン機能
  • [ ] SSR
  • [ ] Channelによる記事の更新
  • [ ] 記事の編集・削除
  • [ ] インクリメンタル検索
  • [ ] ページング
  • [ ] 管理画面
  • [ ] コメント機能

所感

Elixir全く書いていない

ぽよんと表示されるmodalコンポーネントを作った

f:id:bokuweb:20160318085435g:plain

吹き出しコンポーネントを作った時から、SVGで面白い動きのコンポーネントが作ってみたいと思っていて、その習作としてSVGで描画したぽよんと表示されるmodalコンポーネントを作ってみた。

blog.bokuweb.me

作ったもの

github.com

デモ

React-elastic-modal example

使い方

インストール

npm i react-elastic-modal

サンプル

以下のように使用する。極力react-modalに似せたつもり。

<Modal
  isOpen={ this.state.isOpen }
  onRequestClose={ () => this.setState({ isOpen: false }) }
  modal={{
    width: '50%',
    height: '360px',
    backgroundColor: '#fff',
    opacity: 0.5,
  }}
  overlay={{
    background: 'rgba(0, 0, 0, 0.4)',
  }}
>
  <div>modal example</div>
</Modal>

このコンポーネントについて

使用例

@59nagaさんが以下のサイトで使ってくれている。ありがとうござます。

https://cdn.berabou.me/

SVGまわり

以下のように書いて、rafでアニメーションしてる。なんかあまりいい方法とは思えない。また、親から例えばサイズを100px×100pxでもらった場合、伸縮を表現するためにSVG領域は110px×110px確保している。アニメーション完了後いサイズを戻せばいいんだろうけど、今は放置している。SVGの機能についても、もっと理解を深める必要がありそう。

modalのコンテンツはSVGとは別レイヤーにしてz-indexで被せている。この辺りも正攻法を把握してない。

<svg
  width={`${100 + svgMarginRatio * 200}%`}
  height={`${100 + svgMarginRatio * 200}%`}
  style={{
    position: 'absolute',
    top: `-${100 * svgMarginRatio}%`,
    left: `-${100 * svgMarginRatio}%`,
    transform: `scale3d(${this.state.scale}, ${this.state.scale}, 1)`,
    opacity: this.props.modal.opacity || 1,
  }}
>
  <path d={ `M ${x0} ${y0}
             Q ${cx} ${top} ${x1} ${y0}
             Q ${right} ${cy} ${x1} ${y1}
             Q ${cx} ${bottom} ${x0} ${y1}
             Q ${left} ${cy} ${x0} ${y0}` }
    fill={ this.props.modal.backgroundColor }
  />
</svg>

中央寄せの話し

modalを中央寄せする中で最近は以下の方法がかなりお気に入りで使用していたんだけど、要素のwidth/heightが奇数の場合、transform: translate(-50%, -50%)で座標が小数点になって表示がぼやけるってことに遭遇した。これだったらボックスのサイズを知る必要がなく便利なんだけど、結局古のネガティブマージンで対応することにした。css難しい。

.outer {
  position: relative;
}
.inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

テストまわり

最近はenzymeを使っているんだけどまだ理解できていないところが多い。基本shallowを使ってテスト(この場合はmockは不要?)して、必要に応じてmountするという方法をとってるんだけどそういうスタンスでいいんだろうか。

たとえば、defaultPropsの値を確認したかったり、componentDidMountを発火させたかったり、element.clientHeightを取りたいような場合はどうしいてもmountする必要がある。その場合は結局mockeryとかproxyquireとかでmockに置き換える必要があると思う(今回は単一のコンポーネントなので必要ないけど)んだけどそうのようなスタンスでいいのか不明だ。

もう一点ハマった点は、modalclinetHeight/Widthが常に0が取れてきていて、なんだろうと思っていたんだけどマウントの仕方が悪かったよう。どうやら以下のようにしているとサイズが出ないようで、

const modal = mount(
  <Modal ..... />
);

正しくは以下のようにdivにアタッチする必要があるよう。

const modal = mount(
  <Modal ..... />, { attachTo: div }
);

そのため、beforeとafterで以下のようにしている。

describe('Modal test', () => {
  let div;
  beforeEach(() => {
    div = document.createElement('div');
    document.body.appendChild(div);
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

 ...

さいご

SVGは楽しいんだけど、もう少し、SVG自身の機能や作法を学ぶ必要がありそう。またモーフィング周りにいいライブラリがあるともっと楽しめそうなんだが、sebmarkbage/artこのあたりとか使えるんだろうか。

ElixirですごいE本 10章

しばらく停止していたElixirの勉強、順番を入れ替えて10章から再開することにした。

すごいErlangゆかいに学ぼう!

すごいErlangゆかいに学ぼう!

10.4 さようなら、いままで魚をありがとう

プロセスを生成する

iex(1)> spawn fn -> 2 + 2 end
#PID<0.59.0>

http://elixir-lang.org/getting-started/processes.html#spawn

  • <0.59.0> はプロセス識別子
  • プロセスを表す任意の値
defmodule LearnProcess do
  def g(x), do: :timer.sleep 10, IO.puts x

  def start do
    Enum.each(Range.new(1, 10), fn(i) ->
      spawn fn -> IO.puts i end
    end)
  end
end

LearnProcess.start

- 結果

6
2
1
4
8
3
9
10
5
7
  • :をつけるとErlangの関数が呼べる
  • なのでio:formatもこんなふうに呼べる:io.format("Hello, world!~n")
iex(1)> self()
#PID<0.57.0>
iex(2)> Process.alive?(self())
true
iex(5)> self()
#PID<0.57.0>
iex(6)> exit(self())
** (exit) #PID<0.57.0> 
  • self()で現在のプロセスのpid
  • alive?で生存を確認
  • pid変わってない..

メッセージを送信する

iex(1)> send self(), {:hello, "world"}
{:hello, "world"}
iex(2)> send self(), "hello"
"hello"
iex(3)> send self(), "hello"
"hello"
iex(4)> flush()
{:hello, "world"}
"hello"
"hello"
:ok
iex(5)> flush()
:ok
  • send/2でメッセージ送信
  • flush/0はメールボックスにある全てのメッセージを表示し,空にする

メッセージを受信する

  • dolphin.ex
defmodule Dolphins do
  def dolphin1 do
    receive do
      :do_a_flip -> IO.puts "How about no?"
      :fish -> IO.puts "So long and thanks for all the fish"
      _ -> IO.puts "Heh, we're smarter than you humans."
    end
  end
end
iex(1)> c("dolphins.ex")
iex(2)> send self(), :fish
:fish
iex(3)> spawn(Dolphins.dolphin1)
So long and thanks for all the fish
** (ArgumentError) argument error
    :erlang.spawn(:ok)
  • 受け取れてるがargument errorでてる

こうか。spawn/3の第二引数はatom

iex(1)> dolphin = spawn(Dolphins, :dolphin1, [])
#PID<0.60.0>
iex(2)> send dolphin, "oh, hello dolphin!"
Heh, we're smarter than you humans.
"oh, hello dolphin!"
iex(3)> send dolphin, :fish
:fish
iex(4)> send dolphin, :fish
:fish
iex(5)> send dolphin, :fish
:fish

プロセス終了していない。

もしパターンにマッチするメッセージがメールボックスに無ければ,現在のプロセスはマッチするメッセージがくるまで待ち続けます.タイムアウトを指定することもできます

defmodule Dolphins do
  def dolphin1 do
    receive do
      :do_a_flip -> IO.puts "How about no?"
      :fish -> IO.puts "So long and thanks for all the fish"
      _ -> IO.puts "Heh, we're smarter than you humans."
    after
      1_000 -> IO.outs "nothing after 1s"
    end
  end
end
iex(11)> c("ch10.ex")
[Dolphins]
iex(12)> dolphin = spawn(Dolphins, :dolphin1, [])
#PID<0.132.0>
nothing after 1s
  • タイマーを付けてみる、1sメッセージがなければ終了
defmodule Dolphins do
  def dolphin1 do
    receive do
      :do_a_flip -> IO.puts "How about no?"
      :fish -> IO.puts "So long and thanks for all the fish"
      _ -> IO.puts "Heh, we're smarter than you humans."
    after
      1_000 -> IO.puts "nothing after 1s"
    end
  end

  def dolphin2 do
    receive do
      {:do_a_flip, from} -> send from, "How about no?"
      {:fish, from} -> send from, "So long and thanks for all the fish"
      _ -> IO.puts "Heh, we're smarter than you humans."
    end
  end
end
iex(1)> c("ch10.ex")
[Dolphins]
iex(2)> dolphin = spawn(Dolphins, :dolphin2, [])
#PID<0.68.0>
iex(3)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(4)> flush()
"How about no?"
:ok
iex(5)> send(dolphin, {:fish, self()})
{:fish, #PID<0.57.0>}
iex(6)> flush()
:ok
  • タプルにpidを詰めて送信、送り返す
  • 送り返した後プロセス終了?
defmodule Dolphins do
  def dolphin3 do
    receive do
      {:do_a_flip, from} ->
        send(from, "How about no?")
        dolphin3
      {:fish, from} -> send(from, "So long and thanks for all the fish")
      _ ->
        IO.puts "Heh, we're smarter than you humans."
        dolphin3
    end
  end
end
iex(8)> dolphin = spawn(Dolphins, :dolphin3, [])
#PID<0.83.0>
iex(9)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(10)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(11)> send(dolphin, {:do_a_fl, self()})
Heh, we're smarter than you humans.
{:do_a_fl, #PID<0.57.0>}
iex(12)> flush()
"How about no?"
"How about no?"
:ok
iex(13)> send(dolphin, {:do_a_fl, self()})
Heh, we're smarter than you humans.
{:do_a_fl, #PID<0.57.0>}
iex(14)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(15)> flush()
"How about no?"
:ok
iex(16)> send(dolphin, {:fish, self()})
{:fish, #PID<0.57.0>}
iex(17)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(18)> flush()
"So long and thanks for all the fish"
:ok
iex(19)> flush()
:ok
iex(20)> send(dolphin, {:do_a_flip, self()})
{:do_a_flip, #PID<0.57.0>}
iex(21)> flush()
:ok
  • メッセージを送り返したあと再帰でメッセージ待機

国内のオープンなslack teamを検索できるslack list jaを作った

f:id:bokuweb:20160308143401p:plain

国内のオープンなslack teamを検索できるslack list jaを作った

Slack-list-ja

経緯

最近、リモートワーカーのためのslack teamが作成されて参加させてもらっている。リモートワークならではの健康面などの知見・意見が交換されていて、非常に有用だと思っている。リモートワークして2ヶ月が経とうとしているんだけど、リモートワーク控えめに言っても最高なのでもっと広まって欲しい。

リモートワーカー Slack Team のご紹介 - 9mのパソコン日記

そんな中、国内にオープンなslack teamがどれだけあるんだろう?と調べ始めたら、海外にはそういったslack teamをリスト化したページSlack Listなるものが存在しており、remote-workers-jpの布教も兼ねて国内版を作ってみることにした。

技術的なとこ

せっかく作るんだから何か触ったことないものを。ということで以下を触ってみた。

  • Inferno

github.com

  • Bulma

bulma.io

Inferno

www.moongift.jp

APIがReactに似せられた、Reactよりも高速なライブラリらしい。サーバーサイドレンダリングにも対応。こんな感じでReactのように書ける。

import { Component } from 'inferno-component';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    }
  }
  render() {
    return (
      <div>
        <h1>Header!</h1>
        <span>Counter is at: { this.state.counter }</span>
      </div>
    )
  }
}

<div {...props} />みたいに書けなかったり、微妙に違いはあれど、ほぼ違いを意識せずに書けるとは思う。ただ、今回はバグなのか使用方法が間違っていたのか、意図しない挙動に遭遇して、残念ながら早々にReactに戻してしまった。それでもほとんどimportとか.babelrcを直すくらいだったと記憶している。あと、(当然ながら)データ・描画数が少なくて、パフォーマンスの違いを感じられるようなことはなかった。これは、後述するが、(見つけられていないだけかもしれないが)国内にはオープンなslack teamが少なくてデータ数が数個だったからだ。サーバーサイドレンダリングなどは試せていない。近いうちにもう一回触ってみたい。

Bulma

FlexboxベースのモダンなCSSフレームワーク。と書いてある。自分はMaterial DesignやBootstrapがあまり好きじゃなくて、なんかシンプルなのがないかと模索していたところ、こいつが公開されていて使用してみることにした。Gridとかいい感じ。react-bulmaを作ろうと思ってリポジトリ作ったけど、あまり作業できていない。使用感としては悪くないけど、個人的にはCSSフレームワークは使わないにこしたことないという考えなので、今後使うことはすくないかも。

f:id:bokuweb:20160308152034p:plain

ただ、CSS以下を追加しただけなので採用としては成功していると思う。

.hero-content
  padding 120px 0 80px
  background #f5f7fa

.logo
  height 200px
  width auto

.card
  margin 0 auto

  p.title
    margin 0

.tag
  margin 0 5px 0 0

footer
  text-align center
  margin 40px 0 0 0

作ってみて

作ったあとに驚いたんだが、探してみても国内に全然オープンなslack teamがなかった。オリジナルのslack listは50〜60くらい?slack list jaにはまだ5つしか登録されていない。teamはslack-list-ja/teams.json at gh-pages · bokuweb/slack-list-ja · GitHubで管理してるので、追加・編集はプルリクエストいただけるとありがたい。「こんなのもある」だとか「これを追加して」っての歓迎。

最後に

リモートワークしている方、導入したい方以下より参加できます。

Join リモートワーカー on Slack!

まだ見ぬslack teamの追加は以下よりお願いします。

github.com

追記

公開日からPRをいただいて、リストが充実してきました。 ご協力ありがとうございます。

吹き出しコンポーネントを作った

吹き出しコンポーネントが必要になって作った。

作ったもの

github.com

デモ

React-balloon example

スクリーンショット

f:id:bokuweb:20160301193300g:plain

使い方

インストール

npm i react-balloon

サンプル

以下のように使用する。

<Balloon
  start={{
    box: { x: 100, y: 100, width: 300, height: 105 },
    destination: { x: 400 , y: 400 },
  }}
  style={{ borderRadius: '5px' }}
  backgroundColor="#ECF0F1"
>
  Hello, World!!
</Balloon>

このコンポーネントについて

吹き出し部分をSVGで描画し、ボックスとは独立して吹き出しの指し先も動かせるようにした。SVGはこれまでやろうやろうと思いながら触れてなかったけど、reactとの相性はいい部類なんじゃないかって気がしてきている。

今回は以下のようにSVGを書いている

<svg width="100%" height="100%" style={{ zIndex }}>
    <path
        d={ `M ${base[0].x } ${ base[0].y }
                 Q ${ control.x } ${ control.y } ${ destination.x } ${ destination.y }
                 Q ${ control.x } ${ control.y } ${ base[1].x } ${ base[1].y}` }
        fill={ backgroundColor }
        stroke={ backgroundColor }
        strokeWidth={ 1 }
    />
</svg>

何やらSMILがdeprecatedっぽいので、SVGモーフィング辺りをがんばって解決できればかなり楽しいコンポーネントが作れそうな気がしてる。

ソートとリサイズが可能なペインコンポーネントreact-sortable-paneを作った

今作ってるtwitterクライントでソートとリサイズができるコンポーネントが欲しかったので作った。

作ったもの

github.com

デモ

http://bokuweb.github.io/react-sortable-pane

スクリーンショット

f:id:bokuweb:20160131164948g:plain

使い方

インストール

npm i react-sortable-pane

サンプル

以下のように使用する。

<SortablePane margin={10}>
  <Pane
     width={200}
     height={500}
     style={style}>
     A
  </Pane>
  <Pane
     width={300}
     height={400}
     style={style}>
    B
  </Pane>
</SortablePane>

このコンポーネントについて

もともとは以下のコンポーネントを作ったときのデモだったんだけど、今回コンポーネント化しといた。

blog.bokuweb.me

まだ水平方向しか対応していないけど、垂直方向への対応だとかテストとか充実させたい。使用用途としては以下のように使用することを想定している。

f:id:bokuweb:20160131165555g:plain

react-motionと拙作のreact-resizable-boxを使用している。react-motionを使うと表現の幅が広がるので楽しい挙動のコンポーネントを作れてよい。

React Native for AndroidでGCMを使ってPUSH通知するまでの作業ログ

React Native for AndrodでPUSH通知させた時のおぼえ書き。

Google Cloud Messaging for Android (GCM ) とは?

開発者がサーバから Android デバイス上の Android アプリケーションにデータを送信できるようにする無料のサービス

らしいです。

www.techdoctranslator.com

GCM登録

以下からアプリを登録して、RegistrationKeyやgoogle-services.jsonを取得する。google-services.jsonandroid/appに格納する。

Set up a GCM Client App on Android  |  Cloud Messaging  |  Google Developers

React Nativeでは

以下のパッケージを使うのが楽。

github.com

ただ、依存関係が循環参照みたいなことになっててバージョン指定しないと上手く入らなかった。

npm i --save react-native-gcm-android@0.1.9

手順に添ってandroid/build.gradleなどを設定していく。が、com.android.dex.DexException: Multiple dex files...というエラーが発生し、ビルドに失敗する。エラーで検索すると、android/app/build.gradlemultiDexEnabled trueを追加せよってのがあるんだけど、公式には「追加するんじゃねえ」って書いてあってたまたま見つけたbuild/app/build/を削除することでビルドが通るようになった。

Genymotionに Google Play serviceをインストールする

Genymotion(使っている場合)にGoogle Play serviceをインストールする必要がある。 手順は以下の記事の通りで問題なかった。

androidlover.net

PUSH 通知する

GcmAndroid.requestPermissions();すると以下のコールバックでトークンが受け取れる。

GcmAndroid.addEventListener('register', function(token){
        console.log('send gcm token to server', token);
});

こいつをサーバに送信、端末と紐つけて保持しておきPUSH通知をするのに使用する。

以下を実行する。("GCM_TOKEN"に上記で受け取ったトークンを入力する。)

curl -X POST -H "Authorization: key=YOUR_AUTHORIZATION_KEY" -H "Content-Type: application/json" -d '
{
  "data": {
    "info": {
      "subject": "Hello GCM2",
      "message": "Hello from the server side!"
    }
  },
  "to" : "GCM_TOKEN"
}' 'https://gcm-http.googleapis.com/gcm/send'

コールバックで内容が受け取れる。

GcmAndroid.addEventListener('notification', function(notification){
        console.log('receive gcm notification', notification);
        var info = JSON.parse(notification.data.info);
        if (!GcmAndroid.isInForeground) {
          Notification.create({
            subject: info.subject,
            message: info.message,
          });
        }
      });