0

coffee-scriptからES6に移行する理由

coffeeからES6(babel)に少しずつ書き換える
の続き。

そもそも書き直す必要があるのか


正直シンタックスはcoffeeの方が好き。無駄にカッコつけなくていいし、インデントでpipeやpromiseをつないで書いていくのも見た目が良い。
coffeeは書きやすい。
まあすでに動いている物を書き直す必要は無いと思う。


移行するとしたら、ネガティブな動機が1つ、ポジティブな動機は2つある。

coffeeの開発の停滞

詳しくcoffeeの開発状況をウォッチしてないからよくわからないけど、詳しい人が「開発が停滞している」と言っているのでそうなんだと思う。
さよなら CoffeeScript – mizchi's blog

coffeeはここ1年ぐらいバージョンが上がった時にdiffだけは見ているのだが、毎回git diffで差分を見て一瞬で理解できるぐらいの更新しかされてない。まあようするに停滞しているのは間違いない。


HTML5の新しいAPIを使いたい

Web Bluetooth APIやWeb MIDI APIを使ってみると、型付き配列、Promise、IteratorなどのES6で増えた機能が前提で作られている。

この中で特にIteratorはES6のfor-of文で書くと綺麗に書けるんだけど、for-ofを使わないとしたらJavaみたいな感じでnext()を呼んだりdoneをチェックしたりとかしなくてはならなくて、すごく面倒くさい。

一方coffeeではfor-of文が対象Objectのkeyとvalueの2つを引数に取ってループを回す構文として実装されている。RubyのHash#eachみたいなやつ。
これはこれで便利なのだが、つまりcoffeeでES6のIteratorを扱うとつらい。
既にfor-ofが予約されてしまっている以上、もしcoffeeにIterator回し構文を追加するとしたら別の名前になる・・のかな?

たぶん、USB MIDIコントローラー250個とか接続できるから非同期でデバイス情報が返ってくるようになっているんだと思う。今後ハードウェアを扱うAPIは増えるだろうから、そういう場合はES6で書いたほうがいい。スマホWebアプリとかでそういう機会が増えると思う。


Map, Set, Promise, Buffer, 型付きArrayなど新しい組み込み型はブラウザならネイティブに実装されているのでcoffeeでも問題ない。というかbabelでも変換していない(ES5に翻訳しようがない)

ESLintが良い

少なくともcoffee-lintより気が利いている、というか勉強になる事をサジェストしてくれる気がする。具体的には忘れたけど。
プロジェクト毎のカスタムも容易でよくできている。

ES6で新しい組み込み型が増えているから、そういうのに合わせて細かくlintしてくれるようになるかもだし。


まとめ

別にふつうのアプリケーションを書くなら今はcoffeeでいいと思う。既に動いているのを書き直す必要もない。

いちはやく新しい文法で書いてみたいとかで無ければ、ES6(というかbabelが)変化激しいし、coffeeでいいかな・・・

ただ、いくつかの新しいHTML5 APIを使う場合はES6で書いたほうが明らかに書きやすくはなる。
ESLint良いよね。
0

Node.jsのfeedparserにドイツ語等を読ませる

ドイツ語などのヨーロッパの言語ではたまにåとかöみたいな文字が使われていて、UTF-8とかじゃなくISO-8859-1などの文字コードが使われている事があるらしい。

ドイツのspiegelという雑誌のRSSがちょうどISO-8859-1で、hubot-rss-readerで読むとところどころ文字化けするようになっていた。

こんな感じ


Node.jsでfeedを読む

feedの取得にはrequest、parseにfeedparserを使うのが多分普通なのだが、この2つはpipeでつないで使うようになっている。

で、最初は
Node.jsで文字コードの自動判別と自動変換 – Qiita
のようにjschardetで文字コードを判別して、iconvで変換してみた。でもrequest→feedparserがstreamなので少しずつchunkで送られてきて、日本語のマルチバイト文字の途中で区切りが来たりする事がある。そのchunkを1つずつjschardetにかけてしまうと全然違う文字コードが出てきたりする事があって良くない。

冷製になって考えたら、XMLなんだから頭に<?xml encoding=”ISO-8859-1″とか書いてあるのを読んでUTF-8じゃなかったら変換するstreamを作ればいい事に気づいた。

streamでXMLを食わせるとUTF-8に変換して吐き出すstream

というわけで、まずXMLのencoding attributeを読んで必要があればUTF-8に変換するstreamを返す関数を作る

charset-convert-stream.coffee
stream = require 'stream'
Iconv = require('iconv').Iconv

module.exports = ->

iconv = null

charsetConvertStream = stream.Transform()

charsetConvertStream._transform = (chunk, enc, next) ->
if m = chunk.toString().match /<\?xml[^>]* encoding=['"]([^'"]+)['"]/
charset = m[1]
if charset.toUpperCase() isnt 'UTF-8'
iconv = new Iconv charset, 'UTF-8//TRANSLIT//IGNORE'
if iconv?
@push iconv.convert(chunk)
else
@push chunk
next()

