Viron(ヴァイロン)ってご存知ですか?
2018年2月に公開されたオープンソースのWebアプリケーションで、Swaggerに対応したAPIを書くだけで管理画面とダッシュボードを構築可能です。
本当にAPIを書くだけでいいので、デザインも考えなくて良いしむしろフロント側のコードを書く必要すらなくなります。
そんなVironを半年近く使ってみたので所感をかきます。
cam-inc/viron github
viron ドキュメント
v1.3.0
を使いましたが、一部 rc
について触れます。
Vironとは?
フロントレス
HTML、CSS、JavaScript等のフロント用ソースコードを記述する必要はありません。APIサーバのみを用意して下さい。
APIサーバのレスポンスからCURDとか検索とか基本的なUIは揃った綺麗な管理画面が構築されます。
ボタンを押すと指定のAPI叩くとかできるので、タスク実行とかCSVダウンロードとかUIに関係ない事はほぼできます。
vironサーバーはAPIサーバについて何も知らなくていいので、github pagesでもAWS S3の静的サイトホスティングでも良いのです。
普通のAPI作る工数からOAS準拠とかviron準拠させるために使った時間を抜き出すとそんなに時間はかかってないです。(規模とか複雑さにもよるけど3~5日くらい)
レスポンシブデザイン
デスクトップ端末とモバイル端末上で画面サイズに応じたレイアウトが設定されます。
何も書かずとも綺麗な管理画面ができるし、開発者は機能の作成だけに集中できます。
サービス毎に統一されたUIになるので利用者も嬉しい。
OAS駆動
OpenAPI Specification 2.0に準拠。
APIを記述するためのフォーマットで、Open API Initiativeによって推進されてるオープンな規格であるOpenAPI Specification 2.0に準拠。
OASに則りAPIを記述するために作成されたドキュメントは「OpenAPIドキュメント」と呼ばれ、JSON/YAMLの2つのフォーマットで記述が可能。
OpenAPI Specificationの旧名はSwagger SpecificationでSwaggerとイコールだと捉えていいと思います。
議論されたルールに乗っ取ったAPIを作れるので勉強になる。
無料/オープンソース
GitHub上でオープンソースとして公開されています。誰でも無料で使用できます。
OSSなので機能追加などのアップデートが誰かの貢献のおかげで利用できる。
issues作るなりPR投げるなりみんなで作れる。ドキュメントもOSSなのでtypo見つけたらPR投げよう。
Viron立ち上げてみる
公式のクイックスタートでHTTPS(8080) にフロント、 HTTPS(3000) にAPIサーバーが立ち上がるので気軽に試せるし、デモもあります。
今回はフロント群だけ欲しいのでdockerでビルド用コンテナ作ってビルドしました。
$ git clone -b v1.3.0 git@github.com:cam-inc/viron.git
$ docker build -t viron-builder:v1.3.0 ./viron
$ mkdir dist
$ docker run --rm -v $(pwd)/dist:/viron/_dist viron-builder:v1.3.0 /bin/bash -c "npm run build && cp -RT /viron/dist /viron/_dist"
$ docker rmi viron-builder:v1.3.0
フロント群がdistに作られたのでDockerfileを作って、公開するnginxコンテナを作ります。
FROM nginx:1.15.0
COPY ./dist /usr/share/nginx/html
$ docker build -t viron:v1.3.0 .
$ docker run --rm -p 3000:80 viron:v1.3.0
ブラウザで http://localhost:3000 を開きます。
出てきた画面からswagger.jsonを返すAPIエンドポイントを登録すると管理画面が構築されます。
APIの定義
公式ドキュメントにほぼ書いている事なのですが、必須APIが3つあり、それぞれ以下を定義します。
- swaggerの取得(API定義)
/swagger.json
- 認証方式
/viron_authtype
- グローバルメニューの取得
/viron
swaggerの取得(API定義) /swagger.json
swaggerの作り方は以下が参考になります
定義はなかなか長くなるのでまずは全体像から見ていきます。
全体像
{
swagger: "2.0",
schemes: ["https"],
host: "api.example.com",
info: {
version: "1.0.0",
title: "XXX 管理",
description: "XXX 管理 - Example Management Console"
},
// vironからAPIのリクエストMIMEタイプ
// vironからファイルをAPIで受け取りたいならmultipart/form-dataを追加します
consumes: ["application/json"],
// APIからvironのレスポンスMIMEタイプ
produces: ["application/json"],
paths: {
//APIのエンドポイントとリクエスト方法、レスポンスについて定義
//長いので下のセクションで説明します
},
definitions: {
//レスポンスやパラメータなどを変数定義して使いまわします
//長いので下のセクションで説明します
}
}
paths
必須API 3種と利用したいAPIのエンドポイントとリクエスト方法、レスポンスについて定義します。
以下はuserの基本的なrest apiを定義しています。
{
// 必須API グローバルメニューの取得
/viron: {
get: {
summary: "エンドポイント全体情報取得",
operationId: "viron#index",
responses: {
200: {
description: "ok",
schema: {
// $refはdefinitionsセクションで定義したschemaを利用
$ref: "#/definitions/Viron"
}
}
}
}
}
// 必須API 認証方式の取得
/viron_authtype: {
get: {
summary: "認証方式一覧",
operationId: "viron_authtype#index",
responses: {
200: {
description: "ok",
schema: {
$ref: "#/definitions/VironAuthtype"
}
}
}
}
}
// 必須API swaggerの取得(API定義)
/swagger.json: {
get: {
summary: "swagger.json取得",
operationId: "swagger#index",
responses: {
200: {
description: "ok",
schema: {
type: "object"
}
},
500: {
description: "Internal Server Error"
}
}
}
}
}
// 利用したいAPIを定義する
/api/v1/users: {
// 一覧画面の定義
get: {
x-swagger-router-controller: "users",
summary: "ユーザー 一覧",
description: "ユーザー 一覧",
operationId: "users#index",
produces: "application/json",
tags: ["users"],
// ?limit=1などurlでパラメータを渡したい場合は in: "query"
// 他にもヘッダに埋め込んだり、bodyにjsonで埋め込んだり、urlに埋め込んだりできる
parameters: [
{
name: "limit",
in: "query",
required: false,
type: "integer"
},
{
name: "offset",
in: "query",
required: false,
type: "integer"
},
{
name: "sort",
in: "query",
required: false,
type: "string"
},
{
name: "id",
in: "query",
required: false,
type: "integer"
},
{
name: "name",
in: "query",
required: false,
type: "string"
},
],
responses: {
200: {
description: "ok",
schema: {
// $refはdefinitionsセクションで定義したschemaを利用
// definitions/UserCollectionはUserの複数系として定義している
$ref: "#/definitions/UserCollection"
}
}
}
}
// 作成画面の定義
post: {
x-swagger-router-controller: "users",
summary: "ユーザー 作成",
description: "ユーザー 作成",
operationId: "users#create",
produces: "application/json",
tags: ["users"],
parameters: [
{
// bodyに埋め込んだユーザー情報をパラメータとして送る
name: "payload",
in: "body",
required: true,
schema: {
// definitions/UserBuildはUserの登録パラメータ
$ref: "#/definitions/UserBuild"
}
}
],
responses: {
200: {
description: "ok",
schema: {
// レスポンスはid付きUserオブジェクト
$ref: "#/definitions/User"
}
}
}
}
},
// ユーザーidをurlに埋め込む
/api/v1/users/{id}: {
// 編集画面の定義
put: {
x-swagger-router-controller: "users",
summary: "ユーザー 編集",
description: "ユーザー 編集",
operationId: "ads#update",
produces: "application/json",
tags: "users",
parameters: [
{
// ユーザーidパラメータがpathに埋め込まれてリクエストされる
name: "id",
in: "path",
required: true,
type: "integer"
},
{
// 他のパラメータはbodyに埋め込まれてる
name: "payload",
in: "body",
required: true,
schema: {
$ref: "#/definitions/UserBuild"
}
}
],
responses: {
200: {
description: "ok",
schema: {
$ref: "#/definitions/User"
}
}
}
},
// 削除ボタンの定義
delete: {
x-swagger-router-controller: "users",
summary: "ユーザー 削除",
description: "ユーザー 削除",
operationId: "users#delete",
produces: "application/json",
tags: "users",
parameters: [
{
name: "id",
in: "path",
required: true,
type: "integer"
}
],
responses: {
204: {
description: "no content"
}
}
}
}
}
definitions
レスポンスやパラメータなどを変数定義して使いまわします。
$refを使って展開します $ref: "#/definitions/User"
{
// 認証方法の取得で使ってる定義
VironAuthtype: {
type: "array",
items: {
$ref: "#/definitions/AuthType"
}
}
},
AuthType: {
required: [
"type",
"url",
"method",
"provider"
],
additionalProperties: false,
properties: {
type: {
type: "string"
},
url: {
type: "string"
},
method: {
type: "string"
},
provider: {
type: "string"
}
}
},
// すでに登録されている単数モデル
User: {
type: "object",
required: ["name"],
additionalProperties: false,
properties: {
id: {
type: "integer",
description: "ID",
example: 1
},
name: {
type: "string",
description: "名前",
example: "山田"
}
}
},
// 登録する前の単数モデル
UserBuild: {
type: "object",
required: ["name"],
additionalProperties: false,
properties: {
name: {
type: "string",
description: "名前",
example: "山田"
}
}
},
// #indexで返すモデルのリスト
UserCollection: {
type: "array",
items: {
$ref: "#/definitions/User"
}
}
}
認証方式 /viron_authtype
独自にメールアドレスとパスワードを保存したユーザーテーブル作るとか、Googleログインとか利用できる。
[
// メールアドレスとパスワードによる認証。利用しない場合は削除しても良い
{
type: 'email', // メールアドレスとパスワードによる独自認証を利用する場合のtype
provider: 'viron-example',
url: '/signin', // サインインフォームでsubmitする際のリクエストURL
method: 'POST', // submitする際のリクエストメソッド
},
// GoogleOAuthによる認証。利用しない場合は削除しても良い
{
type: 'oauth', // OAuth認証を利用する場合のtype
provider: 'google', // OAuthを提供するプロバイダ。
url: '/googlesignin',
method: 'POST',
},
// 認証方式ではありませんが、サインアウト時にコールするAPIを定義するために必須です。
{
type: 'signout',
provider: '',
url: '/signout',
method: 'POST',
},
];
グローバルメニューの取得 /viron
ほぼほぼ日本語で説明されてるけど一部説明付け足しました。
{
// エンドポイントに関する情報
"theme": "standard", // カラーテーマ
"color": "white", // ドットカラー
"name": "Viron example - local", // エンドポイントの名称。サービス名や環境など
"tags": [ // エンドポイントに付与するタグ
"local",
"viron",
"example"
],
"thumbnail": "https://avatars3.githubusercontent.com/u/23251378?v=3&s=200", // サムネイル画像URL
// グローバルメニュー定義
"pages": [
{
"section": "dashboard" // 大項目。"dashboard" or "manage"
"group": "", // 中項目。空の場合はsection直下にcomponentsを配置
"id": "quickview", // ページのID。全ページでユニークになっている必要があります
"name": "クイックビュー", // ページ名
"components": [ // メニューからページを選択した際に表示されるコンポーネントの一覧
{
"api": { // コンポーネントに表示する値を取得するためのAPI
"method": "get",
"path": "/stats/dau"
},
"name": "DAU", // コンポーネント名
"style": "number" // コンポーネントスタイル。数字(number)、テーブル(table)の他に各種グラフ(graph-*)が利用できます
},
{
"api": {
"method": "get",
"path": "/users"
},
"name": "MAU",
"style": "table",
// ヘッダに値を入れることでページングを可能にする(X-Pagination-Limitなど、後述します)
"pagination": true,
// 検索パラメータとして送れる値を定義
"query": [
{
key: "id",
type: "integer"
},
{
key: "name",
type: "string",
}
{
// 文字列でリクエストするけど、フォームを日付形式にする
key: "created_at",
type: "string",
format: "date-time"
},
],
// ソート可能にする v1.4.0でリリース予定? 現状rcブランチで使えます
// APIにはsortパラメータにコロンとカンマで区切った形式で渡される('id:asc,name:desc')
sort: [
"id",
"name"
],
},
],
},
{
"section": "manage"
"group": "user",
"id": "user",
"name": "ユーザー",
"components": [
{
"api": {
"method": "get",
"path": "/user"
},
"name": "ユーザー",
"style": "table",
"primary": "id", // テーブルデータの主キーにあたるフィールド
"pagination": true, // テーブルスタイルのページャーを有効にするフラグ
"query": [ // テーブルスタイルの検索フィールドを指定
{
"key": "name",
"type": "string"
}
],
"table_labels": [ // テーブルスタイルの見出しにするフィールドを指定
"id",
"name"
],
"actions": [ // 他APIの関連付けが必要な場合は指定。詳細は下記[API間の関連付け]を参照
"/user/download/csv"
]
}
],
},
],
}
ruby on railsでAPI実装する
ruby on railsでAPI実装する場合のだいたいの流れを書きます。
ルーティング
必須API3つと管理画面構築に必要なAPIをルーティングします。
# config/routes.rb
Rails.application.routes.draw do
resource :viron, only: %i[show], format: false
resource :viron_authtype, only: %i[show], format: false
resource :swagger, only: %i[show], constraints: lambda { |req| req.format == :json }
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :users
end
end
end
cors設定
CORS(Cross-Origin Resource Sharing)するための設定を書く必要があります。
Webサイトが持つ情報が別の悪意あるWebサイトに悪用されるのを防ぐために、Same-Origin Policy(日本語では同一生成元ポリシー)が適用されます。
クライアント(ブラウザ)から見知らぬドメイン当てにリクエストを行うことになるのでブラウザがブロックする(親切)ので事前に許可する必要がある。
gem rack-corsがよしなに Access-Control-Allow-xxx
や Access-Control-Expose-xxx
ヘッダをつけてくれます。
# Gemfile
gem 'rack-cors'
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ['viron.example.com'] # 許可するhost
resource '/api/*', headers: :any, methods: [:get, :post, :delete, :put, :options], expose: [
'Origin', 'Content-Type', 'Content-Disposition', 'Accept', 'Authorization',
'X-Requested-With', 'X-Authorization',
'X-Pagination-Limit', 'X-Pagination-Total-Pages', 'X-Pagination-Current-Page',
]
resource '/viron', headers: :any, methods: [:get]
resource '/viron_authtype', headers: :any, methods: [:get]
resource '/swagger.json', headers: :any, methods: [:get]
end
end
swagger定義
swagger-docsというgemはルーティング(やたぶんモデル)を見て動的にswagger定義を書いてくれる!!!のですが、
I don't currently have any plans to add support for v2.0 at this time due to time constraints
v2.0のサポートを追加する予定はありませんとのことなのである程度楽にかける swagger-blocks
を利用しました。
gem 'swagger-blocks'
あとは以下を参考に定義をもくもくと書いていきます。
管理画面は設定ファイルぐらいシンプルに作れるべき!『Viron』を使ってみました
ページネーション対応
グローバルメニューの取得(/viron
)でページネーションを有効にしていて、ページ総数や現在のページ数などを指定ヘッダにいれてレスポンスを返すとページネーションしてくれます。
response.headers['X-Pagination-Limit'] = @limit
response.headers['X-Pagination-Total-Pages'] = (@users.unscope(:limit).unscope(:offset).count / @limit.to_f).ceil
response.headers['X-Pagination-Current-Page'] = @offset / @limit + 1
ソート
v1.4.0でリリース予定?、rcブランチで使える機能ですが、
グローバルメニューの取得(/viron
)でソートする列を指定していればソート可能になります。
リクエストに id:asc,name:desc
のような形式で送られてくるのでよしなに処理します。
def parse_order query
query
.split(',')
.map{|v| v.split(':') }
.select{|v| v.size === 2 && v[0].present? && v[1].present? && ['desc', 'asc'].include?(v[1]) }
.map{|v| [v[0], v[1]] }
.to_h
end
params[:sort]
=> 'id:asc,name:desc'
order_params = parse_order(params[:sort])
=> {"id"=>"asc", "name"=>"desc"}
users.order!(parse_order(params[:sort])) if params[:sort].present?
controller
だいたいこんな感じになりました。
# 雰囲気users_controller.rb
class Api::V1::UsersController < ApplicationController
before_action :set_limit_offset, only: [:index]
before_action :set_users, only: [:index]
before_action :set_user, only: [:create, :show, :update, :destroy]
before_action :set_pagination_headers, only: [:index]
def index
render json: @users
end
def show
render json: @user
end
def create
if @user.save
render json: @user, status: :created
else
render json: @user.errors, status: :unprocessable_entity
end
end
def update
if @user.update(update_params)
render json: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end
def destroy
@user.destroy
end
private
def set_limit_offset
@limit = params[:limit]&.to_i || 100
@offset = params[:offset]&.to_i || 0
end
def set_users
@users = User.all.limit(@limit).offset(@offset)
@users.order!(parse_order(params[:sort])) if params[:sort].present?
@users.where!(search_params) if search_params.present?
end
def set_pagination_headers
response.headers['X-Pagination-Limit'] = @limit
response.headers['X-Pagination-Total-Pages'] = (@users.unscope(:limit).unscope(:offset).count / @limit.to_f).ceil
response.headers['X-Pagination-Current-Page'] = @offset / @limit + 1
end
def search_params
params.permit(:id, :name)
.select {|key, value| value.present? }
end
def update_params
params.require(:user).permit(:name)
end
end
所感
いいところ
表のみならずグラフ表示やダッシュボードもあるのでBIツールにもなり得そうです。
API書くだけでいいので、ユースケースに合えば(ドラッグしたりUIゴリゴリじゃなければ)かなりの速度で開発出来ます。
actionを定義出来る(パラメータ指定してCRUD以外にもAPI叩ける)のでCSVダウンロードしたりバッチ起動したりとか柔軟に機能を追加出来ます。
swaggerはもともとAPI仕様を作ったり、テストも出来る便利な規格でしたが、Vironで管理画面も自動構築出来てしまうので1石3鳥です。
とはいえ
日付の入力フォームが(todayやnowなど補完はしてくれるが)文字列なので厳しいです。これに関しては DatePicker UI Component #269 で進んでいるようなので今後に期待します。
ソートはまだ正式リリースされていないです。次のバージョンでリリースされるのかな。Table filter #341
オートコンプリートに関しては、補完はしてくれるのですが、表示する値(名前など)とパラメータにする値(IDなど)を分ける事が出来ず、これも今後に期待です。
1レコードの編集に2click以上(歯車押して編集押す or レコード押して表示して編集押す)なのも地味にきます。
ほんとに書かなくなるのでたまにjs書きたくなる。
さいごに
ユースケース確認して要件合えば今後も使いたいと思います。
導入に時間はかからないので試してみてはどうでしょうか?