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

Tech memo

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

Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA フロントエンド編

はじめに

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

今回はフロントエンド編。
DB との接続なしのモックアップを作っていく。
Material Design for Bootstrap も気になっていたのでついでに使ってみる。

ダミーテキストは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. Lumen ルーティングの作成
  2. 必要なCSS/JSファイルのダウンロードと設置
    1. Material Design for Bootstrapで使うCSS/JSファイルのダウンロードと設置
    2. Riot で使う JS ファイルの設定
  3. ベースとなる View の作成
  4. CSS の作成
  5. Riot タグの作成
    1. ナビゲーション
    2. 一覧画面
    3. 詳細画面
    4. 編集画面
  6. Riot ルーターの作成

1. Lumen ルーティングの作成

ベースとなる View ファイルを読み込むためのルーティングを routes/web.php に作成する。

$app->get('/', ['as' => 'index', function () use ($app) {
    return view('index');
}]);

$app->get('/categories', function ()  {
     return redirect()->route('index');
});
$app->group(['prefix' => 'categories'], function ($app) {
    $app->get('{any}', function ()  {
        return view('index');
    });
});

/categories/posts でルートを切っているのは、
これらを切らないと /categories/{category}/posts/{id} の画面でリロードすると ルートが存在しなくてエラーになってしまうため。

/ へのリダイレクトや index.blade.php を表示させることで、
いつでもベースとなる view を表示させ、
そのあと Riot のルーティングにより
URL で指定された view を表示させるようにする。

2. 必要なCSS/JSファイルのダウンロードと設置

2.1. Material Design for Bootstrapで使うCSS/JSファイルのダウンロードと設置

今回は Material Design for Bootstrap を使うため、
必要なファイルをダウンロードする。

ダウンロードしたらそれぞれ以下のようにファイルを設置する。

また、自分で記述する CSS の空ファイル(blog.css) もここで作成しておく。

root
├── public
│   ├── css
│   │   ├── bootstrap.css
│   │   ├── bootstrap-material-design.css
│   │   ├── ripples.css
│   │   └── blog.css
│   │
│   └── js
│       ├── jquery-3.1.1.js
│       ├── bootstrap.js
│       ├── material.js
│       └── ripples.js
│    
│    

2.2. Riot で使う JS ファイルの設定

Riot では以下の2つを使う。

  • Riot 本体 -riot+compiler.js の『ダウンロード』よりダウンロード
  • Riot router (ルーティングライブラリ)
    • route.min の 『Download by yourself』より ダウンロード

それぞれ public/jsに設置する。

また、Riot のルーターを定義するファイルとして
public/js/app.js の空ファイルを用意しておく。

3. ベースとなる View の作成

JS/CSS を配置したら、ベースとなる view resources/views/index.blade.php を作成する。
SPA のため、用意する view は これだけ。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta http-equiv="Cache-Control" content="no-cache">
  <meta http-equiv="Pragma" content="no-cache">
  <meta http-equiv="cache-control" content="no-cache">
  <meta http-equiv="expires" content="0">
  <meta name="description" content="">
  <meta property="og:type" content="website">
  <meta property="fb:app_id" content="">
  <meta property="og:title" content="">
  <meta property="og:description" content="">
  <meta property="og:image" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Blog</title>

  <!-- font -->
  <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <!-- font -->

  <!-- Material Design for Bootstrap -->
  <link href="{{ url('/css/bootstrap.css') }}" rel="stylesheet">
  <link href="{{ url('/css/bootstrap-material-design.css') }}" rel="stylesheet">
  <link href="{{ url('/css/ripples.css') }}" rel="stylesheet">
  <!-- Material Design for Bootstrap -->

  <link href="{{ url('/css/blog.css') }}" rel="stylesheet">
</head>


