WebStormでESLintを使う
数日、バタバタと調査しては考えが変わる毎日ですが、ES6-classでReactアプリを書く前提が整ってきました。
- classボディ外に、propTypes/contextTypes/childContextTypesを書くのも、そういうものと思えばまあいいか
- MixinはAOP的なラッパーコンポーネントを生成する関数で書く。移行もすでにES6記法で書いてたのでさほど面倒ではない
- contextはES6-classでも大丈夫だった
- BabelでES5へトランスパイルし、Browserifyでクライアント用はまとめる。
- テスト周りがちょっと試行不十分だけど、Jest&Babel&Gulpで簡単なものはOK
おそらく最後にして最大の課題はIDEのことです。コード的にはOKでもIDEがエラーや警告だしたり、逆に出すべきところで沈黙してたりすると書き方云々以上にストレスですから。今、私はWebStorm11をサブスクリプション購入してますのでそこでの設定。Preferencesを眺めてちょこちょこ試したらうまくいった(ドキュメントはあるけど、それだけ読んで調べられるかというと、込み入ったことは進化が早すぎるからか追いついていないようで期待できないし、恐ろしいことに有料の商用IDEだからなのかググっても情報がなかなかでてこない)。
まず、WebStorm標準のソースインスペクタを切る。[Editor]->[Inspections]->[JavaScript]のチェックを外す。画像は[Code quality tools]->[ESLint]がチェックされているけどこれは後の作業で自動でチェックされます。
すでにBabel/BabelのReact及びES2015プリセットが入ってる前提で、以下のnpmインストールを実行。
$ npm install --save-dev eslint $ npm install --save-dev eslint-plugin-react $ npm install --save-dev babel-eslint
ちなみに私のpackage.jsonのdevDependenciesは今日時点で以下のようになってます。全部プロジェクトのnode_modulesに入れるようにしていて、グローバルにはnpmとnしか入れてません。それが正しいのか正しくないのかは知らない。
{ "devDependencies": { "babel-eslint": "^4.1.8", "babel-jest": "^6.0.1", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "babelify": "^7.2.0", "browserify": "^12.0.2", "eslint": "^1.10.3", "eslint-config-standard": "^4.4.0", "eslint-plugin-react": "^3.16.1", "eslint-plugin-standard": "^1.3.1", "gulp": "^3.9.0", "gulp-babel": "^6.1.2", "gulp-sass": "^2.2.0", "gulp-zip": "^3.1.0", "jest-cli": "^0.8.2", "react-addons-test-utils": "^0.14.6", "vinyl-source-stream": "^1.1.0" } }
[Languages & Frameworks]->[JavaScript]->[Code quality tools]->[ESLint]をEnableにチェック!NodeとESLintの場所を適切に設定する。
プロジェクトルートに.eslintrc.jsonを配置します。設定ウィザードをコマンドラインで「eslint --init」実行してみたけど大したもの吐いてくれなかったので結局はググって拾ってきたのをいじりました。以下、何も省略せずに今日の私の設定です。ポイントは"parser"にbabel-eslintを設定しているところと"plugins"にreactを追加しているところ。eslint標準のパースに任せるとどうも何もおかしくないところで構文解析エラーを出したりするのでうまくない。先にnpmインストールしているbabel-eslintが全部キッチリと一本化して素敵なようす。
// .eslintrc.json { "parser": "babel-eslint", "extends": "eslint:recommended", "env": { "es6": true, "node": true, "jasmine": true, "browser": true }, "ecmaFeatures": { "jsx": true, "experimentalObjectRestSpread": true }, "plugins": [ "react" ], "rules": { "strict": [2, "global"], "no-const-assign": 2, "no-console": 0, "no-unused-vars": 1, "indent": [2, 4], "quotes": [2, "single"], "linebreak-style": [2, "unix"], "semi": [2, "always"], "react/display-name": 0, "react/forbid-prop-types": 0, "react/jsx-boolean-value": 0, "react/jsx-closing-bracket-location": 0, "react/jsx-curly-spacing": 1, "react/jsx-equals-spacing": 1, "react/jsx-handler-names": 1, "react/jsx-indent-props": 0, "react/jsx-indent": 1, "react/jsx-key": 1, "react/jsx-max-props-per-line": 0, "react/jsx-no-bind": [1, {"allowArrowFunctions": true}], "react/jsx-no-duplicate-props": 1, "react/jsx-no-literals": 0, "react/jsx-no-undef": 1, "react/jsx-pascal-case": 1, "react/jsx-quotes": 1, "react/jsx-sort-prop-types": 0, "react/jsx-sort-props": 0, "react/jsx-uses-react": 1, "react/jsx-uses-vars": 1, "react/no-danger": 0, "react/no-deprecated": 1, "react/no-did-mount-set-state": 1, "react/no-did-update-set-state": 1, "react/no-direct-mutation-state": 1, "react/no-is-mounted": 1, "react/no-multi-comp": 0, "react/no-set-state": 0, "react/no-string-refs": 1, "react/no-unknown-property": 1, "react/prefer-es6-class": 0, "react/prop-types": 1, "react/react-in-jsx-scope": 1, "react/require-extension": 1, "react/self-closing-comp": 1, "react/sort-comp": 0, "react/wrap-multilines": 1 } }
以下、発見と感想。
- "react/jsx-boolean-value" は意味あんのかな?true縛りの記述にしてfalseは書かせないとか。。。
- "react/jsx-no-literals" はスタイルとしてよりダメな方かなと。まあどちらでもいいけど
- "react/jsx-no-bind" は全部禁じちゃうと手が出なくなる。 [1, {"allowArrowFunctions": true}]にしました。このことによって、bind(this, "flag")を使わなくても onClick={e => this.handleClick("flag", e);} ってできるようになる。
- "react/no-string-refs" は謎でなく目うろこだった。これまでref="foo"だったところを、ref={ref => this.foo = ref}ってref-callbackを使うと。このref-callbackはReactの公式ドキュメントにちゃんと書いてあった! (https://facebook.github.io/react/docs/more-about-refs.html)Stringで名前つけて参照するのと違って堅くなっていいね!
- "react/no-set-state" は、this.setState({foo: bar}); と言う状態操作を禁じる。禁じるとユーザーリアクションのあるアプリ書けないと思うんですけど?私にはまだ謎。
- 最後に"react/prefer-es6-class"!。私はこれを1に設定しました。ES6-classを使わないと警告。今はたくさん警告出てます。
ESLintサイコー。細かく警告やエラーに調整できた上で、ソースコードではコメントで部分抑制かけられるので堅め設定しておいて適宜明示的に緩めるというのがサイコー。WebStormの標準インスペクタでもなく、ESLintの標準パーサーでもなく、eslint-babelにするってのがまたサイコー。
Reactコンポーネント名の規則
つまらないハマりがあったので。
const foo = React.createClass({ render() { return <div>Foo</div>; } }); console.log("JSX:" + ReactDOMServer.renderToStaticMarkup(<foo/>)); console.log("API:" + ReactDOMServer.renderToStaticMarkup(React.createElement(foo)));
上記のようなコードがあった時、出力は以下のようになりました。
JSX: <foo></foo> API: <div>Foo</div>
JSXをBabelで変換していますが、こちらは「React.createElement("foo", null)」と変換してしまい、結果としてそのままマークアップに出てきちゃう。原因はJSXで先頭小文字の名前のコンポーネントをHTMLタグと認識してトランスパイルしちゃうから。回避方法はReactコンポーネントの命名規則として先頭大文字にするということです。
ReactのES6-classが所詮糖衣構文しかも甘くない
(承前:昨日の追記)
react-mixin(https://github.com/brigand/react-mixin)でES6のclass構文を試してみました。書いてから気がついたのは、短時間で試したぐらいの中ではちょっとコード書き直すだけでどんどん筋が悪い体裁になっていくので、時期尚早というか私にはまだ無理ゲー。もうチョイ調査が必要。
- propTypesの宣言がclassブロック外に記述
- getDefaultPropsの対応がclassブロック外に記述
- mixinの対応でreact-mixinを使ってみたけど。。。
- contextTypesの宣言はどうするのかわからず、contextがきっちり動く書き方が発見できなかった
無理にclass構文を使うのはやはりやめたほうが良いのだろうか。サンプルでよく見るようなシンプルなものだったら綺麗に書けるけど。。。どうせ今はbabelでトランスパイルするので所詮糖衣構文しかも甘くない!ってんで確信なければハマる必要がないかなあ。
一方で、mixinをラッパーコンポーネントのイディオムで書き直した方は筋が良いように思いました。今までのMixinの書き方から直して下記の感じかな。MixinをReactコンポーネントを返す関数にしてみました。クロージャーで引数コンポーネントを持ち回るのは、react-side-effectのソース見て学んだ。
function WrapByMixBar(WrappedComponent) { return React.createClass({ displayName: "Bar$" + WrappedComponent.displayName, // Babelが自動で書き出さない getDefaultProps() { return {fromMixBar: "Bar"}; }, render() { return <WrappedComponent {...this.props}/>; } }); } const Foo = WrapByMixBar(React.createClass({ displayName: "Foo", // Babelが自動で書き出さない render() { return <div>Foo {this.props.fromMixBar}</div>; } })); console.log(ReactDOMServer.renderToStaticMarkup(<Foo/>));
より直感的?Mixinもちゃんとした(Duck-Typingではない)Reactコンポーネントになったので、クライアントサイドレンダリングだったらChromeデベロッパーツールのReactプラグインで中身見た時にきちんと層状に表示されます。コアとアスペクトが分離表示されてこちらが旧来より正しいように思う。その際にはBabelが対応しきれないdisplayNameを工夫しておくと吉。
追記
contextもなんか勘違いしていたみたいで、改めて書き下ろしてみたらES6-classでサクッと動いた!
class PutContext extends React.Component { getChildContext() { return {color: "#03a9f4"}; } render() { return <div>{this.props.children}</div>; } } // propTypesと同じスタイルの設定 PutContext.childContextTypes = {color: React.PropTypes.string}; class GetContext extends React.Component { render() { const color = this.context.color; return <div style={{color: color}}>the context color is {color}.</div>; } } // propTypesと同じスタイルの設定 GetContext.contextTypes = {color: React.PropTypes.string}
Mixinも先のやり方で移行して、contextもOKとなると、記事タイトルから一転してES6-classで良くなったかも。
関数型的なJS
- 作者: Michael Fogus,和田祐一郎
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/01/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (5件) を見る
ECMAScript6まじりのJavaScriptに手が馴染んできました。Reactがclass構文をサポートしながらも、現バージョンの0.14.xではまだmixinがサポートされていないためほぼそこだけはReact#createClass()を用いてます。これはreact-routerもmaterial-uiもそうしてるんで今のメジャーソリューションなんだと思います。他はBabelで通る限り極力6で書く。手に馴染んでくると、朧げながらもベストプラクティスが見えてくるものだと思います。ということで。。。
Reactで可変長のリスト表示を作る時によく見るmapがきっかけでしたが、その後にもすでにECMAScript5からArray型にreduceだとかforEachだとか標準で入ってたことを知り、関数をダイナミックに作って返すなんてのはPassport.jsなどのExpressミドルウェアライブラリでは至極よく見る作りだったのでこう言った関数型的なところのプラクティスを一度ちゃんとさらっておきたいなあと思って、正月に日本に帰った時に「JavaScriptで学ぶ関数型プログラミング」を買ってきてました。JavaScriptじゃない言語で読んでも疲れちゃうなあと思ってたのでこの本見つけた時には喜んだ。で、読んだ。。。うざくて読みずれー。
5が見えてるけど普及がこれからの時期に3前提でUnderscoreを用いて説明してるためにちょっと迂遠なのと、各所で「すごいでしょう?これが関数型のパワーなのです」ということを修辞を変えて繰り返し繰り返し繰り返し繰り返し(中略)繰り返しのために、大事なことと大事じゃないことがごっちゃになる。
JavaScriptパターン ―優れたアプリケーションのための作法
- 作者: Stoyan Stefanov,豊福剛
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/02/16
- メディア: 大型本
- 購入: 22人 クリック: 907回
- この商品を含むブログ (77件) を見る
実は「JavaScriptパターン」第4章:関数の方が概要大体のことを得ることができます。「関数型プログラミング」の方はいろいろ丁寧に書いているし、より高度なのにもったいない。手頃かつ他にない良書の素質があったのに無駄なウザさで減点。でも総合点では良書かなあ。この本を読んでからはreduceRightとか使うようになりました。
ECMAScript6で学ぶ関数型プログラミング、がすでに世の中みんな関数型っぽく書いているよねーって前提で淡々と書かれたら神書の予感。
別の話だけど、結構Reactで書けるようになってきたので、すぐにはやらないけどReact Nativeもちょっとさらっておきたかった。
Learning React Native: Building Native Mobile Apps with JavaScript
- 作者: Bonnie Eisenman
- 出版社/メーカー: O'Reilly Media
- 発売日: 2015/12/03
- メディア: Kindle版
- この商品を含むブログを見る
日本語の訳書が出てなかったら原書を紙で買います。ここは米国なんで在庫も豊富で安いし、ちょっと読んでダメだったら返品できる。それにしても私はオライリー好きなのかな。オライリーじゃなきゃManning(〜in Actionって仮装した人が表紙になるやつね)か。一緒に買ったけどまだ届かないのも、どうやらオライリー関係会社っぽい?なぜか出版社のWEBサイトが一緒。
- 作者: Wilson Da Rocha França
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/06/22
- メディア: ペーパーバック
- この商品を含むブログを見る
追記
GitHub - brigand/react-mixin: mixins in react with es6 style classes
react-routerのドキュメントでたまたまこちらへ誘導されましたが、ES6-classでMixinを実現しましょうライブラリ。さらに承前としてラッパーコンポーネント的にmixin機能を代替する記事 (https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.nlylwdx3d) 。おや?Reactはmixinをなかったことにしたい?現バージョンのドキュメントにはその痕跡見つからなかったけど。。。同じく将来も残るか怪しいって公式ドキュメントに書かれているcontextと共にバリバリ使ってるんで、ちょっと悩ましい。タイムラグのために最新では議論が違う方に行ってるのであればいいんだけど。react-routerがv2からmixinを廃止推奨してきたのはそのためか?代替案がcontext利用なので、それもまた悩ましいはずなのですけど。
とはいえ、ラッパーコンポーネントのイディオムはreact-side-effect(https://github.com/gaearon/react-side-effect/blob/master/src/index.js)のソースコードでも見て、ちょっと賢いと思ったので、使い方考えてみよう。
MongooseでのCRUDと謎挙動
const Mongoose = require("mongoose"); const Schema = Mongoose.Schema; Mongoose.connect("mongodb://localhost:27017/sample"); const Person = Mongoose.model("person", // <-この"person"が後で問題になる new Schema({ created: {type: Date, default: Date.now}, name: String }) ); export {Person};
こんな風に適当なPersonスキーマを作って、
const Express = require("express"); import {Person} from "./Models"; const APIRoute = Express.Router(); APIRoute.post("/person", (req, res) => { let upsertPerson = new Person(req.body); Person.findOneAndUpdate( {_id: upsertPerson._id}, upsertPerson, {upsert: true}, (error) => { /*割愛。*/ res.sendStatus(200);} ); }); APIRoute.delete("/person", (req, res) => { let deletePerson = new Person(req.body); deletePerson.remove((error) => { /*割愛。*/ res.sendStatus(200);}); }); APIRoute.get("/person", (req, res) => { Person.find().limit(10).sort({_id: -1}) .exec((error, list) => {/*割愛。*/ res.send(list);}); }); export default APIRoute;
expressの本体は以下。
const Express = require("express"); const bodyParser = require("body-parser"); import APIRoute from "./routes/APIRoute"; app.use(bodyParser.json()); app.use("/api", APIRoute); app.listen(8080);
送信側は、Reactのコンポーネントの中から抜粋。
//前略 changePerson(person, remove) { const method = remove ? "del" : "post"; SuperAgent[method]("/api/person") .set("Content-Type", "application/json") .send(person) .end((err, res) => { if (res.statusCode == 200) { this.loadList(); } else { this.setState({error: err}); } }); }, loadList() { SuperAgent.get("/api/people") .end((err, res) => { if (res.statusCode == 200) { this.setState({list: res.body}); } else { this.setState({error: err}); } }); }, //後略
これでちゃんとデータのCRUDができますが。。。謎挙動が一つ。こんだけ"person"を連呼しているのに、Mongo上に作られるCollectionの名前が複数形の"people"になる!これが仕様なのですが、わざわざ何でこんなことをするのか、mongooseの実装ポリシーがわからない。あまりにも謎すぎてググったら、やはり皆が頭を悩ませていました。例えば"Activity"を指定したら"Activities"になるし、単数形と複数形が同じ単語の場合は変換されないという無駄に芸が細かい。
ということで、冒頭のスキーマコードではCollection名を設定する以下の箇所を正すべき?正さないべき?直さなくても動きますし、mongoの実際のコレクション名と設計は別のことですけどね。直さない方がいいのかなぁ。
const Person = Mongoose.model("people", // <-"person"を"people"に変更?しなくても良い
material-uiのi18n対策のひとつ
Material Designを実現するReactコンポーネントセットであるmaterial-uiは有用な上で、ソースを読んでも大変参考になるものが多くあります。中でもDatePickerのコードはReactでの動作エフェクトの作り方の他、コンポーネントを組み合わせてコンポーネントを作るというReactの基本的なことまで、多様なテクニックの良質なサンプルともなってます。しかしそんなmaterial-uiもL10nについては手が付いてません(v0.14.2現在)。
https://github.com/callemall/material-ui/blob/v0.14.2/src/utils/date-time.js
warning(locale === 'en-US', 'Wrong usage of DateTimeFormat. The ' + locale + ' locale is not supported.');
上記リンクの日付フォーマット関数を見れば様子がわかろうというもの。おいおい中身は"en-US"一択かよ。つまりは一応i18nのきっかけは作ってくれてるけど、中身の実装が追いついていないようです。コア開発者は昼間の仕事としてやってる風で日々の進化も早く、磨かれていってる感が週単位ぐらいで見て取れるのでそのうち対応するのでしょう。
こちらはなんちゃってで日本語対応だけしてみました。中国語と英語はライブラリに丸投げです。
const moment = require("moment"); require('moment/locale/ja'); require('moment/locale/zh-tw'); // material-uiのDatePickerコンポーネントの表示を国際化する function MomentFormat(locale, options) { this.format = function(date) { var format1 = "ddd, MMM DD"; var format2 = "MMMM YYYY"; var format3 = "MMM/DD/YYYY"; if(locale && locale.startsWith("ja")) { format1 = "M月D日, dddd"; format2 = "YYYY年 M月"; format3 = "YYYY年M月D日(ddd)" } let m = moment(date); m.locale(locale); // 将来への意欲は感じるが、現時点は無意味なmaterial-uiのoptions! if(!options) { return m.format(format3); } else if (options.month === 'short' && options.weekday === 'short' && options.day === '2-digit') { return m.format(format1); } return m.format(format2); }; } export default MomentFormat;
こんなのを書いて、次のように使う。formatDateはプレースホルダ用でDateTimeFormatとlocaleは入力ダイアログ用。
<DatePicker autoOk={true} wordings={{cancel: this.state.captionCancel}} formatDate={new MomentFormat(this.state.locale).format} DateTimeFormat={MomentFormat} locale={this.state.locale}/>
で、こうなる。ここまでやってもダイアログの最上部の年表示は仕込めず数字だけ。「2016年」と年だけの表示でもi18nすべきことは米国人には想像できなかったか。
日付のL10nでmoment.jsを使ってますが、こちらのキモはロケールの読み込み方法です。
require('moment/locale/ja');
これをやらないとmoment.locale()が空っぽの関数のまま。requireして初めてlocale("ja")が効くようになります。アプリケーションとして対応するロケール毎に同じことをやってあげれば良いのですね。私は全く読めませんがアラビア語などもmoment.jsは対応しています。
追記 v.0.14.3
さて、後日(1/27)にmaterial-uiをv.0.14.3にアップデートしたらDatePickerもちょっと進化しました。
なんと!曜日がi18nされた!ただし相変わらずen-US以外へのL10nはされてない。そのため、以下に書き直し。"narrow"なるOptionが増えてましたので、そちらに対応しました。
"use strict"; const moment = require("moment"); require('moment/locale/ja'); require('moment/locale/zh-tw'); function MuiDateFormat(locale, options) { this.format = function(date) { var format1 = "ddd, MMM DD"; var format2 = "MMMM YYYY"; var format3 = "MMM/DD/YYYY"; if (locale && locale.startsWith("ja")) { format1 = "M月D日 dddd"; format2 = "YYYY年 M月"; format3 = "YYYY年M月D日(ddd)" } let m = moment(date); m.locale(locale); // ガンバレ、その意気だ! if (!options) { return m.format(format3); } else if (options.month === 'short' && options.weekday === 'short' && options.day === '2-digit') { return m.format(format1); } else if (options.month === 'long' && options.year === 'numeric') { return m.format(format2); } else if (options.weekday === 'narrow') { return m.format("dd"); } }; } export default MuiDateFormat;
Reactコンポーネントをファイル分割したら沈黙してしまった件
Reactで大きな画面を作るうちに、人の習性としてReactコンポーネントをファイル分割したくなります。
var React = require('react'); var Item = React.createClass({ render: function () { return <li value={this.props.value}>{this.props.text}</li>; } }); var List = React.createClass({ render: function() { var items = this.props.data.map(function(i) { return <Item {...i}/> }); return ( <ol>{items}</ol> ); } }); var data = [ {text: 'One', value: 1}, {text: 'Four', value: 4}, {text: 'Five', value: 5}, {text: 'Seven', value: 7} ]; var ReactDOM = require('react-dom'); ReactDOM.render(<List data={data}/>, document.getElementById('List'));
と、まあこんなサンプルがあったとして、気持ち悪いのはコンポーネントの宣言記述と、その利用が同じファイルでやってること。Reactの説明で多くの場合はコンポーネントの宣言直下でReactDOM.render()を行ってますが、まあ実践では違う作りになるんじゃないかなと。で、以下みたいにするとします。
- コンポーネント(List.jsx)これは動かないダメな例!
// List.jsx var React = require('react'); var Item = React.createClass({ // 省略 }); module.exports = List = React.createClass({ // 省略 });
- 描画部の別ファイル
var data = [ // 省略 ]; var ReactDOM = require('react-dom'); var List = require('./List.jsx'); ReactDOM.render(<List data={data}/>, document.getElementById('List'));
汎用たるコンポーネントと、個別たる描画利用部が分かれていい感じ。requireもreactとreact-domがそれぞれで、なるほどこれがデザインかと。しかしこれらをBrowserifyでまとめたらさっきまで動いていたのに沈黙してしまいました。
しばらく嵌るうちにわかったのは、Listコンポーネントがexportされているけど、その中で使われているItemコンポーネントがexportされていないから。全部同じファイルだったら見えるし、描画-List-Itemと三つのファイルに分割する(もちろんItemも適切にexportする)のならば動きます。ただし描画部で、ReactDOM.render()の引数に渡しているJSXがTranspileした結果、React.createElement(List, {data: data})と変換されているので、Reactをコード上でてこなくてもrequireしなくちゃならなそうだったりと新たな痒さも。
コンパイラ言語だったり、JavaScriptでもクロージャーみたいに、スコープ見えるはずと思っていてもBrowserifyによる力技なコピーかき集め作業の前には通用しなかった。Reactコンポーネントはファイル毎にひとつづつってのがReact-Lintも警告するような常識だったようですが、かといって細かいファイルたくさんは作りたくない。。。はてさて。