return charsetConvertStream


ドイツ語のfeedを読む

requestのon “response”からpipeでcharsetConvertStreamを通してfeedparserに繋ぐと、いい感じにUTF-8にエンコードされて出てくる。

FeedParser = require 'feedparser'
request = require 'request'
charsetConvertStream = require './charset-convert-stream'

feedparser = new FeedParser

req = request
uri: process.argv[2] or 'http://www.spiegel.de/schlagzeilen/tops/index.rss'
timeout: 10000
encoding: null # null指定でrequestが勝手にエンコードしなくなる
headers:
'User-Agent': 'test-feed-reader'

req.on 'error', (err) ->
console.error err

req.on 'response', (res) ->
if res.statusCode isnt 200
return console.error "statusCode: #{res.statusCode}"
this
.pipe charsetConvertStream()
.pipe feedparser

feedparser.on 'error', (err) ->
console.error err

feedparser.on 'data', (entry) -> # エントリーが1件ずつイベントで出てくる
console.log entry.title
console.log entry.summary or entry.description


文字化けなくなった

ここまでやって気づいたんだけど、 feedparser.meta[‘#xml’].encoding にエンコード情報が入っているのでこれを使ってtitleやdescriptionなど必要な所だけ取り出してからiconv.convertしても良かったかも・・
0

coffeeからES6(babel)に少しずつ書き換える

coffee-scriptで書いていたwebアプリをES6(babel)に書きなおした。

全部を一気に書き直してハイ動いたーとやるのは無理なので、coffeeとES6のファイルが混在しても動かせるようにして、少しずつ書き直した。


書き直したのはこれ
React+Fluxxor+socket.ioでfluxなチャットを作った
https://github.com/shokai/node-flux-boilerplate

インストール

% npm install babel babelify browserify watchify -save-dev
とやっていたのだが、数日前にbabelが5から6にアップデートされて、babel-coreやbabel-preset-*など色々分割されて、大分色々変わってしまった。周辺ツールも対応の過渡期だったのでbabel5系を使うようにバージョンを指定した。

  "devDependencies": {
"babel": "~5",
"babelify": "~6",
"browserify": "~11",
"watchify": "~3.5"
}


クライアント側


coffeeで書いたReactのプログラムをbrowserifyでjsに変換して1ファイルに固めていたので、単純にtransformを複数指定してやったらES6とcoffeeを混在しても大丈夫になった。

% browserify --debug --extension=.cjsx --extension=.coffee --extension=.es6 -t babelify -t coffee-reactify client/app.jsx -o public/js/bundle.js

coffeeもES6もまとめてES5になってbundle.jsができる
もちろんwatchifyでも同様にES6/coffee混在可能。

babel6だったらたぶんこれで混在できる。browserifyやbabelifyも最新に上げてから
% browserify --debug --extension=.cjsx --extension=.coffee --extension=.es6 -t [ babelify --presets [ react es2015 ] ] -t coffee-reactify client/app.jsx -o public/js/bundle.js


サーバー側

サーバー側はregisterを使う。registerはrequire関数をhookして実行前に翻訳してくれるしくみなので、例えばbabel/registerを使うとcoffeeからES6をrequireして実行できる。

Require Hook · Babel
register.coffee

最初に起動するファイルがserver.coffeeの場合

こうやってアプリを起動している場合
% coffee server.coffee # 起動

babel/registerで動的にES6を実行できるようにする。server.coffeeのなるべく上の方で

require "babel/register"

を書いておくと、これで以後coffee内からES6をrequireするとbabelが逐次変換してくれるので、混ぜて書ける。
require "models/user" # user.coffeeが読み込まれる
require "controller/main.es6" # main.es6が読み込まれる

babel 6.x系だと別のnpmに切り出されて、babel-core/registerに変わった。


最初に起動するファイルがserver.es6の場合


最初に呼び出すファイルをES6で書きなおしたら、
% babel-node server.es6 # 起動

coffee-script/registerで動的にcoffeeを実行する。server.es6のなるべく上の方で
require("coffee-script/register");

以後ES6なスクリプトからcoffeeをimport/requireすると逐次変換して実行される。

同じファイル名の.coffeeと.es6があったらどうするか

拡張子まで書けば指定できる。
require("foo.es6");

ES6に統一し終わったら.coffeeファイルを消して、requireの拡張子も消せばok
require("foo");


npmの場合

coffeeで書いてES5にトランスパイルしてnpmjs.orgにリリースしているプロジェクトを部分的にES6で書き直す場合は、試してないけどたぶんビルドプロセスを
coffeeの変換の後にbabelの変換、という順に実行してbabel側のファイルで上書きしてやるようにすればいいのではないかと思う。

0

HerokuのHubotをProcess Schedulerで寝かす前に報告させる



HerokuのHubotを寝させる

無料で使うには1日6時間寝かせないとならないので
Heroku | Heroku’s Free (as in beer) Dynos

dashboard.heroku.comからaddonにProcess Schedulerを追加して6時間寝させる。


参考:HerokuでHubotを指定の時間に寝かせる – はらへり日記


寝る前に報告させる

寝る前に「寝ます」とか「寝ましょう」とか言ってくれた方が安心感がある。

herokuはプロセスの終了時にSIGTERMを投げてくるので、それを受ければいい。

nodeだとprocess.on(‘SIGTERM’, function(){ });でキャッチできるはずだけどHubotではできなかった。ソースをよく見たらbin/hubotが先にSIGTERMにイベントを登録して、その中でexitしていた。なのでSIGTERMイベントを上書きした。

module.exports = (robot) ->

## 起きた時、slack-adapterがつながるのを待って通知
cid = setInterval ->
return if typeof robot?.send isnt 'function'
robot.send {room: "#general"}, "ガバリ"
clearInterval cid
, 1000

## 寝た時、通知してからexitする
on_sigterm = ->
robot.send {room: "#general"}, 'スヤリ'
setTimeout process.exit, 1000

if process._events.SIGTERM?
process._events.SIGTERM = on_sigterm
else
process.on 'SIGTERM', on_sigterm

processがイベント管理に使っているEventEmitterは先に登録したイベントが先に実行される。先にbin/hubotが登録したイベントを削除するにはイベント登録時にreturnされるidを使うしか無いので、仕方なく_eventsプロパティを直接書き換えた。
hubotの仕様が変わった時に挙動がおかしくなるかもしれないけど、例えばsocket.ioがhttp.serverに/sockets/socket.io.jsを登録する処理とかでも_eventsの順序入れ替えをやっているし、まあしょうがない。EventEmitterとはそういう物だと思うしかない。

先に登録されていなければ普通にonで登録する。

本当は上書きじゃなくて先に実行されるようにしようかと思ったけど、eventemitterは登録されているコールバックが複数なら配列で持ち、1つなら配列ではなく関数を直接持つので、なんだか面倒になって上書きにした。
0

watchifyでcjsxをビルド

Gruntでwatchしてbrowserifyでcjsx(coffeeで書いたReactのjsx)をビルドしてるって書いたけど
WEB+DB PressのReactの記事が良かったのでcoffeeで書きなおした

たくさんnpmをrequireする様になったら毎回5〜6秒かかって遅くなったので、gulpにしなければならないのか?とか思ったけどgulpのデバッグが難しすぎて挫折した。


watchify

watchifyはbrowserifyのラッパーで、ファイル更新を検知してbrowserifyしなおしてくれる。
ただ更新見てるだけだろって思ってスルーしてたんだけど、よく見たら更新されたファイルだけを変換しなおしてた。
速い。2回目のビルドから1秒弱になる。


オプション

verbose指定しないとビルドした事すら表示されないので付ける。
あとはbrowserifyに付けるオプションと同じものが使えて、–debugでインラインSourceMapを出力、extensionで.js以外にも探す拡張子を追加、coffee-reactifyをtransformに指定してcjsxをコンパイルしてbundle.jsに書き出す。

watchify --verbose --debug --extension=.cjsx --extension=.coffee -t coffee-reactify client/app.cjsx -o public/js/bundle.js

watch先はclinet/app.cjsxしか書いていないが、require先のファイルを全て監視してくれている。

npm run

watchifyはオプションが長くて毎回打つの無理なので、package.jsonのscriptsにコマンドを書いておく。
「npm run コマンド名」で実行できる。

{
"name": "node-flux-boilerplate",
"private": true,
"version": "0.0.1",
"description": "react+fluxxor+socket.io chat",
"main": "server/app.coffee",
"scripts": {
"start": "coffee server/app.coffee",
"build": "grunt build",
"watchify": "watchify --verbose --debug --extension=.cjsx --extension=.coffee -t coffee-reactify client/app.cjsx -o public/js/bundle.js",
"test": "grunt"
},
〜〜(略)〜〜

この中は./node_modules/.bin/にPATHが通った状態になっているのでwatchifyやgruntがグローバルインストールする必要も無くなって便利。



flux-boilerplate

grunt-contrib-watchで監視するのやめて、watchifyを使うようにした。

https://github.com/shokai/node-flux-boilerplate

React+Fluxxor+socket.ioでfluxなチャットを作った


他のビルドタスク

cssとかも、cssメタ言語で作って各DOMにクラスを振るのじゃなくて、React内のInline Stylesで書いてmixinする方がいい気がしたのでbrowserify以外に継続的なビルドタスクが必要無い気がする。

参考:reactjs – React.js + CSS – Qiita