<body>
  <header></header>
  <article></article>

  <!-- Material Design for Bootstrap -->
  <script src="{{ url('/js/jquery-3.1.1.js') }}"></script>
  <script src="{{ url('/js/bootstrap.js') }}"></script>
  <script src="{{ url('/js/material.js') }}"></script>
  <script src="{{ url('/js/ripples.js') }}"></script>
  <script>$.material.init();</script>
  <!-- Material Design for Bootstrap -->

  <!-- Riot -->
  <script src="{{ url('/js/riot+compiler.js') }}"></script>
  <script src="{{ url('/js/route.min.js') }}"></script>
  <script src="{{ url('/tags/raw.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/navbar.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/list.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/post.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/edit.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/js/app.js') }}"></script>
  <!-- Riot -->

  <!-- Saved ダイアログ -->
  <div id="saved-dialog" class="modal fade" tabindex="-1" style="display: none;">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
          <p>Saved.</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" data-dismiss="modal">OK<div class="ripple-container"><div class="ripple ripple-on ripple-out" style="left: 40.6562px; top: 20px; background-color: rgb(0, 150, 136); transform: scale(10.875);"></div></div></button>
        </div>
      </div>
    </div>
  </div>
  <!-- Saved ダイアログ -->

  <!-- Connection エラー ダイアログ -->
  <div id="connection-error-dialog" class="modal fade" tabindex="-1" style="display: none;">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
          <p>Connection error. Try again.</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" data-dismiss="modal">OK<div class="ripple-container"><div class="ripple ripple-on ripple-out" style="left: 40.6562px; top: 20px; background-color: rgb(0, 150, 136); transform: scale(10.875);"></div></div></button>
        </div>
      </div>
    </div>
  </div>
  <!-- Connection エラー ダイアログ -->

</body>

</html>

ポイント

  • <header></header> に Riot のカスタムタグを挿入する。
  • <article></article> にも Riot のカスタムタグを挿入する。この部分が切り替わることで SPA を実現。

4. CSS の作成

全体で利用する共通の CSS を public/css/blog.css に書いていく。
こんな感じ。

body {
    line-height: 1.8;
    font-size: 16px;
}

article {
    width: 65%;
    min-width: 300px;
    margin: 0 auto;
}

a:hover {
    text-decoration: none;
}

#edit-btn {
    position: fixed;
    bottom: 40px;
    right: 2%;
}

.label-Tech {
  background-color: #ff5722;
}

.label-Book {
  background-color: #03a9f4;
}

.label-Hobby {
  background-color: #4caf50;
}

.label-Others {
  background-color: #9e9e9e;
}

.p15 {
    padding: 15px;
}

.p50 {
    padding: 50px;
}

.pt0 {
  padding-top: 0!important;
}

.mt10 {
    margin-top: 10px;
}

.mt20 {
    margin-top: 20px;
}
.mr5 {
    margin-right: 5px;
}

.mb0 {
    margin-bottom: 0;
}

.right-align {
  text-align: right;
}

.center {
    text-align: center;
}

5. Riot タグの作成

ようやく Riot タグを作っていく。
ここでは、 - navbar.tag – ヘッダーのナビゲーション - raw.tag – Riot による自動エスケープを避けるためのタグ - list.tag – ブログ記事一覧画面 - post.tag – ブログ記事詳細画面 - edit.tag – ブログ記事編集画面

を作成する。
それぞれ、public/tags 以下に作成。

5.1. ナビゲーション

navbar.tag

<navbar>
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <h1>
          <a class="navbar-brand" href="/">Blog</a>
        </h1>
      </div>

      <!-- Collect the nav links, forms, and other content for toggling -->
      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav navbar-right">
          <li each="{ categories }"><a href="/categories/{ name.toLowerCase() }">{ name }</a></li>
        </ul>
      </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
  </nav>

  <style>
    h1 { margin: 0; }
  </style>

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

</navbar>

ポイント

  • <navbar></navbar> で囲むことで、 viwe 側で このタグが使えるようになる。
  • html を記述し、そのタグに適用したいスタイルを <style> に記述し、そのタグに適用したい JS を <script> に記述。
  • データは this.categories のように、this(Riot タグインスタンス) のプロパティとして持たせることでタグ内で使えるようになる。
  • 持たせたデータは { } で囲むことで展開される。
  • {} 内ではjavascript が書ける!
  • ぐるぐる回したい場合は、<li each="{ categories }"><a href="/category/{ name.toLowerCase() }">{ name }</a></li> のように each="{ categories }" とかく。

