hina2go

主に技術系のこことか。最近React始めました。

ReactでGithubのリポジトリブラウザを作ってみる (2)

前回の続き。

hinathy.hatenablog.com

お題

今回はちょっとしたお題。 ユーザー名が未入力でもボタンが押せちゃうのは、ユーザービリティが良くないので改善します。 具体的には、ユーザー名未入力の場合は、Updateボタンをdisableします。

以下のように、disable={!this.state.username}してあげるだけで実現できます。

class SearchBox extends React.Component {
  ... (snip) ...

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <input type="text" onChange={this.handleChange} placeholder="Enter username" />
          <input type="submit" value="Update" disabled={!this.state.username} /> // <= (*) ここ
        </form>
      </div>
    );
  }

  ... (snip) ...
}

ポイントは以下でしょうか。

  • usernameを、ステートで保持する。
  • ユーザー名のテキストボックスの入力内容の変化をonChange()でハンドリングする。
  • onChange()が呼び出されたら、ステートに保持しているusenamesetState()で更新する。

ソースコード

github.com

ReactでGithubのリポジトリブラウザを作ってみる (1)

GithubRepositories APIは、指定したユーザーのリポジトリを取得することができます。

$ curl -s 'https://api.github.com/users/toshiyukihina/repos'
[
  {
    "id": 38911425,
    "name": ".emacs.d",
    "full_name": "toshiyukihina/.emacs.d",
    "owner": {
      "login": "toshiyukihina",
      "id": 1883527,
      "avatar_url": "https://avatars.githubusercontent.com/u/1883527?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/toshiyukihina",
      "html_url": "https://github.com/toshiyukihina",
      "followers_url": "https://api.github.com/users/toshiyukihina/followers",
      "following_url": "https://api.github.com/users/toshiyukihina/following{/other_user}",
      "gists_url": "https://api.github.com/users/toshiyukihina/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/toshiyukihina/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/toshiyukihina/subscriptions",

... (snip) ...

このAPIを使って、指定したユーザーのリポジトリ一覧を取得するアプリを作ってみます。

イメージはこんな感じです。

f:id:hinathy:20160428143709p:plain

コンポーネントの構成は以下。

  • App
    • SearchBox
    • RepositoryList
      • Repository

準備

ここからプロジェクトの雛形をダウンロードして、bundle install --path vendor/bundle && npm installしておきます。

コンポーネントのファイルを生成する

$ for c in app search_box repository_list repository; do bundle exec rails g react:component $c; done

Repositoryを実装する

Repositoryは、以下をパラメータとしてとります。

  • name
  • description
import React from 'react';

class Repository extends React.Component {

  render() {
    return (
      <tr>
        <td>{this.props.name}</td>
        <td>{this.props.description}</td>
      </tr>
    );
  }
  
}

Repository.PropTypes = {
  name: React.PropTypes.string.isRequired,
  description: React.PropTypes.string.isRequired
};

export default Repository;

RepositoryListRepositoryレンダリングする

Repositoryに渡すデータは、RepositoryListの上位コンポーネントAppからrepositoriesというPropで渡されることにします。

import React from 'react';
import Repository from './repository';

class RepositoryList extends React.Component {

  render() {
    const repositoryNodes = this.props.repositories.map((repository) => {
      return (
        <Repository
            key={repository.id}
            name={repository.name}
            description={repository.description}
        />
      );
    });

    return (
      <div>
        <table>
          <thead>
            <tr>
              <th>Name</th>
              <th>Description</th>
            </tr>
          </thead>
          <tbody>
            {repositoryNodes}
          </tbody>
        </table>
      </div>
    );
  }
  
}

RepositoryList.PropTypes = {
  repositories: React.PropTypes.arrayOf(React.PropTypes.object)
};

export default RepositoryList;

AppRepositoryListレンダリングする

RepositoryListに渡すデータは、AppGithub API経由で取得したデータを渡すことを想定します。 ですので、Appの初期ステートとして固定データを与えておきます。

import React from 'react';
import SearchBox from './search_box';
import RepositoryList from './repository_list';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      repositories: [
        { id: '1', name: 'angular-ui-router', description: 'A sample program using angular-ui-router.' },
        { id: '2', name: 'angular-chat', description: 'A chat program using Angular.js.' },
        { id: '3', name: 'hello-redux', description: 'My first redux.' },
        { id: '4', name: 'dotfiles', description: 'My dotfiles.' }
      ]
    };
  }

  render() {
    return (
      <div>
        <SearchBox />
        <RepositoryList repositories={this.state.repositories} />
      </div>
    );
  }
  
}

