Elm言語の作者Evan Czaplickiはブログ「How to Use Elm at Work」で、Elmのプログラムを既存の大きなJavaScriptプロジェクトに組み込んで使うという考えについて述べています。そのため自らreact-elm-componentsというパッケージを開発し、ElmをReactのコンポーネントとして使えるようにしたと述べています。これぞ私の望んでいるものです。
http://elm-lang.org/blog/how-to-use-elm-at-work
react-elm-componentsのgithubページには絵文字チャットのexampleがあります。チャットのコアな部分はElmで書き、それをReactのコンポーネント化して、emojione-pickerというReactのライブラリと組み合わせて使っています。
https://github.com/evancz/react-elm-components/tree/master/example
今回はそのexampleを動かしてみたのでその報告です。サイトの指示通りでは動作せずちょっと苦労しました。AWSのS3にアップロードしてありますので、ぜひアクセスしてみてください。
【デモページ】
https://s3-ap-northeast-1.amazonaws.com/react-d3-app/react-elm/index.html
1.インストール
まずサイトの指示通り、以下のコマンドでインストールします。
git clone https://github.com/evancz/react-elm-components.git
cd react-elm-components/example/
ここでpackage.jsonを書き換える必要があります。1行目を2行目で置き換えます。ここに気づかず起動時にエラーが出て時間をつぶしてしまいました。
"react-elm-components": ".."
"react-elm-components": "^1.0.1"
最後に、以下のコマンドでインストールし、アプリを立ち上げます。表示されたURLにアクセスすると、絵文字チャットが使えるようになります。
npm install
npm run serve
2.設定ファイル
開発環境の設定ファイルを見ていきましょう。
最初はpackage.jsonです。
{
"private": true,
"scripts": {
"make": "./node_modules/.bin/webpack -d",
"serve": "npm run make && ./node_modules/http-server/bin/http-server -o -c-1"
},
"dependencies": {
"emojione-picker": "^0.3.6",
"react": "~0.14.0",
"react-dom": "~0.14.0",
"react-elm-components": "1.0.1"
},
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-plugin-uglify": "^1.0.2",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"elm-webpack-loader": "^3.0.3",
"http-server": "^0.9.0",
"uglify-js": "^2.6.4",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
}
}
package.jsonで注意したいのは以下の2行です。elm-webpack-loaderはelmプログラムのためのローダです。webpack.config.jsで指定しています。
https://github.com/elm-community/elm-webpack-loader
"react-elm-components": "1.0.1"
"elm-webpack-loader": "^3.0.3",
次にwebpack.config.jsをみます。詳細は以下のサイトを参照してください。
https://github.com/webpack/docs/wiki/configuration
var webpack = require('webpack');
var path = require('path');
var BUILD_DIR = path.resolve(__dirname, 'build');
var APP_DIR = path.resolve(__dirname);
var config = {
entry: APP_DIR + '/index.jsx',
resolve: {
modulesDirectories: ['node_modules'],
extensions: ['', '.js', '.elm']
},
module : {
loaders : [
{
test : /\.jsx?/,
include : APP_DIR,
exclude: [/elm-stuff/, /node_modules/],
loader : 'babel'
},
{
test: /\.html$/,
loader: "html"
},
{
test: /\.elm$/,
exclude: [/elm-stuff/, /node_modules/],
loader: 'elm-webpack'
}
],
noParse: /\.elm$/
},
output: {
path: BUILD_DIR,
filename: 'bundle.js'
}
};
module.exports = config;
以下の行で、elmモジュールを探すためにElmファイルの拡張子を教えてあげます。
extensions: ['', '.js', '.elm']
以下の行で、jsxファイルのロード条件が指定されています。elm-stuff配下は除かれます。
{
test : /\.jsx?/,
include : APP_DIR,
exclude: [/elm-stuff/, /node_modules/],
loader : 'babel'
},
以下の行で、elmファイルのロード条件が指定されています。elm-stuff配下は除かれます。またelm-webpack-loaderを指定します。一般的にxxx-loader の -loader 部分は省略可能です。
{
test: /\.elm$/,
exclude: [/elm-stuff/, /node_modules/],
loader: 'elm-webpack'
}
以下の行で、elmファイルはrequireやdefineの対象にならないので、パースしないように指示しています。
noParse: /\.elm$/
最後の設定ファイルはelm-package.jsonです。このファイルでロード時に必要なelmパッケージがインストールされます。
{
"version": "1.0.0",
"summary": "A chat room with an emoji picker",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"elm"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.0.0 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/websocket": "1.0.2 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}
3.ソースファイル
ソースファイルはindex.jsxとChat.elm、index.htmlの3つで全てexampleディレクトリにあります。
まずReactプログラムであるindex.jsxから見ていきましょう。
import React from 'react'
import ReactDOM from 'react-dom'
import EmojiPicker from 'emojione-picker'
import Elm from 'react-elm-components'
import { Chat } from './Chat'
const EmojiChatRoom = React.createClass({
render: function() {
const flags = 'wss://echo.websocket.org';
let sendEmojiToChat = function() {};
function setupPorts(ports) {
sendEmojiToChat = ports.emoji.send;
};
function handleChange(emoji) {
const str = String.fromCodePoint(parseInt("0x" + emoji.unicode));
sendEmojiToChat(str);
}
return (
<div className="emoji-chat">
<EmojiPicker onChange={handleChange} />
<Elm src={Chat} flags={flags} ports={setupPorts} />
</div>
);
}
});
ReactDOM.render(
<EmojiChatRoom />,
document.getElementById('chat')
);
まず以下を見てください。2つのReactコンポーネントをimportしています。EmojiPickerは普通のReactコンポーネントです。ElmはElmプログラム用のコンポーネントになります。
import EmojiPicker from 'emojione-picker'
import Elm from 'react-elm-components'
Elmのプログラム自体は以下のようにしてimportしています。ChatはChat.elmのモジュール名です。
import { Chat } from './Chat'
最終的に以下のjsxで、ElmモジュールがReactコンポーネントとして表示されることになります。src={Chat}でElmモジュールを指定します。flags={flags} でflagsとしてChatにWebSocketサーバ(wss://echo.websocket.org)を渡します。
<Elm src={Chat} flags={flags} ports={setupPorts} />
ここで ports={setupPorts} に注目します。setupPorts関数 はReact内で以下のように定義されています。引数にportsを取り、ports関連の定義や処理をここで行うようにしています。
function setupPorts(ports) {
sendEmojiToChat = ports.emoji.send;
};
2番目のソースコードですが、以下のChatプログラムのメインになります。メインのプログラムがElmで書かれていて、Reactはemojione-pickerコンポーネントを使いたかったから、という位置づけです。あまり下手な説明を加えるよりソースとコメントを読んでいただければ処理の流れは容易につかめると思います。ElmプログラムはHaskellと同じ顔立ちをしていて、純粋関数型なので宣言的で読みやすいものです。
port module Chat exposing (main)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import WebSocket as WS
main : Program String Model Msg
main =
Html.programWithFlags
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ server : String
, input : String
, messages : List String
}
init : String -> ( Model, Cmd Msg )
init server =
( Model server "" [], Cmd.none )
-- UPDATE
type Msg
= Input String
| Send
| NewEmoji String
| NewMessage String
{-| Our update function reacts to a few different messages.
1. Typing into the text field
2. Clicking the "Send" button
3. Emoji sent in from JavaScript
4. Messages received from the chat server
-}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg ({ server, input, messages } as model) =
case msg of
Input newInput ->
( { model | input = newInput }
, Cmd.none
)
Send ->
( { model | input = "" }
, WS.send server input
)
NewEmoji emoji ->
( { model | input = input ++ emoji }
, Cmd.none
)
NewMessage newMessage ->
( { model | messages = newMessage :: messages }
, Cmd.none
)
-- SUBSCRIPTIONS
{-| This port lets outsiders send in emoji characters as a string.
We can subscribe to these messages from within Elm.
-}
port emoji : (String -> msg) -> Sub msg
{-| We subscribe to two kinds of messages.
1. We want messages from the websocket chat server.
2. We want messages sent through emoji port from JavaScript.
-}
subscriptions : Model -> Sub Msg
subscriptions { server } =
Sub.batch
[ WS.listen server NewMessage
, emoji NewEmoji
]
-- VIEW
view : Model -> Html Msg
view model =
div [ class "chat-container" ]
[ input [ class "chat-message-input", onInput Input, value model.input ] []
, button [ onClick Send ] [ text "Send" ]
, div [ class "chat-messages" ] (List.map viewMessage (List.reverse model.messages))
]
viewMessage : String -> Html msg
viewMessage msg =
div [] [ text msg ]
このChat.elmはCmd/Subで外界とコミュニケートしていますが、特に注目してほしいのが、React のemojione-pickerコンポーネントのイベントハンドラからのemoji.sendを可能にしている以下のport宣言です。
port emoji : (String -> msg) -> Sub msg
そしてemoji.sendをリッスンしているのが以下のsubscriptions関数です。これによってReactコンポーネントとElmモジュールとの通信が行われています。
subscriptions : Model -> Sub Msg
subscriptions { server } =
Sub.batch
[ WS.listen server NewMessage
, emoji NewEmoji
]
最後になりますが、3番目のソースコードのindex.htmlです。お決りの感じですね。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>React + Elm</title>
<link href="node_modules/emojione-picker/css/picker.css" rel="stylesheet" />
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<div id="chat"></div>
<script src="build/bundle.js"></script>
</body>
</html>