ちなみに、Riot 3 からは Scoped CSSがデフォルトに なったため、
<style> 要素に scoped 属性を追加する必要がなくなった!

あと、<script>タグはあってもなくてもOK

5.2. Riot による自動エスケープを避けるためのタグ

Riot はテンプレート変数を自動でエスケープするため、改行などが反映されない。
エスケープしないでHTMLを表示する を参考にエスケープしないカスタムタグを定義する。

raw.tag

<raw>
  <span></span>

  <script>
    this.root.innerHTML = opts.content
  </script>
</raw>

ポイント

  • this.root : 作成した Riot タグ自身

5.3. 一覧画面

list.tag

<list>
  <ul>
    <li each="{list}">
      <div class="card p15">
        <div class="card-block">
          <h2 class="card-title mb0"><raw content="{ title }"></raw></h2>

          <a each="{ category in categories }" href="/categories/{ category.toLowerCase() }">
            <span  class="label label-{ category } mr5">{ category }</span>
          </a>

          <p class="card-text mt20"><raw content="{ text }"></raw></p>
          <div class="right-align">
            <a href="/posts/{ id }" class="btn btn-primary btn-raised">Contiune</a>
          </div>
        </div>
      </div>
    </li>
  </ul>
  <a id="edit-btn" href="/posts/new/edit" class="btn btn-danger btn-fab">
    <i class="material-icons">mode_edit</i>
  </a>


  <style>
    ul {
        padding-left: 0;
    }

    ul li {
        list-style: none;
        margin-bottom: 20px;
    }
  </style>


  <script>
    this.list = [
      {
        id: 1,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Tech', 'Book'],
      },
      {
        id: 2,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Book', 'Hobby', 'Others']
      },
      {
        id: 3,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Hobby', 'Others']
      },
      {
        id: 4,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Others']
      },
    ];
    console.log(this.list)
  </script>
</list>

ポイント

  • ぐるぐる回す eachは、<a each="{ category in categories }" href="/category/{ category.toLowerCase() }"> のように、each={ category in categories } としても使える。

5.4. 詳細画面

post.tag

<post>
  <div class="card p15">
    <div class="card-block">
      <h2 class="card-title mb0"><raw content="{ title }"></raw></h2>

      <a each="{ category in categories }" href="/categories/{ category.toLowerCase() }">
        <span  class="label label-{ category } mr5">{ category }</span>
      </a>

      <p class="card-text mt10"><raw content="{ text }"></raw></p>
    </div>
  </div> 
  <a id="edit-btn" href="/posts/{ id }/edit" class="btn btn-danger btn-fab">
    <i class="material-icons">mode_edit</i>
  </a>

  <style></style>

  <script>
    this.id         = 1
    this.title      = 'Lorem ipsum dolor si'
    this.text       = 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec'
    this.categories = ['Tech', 'Book']
  </script>
</post>

一覧画面と詳細画面で使った文法とほぼ同じ。

5.5. 編集画面

submit などのファンクションはまだ実装しない。

edit.tag

<edit>

  <form id="edit-form" class="form-horizontal card p50">
    <fieldset>
      <legend>Edit</legend>
      <div class="form-group">
        <label for="inputTitle" class="col-md-1 control-label">Title</label>
        <div class="col-md-11">
          <input type="text" name="title" class="form-control" id="inputTitle" placeholder="Title..." value="{ post.title }">
          <span class="help-block">100 文字以内</span>
        </div>
      </div>


      <div class="form-group">
        <label for="" class="col-md-1 control-label pt0">Category</label>
        <div class="checkbox col-md-2" each="{ categories }">
          <label>
            <input type="checkbox" name="categories[]" value="{ id }" checked="{ (post.categories)&&(post.categories.indexOf(name) >= 0) }"><span class="checkbox-material"><span class="check"></span></span> { name }
          </label>
        </div>
      </div>


      <div class="form-group">
        <label for="inputText" class="col-md-1 control-label">Text</label>
        <div class="col-md-11">
          <textarea name="text" class="form-control" rows="15" id="inputText" value="{ post.text }"></textarea>
          <span class="help-block">5000 文字以内</span>
        </div>
      </div>

      <div class="form-group center">
        <div class="col-md-11">
          <button type="button" class="btn btn-default" onClick="{ cancel }">Cancel</button>
          <button type="submit" class="btn btn-primary" onClick="{ submit }">Submit</button>
        </div>
      </div>

    </fieldset>
  </form>


  <style>
  </style>


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

    this.post = {}

    cancel() {
      history.back()
    }

    submit(e) {
      e.preventDefault()
    }
  </script>

