mountable engineで管理画面を作る

[表参道.rb #17 ~管理画面について語ろう~ - connpass](http://omotesandorb.connpass.com/event/44949/) の発表資料

---
# 自己紹介
* sue445
* ドリコム
  * 高機能雑用（アプリからインフラまで）
  * プリキュアの人
* 主に [Shibuya.rb](https://shibuyarb.doorkeeper.jp/) 辺りに参加
* 【宣伝1】 [sue445 Advent Calendar 2016 - Qiita](http://qiita.com/advent-calendar/2016/sue445)
* 【宣伝2】 [ドリコム Advent Calendar 2016 - Adventar](http://www.adventar.org/calendars/1768)

---
## 最近の仕事
とある社内アプリのリプレイス

* Ruby 2.1.2 -> 2.3.3
* Rails 4.0.0 -> 5.0.0.1
* Resque -> Sidekiq
* capistrano 2系 -> 3系
* Bootstrap 2系 -> 3系
* Debian Lenny [^lenny] -> CentOS 7

[^lenny]: Lennyのサーバなんて古すぎて誰もサワレニー

---
## 最近のトラブル
![image](https://qiita-image-store.s3.amazonaws.com/0/8059/af765d74-db75-bd0a-e558-d8ebaa1e0a5c.png)

---
![image](https://qiita-image-store.s3.amazonaws.com/0/8059/08368f25-d78d-2894-bec8-fad944e57ff0.png)

--- 
# Agenda
* mountable engineとは？
* 2種類のやり方を紹介
  * 1. アプリ本体の管理画面をengineにする
  * 2. gemに管理画面を内包してアプリ本体から使う

---
# mountable engineとは？
* Railsアプリに他のRailsアプリをgemとして組み込む仕組み
  * 例） [RailsAdmin](https://github.com/sferik/rails_admin)
* MVC全部gemとして別アプリ提供することができる
* 長いので以下engineで表記

---
# 2種類のやり方を紹介
1. 普通のRailsアプリの管理画面をengineに切り出す
2. engine gemに管理画面のengineを内包してアプリ本体から使う
  * engine in engine

Rails 5.0.0.1で確認

---
# 1. 普通のRailsアプリの管理画面をengineに切り出す
## ユースケース
* ユーザからのリクエストを受ける本番アプリ（production-app）は複数サーバにデプロイしつつも、本番の管理画面（production-admin）は1サーバだけにデプロイしたい [^production-app]
* しかしproduction-appでは管理画面の機能は組み込みたくないので完全に切り離せるようにしたい
* 管理画面以外にもデバッグ系の機能をengineに切り出したいというケースもある
  * 開発環境では使いたいがステージングや本番だと使いたくない

[^production-app]: ここで書いている production-appやproduction-adminというのはRailsのenvironmentのことではなく、capistranoでデプロイする時のstageのようなものを指しています。

---
## やり方
### rails plugin new admin
トップディレクトリ（ `Gemfile`や `Ralefile`がある場所）で下記を実行するとadminディレクトリが作られる

```shell-session
$ ./bin/rails plugin new admin --skip-bundle --mountable
      create
      create  README.md
      create  Rakefile
      create  admin.gemspec
      create  MIT-LICENSE
      create  .gitignore
      create  Gemfile
      create  app
      create  app/controllers/admin/application_controller.rb
      create  app/helpers/admin/application_helper.rb
      create  app/jobs/admin/application_job.rb
      create  app/mailers/admin/application_mailer.rb
      create  app/models/admin/application_record.rb
      create  app/views/layouts/admin/application.html.erb
      create  app/assets/images/admin
      create  app/assets/images/admin/.keep
      create  config/routes.rb
      create  lib/admin.rb
      create  lib/tasks/admin_tasks.rake
      create  lib/admin/version.rb
      create  lib/admin/engine.rb
      create  app/assets/config/admin_manifest.js
      create  app/assets/stylesheets/admin/application.css
      create  app/assets/javascripts/admin/application.js
      create  bin/rails
      create  test/test_helper.rb
      create  test/admin_test.rb
      append  Rakefile
      create  test/integration/navigation_test.rb
  vendor_app  test/dummy
```

https://github.com/sue445/admin_example/commit/c18a92890b36495a8a796c3af234badd503d572a

---
### admin/admin.gemspec を編集
* gemspecにTODOが残ってると `bundle install` できないので適当に削る
* https://github.com/sue445/admin_example/commit/4d14b4274f61fbcd2a49bf220c713cb160ceb35c

---
### 親アプリのGemfileに組み込む
本体側の `Gemfile` に一行追加し、[^admin]

[^admin]: admin groupを付けているのは、本体とadminを別々に動かす時に本体のデプロイ時にadminを除外するため。（ `capistrano-bundler` なら `bundle_without` にadminを追加）

```ruby:Gemfile
group :admin do
  gem "admin", path: "admin"
end
```

`routes.rb` でmountするだけ。

```ruby:config/routes.rb
mount Admin::Engine => "/admin"
```

https://github.com/sue445/admin_example/commit/79b817b34b70a2a5301c238a2a1f6e5f3ab76d9f

`./bin/rails routes` してadminが見えれば成功

```shell-session
$ ./bin/rails routes
Prefix Verb URI Pattern Controller#Action
 admin      /admin      Admin::Engine

Routes for Admin::Engine:
```

---

## メリット :ok_woman: 
* 管理画面のみで必要な機能をアプリ本体に含める必要がなくなる
  * admin gemを読まなければ管理系の機能は一切使えなくなるのでコード内でif文書くよりもシンプル

---
## 注意点
* 本体がAPI mode の時にengineに一部悪影響が出ることがある
  * 管理画面エンジンのルーティングで、`resources` を `only` や `except` 句を使わずそのまま使っていた場合、`new` と `edit` のルーティングされない
  * API modeの時は `new` と `edit`が不要なのだが、その影響をマウントしたengineも受けてしまう
  * https://github.com/rails/rails/blob/v5.0.0.1/actionpack/lib/action_dispatch/routing/mapper.rb#L1142-L1148
  * アプリ本体をAPI modeｗｐ有効にする場合は `only` でactionを全部指定すれば回避できる

---
# 2. engine gemに管理画面のengineを内包してアプリ本体から使う
## ユースケース
* 1つのgemで「エンドユーザにAPIとして提供するengine」と「管理画面のengine」を提供する
  * 前者でmodelを提供しているとしたら、そのmodelのCRUDを管理画面で提供

---
## やり方
### さっきと同じようにengineを作る
```shell-session
rails plugin new engine_example --skip-bundle --mountable
```

https://github.com/sue445/engine_example/commit/045311540dd0d9561dbe96e86707aea2887f5ba5

---
### gemspecからTODOを削る
* 同じくそのままだと `bundle install` でエラーになるので

https://github.com/sue445/engine_example/commit/f3dd906b11937e21465fc35b19c255c53e5f98fa

---
### engines/admin/ を作った後に下記ファイルを作成
module名はいい感じに変更すること

```ruby:engines/admin/config/routes.rb
EngineExample::Admin::Engine.routes.draw do
end
```

```ruby:engines/admin/lib/engine_example/admin/engine.rb
module EngineExample
  module Admin
    class Engine < ::Rails::Engine
      isolate_namespace EngineExample::Admin
    end
  end
end
```

```ruby:engines/admin/lib/engine_example/admin/railtie.rb
require "engine_example/admin/engine"

module EngineExample
  module Admin
    class Railtie < ::Rails::Railtie
    end
  end
end
```

https://github.com/sue445/engine_example/commit/e2a136186473025f510de72b7ee2f401157ac743

---
### engine本体からadminのengineを読み込む
それぞれ下記を追加

```ruby:engine_example.gemspec
s.require_paths = ["lib", "engines/admin/lib"]
```

```ruby:lib/engine_example/railtie.rb
require "engine_example/admin/railtie"
```

```ruby:lib/engine_example.rb
require "engine_example/railtie"
```

```ruby:test/dummy/config/routes.rb
mount EngineExample::Admin::Engine => "/admin/engine_example"
```

https://github.com/sue445/engine_example/commit/598eb2ec90e19853a5490a0ffbf888e0ed54125b

---
### 動作確認
`./bin/rails app:routes` してadminが見えれば成功 [^dummy]

```shell-session
 $ ./bin/rails app:routes
              Prefix Verb URI Pattern           Controller#Action
      engine_example      /engine_example       EngineExample::Engine
engine_example_admin      /admin/engine_example EngineExample::Admin::Engine

Routes for EngineExample::Engine:


Routes for EngineExample::Admin::Engine:
```

[^dummy]: test/dummy/ 配下のrake taskを呼びたい場合は task名の前に `app:` が付きます。（例：`rails routes` -> `rails app:routes`）

---
### メリット :ok_woman: 
* gem化によって複数アプリでロジックが共通化されていると同時に、管理画面も共通化されていると運用コストが抑えられる
  * 特に複数アプリ運用してる時にCSの学習コストが抑えられる

---
### 開発Tips
* `cd test/dummy/` で `bundle exec rails s` すればdummyのRailsアプリが起動するのでviewの確認が捗る
* 1つのgemに複数のengineを含めるか、それぞれgemを分けるかは適材適所
  * 基本的には一緒の方が管理しやすいが、メインのengineが巨大だと分けた方がいい

---
#### Adminのみ特定の認証をかけたい時（例：devise）
```ruby:confg/routes.rb
Rails.application.routes.draw do
  mount EngineExample::Engine => "/engine_example"

  authenticate :admin_user do
    mount EngineExample::Admin::Engine => "/admin/engine_example"
  end
end
```

* [devise](https://github.com/plataformatec/devise) だと `authenticate` で囲むだけ
* engineの外側に認証を持たせることで、engine側で認証機能を作る必要がなくなる
* 1つのengine内で複数の認証を持たせるのは大変なので、ユーザに提供するengineと管理画面engineとで分けてしまうのが楽

---
# 参考文献
* [Getting Started with Engines — Ruby on Rails Guides](http://guides.rubyonrails.org/engines.html)
  * 日本語訳 [Rails エンジン入門 | Rails ガイド](http://railsguides.jp/engines.html)
* [Mountable Engine だらけの Rails アプリ開発 - onk.ninja](http://blog.onk.ninja/2014/12/02/mountable_engine)
  * rspecを使いたい場合は 「Mountable Engine のテストの書き方」を参照
* [TechRabbit: Rails 4 Engines](http://tech.taskrabbit.com/blog/2014/02/11/rails-4-engines/)
* [Rails3 Recipe Book Gaiden // Speaker Deck](https://speakerdeck.com/a_matsuda/rails3-recipe-book-gaiden)
  * [124枚目](https://speakerdeck.com/a_matsuda/rails3-recipe-book-gaiden#124) の「Engine gemにEngineをmountする」と https://github.com/amatsuda/hocus_pocus が参考になる
* [Rails Engineを使ってAPIと管理画面を分離する - blog.daich.org](http://blog.daich.org/2015/12/18/rails-engine/)