読者です 読者をやめる 読者になる 読者になる

Tech memo

日々学んだ技術のびぼうろく

Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA つなぎこみ編

Lumen Riot.js PHP Javascript

はじめに

最近 Riot が気になっていたので、
Laravel製軽量フレームワーク Lumen 5.4 と Riot 3.3 で簡単なブログを作ってみることにした。

今回はいよいよフロントエンドとバックエンドをつなぎこむ。 フロントエンドの ajax まわりを実装していく。

他のエントリーはこちら。

www.yjhm214.com

www.yjhm214.com

スポンサーリンク

作るもの

いろいろ頑張って最終的にこんな感じのブログを作る。

  • 記事一覧の表示(カテゴリーの絞り込みあり)
  • 記事詳細の表示
  • 記事の新規作成・更新

記事一覧
f:id:yjhm214:20170325170558p:plain

記事詳細
f:id:yjhm214:20170325170608p:plain

記事編集
f:id:yjhm214:20170325170616p:plain

記事保存 f:id:yjhm214:20170325170621p:plain

前提

  • Lumen 5.4.5
  • Riot.js 3.3.1

手順

  1. ナビゲーション画面(ajax でカテゴリー一覧を取得)
  2. 一覧画面(ajax で記事一覧を取得)
  3. 詳細画面(ajax で記事詳細を取得)
  4. 編集画面(ajax で新規作成・更新処理)

1. ナビゲーション

ナビゲーションに表示カテゴリーをマスターデータから取ってくる。
カスタムタグの <script> 部分を変更する。

navbar.tag

  <script>
    // this.categories = [
    //   {id: 1, name: 'Tech'},
    //   {id: 2, name: 'Book'},
    //   {id: 3, name: 'Hobby'},
    //   {id: 4, name: 'Others'},
    // ]
    // console.log(this.categories)

    // ajax 呼び出しの中では、`this` が Riot のタグインスタンスではなく、ajax のレスポンスオブジェクトを参照してしまうため、`this` を `self` に代入。
    var self = this

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/categories',
      data : {}
    })
    .done(function(res){
      self.categories = res
      // テンプレート変数と子要素を更新
      self.update()
    })
    .fail(function(){
      // bootstrap modal を表示
      $("#connection-error-dialog").modal();
      // bootstrap modal が閉じた時のイベント
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET categories')
    })

  </script>

ポイント

  • var self = this とすることで Riot のタグインスタンスを ajax 内でも使えるようにする。
  • self.update()で現在のタグインスタンス上のすべてのテンプレート変数を更新する。(参考)
  • $("#connection-error-dialog").modal(); は Bootstrap の modal 関数
  • $('#connection-error-dialog').on('hidden.bs.modal', function (e) {}); も同じく Bootstrap の modalイベント。

2. 一覧画面

list.tag

  <script>
  
    // this.list = [
    //   {
    //     id: 1,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Tech', 'Book'],
    //   },
    //   {
    //     id: 2,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Book', 'Hobby', 'Others']
    //   },
    //   {
    //     id: 3,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Hobby', 'Others']
    //   },
    //   {
    //     id: 4,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Others']
    //   },
    // ];
    // console.log(this.list)


    var self = this

    // opts 変数の中にルータから渡されたオブジェクトが格納されている
    console.log(opts)
    var params = {}
    if (opts.categoryName) params.category = opts.categoryName

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/posts',
      data : params
    })
    .done(function(res){
      self.list = res

      for (var i=0; i<self.list.length; i++) {
        // タイトルが60文字以上だった場合省略
        if (self.list[i].title.length >= 60) {
          self.list[i].title = self.list[i].title.slice(0, 60) + '...'
        }

        // テキストが100文字以上だった場合省略
        if (self.list[i].text.length >= 100) {
          self.list[i].text = self.list[i].text.slice(0, 100) + '...'
        }
        self.list[i].title = self.list[i].title.replace(/(\r\n)|\n|\r/g, "<br />")
        self.list[i].text  = self.list[i].text.replace(/(\r\n)|\n|\r/g, "<br />")
      }

      self.update()

      // ajax 終了前にタグがマウントされてしまうため、
      // タグを再マウントして描画しなおす。
      riot.mount('raw')

    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET posts.')
    });
  </script>

