フリーランスのエンジニア道

フリーランスエンジニアの不安を解消するブログ

Rails5.2+React+WebpackerでモダンなWebフロントエンド開発を1時間でキャッチアップしよう

 

ゴール

新しくWebサービスを立ち上げる時に、2018年的なモダンなフロントエンド開発環境をサクッと作れるようになる。今回はRailsメインで、Reactはコンポーネントとして都度使うようなユースケースとして使います。

 

React in Rails的な感じで、SPAの話ではありません。

 

兎にも角にもrails newする

rails new rails5.2-react-webpacker -S -T --skip-turbolinks --database=mysql --webpack=react
  • -S Sproketsをスキップ
  • -T Minitestをスキップ
  • --skip-turbolinks turbolinkをスキップ
  • --database=mysql とりあえずmysqlを使います
  • --webpack=react webpacker+reactを使います

 

formanでサーバー起動設定する

rails sしてbin/webpacker-dev-server起動するのは大変面倒くさいため、foremanを入れます。

Gemfile


group :development do
  gem 'foreman'
end

bundle install

foremanはProcfileを準備する必要があるので作ります

Path: /rails5.2-react-webpacker/Procfile

rails: rails s -p 3000
webpack: bin/webpack-dev-server

bundle exec foreman startでrails/webpackが起動する設定ができました


~/workspace/rails5.2-react-webpacker $ bundle exec foreman start
12:38:49 rails.1   | started with pid 68729
12:38:49 webpack.1 | started with pid 68730
12:38:51 rails.1   | => Booting Puma
12:38:51 rails.1   | => Rails 5.1.6 application starting in development
12:38:51 rails.1   | => Run `rails server -h` for more startup options
12:38:51 webpack.1  10% building modules 2/2 modules 0 active                                      . 12:38:51 webpack.1 | Project is running at http://localhost:3035/
12:38:51 webpack.1 | webpack output is served from /packs/
12:38:51 webpack.1 | Content not from webpack is served from /Users/kitahashiryoichi/workspace/rails5.2-react-webpacker/public/packs
12:38:51 webpack.1 | 404s will fallback to /index.html
    

起動できたかチェック

http://localhost:3000にアクセス

f:id:kitahashi-ryoichi:20180426124117p:plain

 

webpackerでbootstrapを入れてみよう!

これまでのRailsはapp/assets/stylesheetsとかにCSSを入れてきましたが、webpackでJavascript LibraryとCSSを管理することができます。

Webpack · Bootstrap

 

Rails5.1からnpmではなくyarnがデフォルトになりました。npmの上位互換なので特に気にせず使えます。

GitHub - yarnpkg/yarn: 📦🐈 Fast, reliable, and secure dependency management.

yarn add bootstrap
yarn add jquery
yarn add popper.js

Bootstrap V4.1はjqueryへの依存とpopper.jsへの依存があるので、合わせてインストールしています。この辺はチュートリアル通り。

 

Bootstrap.jsの都合上グローバルにjqueryとpopper.jsが居ないと動かない仕様なので、webpackにプラグインとして入れます。 

app/config/webpack/environment.js


const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.prepend(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/dist/jquery',
    jQuery: 'jquery/dist/jquery',
    Popper: 'popper.js/dist/popper'
  })
)
module.exports = environment

 

CSSとJSの読み込み先をwebpackにする

app/views/layouts/application.html.erb

【before】

<%= stylesheet_include_tag 'application' %>
<%= javascript_include_tag 'application' %>

【after】

<%= stylesheet_pack_tag 'application' %>
<%= javascript_pack_tag 'application' %>

 

これでRails+WebpackerでBootstrapまで使えるようになりましたー!

 

webpackerでReactが動く所まで設定する

このままだと諸々Reactは動かないのでいくつかライブラリを入れます。

yarn add webpacker-react

 

各種ライブラリーとbabel-polyfillをimportします。

app/javascript/packs/application.js


import 'babel-polyfill'
import 'stylesheets/application'
import 'bootstrap'
import WebpackerReact from 'webpacker-react'

console.log('Hello World from Webpacker')

 

railsのgenerators設定もついでにしとこう

webpackを使うのでassetsにjs/cssを吐かないように、helperも切っときます。

app/config/application.rb

config.generators do |g|
    g.assets false
    g.helper false
end

 

さっそくCRUD画面作ってみよー

/todos TODO一覧画面をつくりまーす

f:id:kitahashi-ryoichi:20180426134513p:plain

いつも通りtodos_controllerを作って


rails g controller todos

config/routes.rb

Rails.application.routes.draw do
  resources :todos
end

app/controllers/todos_controller.rb

class TodosController < ApplicationController
  def index
    @todos = %w(hoge hage)
  end
end

viewでreact-railsのhelperメソッドreact_componentを使います