export default App;

ここまでの実装で、以下のような画面になりました。

f:id:hinathy:20160428130031p:plain

SearchBoxの実装

SearchBoxは、入力されたGithubユーザー名を上位コンポーネントであるAppにコールバックします。 ですので、Propとして渡されたonSubmitを、Updateボタンクリック時に呼び出します。

import React from 'react';

class SearchBox extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      username: ''
    };

    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();

    this.props.onSubmit(this.state.username);
  }

  handleChange(e) {
    this.setState({username: e.target.value});
  }

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <input type="text" onChange={this.handleChange} placeholder="Enter username" />
          <input type="submit" value="Update" />
        </form>
      </div>
    );
  }
  
}

SearchBox.PropTypes = {
  onSubmit: React.PropTypes.func.isRequired
};

export default SearchBox;

AppSearchBoxを組み込む

render()で、SearchBoxレンダリングします。 同時に、SearchBoxのUpdateボタンがクリックされた時に呼び出してもらうhandleSubmit()をPropで渡しています。

HTTPクライアントは、superagentを使っていますので、npm install superagent --saveしています。

あと、初期ステートとしてthis.state.repositoriesに与えていたダミーデータは、空の配列にしています。

import React from 'react';
import SearchBox from './search_box';
import RepositoryList from './repository_list';
import request from 'superagent';

class App extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      repositories: []
    };

    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(username) {
    request.get(`https://api.github.com/users/${username}/repos`)
           .end((err, res) => {
             if (res.ok) {
               this.setState({repositories: res.body});
             } else {
               console.error(err);
             }
           });
  }

  render() {
    return (
      <div>
        <SearchBox onSubmit={this.handleSubmit} />
        <RepositoryList repositories={this.state.repositories} />
      </div>
    );
  }
  
}

export default App;

スクリーンキャプチャ

こんな感じです。

gyazo.com

ソースコード

ここまでのコードは、release/0.0.1としてコミットしています。

github.com

Material UIをためす

Material UIを色々試してみます。 react-browserify-rails-seedからSeedプロジェクトをダウンロードして色々試します。

前準備

ダウンロードしたSeedを展開します。

2016/04/21現在、material-uiが依存するreactのバージョンは、14でなければならないみたいなので、package.jsonを下記のように変更しておきます。

