YQLを利用してRSSを取得する

はてブのホットエントリーをJSON形式で取得してReduxを利用したクライアントで表示してみます。

完成品

f:id:ktdk:20160802104044g:plain

(左に寄っちゃってますが...)

YQL (Yahoo! Query Console)

CORSによるクロスドメイン制限があるため、Ajaxで直接RSSを取得することはできません。
PHP等のサーバーサイドでRSSを取得してクライアントに表示する必要があります。

今回はサーバーの実装をしないので、YQLを利用します。

developer.yahoo.com

こんな感じでSQLのようにクエリを実行すると結果が返ってきます。

f:id:ktdk:20160802102122p:plain

最下部にあるTHE REST QUERYcurlで叩いてみると、RSSJSON形式で取得できます。

$ curl -X GET "https://query.yahooapis.com/v1/public/yql?q=SELECT%20*%20FROM%20rss%20WHERE%20url%20%3D%20'http%3A%2F%2Fb.hatena.ne.jp%2Fhotentry.rss'&format=json&diagnostics=true&callback="

{"query":{"count":30,"created":"2016-08-02T01:23:13Z","lang":"en-US","diagnostics":{"publiclyCallable":"true","url":{"execution-start-time":"1","execution-stop-time":"452","execution-time":"451","content":"http://b.hatena.ne.jp/hotentry.rss"},"user-time":"454","service-time":"451","build-version":"0.2.39"},"results":{"item":[{"about":"http://www.okinawatimes.co.jp/articles/-/55298","title":"スク水揚げ 奥武島で今年も大漁 | 沖縄タイムス+プラス ニュース","link":"http://www.okinawatimes.co.jp/articles/-/55298","description":"【南城】南城市の奥武島でスク漁が始まり、島の海人が
...

Fetch API

RSSを取得するだけなのでsuperagent等のモジュールではなくFetch APIを利用してみます。

Fetch Standard (日本語訳)

redux-actions、redux-promiseと組み合わせる

fetch()はPromiseを返すのでredux-promiseにそのまま渡します。
then()でreturnした結果がreducerのnextのaction.payloadに渡されます。

// api/bookmarks.js
import querystring from 'querystring'
import { getRssUrl } from '../constants/ApiCategory'

const YQL_URL = "https://query.yahooapis.com/v1/public/yql"

const defaultParams = {
  format: 'json',
  diagnotistics: true
}

export default {
  fetchBookmarks: (category) => {
    const sql = `SELECT * FROM rss WHERE url = '${getRssUrl(category)}'`
    const data = Object.assign({}, defaultParams, { q: sql })
    const url = `${YQL_URL}?${querystring.stringify(data)}`

    // Promiseを返す
    return fetch(url)
      .then((res) => {
        return res.json()
      })
      .catch((err) => {
        console.log(err)
      })
  }
}
// actions/api.js
import { createAction } from 'redux-actions'
import * as types from '../constants/ApiActions'
import api from '../api/bookmarks'

export const fetchBookmarks =
  createAction(types.FETCH_BOOKMARKS, api.fetchBookmarks)
// reducers/bookmarks.js
const bookmarks = handleActions({
  [types.FETCH_BOOKMARKS]: {
    next: (state, action) => {
      let items = action.payload.query.results.item
      return items.map(b => bookmark(b, action))
    },

    throw: (state, action) => {
      return []
    }
  }
}, [])

今回は1つのstateを全タブで共有しているためクリック毎にrequestが飛んでしまいますが、
カテゴリ(タブ)ごとにstate持たせて管理するとrequest回数も減らせてサクサク動きますかね。

ソースコード

github.com

How to build Rails5 API + Redux ToDo Application その4

前回はクライアントをRedux化(API叩かない版)しました。

kogoto.hatenablog.com

これにAPIを叩かせて完成とします。

github.com

CORS

実装前に、クロスドメインの問題があるので、Railsのほうでごにょごにょします。

# config/application.rb
module Rails5ReactReduxTodo
  class Application < Rails::Application

    # ...

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :patch, :delete]
      end
    end
  end
end

SuperAgent

AJAXしたいだけなのでjQueryではなくHTTPリクエストに特化したライブラリを利用します。

github.com

$ npm install --save superagent

まず、Railsサーバーを起動してREPLからAPIを叩いてみます。

サーバーを起動

$ rails s

REPLから実行

$ node

get: ToDO一覧を取得

> request = require('superagent')
> request.get('http://localhost:3000/todos').end(function(err, res) { console.log(res.body) })
> [ { id: 1,
    title: '単2電池を買いに行く',
    completed: false,
    order: 1,
    created_at: '2016-07-29T00:20:23.797Z',
    updated_at: '2016-07-29T00:20:23.797Z' } ]

post: ToDOを登録

> request.post('http://localhost:3000/todos').send({ todo: { title: 'ストレッチ', order: 1 }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  title: 'ストレッチ',
  completed: null,
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:23:17.983Z' }

patch: ToDOを完了状態に更新

> request.patch('http://localhost:3000/todos/2').send({ todo: { completed: true }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  completed: true,
  title: 'ストレッチ',
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:25:39.456Z' }

delete: ToDOを削除

> request.del('http://localhost:3000/todos/2').end(function(err, res) { console.log(res.body) })
> {}

こんな感じでWeb APIにアクセスできます。

redux-promise

redux-promiseはcreateActionにPromiseを渡すとよしなにしてくれるMiddlewareです。
redux-actionsと併せて使います。

まずはPromiseを返すWeb APIを作成します。
以下はToDO一覧を取得するAPIの場合です。

// api/todos.js
export default {
  fetchTodos: () => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .end((err, res) => {
          if (!res || res.status === 404) {
            reject()
          } else {
            resolve(res.body)
          }
        })
    })
  },
  /* ... */
}

Action作成時にこの関数を渡します。

// actions/api.js
export const fetchTodos = createAction(ApiAction.FETCH_TODOS, api.fetchTodos)

Actionのハンドリングをします。
nextのaction.payloadにはresolveの引数であるbody(todos)が格納されています。

// reducers/todos.js
const todos = handleActions({
  [ApiAction.FETCH_TODOS]: {
    next: (state, action) => action.payload,
    throw: (state, aciton) => []
  },
  /* ... */
}, [])

このActionをcomponentDidMount時に呼び出します。

// App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actionCreators from './actions/api'

export default class App extends Component {
  componentDidMount() {
    this.handleFetchTodos()
  }

  handleFetchTodos() {
    this.props.fetchTodos()
  }
  /* ... */
}

App = connect(null, actionCreators)(App)

export default App

これを実行しredux-loggerを利用してActionのログを見てみます。

f:id:ktdk:20160730125821p:plain

自前でログを仕込んでみたところrequest, responseの間に1回目のFETCH_TODOSが発行されています。
(おそらくこの部分はredux-promiseが処理してくれている)

で、resolveのタイミングで2回目のFETCH_TODOSが発行されhandleActionsのnextがコールされます。

これで非同期でAPIを叩いて動くToDOアプリケーションの完成です。

つぎは何作ろっかなー

How to build Rails5 API + Redux ToDo Application その3

前回はReactでToDOクライアントを実装しました。

kogoto.hatenablog.com

で、これをRedux化してみます。

いきなりAPI叩くとややっこしいので、まずはクライアント単独で動作するものを作成してみます。

ソースコードはこちら。

github.com

create-react-app使ってるので、clientの下でnpm installしてから

$ npm start

ってやるとブラウザが起動して実行開始されます。

モジュールのインストール

$ npm install --save redux react-redux redux-actions

ディレクトリの再構成

%RAILS_ROOT%にclientフォルダを作成しそこにプロジェクトを配置します。
また、Reduxの作法にしたがったディレクトリ構成にします。

src
├── App.js
├── actions
│   └── index.js
├── components
│   └── Todo.js
├── constants
│   └── ActionTypes.js
├── containers
│   ├── TodoForm.js
│   └── TodoList.js
├── index.js
├── reducers
│   ├── form.js
│   ├── index.js
│   └── todos.js
└── store
    └── index.js

Actionの洗い出し

今回のToDOで発生するActionをリストアップします。

  • 優先順位変更
  • タイトル入力
  • 登録処理
  • 削除処理
  • 完了⇔未完了の切り替え

これを元にActionを生成します。

// constants/ActionTypes.js

export const CHANGE_ORDER = 'CHANGE_ORDER'
export const CHANGE_TITLE = 'CHANGE_TITLE'
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
// actions/index.js

import { createAction } from 'redux-actions'
import * as types from '../constants/ActionTypes'

export const changeOrder = createAction(types.CHANGE_ORDER)
export const changeTitle = createAction(types.CHANGE_TITLE)
export const addTodo = createAction(types.ADD_TODO)
export const deleteTodo = createAction(types.DELETE_TODO)
export const toggleTodo = createAction(types.TOGGLE_TODO)

Stateの洗い出し

各コンテナ・コンポーネントがもつStateをリストアップします。

TodoForm

  • 入力中のタイトル
  • 入力中の優先順位
  • 登録ボタンの有効/無効状態

TodoList

  • 登録されたToDO一覧

Todo

  • タイトル
  • 優先順位
  • 完了/未完了状態

で、これを元にReducerを作成します。

// reducers/form.js

const initialState = {
  title: '',
  order: 1,
  completed: false,
  disabled: true
}

const form = handleActions({
  /* ... */
}, initialState)

export default form
// reducers/todos.js

const initialState = {
  title: '',
  order: 1,
  completed: false
}

const todo = handleActions({
  /* ... */
}, initialState)

const todos = handleActions({
  /* ... */
}, [])

export default todos

これらをcombineReducersでくっつけてstoreとします。

と書いてるとだんだんややこしくなってくるので、図にしてみました。

f:id:ktdk:20160728182352p:plain

ユーザーからの操作でReducerに対してActionが発行され、Stateが更新されます。
更新されたStateで再描画される、ってなわけです。

ひとまずRedux方式になったので、次回はこれにAPIを叩かせたいと思います。

How to build Rails5 API + Redux ToDo Application その2

前回はRails5でToDOリストのREST APIを実装しました。

kogoto.hatenablog.com

今回はクライアント側の実装をしてみます。

イメージ

f:id:ktdk:20160126104936g:plain

言語・ツール

  • ES6 (ES2015)
  • React
  • gulp
  • webpack
.
├── components
│   ├── todo-box.js
│   ├── todo-form.js
│   ├── todo-list.js
│   └── todo.js
└── main.js

コンポーネントの構成

f:id:ktdk:20160126094819p:plain

各パーツをコンポーネント化していきます。

実装してみる

まずTodoから。StateとしてCompleted(完了/未完了)状態を持っています。
完了時にバックカラーが変更され削除時には列ごと消えます。

// components/todo.js

import React, { Component } from 'react'

export default class Todo extends Component {
  constructor(props) {
    super(props)
    this.state = { completed: props.completed }
  }

  rawMarkup() {
    var rawMarkup = marked(this.props.children.toString(), { sanitize : true })
    return { __html: rawMarkup }
  }

  render() {
    return (
      <tr className={this.state.completed ? "success": ""}>
        <td>
          <input
            type="checkbox"
            onChange={(e) => this.props.onTodoCompleted(this, e.target.checked)}
            checked={this.state.completed ? "checked": ""}
          />
        </td>
        <td>{this.props.order}</td>
        <td><span dangerouslySetInnerHTML={this.rawMarkup()} /></td>
        <td>
          <input
            className="btn btn-danger"
            type="button"
            onClick={() => this.props.onTodoDelete(this.props.id)}
            value="削除"
          />
        </td>
      </tr>
    )
  }
}

Todoを管理するTodoListを作成します。

// components/todo-list.js

import React, { Component } from 'react'
import Todo from './todo'

export default class TodoList extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    let todoNodes = this.props.data.map(todo => {
      return (
        <Todo
          url={this.props.url}
          completed={todo.completed}
          order={todo.order}
          id={todo.id}
          key={todo.id}
          onTodoDelete={this.props.onTodoDelete}
          onTodoCompleted={this.props.onTodoCompleted}
        >
          {todo.title}
        </Todo>
      )
    })

    return (
      <div className="row">
        <table className="todoList table table-striped">
          <thead>
            <tr>
              <th width="10%">完了</th>
              <th width="20%">優先順位</th>
              <th>やること</th>
              <th width="20%"></th>
            </tr>
          </thead>
          <tbody>
            {todoNodes}
          </tbody>
        </table>
      </div>
    )
  }
}

次にTodoFormです。優先順位とTodoを入力して登録する普通のフォームです。

// components/todo-form.js

import React, { Component } from 'react'

export default class TodoForm extends Component {
  constructor(props) {
    super(props)
    this.state = { order: 1, title: '' }
  }

  handleOrderChange(e) {
    this.setState({ order: e.target.value })
  }

  handleTitleChange(e) {
    this.setState({ title: e.target.value })
  }

  handleSubmit(e) {
    e.preventDefault()
    let order = this.state.order
    let title = this.state.title.trim()

    if (!order || !title) {
      return
    }

    this.props.onTodoSubmit({ order: order, title: title, completed: false })
    this.setState({ order: 1, title: '' })
  }

  render() {
    return (
      <div className="row">
        <form className="todoForm form-horizontal" onSubmit={this.handleSubmit.bind(this)}>
          <div className="form-group col-md-2">
            <input
              id="todo-order"
              className="form-control"
              type="number"
              value={this.state.order}
              onChange={this.handleOrderChange.bind(this)}
            />
          </div>
          <div className="form-group col-md-8">
            <input
              id="todo-title"
              className="form-control"
              type="text"
              value={this.state.title}
              onChange={this.handleTitleChange.bind(this)}
            />
          </div>
          <div className="col-md-2">
            <input className="btn btn-primary" type="submit" value="登録" />
          </div>
        </form>
      </div>
    )
  }
}

んでこれらを管理するTodoBoxを作成します。
GET, POST, PATCH, DELETE処理はすべてここに実装しています。
これらを下位のコンポーネントに引き渡して状態を更新していく流れです。

import React, { Component } from 'react'
import TodoList from './todo-list'
import TodoForm from  './todo-form'

export default class TodoBox extends Component {
  constructor(props) {
    super(props)
    this.state = { data: [] }
  }

  loadTodosFromServer() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: (data) => {
        this.setState({ data: data })
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoSubmit(todo) {
    todo.id = Date.now()

    let todos = this.state.data
    let newTodos = todos.concat([todo])
    this.setState({ data: newTodos })

    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: { todo: todo },
      success: (data) => {
        this.loadTodosFromServer()
      },
      error: (xhr, status, err) => {
        this.setState({ data: todos })
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoCompleted(todo, completed) {
    $.ajax({
      url: this.props.url + '/' + todo.props.id,
      dataType: 'json',
      method: 'PATCH',
      data: { todo: { completed: completed } },
      success: (data) => {
        todo.setState({ completed: data.completed })
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoDelete(id) {
    $.ajax({
      url: this.props.url + '/' + id,
      dataType: 'json',
      method: 'DELETE',
      success: (data) => {
        this.loadTodosFromServer()
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  componentDidMount() {
    this.loadTodosFromServer()
    // setInterval(this.loadTodosFromServer.bind(this), this.props.pollInterval)
  }

  render() {
    return (
      <div className="todoBox">
        <h1>Todoリスト</h1>
        <TodoForm onTodoSubmit={this.handleTodoSubmit.bind(this)} />
        <TodoList
          data={this.state.data}
          url="/todos"
          onTodoCompleted={this.handleTodoCompleted.bind(this)}
          onTodoDelete={this.handleTodoDelete.bind(this)}
        />
      </div>
    )
  }
}

エントリポイントでこれらのコンポーネントを読み込みます。

import React      from 'react'
import { render } from 'react-dom'
import TodoBox    from './components/todo-box'

let content = document.getElementById('todos')

render(
  <TodoBox url="/todos" pollInterval={2000} />,
  content
)

webpackしたものをHTMLに組み込んで完了です。

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Rail5 + React Todo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
  </head>
  <body>
    <div id="todos" class="container"></div>
    <script type="text/javascript" src="dist/bundle.js"></script>
  </body>
</html>

ReactはあくまでUIに特化したライブラリなので、正直Ajax周りの処理をどこに書いてどうしたらいいかってのが全然わかりませんでした。

これをRedux化するとどうなるんでしょうかね。続きは次回。

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

Hello, Mix!

Mixとは

Elixirのプロジェクト管理ツールです。

プロジェクトの作成

$ mix new hello_world

* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/hello_world.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_world_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd hello_world
    mix test

Run "mix help" for more commands.
  • README.md
    • プロジェクトの説明
  • .gitignore
    • gitの管理下に置かないファイルの定義
  • mix.exs
    • プロジェクトの設定
  • config/*
    • アプリケーションの設定
  • lib/*
    • ライブラリ
  • test/*

メインモジュールの作成

プロジェクトが作成されたので、プロジェクトのディレクリに移動してビルドしてみます。

$ cd hello_world
$ mix escript.build

Compiled lib/hello_world.ex
Generated hello_world app
Consolidated List.Chars
Consolidated Collectable
Consolidated String.Chars
Consolidated Enumerable
Consolidated IEx.Info
Consolidated Inspect
** (Mix) Could not generate escript, please set :main_module in your project configuration (under :escript option) to a module that implements main/1

「プロジェクト設定の:escriptオプションにmain関数を実装している:main_moduleを指定しろ」 とのことなので、そのようにします。

# ./mix.exs

defmodule HelloWorld.Mixfile do
  use Mix.Project

  def project do
    [app: :hello_world,
     version: "0.0.1",
     elixir: "~> 1.2-rc",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  # ... 以下略
end
# ./lib/hello_world.exs

defmodule HelloWorld do

  def main(args) do
    IO.puts "Hello, World!!"
  end

end

ビルド&実行

メインモジュールを追記後、再度ビルドします。

$ cd hello_world
$ mix escript.build

Compiled lib/hello_world.ex
Generated hello_world app
Consolidated List.Chars
Consolidated Collectable
Consolidated String.Chars
Consolidated Enumerable
Consolidated IEx.Info
Consolidated Inspect
Generated escript hello_world with MIX_ENV=dev

ビルドに成功すると直下にバイナリが作成されるので実行してみます。

$ ./hello_world

Hello, world!

Programming Elixir: Functional, Concurrent, Pragmatic, Fun

Programming Elixir: Functional, Concurrent, Pragmatic, Fun

Ansible入門

Ansibleとは

Chef, Puppetと同様の構成管理ツール

インストール

$ git clone git://github.com/ansible/ansible.git --recursive
$ cd ./ansible
$ source ./hacking/env-setup
$ ansible
Usage: ansible <host-pattern> [options]

Options:
  -a MODULE_ARGS, --args=MODULE_ARGS
                        module arguments
  --ask-become-pass     ask for privilege escalation password
...

リモートホストの管理

※事前にVagrant等でSSHログインできるホストを用意しておく

Inventoryの作成

$ vim ~/ansible_hosts

[webservers]
192.168.33.100 ansible_user=vagrant ansible_ssh_pass=vagrant

疎通確認

$ ansible all -m ping -i ~/ansible_hosts

192.168.33.100 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

しゃべらせてみる

$ ansible all -a "/bin/echo hello" -i ~/ansible_hosts

192.168.33.100 | SUCCESS | rc=0 >>
hello

リモートホストの構成

ex.) Apache

Playbook

# httpd.yml

---
- hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  become: true
  tasks:
  - name: ensure apache is at the latest version
    yum:
      name: httpd
      state: latest
  - name: ensure apache is running (and enable it at boot)
    service:
      name: httpd
      state: started
      enabled: yes
  handlers:
    - name: restart apache
      service:
        name: httpd
        state: restarted

実行

$ ansible-playbook -i ~/ansible_hosts httpd.yml

PLAY ***************************************************************************

TASK [setup] *******************************************************************
ok: [192.168.33.100]

TASK [ensure apache is at the latest version] **********************************
changed: [192.168.33.100]

TASK [ensure apache is running (and enable it at boot)] ************************
changed: [192.168.33.100]

PLAY RECAP *********************************************************************
192.168.33.100             : ok=3    changed=2    unreachable=0    failed=0

入門Ansible

入門Ansible