デモなどで使えるGitコミットを簡単にresetし切替えるツールを作りました
私は仕事がら、人前でコード作成のデモを行う事がよくあります。その場でバリバリとライブ・コーデイング出来るとカッコ良いですが、間違えたり、コードを書くことに気を取られ説明がおろそかになったりすることも起こりがちです。
そこで説明のストーリに沿ってコード作成の過程をgitにコミットしておき、コマンドで簡単に git reset を行い、その時点でのコードを説明したり、動かしたりできるツールを作りました。
上の画像のように、このコマンドを起動するとgit logが表示され、現在のコミットが赤い色で表示されます、ここで n キーを押すと次のコミットに git reset できます。また p キーで前のコミットに git reset したり、カーソル上下キーでコミットを移動して RETURNキーでそこにgit reset できます。
このコマンドを実行すると git reset されてしまうので、 -c オプション指定で 予めファイルに全git logを書いておきます。
rubyで作ってあります、 gem 等のインストールは不要です。
#!/usr/bin/env ruby require 'io/console' GIT_LIST_PATH = "#{ENV['HOME']}/tmp/git-demo.lst" RED = "\e[31m" YELLOW = "\e[33m" BLACK = "\e[30m" FIN = "\e[0m" UP = "\e[A" DOWN = "\e[B" RETURN = "\r" def show_log_item(item, current) if current print "#{RED}#{item[0]} #{BLACK}#{item[1]}#{FIN}#{RETURN}" else print "#{YELLOW}#{item[0]} #{BLACK}#{item[1]}#{FIN}#{RETURN}" end end def show_git_log(log, current_index) log.each_with_index do |item, ix| show_log_item(item, ix == current_index) puts end end def get_current_hash IO.read("|git log --pretty=format:'%h' -1") end def find_current_hash(log, current_hash) log.find_index {|item| item[0] == current_hash} end def wait_key key = STDIN.getch if key == "\e" && STDIN.getch == "[" case STDIN.getch when "A" then "↑" when "B" then "↓" else "" end else key end end def cursor_up print UP STDOUT.flush end def cursor_down print DOWN STDOUT.flush end def set_cursor(log, current_index) (log.size - current_index).times { cursor_up } end def reset_cursor(log, current_index) (log.size - current_index).times { cursor_down } end def move_current_postion(log, index, delta) show_log_item(log[index], false) if delta > 0 if index < log.size - 1 cursor_down index += 1 end else if index > 0 cursor_up index -= 1 end end show_log_item(log[index], true) index end def get_git_log IO.readlines(GIT_LIST_PATH).map {|s| s.scan(/^(\w+) (.*)\n?/)[0]} end def put_git_log system "git log --pretty=format:'%h %s' > #{GIT_LIST_PATH}" end def git_reset_hard(log, index) system "git reset --hard #{log[index][0]}" end def main log = get_git_log current_index = find_current_hash(log, get_current_hash) show_git_log(log, current_index) set_cursor(log, current_index) do_reset = false while true case wait_key when 'n' current_index = move_current_postion(log, current_index, - 1) do_reset = true break when 'p' current_index = move_current_postion(log, current_index, + 1) do_reset = true break when "\r", "\n" do_reset = true break when "\C-c", "q" break when "↑" current_index = move_current_postion(log, current_index, - 1) when "↓" current_index = move_current_postion(log, current_index, + 1) end end reset_cursor(log, current_index) git_reset_hard(log, current_index) if do_reset end if ARGV.size > 0 if ARGV[0] == "-c" put_git_log puts "#{GIT_LIST_PATH} created." else puts "#{$0} -c" puts " or " puts "#{$0} " puts " n: git reset --hard Next commit" puts " p: git reset --hard Pervious commit" puts " ↑ ↓: seek commit" puts " RETURN: git reset --hard Current commit" puts " q: quit" end else main end
React.jsの紹介的なセミナーを行います
React.jsの開発も終わり、得られた知見をもとに 5月23日 に React.jsの紹介的な無料セミナーを行います。
内容は、React.js がなぜ良いのか、開発環境について、簡単なアプリをライブで作成・・・ などの React.jsの紹介的な内容になります。
会場は人材系の会社で、終了後に軽食の出る懇親会をスポンサーしてもらっています。人材系の会社ですがブラックな会社ではないので安心して御参加ください。
React.js の仕事 ほぼ完了しました!
ここ数ヶ月、久々に詰めて行っていた Recat.js の仕事がほぼ終わりました !!
仕事の詳細は書けませんが。 既存のjQueryベースのWebアプリがメンテナンス不可能に近くなっているので Recat.js に置き換えました。
既存のアプリは
- Backend は Ruby on Rails
- jQueryで作られてた Frontend の JS は約 14K 行 + テンプレート 4K 行
- アプリは大きく3つに別れるが、共有部分無しでコピーペーストで作られている
今回の仕事
- Backend は 落ちていたテストを修正したくらい
- End to End テストが無かったので書いた
- Turnip(RSpec, Gherkin) + Capybara + Poltergeist + Phantom.js
- Featureファイル 2K行、 steps 1.2K行
- 開発期間 約1ヶ月
- jQuery から Reactへの移行
- UI/UX はごく一部を除いて同じ (CSSは基本そのまま)
- Flux/Redux 等は使わず
- ES2016, JSX を使用
- 対応ブラウザーは最新のもの+IE11のみにしてもらった
- いくつかのコンポーネントを共有
- いくつかのReactではないJSライブラリーを開発期間の都合で残した
- NicEdit
- Nestable(+ jQuery)
- 開発環境は サーバーサイドプログラマーのためのReact.js 入門 1. 開発環境の構築で基本は書いたもの
- 開発期間 約1.5ヶ月 (普通にやったら 2.5ヶ月くらい? 詰め過ぎて腰を痛めました ^^;)
- 移行後のJSは 11K行
感想など
- 既存アプリは構成など初期設計は良く出来ている感じで実はそれほど悪いものではないが、実装はコピーぺの山だし無意味な部分も多数あった
- jQueryベースのものはページの変更後の再表示がかなりの部分で全書き換えになっていたが、 React版は Reactのおかげで最小限の書き換えになり高速化された!
- End to End テストを書くことでFrontendの動き、実装に関する理解が深まりました
- React.js は評判通りコードが長くなる、しかし構造が判りやすくメンテナンスしやすいのではと思える
- アプリの行っている事が比較的シンプルで Flux/Redux 等は コード量が増える割に、メリットが少なそうだと思えたので採用しなかった
- さすがに1万行くらいになるとWebpack(ESLint+Babel)に時間がかかるように成るがなんとか使えるレベルではあった
- Reactのコードは長くなるが難しいコードを書いているわけではないので CoC(convention over configuration)なライブラリー/トランスレータが出たら良いな・・・
- 今回得られた知見を元に React.js の教育も開始したいなと思います。そのために React.jsの入門記事を書きました。
- End to End テストのおかげで幾つかのバグが発見できましたが、それ以上に修正した後でテストが通れば「まあ酷いバグはないさ〜!」という安心感が良いですね ^^)
サーバーサイドプログラマーのためのReact.js 入門 4. react-router、更新系ページの追加
React.jsの仕事の納期におわれ1ヶ月ぶりになってしましたましたが、今回は
- react-router の導入
- 更新系ページの追加
を行い、Todoアプリを完成したいと思います。
react-router
JSで作られたアプリはSPA(Single Page Application)と呼ばれているように物理的には1つのページ(URL)内で動作します。しかしアプリがURLに対応してないとブラウザーのbackボタンに対応出来ないないですし、プログラムの構成を考えるのも面倒です。
そこで、URLとアプリ(JS)を対応づける react-router を使うと Rails の routes のように URLにアプリを対応づけ出来、今ままで Rails 等で開発してきた人にはとても嬉しいことです。
react-router には チュートリアル や充実したドキュメント がありますので、しっかり理解するには チュートリアル を試し、ドキュメント に目を通しておくと良いと思います。
react-router を組み込んでみる
前回作った TODO 一覧に react-router を組み込んで Railsのshow ページと同じものを追加してみましょう。
react-router インストール
$ cd frontend $ npm install --save react-router
index.js ページの作成
前回作ったアプリは全てのコードが src/js/index.js にありましたが、 一覧の機能は src/js/IndexPage.js にしましょう。 src/js/index.js をコピーまたはリネームし src/js/IndexPage.js を作ります(修正は次で行います)
src/js/index.js はルーテイング情報を書きます。
import React from 'react' import { render } from 'react-dom' import { Router, Route, IndexRoute, hashHistory } from 'react-router' import IndexPage from './IndexPage' import ShowPage from './ShowPage' render(( <Router history={hashHistory}> <Route path="/" > <IndexRoute component={IndexPage}/> <Route path="/:id" component={ShowPage}/> </Route> </Router> ), document.getElementById("example") )
ページ繊維の履歴管理にはいくつかの方法が選べます。 ドキュメントのHistories に説明があります。
今回はhashHistoryを使ってみます、ドキュメントに本来は browserHistory を使うべきだと書かれていますが browserHistory を使うと Rails で使っているURLがかぶってしまうので、ここでは /#URL
を使うhashHistoryにしました Router history=
で hashHistory を指定しています。
ルーテイング は /
で一覧(IndexPage.js) /ID番号
で詳細表示(ShowPage.js) が動作するように指定しています。 Rails のルーテイング同様に /:id
の id の部分はパラメーターとして取得できます。
画面へReact.jsの結果を表示する render() はこちらに移動します。
IndexPage.js 変更
前回の index.js をコピーした IndexPage.js は少し変更します。変更点はコメントに ★ を付けたところで
- import の変更、
import ReactDOM from 'react-dom'
は削除されました - IndexPageクラスが外部から参照できるように export default を追加
- Showpage.js へのリンク
<Link>
は<a>
タグが作成されます、Railsのlink_to
と同じようなものですね。<Link>
は単純に<a>
を生成しているのでは無く色々な処理を行っています - 画面へReact.jsの結果を表示する render() は index.js に移行したので削除されました
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' // ★ import axios from 'axios' export default class IndexPage extends Component { // ★ constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } render() { return( <div> <h2>List of Todos</h2> <TodoList todos={this.state.todos}/> </div> ) } } class TodoList extends Component { render() { return( <table> <thead> <tr> <th>Due</th> <th>Task</th> <th colSpan="3"></th> </tr> </thead> <tbody> {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} />))} </tbody> </table> ) } } TodoList.propTypes = { todos: PropTypes.array.isRequired } class TodoListItem extends Component { render() { return( <tr> <td> {this.props.todo.due} </td> <td> {this.props.todo.task} </td> <td><Link to={`/${this.props.todo.id}`}>Show</Link></td> // ★ </tr> ) } } TodoListItem.propTypes = { todo: PropTypes.object.isRequired }
ShowPage.js 作成
詳細表示のページ ShowPage.js は以下のようになります。 URLで指定される ID番号は this.props.params.id
で取得できます。その他は特に説明する事はないと思います。
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import axios from 'axios' export default class ShowPage extends Component { constructor(props) { super(props) this.state = {todo: {}} } componentDidMount() { this.todoFind(this.props.params.id) } componentWillReceiveProps(nextProps) { this.todoFind(nextProps.params.id) } todoFind(todoId) { axios.get(`/todos/${todoId}.json`).then((response) => { this.setState({todo: response.data}) }).catch((response) => { console.log(response) }) } render() { return( <div> <h2>Show</h2> <p> <strong>Due:</strong> {this.state.todo.due} </p> <p> <strong>Task:</strong> {this.state.todo.task} </p> <Link to="/">Back</Link> </div> ) } } ShowPage.propTypes = { params: PropTypes.object }
ここまで出来れば、上の画像のように 一覧の show リンクをクリックすると詳細画面が表示されます
更新系ページの追加
Rails の CSRF対策を無効にする
追加・更新を行うにはRuby on RailsのCSRF対策に対応しないといけませんが、今回は安易にスキップします。 実際のアプリでは行わないで下さいね!
- ../app/controllers/todos_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] skip_before_action :verify_authenticity_token # ★
新規作成ページのルーティング情報を追加
まずは、index.js にルーティング情報を追加します、コメントに ★ を付けたところ
import { render } from 'react-dom' import { Router, Route, IndexRoute, hashHistory } from 'react-router' import IndexPage from './IndexPage' import ShowPage from './ShowPage' import NewPage from './NewPage' // ★ render(( <Router history={hashHistory}> <Route path="/" > <IndexRoute component={IndexPage}/> <Route path="/new" component={NewPage}/> // ★ <Route path="/:id" component={ShowPage}/> </Route> </Router> ), document.getElementById("example") )
新規作成ページの追加
新規作成ページに対応する NewPage、 TODO情報のフォームに対応する TodoForm に分けました。Railsと同じように編集ページと共通になるフォーム部分を別のコンポーネント(モジュール)にしました。
React.js では state(状態、データ)は上位のコンポーネントに置き、下位のコンポーネントへpropsで渡します。 またボタンを押した等のイベントは下位のコンポーネントでは処理せずに上位のコンポーネントで処理するのが定跡(ベストプラクティス)です。
TODO情報をバックエンドに登録(ポスト)する todoCreateメソッド はNewPageに置いています。 todoCreateメソッド は下位のTodoFormの中にも置けますが、登録した後に state が変化する事は良くあるので、この方が良いと思います。また、通信やモデル(ここでは stateのtodo)変更は上位のコンポーネントに集めるのは下位コンポーネントの汎用性やコードの見通しがよくなります。
todoCreateメソッド内の this.props.history.push()
は指定されたURL(Path)への遷移を起こします。
formChangeメソッドに付いては TodoForm の方で説明します。
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import axios from 'axios' import TodoForm from './TodoForm' export default class NewPage extends Component { constructor(props) { super(props) this.state = {todo: {}} } todoCreate() { axios.post('/todos.json', {todo: this.state.todo}).then((response) => { this.props.history.push(`/${response.data.id}`) }).catch((response) => { console.log(response) }) } formChange(todo) { this.setState({todo: todo}) } render() { return( <div> <h2>New</h2> <TodoForm todo={this.state.todo} buttonLable="Create" onChange={this.formChange.bind(this)} onSubmit={this.todoCreate.bind(this)} /> <Link to="/">Back</Link> </div> ) } } NewPage.propTypes = { params: PropTypes.object, history: PropTypes.object }
TodoForm は主にを <form>
フォームを扱っています。 React.js でのフォームの扱いは ドキュメントのForms に書かれているます。フォーム内の <input>
のハンドリングは、全ての変更をアプリのコードで扱う Controlled Components と Reactに任せる
Uncontrolled Components があります。ここでは分かりやすい Controlled Components を使っています。
Controlled Componentsでは<input>
に変化がおきるとその変化をアプリのコードで state に反映し、stateの値から <input>
を再表示します(再表示は React.js が行います)。
ここでは state は上位コンポーネントにあるので、上位コンポーネントの formChange メソッドを呼び出しています。その結果、上位コンポーネントから変更された state の値を props で受け取った TodoForm が再表示されます(再表示は React.js が行います)。
React.js では <input>
の値などを参照するには タグに ref
属性を指定し、コードからは this.refs.ref属性値
で取得します。 refに付いては ドキュメントのRefs to Components に書かれています。
submitボタンが押された時の動作はここでは <button>
の onClick
でハンドリングしています。 event.preventDefault()
で他のタグへのイベントの伝搬をキャンセルし、上位コンポーネントのメソッドを呼び出しています。
日付の入力はRailsでは年月日の <select>
になりますが、面倒なので通常の <input type="text">
にしました。たぶんカレンダー入力の React Date Picker などを使えば良いのかなと思います。
import React, { Component, PropTypes } from 'react' export default class TodoForm extends Component { change() { this.props.onChange({due: this.refs.due.value, task: this.refs.task.value}) } submit(event) { event.preventDefault() this.props.onSubmit() } render() { return( <form> <div className="field"> <label htmlFor="todo_due">Due</label><br/> <input type="text" ref="due" id="todo_due" value={this.props.todo.due} onChange={this.change.bind(this)} /> </div> <div className="field"> <label htmlFor="todo_task">Task</label><br/> <input type="text" ref="task" id="todo_task" value={this.props.todo.task} onChange={this.change.bind(this)} /> </div> <div className="actions"> <button onClick={this.submit.bind(this)}>{this.props.buttonLable}</button> </div> </form> ) } } TodoForm.propTypes = { todo: PropTypes.object.isRequired, buttonLable: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired }
IndexPage.js の変更1
新規作成へのリンクを追加します
*** import は変更なし *** export default class IndexPage extends Component { constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { this.todoAll() } componentWillReceiveProps() { this.todoAll() } todoAll() { axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } todoDelete(todoId) { axios.delete(`/todos/${todoId}.json`).then(() => { this.todoAll() }).catch((response) => { console.log(response) }) } render() { return( <div> <h2>List of Todos</h2> <TodoList todos={this.state.todos}/> <br /> <Link to="/new">New Todo</Link> // ★ </div> ) } } *** 以下は同じ ***
IndexPage.js の変更2
新規作成機能を追加し試してみると、新規追加したデータが表示されない時があります。前回書いたように
- 一度表示されたコンポーネントはインスタンスがメモリ上に残っているので、他のコンポーネントから再表示を起こしても componentDidMount は実行されません。その時は componentWillReceiveProps が呼び出されます。
一覧の再表示が正しく行われるように componentWillReceivePropsメソッドでもデータを取得するように変更します。
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import axios from 'axios' export default class IndexPage extends Component { constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { // ★ this.todoAll() } componentWillReceiveProps() { // ★ this.todoAll() } todoAll() { // ★ axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } todoDelete(todoId) { axios.delete(`/todos/${todoId}.json`).then(() => { this.todoAll() }).catch((response) => { console.log(response) }) } render() { *** 以下は同じ ***
変更ページの追加
新規作成ページと同様なので特に説明する必要はないと思います
- index.js
import React from 'react' import { render } from 'react-dom' import { Router, Route, IndexRoute, hashHistory } from 'react-router' import IndexPage from './IndexPage' import ShowPage from './ShowPage' import EditPage from './EditPage' // ★ import NewPage from './NewPage' render(( <Router history={hashHistory}> <Route path="/" > <IndexRoute component={IndexPage}/> <Route path="/new" component={NewPage}/> <Route path="/:id" component={ShowPage}/> <Route path="/:id/edit" component={EditPage}/> // ★ </Route> </Router> ), document.getElementById("example") )
- EditPage.js
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import axios from 'axios' import TodoForm from './TodoForm' export default class EditPage extends Component { constructor(props) { super(props) this.state = {todo: {}} } componentDidMount() { this.todoFind(this.props.params.id) } componentWillReceiveProps(nextProps) { this.todoFind(nextProps.params.id) } todoFind(todoId) { axios.get(`/todos/${todoId}.json`).then((response) => { this.setState({todo: response.data}) }).catch((response) => { console.log(response) }) } todoUpdate() { axios.put(`/todos/${this.props.params.id}.json`, {todo: this.state.todo}).then((response) => { this.props.history.push(`/${response.data.id}`) }).catch((response) => { console.log(response) }) } formChange(todo) { this.setState({todo: todo}) } render() { return( <div> <h2>Edit</h2> <TodoForm todo={this.state.todo} buttonLable="Update" onChange={this.formChange.bind(this)} onSubmit={this.todoUpdate.bind(this)} /> <Link to={`/${this.state.todo.id}`}>Show</Link> | <Link to="/">Back</Link> </div> ) } } EditPage.propTypes = { params: PropTypes.object, history: PropTypes.object }
- IndexPage.js
*** 上部は同じ *** class TodoListItem extends Component { render() { return( <tr> <td><Link to={`/${this.props.todo.id}`}>Show</Link></td> <td><Link to={`/${this.props.todo.id}/edit`}>Edit</Link></td> // ★ </tr> ) } } TodoListItem.propTypes = { todo: PropTypes.object.isRequired } *** 以下は同じ ***
削除機能の追加
TodoListItemに 削除用リンクを追加し、クリックした際にバックエンドの削除APIを呼び出す todoDelete メソッドを IndexPageコンポーネントに追加しました。
削除確認のダイアログをどのコンポーネントで出すかは考慮の余地があると思います。今回は確認ダイアログはUIの一部と考え、下位コンポーネントで確認は行うようにしました。 上位コンポーネントで出すという考え方もあると思います。
import React, { Component, PropTypes } from 'react' import { Link } from 'react-router' import axios from 'axios' export default class IndexPage extends Component { constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { this.todoAll() } componentWillReceiveProps() { this.todoAll() } todoAll() { axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } todoDelete(todoId) { // ★ axios.delete(`/todos/${todoId}.json`).then(() => { this.todoAll() }).catch((response) => { console.log(response) }) } render() { return( <div> <h2>List of Todos</h2> <TodoList todos={this.state.todos} onDelete={this.todoDelete.bind(this)} /> </div> ) } } class TodoList extends Component { render() { return( <table> <thead> <tr> <th>Due</th> <th>Task</th> <th colSpan="3"></th> </tr> </thead> <tbody> {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} onDelete={this.props.onDelete} />))} // ★ </tbody> </table> ) } } TodoList.propTypes = { todos: PropTypes.array.isRequired, onDelete: PropTypes.func.isRequired // ★ } class TodoListItem extends Component { destroy(event) { // ★ event.preventDefault() if (confirm("Are you sure ?")) { this.props.onDelete(this.props.todo.id) } } render() { return( <tr> <td> {this.props.todo.due} </td> <td> {this.props.todo.task} </td> <td><Link to={`/${this.props.todo.id}`}>Show</Link></td> <td><Link to={`/${this.props.todo.id}/edit`}>Edit</Link></td> <td><Link to='' onClick={this.destroy.bind(this)}>Destroy</Link></td> // ★ </tr> ) } } TodoListItem.propTypes = { todo: PropTypes.object.isRequired, onDelete: PropTypes.func.isRequired // ★ }
まとめ
今回までの知識で React.js でアプリ作成は出来るようになるとおもいます。 React.js以外のライブラリーを組みわせるには、もう少し知識がいりますが大抵のことは ドキュメント に書かれていますし、検索をすると Stackoverflow に答えがみつかると思います。
今回、仕事で1万行以上の React.js を書きましたが、Reactの考え方(設計思想)になれるまでは時間がかかりますが、なれれば動きの分かり易いコードが出来る良いライブラリーだと思います。
サーバーサイドプログラマーのためのReact.js 入門 3. いよいよ React.js を書く
いよいよ React.js 入門
開発環境作成に2回も使いましたが、今回から React.js を始めます。
開発環境というかエディターですが、主なエディターは JSX, ES6 をサポートする拡張パッケージ、マクロ等があると思いますので入れておきましょう。もちん WebStorm のようなIDEも良いかもしれません。
これから作るTodoアプリはRuby on Railsのサーバーとhttpプロトコルで通信する必要があります。そこで今回は通信部分には、Promisベースの今風なHTTP clinet ライブラリー axios を使ってみます、 使い方は極めてシンプルです。
インストールは
$ npm install --save-dev axios
まずは、超簡単なReact.js アプリを動かす
前回書いた index.js を以下のように変更してください。もちろんターミナルでは webpack -d -w
を実行して下さい。
また、別のターミナルでは rails s
でRuby on Rails のサーバーも起動しておいて下さい。
import React, { Component } from 'react' import ReactDOM from 'react-dom' import axios from 'axios' class IndexPage extends Component { constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } render() { return ( <div> <h2>List of Todos</h2> {this.state.todos.map((todo) => <div>{todo.task}</div>)} </div> ) } } ReactDOM.render( <IndexPage />, document.getElementById('example') )
React.js は 画面をコンポーネント(部品)に分割し、そのコンポーネントは独自のHTMLタグになります。
上では <IndexPage>
というコンポーネントを定義しそれを一番下の ReactDOM.render() で index.html にある <div id='example'></div>
の中に埋め込むことでコンポーネントが表示されます。
ブラウザーのConsoleに warnig が表示されています。内容については後で説明しますが、このように React は何かおかしいところがあると warnig を表示してくれます。これは開発・デバックの助けになるのでちゃんと対応して下さい。
解説
state
Reactのコンポーネントは Component クラスを継承して作ります。クラスのコンストラクター(constructor) の中で プロパティー(インスタンス変数)を定義しています。
state は重要なプロパティーで、このコンポーネントが保持する状態やデータなどを保持するオブジェクト(Hashとして使ってます)。ここで todos は サーバーから取得したTodo情報の配列です。適切な(後で説明します)初期値を書いておいて下さい。
render
次に render() メソッドを見てみましょう。これは画面に表示されるHTMLを戻すメソッドです。 JS(JavaScript)の中にHTML タグが書かれていますが、これがJSXで環境設定で入れたBabelがこの部分を通常のJSに変換してくれます。このHTMLタグの部分は JSの式として扱われます。
JSXの中の { 。。。 } にはJSの式が書かれます、この値がタグや属性値として使われます。<div>{todo.task}</div>
の todo.task はタスクの文字列になります。
this.state.todos.map
の部分は state 内の todos の値(配列)からtask文字列を表示するHTMLの列を生成しています。 ES6を使ってコンパクトに書いてますが、ES5 で書くと
{this.state.todos.map(function(todo) { return <div>{todo.task}</div> })}
サーバーサイドのviewと似た感じですね、サーバーサイドのviewが1ページ全体(またはページ内の一部分)をリクエスト毎に一度に作成するように Reactも必要なタイミングで一度にページ全体を作成するようにプログラミングします。
jQueryなどを使ったAjaxではプログラマーが変更すべき部分のみを書き換えるようにプログラミングする必要がありました。なぜならHTML全体を書き換えるのでは遅く使いやすいUIを実現できないからです。その代償としてコードが煩雑になり、メンテナンスしづらいコードになりがちでした。
Reactは HTML全体を書き換えるようなプログラミンをしてもライブラリーが現在のDOM(HTML構造)と新しいDOM(HTML構造)を比較し、必要な部分のみを書き換えてくれます。
この Virtual DOM という仕掛けが、画面全体を書き換えるというシンプルなプログラミンモデルと性能を両立してくれます。
Reactが生成したHTML(下の画像)を見ると、data-reactid という属性が各タグにふられています、これはVirtual DOMと実際のDOMの管理に使われているのだと思われます。
componentDidMount (コンポーネントのライフサイクル)
Reactではコンポーネントの表示を明示的な制御はせず Reactライブラリーに任せます。シンプルなコンポーネントは render メソッドだけでも出来ますが、通常はデータを取得したりいろいろな処理が必要になります。そこでReactでは表示処理のあるタイミングで呼び出されるメソッドが決まっています、それを自分でオーバーライトし処理を組み込みます。 詳細はComponent Specs and Lifecycle を見て下さい。凝ったUIを作ったり、React以外のJSライブラリーと組みせる場合などはこのライフサイクルを熟知する必要があります。
現在、Reactを使いアプリを作っていますが、凝った事はしてないので、ほとんどコンポーネントは
- componentDidMount() 最初にコンポーネントが描画された後に呼び出される
- componentWillReceiveProps(nextProps) コンポーネントが再描画される際に呼び出される(正確には新たなpropsを受け取る時、propsは後で説明します)
の2つで済んでます。 Component Specs and Lifecycle に書かれているように データの取得等はcomponentDidMountメソッドに実装します。
componentDidMountは最初の描画の後に呼び出されます、今回のコードではtodosの初期値は空配列なので、最初は <div><h2>List of Todos</h2></div>
が表示されます(もちろん見えませんが)。
その後 componentDidMount が実行され RailsサーバーからTodo情報を取得してきます。 axios の詳細は axiosのGitHub に書かれていますが、見なくとも想像できると思います。
- get() でGETリクエストをサーバーに送り
- データが取得できると then() に書かれた(無名)関数が実行されます
- エラーが起きた場合は catch() に書かれた(無名)関数が実行されます
データが取得できたら this.setState()
メソッドが呼び出されていますが Reactではstateの更新はプロパティーへの代入ではなく、このメソッドを呼び出す必要があります。
this.setState()
は変更された state を使い再描画を行います。(上のrenderで説明したように Virtual DOM により実際に描かれるのはtodo.task表示のdivタグのみです)
注意点
私が、初めて Reactを使ってハマった点などを書いておきます。
- 一度表示されたコンポーネントはインスタンスがメモリ上に残っているので、他のコンポーネントから再表示を起こしても componentDidMount は実行されません。その時は componentWillReceiveProps が呼び出されます
- 上にも書きましたが、最初はデータが空(初期値)の状態で描画が起きるので、初期値は重要です。適切な値を設定しておくか、renderの方で空データを考慮しないとnull絡みのエラーが発生します
- renderで描かれるコンポーネントは基本的に1つのタグである必要があります、今回のコードではHTML的には最初のdivタグはいらないのですが、 このReactの制限から div で括っています
- List of Todosの画面で発生していた Warning: Each child in an array or iterator should have a unique "key" prop. ... ですが、リストやテーブルのように要素数が可変のタグを正しくReactが認識出るようにkey 属性でユニークな値を設定する必要があります。今回のコードは正しくは以下のように書きます
・・・ 省略 ・・・ render() { return ( <div> <h2>List of Todos</h2> {this.state.todos.map((todo) => <div key={todo.id}>{todo.task}</div>)} </div> ) } } ・・・ 省略 ・・・
Todoリスト表示完成版
Todoリストの表示ページを、
- ページ全体
- Todoリスト表示
- Todo一項目表示
というコンポーネントに分割してみましょう。コンポーネントにはHTMLの属性のように値を渡すことができます。
値を受け取るコンポーネント側では、this.props.属性名
で値をアクセスします。 この props も重要なプロパティーです。また、propsの値をコンポーネント内で変更しないのが React の考え方です。
TodoList.propTypes = {
から始まる部分ですが、propの型を宣言する部分です。無くても動作しますが型宣言をしておくと 違う型が渡された場合 waring が発生するので、開発には役立つと思います。
詳細は Prop Validation を参照して下さい
import React, { Component, PropTypes } from 'react' import ReactDOM from 'react-dom' import axios from 'axios' class IndexPage extends Component { constructor(props) { super(props) this.state = {todos: []} } componentDidMount() { axios.get('/todos.json').then((response) => { this.setState({todos: response.data}) }).catch((response) => { console.log(response) }) } render() { return( <div> <h2>List of Todos</h2> <TodoList todos={this.state.todos}/> </div> ) } } class TodoList extends Component { render() { return( <table> <thead> <tr> <th>Due</th> <th>Task</th> <th colSpan="3"></th> </tr> </thead> <tbody> {this.props.todos.map((todo) => (<TodoListItem key={todo.id} todo={todo} />))} </tbody> </table> ) } } TodoList.propTypes = { todos: PropTypes.array.isRequired } class TodoListItem extends Component { render() { return( <tr> <td> {this.props.todo.due} </td> <td> {this.props.todo.task} </td> </tr> ) } } TodoListItem.propTypes = { todo: PropTypes.object.isRequired } ReactDOM.render( <IndexPage />, document.getElementById('example') )
予告
次回は、Todo情報の変更などに付いて書きます。
サーバーサイドプログラマーのためのReact.js 入門 2. 開発環境の構築の続き
これから作るアプリ
今回と次回で作るアプリですが Ruby on Railsのscaffoldジェネレータが生成する古き良き時代の単純なWebアプリケーションと同じ動きをする、React.js を使った Single page application (SPA)のフロントエンドを作っていきます。
バックエアンド(APIサーバー)は Ruby on Rails を使うことにします。
バックエンドの構築
Ruby on Railsのインストール等はネット上の情報を参考にしてください。 私の会社 EY-OfficeのWikiにもインストール方法を書いています (少し古いのでバージョンは最新のものを入れて下さい)
Railsがインストールできたら、以下のようにプロジェクトを作り scaffold ジェネレータを使い、Railsアプリを作ります。
$ rails new todo_app $ cd todo_app $ rails generate scaffold todo due:date task:string $ rake db:migrate $ rails server
http://localhost:3000/todos をブラウザーからアクセスし、New Todo ページでデータを作成したり、アプリの動きを見てください。
http://localhost:3000/todos.json をアクセスしてみて下さい。 scaffold ジェネレータが作ったコードはデフォルトでJSONを出力できます、これでバックエンドは完成です!
rails server
で起動したサーバーはReact.js でも使いますので、そのまま動かしておいて、以下の作業は別のターミナルで行ってください。
React開発環境の構築
前回は開発環境を構築する前準備でした、今回は具体的な開発環境を作り開発してみます。
Reactに行く前に
Railsの環境ではスタティックなコンテンツ等は public/ に置きます。
そこで JS が入るディレクトリーとJSを表示するための index.html を作成ておきます。
$ mkdir public/js $ vi public/index.html
- index.html
<!DOCTYPE html> <html> <head> <title>ReactTodo</title> <meta charset="utf-8"> </head> <body> <div id="example"></div> <script type="text/javascript" src="js/app.js" charset="utf-8"></script> </body> </html>
React開発環境の構築
今回は Railsプロジェクトの中に frontend ディレクトリーを作りそこで開発することにします。npm init でいろいろとたずねれますが、気にせず return していっても良いです。
$ mkdir frontend $ cd frontend $ mkdir -p src/js src/css $ nam init -y
- React.js、babel、ESLint、webpack のインストール。 XXXX-loader はwebpack用のモジュールです。
本日ためしたところ eslint 2.0.0 をインストールすると babelが動作しませんでしたので @1.10 でバージョン 1.10.X をインストールしています。非互換な変更があったようです
$ npm install --save react react-dom $ npm install --save babel-loader babel-preset-es2015 babel-preset-react $ npm install --save eslint eslint-loader eslint-plugin-react babel $ npm install --save css-loader style-loader $ npm install -g webpack
- babel 設定ファイル .babelrc の作成。ES6(正式にはES2015)とJSX(React)の変換を行います
{ "presets": ["es2015", "react"] }
- ESLint 設定ファイル .eslintlrc の作成、後で説明します。
{ "env": { "browser": true, "node": true, "es6": true }, "parserOptions": { "sourceType": "module", "ecmaFeatures": { "experimentalObjectRestSpread": true, "jsx": true } }, "extends": ["eslint:recommended", "plugin:react/recommended"], "plugins": [ "react" ], "rules": { "strict": [2, "function"], "no-undef": 2, "no-console": 0, "arrow-parens": [2, "always"] } }
- webpack の設定ファイル webpack.config.js の作成
設定ファイルの詳細は webpackのドキュメント や各ローダーのドキュメントを見てください。日本語の記事もいくつかありますので、両方を見ながら覚えて行って下さい。 (いずれ記事を書きたいですが、私もまだまだ苦労しています・・・)
module.exports = { entry: { app: "./src/js/index.js" }, output: { path: '../public/js', filename: "[name].js" }, module: { preLoaders: [{ test: /\.jsx?$/, exclude: /node_modules/, loader: "eslint-loader" }], loaders: [{ test: /\.css$/, loader: "style!css" }, { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' }] }, resolve: { extensions: ['', '.js', '.jsx', '.css'] }, eslint: { configFile: './.eslintrc' } };
試してみよう!
やっと環境ができました、さっそく試してみましょう。 まずは、 Reactの Getting Started のコードを試してみましょう
- src/js/index.js
var React = require('react'); var ReactDOM = require('react-dom'); ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') );
ターミナルで webpack -d -w
を入力し、少し待つと以下のように表示されます
- app.js とマップファイルが生成されました。
- また、index.js では React が定義されているが使われてないというエラーが出ています。
$ pwd /Users/XXXXXX/todo_app/frontend $ webpack -d -w Hash: cf1445dbfe9ce6c676f1 Version: webpack 1.12.13 Time: 2268ms Asset Size Chunks Chunk Names app.js 764 kB 0 [emitted] app app.js.map 846 kB 0 [emitted] app + 180 hidden modules ERROR in ./src/js/index.js /Users/yy/tmp/todo_app/frontend/src/js/index.js 1:8 error "React" is defined but never used no-unused-vars ✖ 1 problem (1 error, 0 warnings)
ここでは、ESLintの出したエラーは一旦無視(他のエラーはちゃんと追求して下さいね)してブラウザーで http://localhost:3000 をアクセスすると 画面に Hello、wold! が表示されます。
上手くいかない場合はブラウザーのコンソール(開発ツール、インスペクター)を見てください。エラーや警告が出ているかもしれません。
上で書いたJSは ES5 ですが ES6 に変えてみましょう。同じエラーは発生しますが Hello, world! が表示されるはずです
import React from 'react' import ReactDOM from 'react-dom' ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') )
Next Steps のコードも試してみてください。
ESLint
JavaScriptは動的な言語です、従って実行してみるまでエラーが発見できません。 テストを書いてこまめに開発していていれば良いのですが、ブラウザーを操作してやっと開発中のページを実行した途端に、つまらない文法エラー発生! などということになりがちです。
そこで大きな助けになるのが ESLint です。文法エラー、未定義エラー、定義されているが未使用などを実行前にチェックしてくれるので開発がはかどります!! 特に Reactでは JS の中に HTML が書かれる JSX は、なれるまで間違いが発生しやすのでお勧めです。
ただし、JSは動的な言語なのでチェックは完璧にはできません。上の Hello,world! 出ていたエラーは Reac変数がプログラムの中で使われていないので発生しました。しかし、この行を消してしまうと React.js が読み込まれないので実行できません。
このような場合は、ソースコードか設定ファイルに指定することで回避できます。ここでは .eslintlrc に React は使われてなくても良いというrule を設定を追加してみましょう。 詳しくは Configuring ESLint をみてください。
{ ... ここまでは同じ... "rules": { "strict": [2, "function"], "no-undef": 2, "no-console": 0, "arrow-parens": [2, "always"], no-unused-vars: [2, {"varsIgnorePattern": "^React$"}] } }
次回は、いよいよ React.js ^^)/
サーバーサイドプログラマーのためのReact.js 入門 1. 開発環境の構築
最近、バリバリと jQueryベースのフロントエンドを React.js に置き換えています。
私も主に Ruby on Rails等の サーバーサイドエンジニアで、最近のフロントエンド開発を本格的に開発するのは初めてです。いろいろとつまずきながら進んできました。
まずは node.js をインストールし npm を知る
環境構築のためのツールは node.js の上で動きます、そこで node をインストールします。 ツールを動かすだけなので node のバージョンはあまり気にしなくて良いと思います。(2016-02-04日現在ではバージョン5.5.0が入ります)
$ brew install node
node をインストールすると npm コマンドもインストールされます。npmは node.js用ライブラリーと、その管理を行うコマンドです。 Rubyの gem に相当するものですが、さらに Ruby の bundle, rake 的な機能も持っていて node の世界を極めるには非常に重要なものです。
node の生態系は ruby と似ていますが、さらに改良されています。良く使うコマンドは
npm init
カレントディレクトリーにnpm用設定ファイル package.json ファイルを作ります。このファイルにはプロジェクト(ライブラリー)の情報、依存ライブラリーなどの情報が入っています。コマンドを起動すると name: version: などを尋ねてきますが後で package.json ファイルを変更できるので全てreturnでも構いません。npm install --save ライブラリー名
ライブラリーカレントディレクトリー下に(node_modulesディレクトリーを作り)インストールします。 --save を指定すると依存ライブラリーとしての情報が package.json に入ります。npm install -g ライブラリー名
ライブラリーを共通な場所(brewでnodeをインストールした場合 /usr/local/lib/node_modules)にインストールします。コマンドを含むものは -g (--global) で入れるとコマンドもしかるべき場所に入り便利です。npm --help
npm にはたくさんの機能があります、知りたい場合はヘルプを
ビルドツールを選ぶ(知る)
フロントエンドの生態系は凄い速さで進化しています。フレームワークも少し前は AngularJS とか言われてましたが、一昨年あたりから React が主役っぽいし、Reactは表示ライブラリーなので、それを使うためのフレームワークは Flux だとか Redux だとか出来きてます・・・
ビルドツールのようなものには grunt, gulp, babel, bower, browserify, webpack ... とたくさんのもので賑わっていて、何を使えば良いのか迷います。近くにフロントエンドのエンジニアがいれば相談するのが良いと思いますが、やみくもにネットを調べても発散するばかりです !
やりたいことを整理しましょう
まず開発環境の要件というか、やりたい事をまとめましょう。今回の私の要件は
- React を使う
- Redux を使うのかな?
- 現在のJS(ES5)はだるいので、ES6 を使いたい
- 完全なシングルページアプリ (Rails等のviewは使わない)
- ブラウザーは最新でけサポートすれば良い
- 環境構築ツールはシンプルなもにしたい、多少の不便があっても良い
- HTML,CSSもプログラマーが書く(既に出来たHTML/CSSがある)
- 過去の開発環境にはこだわらない
- 過去の(デプロイされた)ディレクトリー構成にはこだわらない
一般論としての良いツールではなく自分のやりたい事に最適なツールを探しましょう
ネットで調べてみる
ビルドツール等に関しては qiita を始め多数の日本語の情報がネットから得られので概要を理解するのに役立ちます。しかし最新の正確な情報は本家(英語)のページを読みましょう。
JSX, ES6
ReactではJSXというJSの中に HTML を埋め込んだものを使ういます(使わない方法もあるようですが)。JSXはそのままでは実行できないので通常のJSに変換するツールが必要になります。
var React = require('react'); var ReactDOM = require('react-dom'); ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') );
また、ES6(正確には ES2015, ECMAScript2015)は全ての主要ブラウザーでサポートされているわけではありません。そこでES6からES5に変換する Babel が良く使われています。
JSXやBabelで変換されたコードをブラウザーで動かすのは良いのですが、デバックはどうするのでしょうか? 変換されたコードでデバックするのでしょうか? 安心して下さい、そのために元のコードでデバックするための ソースマップ(.map) が主要ブラウザーではサポートされています。
ビルドツール
現時点では Browserify か Webpack が良いようです。
Browserify
Browserify はもともと node.js 用のモジュール構造をブラウザーのJSで使えるようにする技術ですが、それを行うための変換ツールでもあります。また、babelやJSXの変換ツールを統合できます。
Browserifyとnpmを使った最小限のビルドツールの作り方が Reactをnpmでビルドする方法 browserify (watchify) + babelify編 にまとめられています。
npm のスクリプト実行機能を使い以下のように browserify(watchify), babelify を使い ES6(babel-preset-es2015), JSX(babel-preset-react)の変換を行います。また、ソースマップ作成にexorcistを使っています。
watchify -t babelify ./app.js -o 'exorcist ./build.js.map > ./build.js' -d
Browserifyはいつかのツールを組み合わせボトムアップにビルド環境を作るツールです。各種ツールやgulpと組み合わせると、自分のやりたいことが実現できると思います。
Webpack
Webpack ホームページの絵からもわかるようにフロントエンドの統合開発ツールです。フレームワークのような作りになっていて、そこに種々のツールをプラグインで組み込み設定ファイルで管理します。
新たにトップダウンで作られたツールなのでわかり易いように思います。ただし、自分のやりたいことが完全に実現できないこともあるかもしれません。
試してみた
BrowserifyとWebpackのどちらが良いのか情報でけでは決めかねたので ReactのGetting Started を両方の環境で試してみました。
その結果、今回の要件にはWebpackが良さそうなので使うことにしました。
予告
次回はReactを使った簡単なアプリ作成を Webpack の環境作りから始めます・・・・