diff --git a/package.json b/package.json
index 60b6e7c..300d86a 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,8 @@
   "author": "Toshiyuki HINA",
   "license": "MIT",
   "dependencies": {
-    "react": "^15.0.1",
-    "react-dom": "^15.0.1"
+    "react": "^0.14.0",
+    "react-dom": "^0.14.0"
   },
   "devDependencies": {
     "babel-plugin-add-module-exports": "^0.1.2",

以下のコマンドを実行します。

$ bundle install --path vendor/bundle && npm install

material-uiのインストール

$ npm install material-ui --save

インストール時にUNMET PEER DEPENDENCY react-tap-event-plugin@^0.2.0と怒られますが、tapイベントは使わないので取り合えず気にしないことに。。

最初のコンポーネント

material-uiREADME.mdのUsageに従って作っていきます。

$ bundle exec rails g react:component app
$ bundle exec rails g react:component my_awesome_react_component

app/views/home/index.html.erbで、Appを読み込みます。

<%= react_component 'App' %>

AppコンポーネントMyAwesomeReactComponentのコードはそれぞれ以下。 READMEに記載されているコードをそのままコピーするとエラーになりますね。。(コンポーネントのパスが違う。)

import React from 'react';
import getMuiTheme from 'material-ui/lib/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/lib/MuiThemeProvider';
import MyAwesomeReactComponent from './my_awesome_react_component';

export default class App extends React.Component {

  render() {
    return (
      <MuiThemeProvider muiTheme={getMuiTheme()}>
        <MyAwesomeReactComponent />
      </MuiThemeProvider>
    );
  }
  
}
import React from 'react';
import RaisedButton from 'material-ui/lib/raised-button'

export default class MyAwesomeReactComponent extends React.Component {

  render() {
    return (
      <RaisedButton label="Default" />
    );
  }
  
}

ここまで出来たら、rails sしてブラウザで確認してみます。http://localhost:3000/home/indexにアクセスしましょう。 以下のような画面が表示されたらOKです。

f:id:hinathy:20160421111625p:plain

ソースコード

https://github.com/toshiyukihina/hello-material-ui

browserify-railsとreact-railsでseedプロジェクトを作る

seedプロジェクトを作っておくとなにかと便利。
ということで、browserify-railsreact-railsでモダンな環境をrails上に構築してみます。

browserify-railsを採用したのは、ここの記事に影響ですね。

Railsのプロジェクトを作る

$ rails new react-browserify-rails-seed -T -B

react-railsbrowserify-railsを追加する

react-railsbrowserify-railsGemfileに追加します。

$ vi Gemfile
... (snip) ...
gem 'react-rails'
gem 'browserify-rails'
... (snip) ...

bundle installします。

$ bundle install --path vendor/bundle

Nodeモジュールを追加する

package.jsonを初期化します。入力は適当に。

$ npm init

react-railsをインストールするとReact関連のライブラリもインストールされますが、それは使わず、npmでインストールした方を使います。

$ npm install --save react react-dom
$ npm install --save-dev babelify browserify browserify-incremental reactify babel-preset-es2015 babel-preset-react babel-plugin-add-module-exports

browserify-railsの設定をする

config/initializers/assets.rbに、browserfiy-railsコマンドラインを設定します。

Rails.application.config.browserify_rails.commandline_options = [
  '-t babelify',
  '-t reactify --extension=".js.jsx"'
]

同時に、.bablercにプリセットの設定をしておきます。

{
  "presets": ["es2015", "react"],
  "plugins": ["add-module-exports"] 
}

Reactの環境をインストールする

$ bundle exec rails g react:install

app/assets/javascripts/application.jsapp/assets/javascripts/components.jsをちょっと手直しします。 前述しましたが、react-railsでインストールされたreactは使わないので削除したりしています。

diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 0c09f83..d324746 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -13,7 +13,5 @@
 //= require jquery
 //= require jquery_ujs
 //= require turbolinks
-//= require react
-//= require react_ujs
 //= require components
 //= require_tree .
diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js
index 9ce7a4f..a541d47 100644
--- a/app/assets/javascripts/components.js
+++ b/app/assets/javascripts/components.js
@@ -1 +1,2 @@
-//= require_tree ./components
+//= require_self
+//= require react_ujs

ReactでHello Worldしてみる

ここまでで環境は整ったので、お試しのReactのコンポーネントを作ってみます。

まずは、ReactコンポーネントレンダリングするためのViewを用意します。

$ bundle exec rails g controller home index

Reactのジェネレータを使って、コンポーネントのひな形を用意します。

$ bundle exec rails g react:component hello

app/assets/javascripts/components/hello.js.jsxを手直しします。生成されたコードをES2015で書き換えています。

import React from 'react'

export default class Hello extends React.Component {
  render() {
    return <div>Hello React</div>;
  }
}

Helloコンポーネントにアクセスできるよう、app/assets/javascripts/components.jsからrequireします。 ReactReactDOMをwindowに生やしているのは、react_componentヘルパーからアクセスできるようにするためです。

diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js
index a541d47..3cc9443 100644
--- a/app/assets/javascripts/components.js
+++ b/app/assets/javascripts/components.js
@@ -1,2 +1,6 @@
 //= require_self
 //= require react_ujs
+
+window.React = require('react');
+window.ReactDOM = require('react-dom');
+window.Hello = require('./components/hello');

動作確認してみます。bundle exec rails sして、http://localhost:3000/home/indexにアクセスしてみましょう。

ソースコード

ソースコードは以下です。

github.com

mysqlに巨大なデータを流し込む時によくあるトラブルとその解決方法

はじめに

Webアプリの開発で、運用データを開発環境にインポートすることってよくあることだと思います。 この記事は、そのときに遭遇したトラブルと、その解決方法です。 環境は OS X El Capitan(10.11.4) ですが、Linuxとかでも解決方法は同じだと思われます。

ちなみに、インポートするデータのサイズ(ファイルのサイズ)は、550M程度でした。

トラブル1

トラブル内容

$ mysql -u root -p testdb < testdb.sql
Enter password:****
ERROR 2006 (HY000) at line 190: MySQL server has gone away    

解決方法

max_allowed_packetのサイズを大きくします。 my.cnfに、以下を記述して再起動しましょう。

[mysqld]
max_allowed_packet=100M

トラブル2

トラブル内容

$ mysql -u root -p testdb < testdb.sql
Enter password:****
ERROR 1118 (42000) at line 207: The size of BLOB/TEXT data inserted in one transaction is greater than 10% of redo log size. Increase the redo log size using innodb_log_file_size.

解決方法

innodb_buffer_pool_sizeinnodb_log_file_sizeのサイズを大きくします。

my.cnfに、以下を記述して再起動しましょう。

[mysqld]
innodb_buffer_pool_size = 1024M
innodb_log_file_size=512M

最後に

自分は、上記設定を~/.my.cnfに記述しました。my.cnfは、読み込み順序があるらしいので、お気をつけください。

$ mysql --help | grep my.cnf
                      order of preference, my.cnf, $MYSQL_TCP_PORT,
/etc/my.cnf /etc/mysql/my.cnf /usr/local/etc/my.cnf ~/.my.cnf

Webサービスでも作ろうか

きっかけ

今年も既に2月終盤。仕事に追われてなんか勉強できていないなぁ。 電車の中で、ぼんやりそんなことを考えていて、なんかWebサービスでもつくってみようかなぁと。

あと、ここ数年、Railsを仕事で使っているので、腕試ししたいですしね。

どんなサービスつくろうか?

これが一番大事なんですが、いざ作るとなるとなかなか思い浮かばないっすね(笑)。 思い付きですが、とりあえずチケットの販売価格の比較サービス作ってみます。 ユーザーにとってメリットあるの?って感じなんですが、割と価格差あるんですよね。

例えば、チケット流通センターTicketCampで比べてみます。 佐野元春の2016/03/26 東京国際フォーラムです。

サービス 価格
チケット流通センター 一階 38列60~70番 9,000円
TicketCamp 一階 34列 9,500円

ほぼ同じ席でも一枚500円の差があるんですよね。 この不況時代、少しでも安く入手したいって人多いと思うんですよね。

仕様はどうしようか?

最初はミニマムに。

  • 以下の2つのサービスからデータをスクレイピングする。(徐々に増やしていく予定。)
  • 同じ日、同じ場所の公演の価格を比較して表示する。
  • 価格の比較表示から、サイトへのリンクを張って、購入サイトに飛べるようにする。

これから

ぼちぼちやっていきます。 成果は、このブログやgithubで公開できればなぁと。

あ、あとこのサービス作ろうと思ったきっかけは、以前購入したRubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例が、積読状態でもったいないっていう動機もあります(笑)。

ではよろしくお願いします。

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Railsのプロジェクトを新規作成する手順

はじめに

グローバルな場所には、必要最小限のgemをインストールして、Railsプロジェクトを新しく作成する手順。 いつも忘れるのでメモ。

事前準備

  • rubyのバージョンは、rbenvで管理するので、予めインストールしておきます。
    • rbenvは、READMEを参考にインストールしてください。
    • rubyのインストールは、rbenvのプラグイン ruby-buildを利用するので、こちらもruby-buildのREADMEを参考にインストールします。
  • rbenvでrubyをインストールした後、gem install bundlerで、bundlerをインストールしておきます。
  • mysqlを使うことが多いので、mysql前提で手順を説明します。こちらも予めインストールしておきます。

環境

手順

作業ディレクトリを作る

作業ディレクトリworkを作成し、その中で作業します。

$ mkdir work && cd $_

Gemfileを作る

まずは、railsをインストールするためだけの、Gemfileを作成します。このGemfileは、一時的なものです。

$ cat << EOS > Gemfile
source "http://rubygems.org"
gem "rails", "4.1.0"
EOS

Railsをインストールする

以下のコマンドで、vendor/bundleディレクトリに、Railsと関連するgemがインストールされます。これで、railsのコマンド(rails newとか)を利用することが出来るようになります。

$ bundle install --path vendor/bundle

Railsのプロジェクトを作成する

ここでは、todoという名前のアプリケーションを作成することとします。--skip-bundleオプションをつけてください。--skip-bundleオプションをつけないと、rails newコマンド実行時に、グローバルな場所に、関連gemがインストールされてしまいます。

$ bundle exec rails new todo --skip-bundle --database=mysql

gemのインストール

todoディレクトリに移動し、関連するgemをインストールします。

$ cd todo
$ bundle install --path vendor/bundle

データベースの準備

$ bundle exec rake db:create
$ bundle exec rake db:migrate

Railsを起動する

ここまでで、railsのプロジェクトの作成と準備が完了しましたので、正常に起動するか確認してみます。以下のコマンドを実行した後、ブラウザから http://localhost:3000 にアクセスしてみてください。

$ bundle exec rails s

以下の画面が表示されれば成功です。

f:id:hinathy:20150804175312p:plain

最後に

作成したプロジェクトをgitに登録します。 gitに登録するにあたり、不要ファイルの登録を防ぐために.gitignoreを作りましょう。 .gitignoreの作成には、giboというツールを使います。READMEを参照してインストールしてください。

$ gibo rails > .gitignore
$ git init
$ git add .
$ git commit -m "initial commit"