ポイント

  • ルータから渡された変数は opts でアクセスできる。ここでは、カテゴリーでソートされたときのカテゴリー名を取得する。
  • テンプレート内にある カスタムタグ<raw>は、ajax 通信が終わる前にマウントされてしまうため、ajax 通信が終わったあとにriot.mount('raw') で再マウントすることで描画し直している。

3. 詳細画面

ここは一覧画面とほぼ同じ。

post.tag

  <script>
  
    // this.id         = 1
    // this.title      = 'Hello, world!'
    // this.text       = 'Some quick example text to build<br/> on the card title and make up the bulk of the cards content.'
    // this.categories = ['Tech', 'Book']

    var self = this

    console.log(opts)
    var id = opts.id

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/posts/' + id,
      data : {}
    })
    .done(function(res){
      self.id         = res.id;
      self.title      = res.title.replace(/(\r\n)|\n|\r/g, "<br />")
      self.text       = res.text.replace(/(\r\n)|\n|\r/g, "<br />")
      self.categories = res.categories

      self.update()

      riot.mount('raw')
    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET post.')
    });
  </script>

4. 編集画面

edit.tag

  <script>
    // this.categories = [
    //   {id: 1, name: 'Tech'},
    //   {id: 2, name: 'Book'},
    //   {id: 3, name: 'Hobby'},
    //   {id: 4, name: 'Others'},
    // ]
    // console.log(this.categories)

    var self  = this
    var id    = opts.id // 'new' か 記事ID
    self.post = {}

    // category の取得
    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/categories',
      data : {}
    })
    .done(function(res){
      self.categories = res
      self.update()
    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET categories.')
    })

    // 更新の場合、デフォルト値を設定
    if (id !== 'new') {
      $.ajax({
        type : 'GET',
        url  : window.location.origin + '/api/v1/posts/' + id,
        data : {}
      })
      .done(function(res){
        self.post = res
        self.update()
      })
      .fail(function(){
        alert('Connection error.');
      })
      .always(function(){
        console.log('GET post.')
      })
    }

    cancel() {
      history.back()
    }

    submit(e) {
      e.preventDefault()
      var form = $('#edit-form')

      var type = 'POST';
      var url  = window.location.origin + '/api/v1/posts'
      var data = form.serialize()

      // 更新の場合はPUT
      if (id !== 'new') {
        type = 'PUT';
        url  = window.location.origin + '/api/v1/posts/' + id
        data = form.serialize() + '&id=' + id
      }

      $.ajax({
        type : type,
        url  : url,
        data : data
      })
      .done(function(res){
        console.log('Submitted.')
        $("#saved-dialog").modal();
        $('#saved-dialog').on('hidden.bs.modal', function (e) {
          window.location.href = window.location.origin
        })

      })
      .fail(function(){
        $("#connection-error-dialog").modal();
        $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
          window.location.href = window.location.origin
        })
      })
      .always(function(){
        console.log('GET categories.')
      })
    }
  </script>

ポイント

  • 編集画面は新規作成と更新があるため、更新の場合は既存データを取得して表示させる

まとめ

Riot と Lumen でブログを作った。
Riot は本当に簡単で、やりたいことは大体ドキュメントを見ればすぐわかる。
書き方もシンプルだし、今後 SPA を作るときは Riot が最も有力な候補となりそう。

関連記事

www.yjhm214.com

www.yjhm214.com

スポンサーリンク

Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA バックエンド編

Lumen Riot.js PHP Javascript

はじめに

最近 Riot が気になっていたので、
Laravel製軽量フレームワーク Lumen 5.4 と Riot 3.3 で簡単なブログを作ってみることにした。

今回はバックエンド編。
ここでは DB や API をさくっと作っていく。

ダミーテキストはWebtoolsのダミーテキストジェネレータで作成している。
これを使えば日本語・英語・数字混合のダミーテキストが作れる。

他のエントリーはこちら。

www.yjhm214.com

www.yjhm214.com

スポンサーリンク

作るもの

いろいろ頑張って最終的にこんな感じのブログを作る。

  • 記事一覧の表示(カテゴリーの絞り込みあり)
  • 記事詳細の表示
  • 記事の新規作成・更新