</edit>

ポイント

  • テキストエリアやテキストボックスの value="{ post.title }" は更新時のデフォルト値
  • Riot のchecked属性selected属性は、checked="{ 変数 }" と書け、変数=false/undefined の場合は無視される。
  • クリックイベントは onClick="{ submit }"と書ける。

6. Riot ルーターの作成

最後にルータを作る。

app.js

route.base('/')
riot.mount('header', 'navbar')

route(function(collection, id, action){
  console.log('collection: ' + collection)
  console.log('id: ' + id)
  console.log('action: ' + action)
  riot.mount('article', collection || 'list', {id: id, action: action})
})

route('/categories/*', function(categoryName) {
  console.log('categoryName ' + categoryName)
  riot.mount('article', 'list', {categoryName: categoryName})
})

route('/posts/*', function(id, action) {
  console.log('id ' + id)
  riot.mount('article', 'post', {id: id})
})

route('/posts/*/edit', function(id) {
  console.log('id ' + id)
  riot.mount('article', 'edit', {id: id})
})

route.start(true)

ポイント

  • route.base('/') でペースパスをデフォルトの#から/に変更。(ルータをカスタマイズする)
  • riot.mount('header', 'navbar')riot.mount(selector, tagName, [opts]) の書き方で、view上のselectorにカスタムタグtagNameを設置する、という意味。
  • route(function(collection, id, action){}) でルーティングの設定を行う。 (ルーティングの設定)
  • route.start(true) でURL変更の検知を開始する。(URL変更の検知)

まとめ

Riot やばい。
Angular とか React とか Vue とかと比べると一番簡単。
学習コストがほぼ0。

Material Design for Bootstrap やばい。
ほとんど CSS 書いてないのにめちゃマテリアルデザインになる。

関連記事

www.yjhm214.com

www.yjhm214.com

スポンサーリンク

AWS VPC/EC2/ALB(+SSL)/お名前.comで作るWebシステム

概要

Web制作においていろいろ使えるツール集のサイトWebtoolsをローンチする際、
こんな感じのシステムを久しぶりに構築した。

f:id:yjhm214:20170325162906p:plain

一般的なシステムだけど、
一からつくるとなると意外といろんなことを考慮しなければいけなかった。

今後のために、忘れがちな手順やはまったところをメモ程度にまとめておく。
一度サクッと全体を読んでから構築に入るといいかも。

いつかここらへん Terraform で全部まとめたいな。

ちなみに、お名前.com ですでにドメインを取得していることが前提。

構築するもの

  • VPC 構築
    • VPC
    • サブネット
    • インターネットゲートウェイ
    • ルートテーブル
  • EC2 構築
    • EC2
    • Elastic IP の割り当て、関連付け
  • ALB(+SSL)の構築
    • SSL 証明書を ACMでつくる
    • ALB
  • ドメインの設定

スポンサーリンク

VPC 構築

つくるもの

  • VPC
  • サブネット
  • インターネットゲートウェイ
  • ルートテーブル

VPC

IPv4 CIDR block は /16 とかにしておけばOK。
こうすることで、サブネットを2つ登録できる。
サブネットを2つつくらないと ALB が作れない。

例: IPv4 CIDR block : 10.0.0.0/16

最初 10.0.0.0/24 で作っちゃったから サブネットが2つ作れなくて ALB 作れんかった。
一度つくったVPCのCIDR は変えられないから、
『VPC再作成 → EC2 を AMI から立て直し』 で直ったけどめんどかった。