app/views/todos/index.html.erb

<%= react_component('Todos', todos: @todos) %>

react_componentがやってるのはデベロッパーツールで見るとこういうこと

f:id:kitahashi-ryoichi:20180426140043p:plain

 

app/javascript/packs/todos.jsx

import React from 'react'

export default class Todos extends React.Component {
  render() {
    const { todos } = this.props
    return (
      <ul className="list-group">
        {todos.map(todo => <li key={todo} className="list-group-item">{todo}</li>)}
      </ul>
    )
  }
}

app/javascript/packs/application.js

import "babel-polyfill"
import 'stylesheets/application'
import 'bootstrap'
import Todos from './todos'
import WebpackerReact from 'webpacker-react'

WebpackerReact.setup({
  Todos
})

console.log('Hello World from Webpacker')

http://localhost:3000/todosにアクセスすると

ドーン! 

f:id:kitahashi-ryoichi:20180426135526p:plain

 

TODO追加機能を作りまーす

今回はモデルを変更するのは割愛して、画面上だけで追加することにしました。

f:id:kitahashi-ryoichi:20180426161345p:plain

app/javascript/packs/todos.jsx

import React from 'react'

export default class Todos extends React.Component {
  constructor(props) {
    super(props)
    this.state = { todos: props.todos }
  }
  addTodo() {
    const { todos } = this.state
    todos.push('追加したよ')
    this.setState({ todos })
  }
  render() {
    const { todos } = this.state
    return (
      <React.Fragment>
        <ul className="list-group">
          {todos.map(todo => <li key={todo} className="list-group-item">{todo}</li>)}
        </ul>
        <button type="button" className="btn btn-primary" onClick={() => this.addTodo()}>
          TODO追加
        </button>
      </React.Fragment>
    )
  }
}

propsをTodosコンポーネントのstateで管理するようにして、addTodoメソッドを生やしました。簡単!

 

onClickの書き方は3種類ありますが、今回はアロー演算子で書いています。 

1. onClick={event => this.addTodo(event)}
2. onClick={this.addTodo.bind(this)}
3. constructorでバインドしておく
constructor(props) {
  super(props)
  this.addTodo = this.addTodo.bind(this)
}

onClick={this.addTodo}

 

Todoを変更できるようにする

Todosは一覧を管理するコンポーネントでTodoは個別のTodoを管理するコンポーネントとして切り出して実装することにします。

app/javascript/packs/todos.jsx

import React from 'react'
import Todo from './components/todo'

export default class Todos extends React.Component {
  constructor(props) {
    super(props)
    this.state = { todos: props.todos }
  }
  addTodo() {
    const { todos } = this.state
    todos.push('追加したよ')
    this.setState({ todos })
  }
  handleUpdateTodo(todo, i) {
    const { todos } = this.state
    todos[i] = todo
    this.setState({ todos })
  }
  render() {
    const { todos } = this.state
    return (
      <React.Fragment>
        <ul className="list-group">
          {todos.map((todo, i) =>
            <Todo
              key={todo}
              todo={todo}
              handleUpdateTodo={todo => this.handleUpdateTodo(todo, i)}
            />
          )}
        </ul>
        <button type="button" className="btn btn-primary" onClick={() => this.addTodo()}>
          TODO追加
        </button>
      </React.Fragment>
    )
  }
}

app/javascript/packs/components/todo.jsx

import React from 'react'

export default class Todo extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = { todo: props.todo, isEdit: false }
  }
  editMode() {
    this.setState({ isEdit: true })
  }
  updateTodo(todo) {
    this.setState({ todo })
  }
  save() {
    const { handleUpdateTodo } = this.props
    const { todo } = this.state
    this.setState({ isEdit: false })
    handleUpdateTodo(todo)
  }
  render() {
    const { todo, isEdit } = this.state
    return (
      <li
        className="list-group-item"
        onClick={() => this.editMode()}
      >
        {isEdit
          ?
          <div className="input-group">
            <input
              type="text"
              defaultValue={todo}
              onChange={e => this.updateTodo(e.target.value)}
              className="form-control"
              autoFocus
            />
            <div className="input-group-append">
              <button
                type="button"
                className="btn btn-outline-secondary"
                onClick={() => this.save()}
              >
                保存
              </button>
            </div>
          </div>
          : todo}
      </li>
    )
  }
}

こんな具合で、TodoのonChangeをトリガーにTodoのstateを更新して、保存のタイミングでTodosから受け取ったhandleUpdateTodoでTodosのstateを更新しています。

 

f:id:kitahashi-ryoichi:20180426165032p:plain

 

今回のソースコードはコチラ

GitHub - kitahashiryoichi/rails5.2-react-webpacker: Rails5.2+React+Webpacker modern frontend tutorial