記事一覧
f:id:yjhm214:20170325170558p:plain

記事詳細
f:id:yjhm214:20170325170608p:plain

記事編集
f:id:yjhm214:20170325170616p:plain

記事保存 f:id:yjhm214:20170325170621p:plain

前提

  • Lumen 5.4.5
  • Riot.js 3.3.1

手順

  1. ファサードと Eloquent ORM を使えるようにする
  2. テーブルの作成
  3. モデルの作成
  4. ルーティングの作成
  5. コントローラの作成
  6. データの挿入

1. ファサードと Eloquent ORM を使えるようにする

ファサードと Eloquent ORM を使えるようにするため、 bootstarp/app.php

// $app->withFacades();

// $app->withEloquent();

の部分のコメントをはずしておく。

2. テーブルの作成

3つのテーブルを作成する。

  • posts
  • categories
  • posts_categories

まずはマイグレートファイルの作成。
ドキュメントルートに移動してコマンドを実行。

$ php artisan make:migration create_posts_table --create posts
$ php artisan make:migration create_categories_table --create categories
$ php artisan make:migration create_posts_categories_table --create posts_categories

database/migrations ディレクトリ以下に現在日時のマイグレートファイルが作成されるので、
それぞれテーブル定義を書いていく。

posts テーブル

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('text');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

categories テーブル

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 100);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('categories');
    }
}

posts_categories テーブル

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreatePostsCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts_categories', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id');
            $table->integer('category_id');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts_categories');
    }
}

テーブル定義ができたらマイグレートを実行してテーブルを作成する。

$ php artisan migrate

3つのテーブルができた!

mysql> desc posts;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255)     | NO   |     | NULL    |                |
| text       | text             | NO   |     | NULL    |                |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

mysql> desc categories;
+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| name       | varchar(100)     | NO   |     | NULL    |                |
| created_at | timestamp        | YES  |     | NULL    |                |
| updated_at | timestamp        | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

mysql> desc posts_categories;
+-------------+------------------+------+-----+---------+----------------+
| Field       | Type             | Null | Key | Default | Extra          |
+-------------+------------------+------+-----+---------+----------------+
| id          | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| post_id     | int(11)          | NO   |     | NULL    |                |
| category_id | int(11)          | NO   |     | NULL    |                |
| created_at  | timestamp        | YES  |     | NULL    |                |
| updated_at  | timestamp        | YES  |     | NULL    |                |
+-------------+------------------+------+-----+---------+----------------+

3. モデルの作成

app の直下にモデルを作成する。

  • app/Post.php
  • app/Category.php
  • app/PostsCategory.php

の3つのモデルを作成。

Posts.php

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model 
{
    protected $fillable = [
        'title', 'text'
    ];
}

Category.php

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model 
{
    protected $fillable = [
        'name'
    ];
}

PostsCategory.php

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class PostsCategory extends Model 
{
    protected $fillable = [
        'post_id', 'category_id'
    ];
}

4. ルーティングの作成

routes/web.php に API のルーティングを追記する。

$app->group(['prefix' => 'api/'. env('API_VERSION', 'v0')], function ($app) {
    $app->get('posts', 'PostController@index');
    $app->get('posts/{id: \d+}', 'PostController@show'); // {param: 正規表現} とすることで正規表現に一致するパラメータのみ許可する。
    $app->post('posts', 'PostController@create');
    $app->put('posts/{id: \d+}', 'PostController@update');
    $app->get('categories', 'CategoryController@index');
});

また、env('API_VERSION', 'v0') のところで API のバージョンを指定しているため、
.envAPI_VERSION の定数を追記しておく。

API_VERSION=v1

5. コントローラの作成

app/Http/Controllers にコントローラを作成する。

  • app/Http/Controllers/PostController.php
  • app/Http/Controllers/CategoryController.php

の2つのコントローラを作成。

PostController.php

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use DB;
use App\Post;
use App\Category;
use App\PostsCategory;