サブネット

アベイラビリティゾーンが異なるよう2つつくる。
ALB を作るときに2つ必要。

例:

  • サブネット1
    • アベイラビリティゾーン : ap-northeast-1a
    • IPv4 CIDR block : 10.0.0.0/24
  • サブネット2
    • アベイラビリティゾーン : ap-northeast-1c
    • IPv4 CIDR block : 10.0.1.0/24

インターネットゲートウェイ

ちゃちゃっとつくる。

つくったあと、『VPC にアタッチ』ボタンからさっき作った VPC にアタッチ。

ルートテーブル

さっき作ったVPCを指定してちゃちゃっとつくる。

つくったあと、

  1. 『ルートタグ』からこんな感じ↓に設定

    • 送信先 : 0.0.0.0/0
    • ターゲット : さっきつくったインターネットゲートウェイ
  2. 『サブネットの関連付け』からさっきつくったサブネットを2つとも関連付け。

EC2 構築

つくるもの

  • EC2
  • Elastic IP の割り当て、関連付け

ここは簡単。

EC2の構築

さっき作った VPC とかを指定してちゃちゃっとつくる。

Elastic IP の割り当て、関連付け

EIP を新しく割り当てて、さっき作った EC2 に関連づける。

ALB(+SSL)の構築

つくるもの

  • SSL 証明書を ACMでつくる
  • ALB

SSL 証明書を ACMでつくる

ACM にいって SSL 証明書をつくる。

ACM で SSL 証明書を申し込む場合、
そのドメインが本当に自分のものかを確認するために認証メールが送信される。
その認証メールを受信して認証をすると、SSL証明書が発行される。

*.example.com というドメインで申し込んだ場合、
下記のメールアドレス宛に認証メールが送信される。

  • ドメインを whoisした時のDomain registrant、Technical contact、Administrative contact
  • administrator@example.com
  • admin@example.com
  • hostmaster@example.com
  • postmaster@example.com
  • webmaster@example.com

これらのメールアドレスでメールが受信できるようにしなければならない。

今回はお名前.com でドメインを購入していたので、
お名前.comのメール専用サーバを申し込んでメール受信設定をしておく必要がある。
ここがわかりやすかった。 AWS上で独自ドメイン+SSLを実現 - S3/ACM/CloudFront/Route 53( + お名前.com)

メール受信方法には、ほかにもこんなやり方もあるみたい。
[ACM] SSL証明書発行時のドメイン認証メールをSESで受け取ってみた

これしらなくてよくわかんなくていろいろ調べた。

メールの設定が終わったら、ACM から SSL 証明書を作成する。
そして認証メールがきたらどれか一つだけでいいので認証処理を行えばOK!

ALB

  • 上で作ったサブネットを2つとも指定。
  • 『証明書タイプ』は『AWS 証明書マネージャ (ACM) から、既存の証明書を選択する』を選択し、さっき作った証明書を指定。
  • セキュリティポリシーはTLS1.2が良さそうなため ELBSecurityPolicy-TLS-1-2-2017-01 を選択。

SSLとTLSの違いはここがわかりやすい。
SSLとTLSの違いと脆弱性

あとは適当に設定。

ドメインの設定

今回はサブドメインでの登録のため、お名前.comでサブドメインの設定を行う。
サブドメインの作成はここがわかりやすかった。

今回は ALB の DNS を登録するため、

  • TYPE は CNAME
  • VALUE は ALB の DNS名

にする。

※参考
Route 53 に委任する場合や移管する場合などは
お名前.comのドメインをAWSで使用する4つの方法 がまとまってていい感じ。

関連記事

www.yjhm214.com

www.yjhm214.com

まとめ

細々とした手順が意外とあったり、
一度設定したらもう戻せないものとかがあるので、
システム構築時にはこういうまとまった手順書があるとありがたいなと思った。

ここらへんを全て Terraform でまとめられたらめちゃくちゃ楽なのに。

スポンサーリンク