class PostController extends Controller
{
    /**
     * 記事一覧を取得する。
     *
     * @param  Request $request
     * @return string
     */
    public function index(Request $request)
    {
        $post = Post::orderBy('id', 'desc');

        if (!empty($request['category'])) {
            $category = Category::where('name', $request['category'])->first();
            if (empty($category)) abort(404);
            $postIds = PostsCategory::where('category_id', $category->id)
                        ->get(['post_id'])
                        ->toArray();
            $ids = [];
            foreach ($postIds as $key => $value) {
                array_push($ids, $value['post_id']);
            }
            $post->whereIn('id', $ids);
        }

        $post = $post->get();
        if (empty($post)) abort(404);

        foreach ($post as $key => $value) {
            $post[$key]->categories = $this->getCategories($value->id);
        }

        return response()->json($post);
    }


    /**
     * 特定の記事を取得する。
     *
     * @param  int $id
     * @return string
     */
    public function show($id)
    {
        $post = Post::find($id);
        if (empty($post)) abort(404);

        $post->categories = $this->getCategories($post->id);

        return response()->json($post);
    }


    /**
     * 記事を作成する。
     *
     * @param  Request $request
     * @return string
     */
    public function create(Request $request)
    {
        $post = new Post;
        $post->title = $request->title;
        $post->text  = $request->text;
        if ($post->save()) {
            if (isset($request->categories) && is_array($request->categories)) {
                foreach ($request->categories as $key => $value) {
                    $postsCategory = new PostsCategory;
                    $postsCategory->post_id     = $post->id;
                    $postsCategory->category_id = $value;
                    $postsCategory->save();
                }
            }
            return response()->json(true);
        } else {
            abort(501);
        }
    }


    /**
     * 記事を更新する。
     *
     * @param  Request $request
     * @param  int $id
     * @return string
     */
    public function update(Request $request, $id)
    {
        if (empty($id)) abort(404);

        $post = Post::find($id);
        if (empty($post)) abort(404);

        $post->title = $request->title;
        $post->text  = $request->text;
        if ($post->save()) {
            PostsCategory::where('post_id', $post->id)->delete();
            if (isset($request->categories) && is_array($request->categories)) {
                foreach ($request->categories as $key => $value) {
                    $postsCategory = new PostsCategory;
                    $postsCategory->post_id     = $post->id;
                    $postsCategory->category_id = $value;
                    $postsCategory->save();
                }
            }
            return response()->json(true);

        } else {
            abort(501);
        }
    }


    /**
     * 特定の記事が属しているカテゴリーを取得する。
     *
     * @param  int $post_id
     * @return array $categories
     */
    private function getCategories($post_id)
    {
        $query = 'select pc.post_id, pc.category_id, (select name from categories as c where c.id=pc.category_id) as category_name from posts_categories as pc where pc.post_id=?';
        $rawCategries = DB::select($query, [$post_id]);
        $categories   = [];
        if (!empty($rawCategries)) {
            foreach ($rawCategries as $key => $value) {
                array_push($categories, $value->category_name);
            }
        }
        return $categories;
    }
}

CategoryController.php

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Category;

class CategoryController extends Controller
{
    /**
     * カテゴリー一覧を取得する。
     *
     * @return string
     */
    public function index()
    {
        $data = Category::orderBy('id', 'asc')->get();
        if (empty($data)) abort(404);

        return response()->json($data);
    }

}

6. データの挿入

categories テーブルにマスターデータを挿入しておく。

insert into categories (name) values ('Tech'), ('Book'), ('Hobby'), ('Others');

また、posts テーブルと posts_categories テーブルにはダミーデータを入れておくとつなぎこみ時に便利。

insert into posts (title, text) values ('Lorem ipsum dolor si', 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec'), ('Lorem ipsum dolor si', 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec'),('Lorem ipsum dolor si', 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec'),('Lorem ipsum dolor si', 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec');


insert into posts_categories (post_id, category_id) values (1, 1), (1, 2), (2, 1), (2, 3), (2, 4), (3, 1), (4, 1), (4, 2), (4, 3), (4, 4);

まとめ

Lumen で ブログシステムのバックエンドを作成した。
Lumen は API だけを作るときとかは簡単に作れるので便利。

関連記事

www.yjhm214.com

www.yjhm214.com

スポンサーリンク