LoginSignup
10
5

More than 3 years have passed since last update.

ゼロから作るPhoenix 1.4 + Vue.jsによるSPA

Last updated at Posted at 2019-06-04

はじめに

先日,Erlang & Elixir Fest 2019に参加し,とても刺激を受けたのでペーペーな私ですが記事を書いてみようと思ったので書きます.
下図に示すような感じで,Phoenix + Vue.jsでSPA(Single page application)をゼロから構成するお話です.チュートリアルですね.

image.png

Phoenixではjsonを返すAPIを作成し,vueからはaxiosを使ってAPIを叩いて表示します.

私自身がPhoenixもVueも始めてから日が浅いので,実際に作ってみた所感を記事に起こしたって感じです(笑)
ゼロから作るとか謳ってますが,phoenixの魔法や色々なライブラリに頼りまくりなので全然ゼロからではありません.
初心者向けくらいの意味でとらえていただけると幸いです.ご容赦ください🙇‍♂️

ここで作成するプロジェクトのソースコードはGitHubで公開しています.(絶賛更新中です...)
全てのコードを解説はできないので是非以下も参照してください.
sample_spa リポジトリ

プロジェクトの作成

今回は,掲示板システムを想定して作成します.
下図の様な感じです.1つの掲示板内には複数の投稿があるというだけです.

image.png

一応,実行環境は以下の通りです.

  • macOS Mojave
  • Elixir 1.8.1
  • Phoenix 1.4.
  • MySQL 5.7

API (Phoenix)の作成

それでは,みんな大好きmix phx.newでPhoenixのプロジェクトを作成します.
プロジェクト名はsample_spaとします.

$  mix phx.new sample_spa --database mysql

MySQLの設定

ここは各々によりますが,ユーザーやパスワードの設定をしておく場合はしておきましょう.
MySQLの設定はconfig/dev.exsに記述します.
適宜,usernamepasswordを設定します.

config/dev.exs
# Configure your database
config :sample, Sample.Repo,
  username: "root",
  password: "",
  database: "sample_spa_dev",
  hostname: "localhost",
  pool_size: 10

ここまで完了したら,dbのセットアップをしてみましょう.

$ mysql.server start
$ mix ecto.setup

MySQLにsample_spa_devというデータベースが作成されていれば問題ないです🙆‍♂️

modelの作成

続いてmodelの作成をします.
今回のboardとpostの2つのモデルは掲示板のお話なので,bbsというコンテキストで定義します.

boardの作成

boardモデルは以下のような要素を持っています.

board
id mysql側でauto_increment
title 掲示板のタイトル
description 掲示板の概要

あんまり魔法は使いたくないのですがboardモデルの作成にはmix phx.gen.jsonという魔法を使わせていただきます🙇‍♂️

$ mix phx.gen.json Bbs Board boards title:string description:string

色々とファイルが作成されますが,注目すべきファイルの簡単な説明を以下に載せます.

  • lib/sample_spa/bbs/board.ex ... 作成したモデルの定義
  • lib/sample_spa/bbs.ex ... bbsコンテキストのビジネスロジックを記述
  • lib/sample_spa_web/controllers/board_controller.ex ... リクエストに対応したビジネスロジックを呼び出す
  • lib/sample_spa_web/views/board_view.ex ... レスポンスの整形を行う

ecto.migrateコマンドでdbにboardsテーブルを作成しましょう.

$ mix ecto.migrate

MySQLにboardsテーブルができてれば問題ないです🙆‍♂️

postの作成

post
id mysql側でauto_increment
body 投稿内容
board_id 掲示板のid

postモデルの作成にはmix phx.gen.schemaで作成します.

$ mix phx.gen.schema Post posts body:string

lib/sample_spa/post.expriv/repo/migrations/~_create_posts.exsが作成されます.

postもbbsコンテキストのmodelなのでわかりやすいようにlib/sample_spa/bbsディレクトリに移動しておきましょう.

$ mv lib/sample_spa/post.ex lib/sample_spa/bbs

移動したら,defmoduleの部分を変更しておきましょう.

lib/sample_spa/bbs/post.ex
defmodule SampleSpa.Post do # 変更前
defmodule SampleSpa.Bbs.Post do # 変更後

boardとpostの関係を追加

boardとpostが一対多の関係であることを追記します.
まずはboard.ex側にpostを複数持つことを記述します.

lib/sample_spa/bbs/board.ex
  schema "boards" do
    field :description, :string
    field :title, :string

    # postとの関係
    has_many :posts, SampleSpa.Bbs.Post, on_delete: :delete_all

    timestamps()
  end

次にpost.ex側にboardを一つ持つことを記述します.

lib/sample_spa/bbs/post.ex
  schema "posts" do
    field :body, :string

    # boardとの関係
    belongs_to :board, SampleSpa.Bbs.Board, foreign_key: :board_id

    timestamps()
  end

最後にマイグレーションファイルに外部キーのboard_idを加えておきましょう.

priv/repo/migrations/~_create_posts.exs
    create table(:posts) do
      add :body, :string

      # board_idを追加する
      add :board_id, references(:boards, on_delete: :delete_all), null: false

      timestamps()
    end

ここまでしたら,ecto.migrateコマンドでdbにpostsテーブルを作成しましょう.

$ mix ecto.migrate

MySQLにpostsテーブルができてれば問題ないです🙆‍♂️

ルーティングの設定

ルーティングの設定の指針を先に示しておきます.
今回は以下の表に示すようなエンドポイントになります.

path httpメソッド 内容
/api/boards GET 掲示板を全件取得
/api/boards POST 掲示板を新規作成
/api/boards/:id GET 指定した掲示板内の投稿を全件取得
/api/boards/:id POST 指定の掲示板内に新規投稿を作成

lib/sample_aps_web/route.exに記述を追加してルーティングの設定を行います.
まずは掲示板の全件取得と掲示板を新規作成するルーティングを追加しましょう.

lib/sample_aps_web/route.ex
  scope "/api", SampleSpaWeb do
    pipe_through :api

    scope "/boards" do
      get "/", BoardController, :index
      post "/", BoardController, :create
      get "/:id", BoardController, :show
      post "/:id", BoardController, :create_post
    end
  end

ルーティングの設定ができてるか確認しましょう.

$ mix phx.routes
board_path  GET   /api/boards                            SampleSpaWeb.BoardController :index
board_path  POST  /api/boards                            SampleSpaWeb.BoardController :create
board_path  GET   /api/boards/:id                        SampleSpaWeb.BoardController :show
board_path  POST  /api/boards/:id                        SampleSpaWeb.BoardController :create_post
 page_path  GET   /                                      SampleSpaWeb.PageController :index
 websocket  WS    /socket/websocket                      SampleSpaWeb.UserSocket

ちゃんとできてますね!

postの新規作成,全件取得の処理の作成

boardの作成・取得の処理は自動生成されていますが,postに関してはないので追加してきます.

viewの用意

最初に,postモデルをレスポンスでjsonとして返すときのviewを用意しておきましょう.

$ touch lib/sample_spa_web/views/post_view.ex
lib/sample_spa_web/views/post_view.ex
defmodule SampleSpaWeb.PostView do
    use SampleSpaWeb, :view
    alias SampleSpaWeb.PostView

    def render("index.json", %{posts: posts}) do
        %{data: render_many(posts, PostView, "post.json")}
    end

    def render("show.json", %{post: post}) do
        %{data: render_one(post, PostView, "post.json")}
    end

    def render("post.json", %{post: post}) do
        %{id: post.id,
          body: post.body}
    end

end

bbs.exにビジネスロジックの記述

lib/sample_spa/bbs.exを編集して,dbにpostを登録する処理とdbから全件取得する処理を追加します.

はじめにaliasにpostを追加してきます.

lib/sample_spa/bbs.ex
 alias SampleSpa.Bbs.Board
 # postを追加
 alias SampleSpa.Bbs.Post

次に処理を追加しましょう.

lib/sample_spa/bbs.ex
  # 掲示板に投稿する
  def create_post(board_id, attrs \\ %{}) do
    board = get_board!(board_id)
    %Post{}
    |> Post.changeset(attrs)
    |> Ecto.Changeset.put_assoc(:board, board)
    |> Repo.insert()
  end

  # 掲示板内の投稿を全て取得する.
  def get_posts_by_board_id!(board_id) do
    from(p in Post,
      where: p.board_id == ^board_id
    )
    |> Repo.all()
  end

controllerに処理を追加

最後にcontrollerを編集します.

先ほどと同様にaliasにpostを追加してきます.

lib/sample_spa_web/controllers/board_controller.ex
  alias SampleSpa.Bbs.Board
  # postを追加
  alias SampleSpa.Bbs.Post

次に,show関数で先ほどのbbs.exのget_posts_by_board_id!を呼び出すように編集します.

lib/sample_spa_web/controllers/board_controller.ex
  # 掲示板の投稿を全件取得
  def show(conn, %{"id" => board_id}) do
    posts = Bbs.get_posts_by_board_id!(board_id)
    conn
    |> put_view(SampleSpaWeb.PostView)
    |> render("index.json", posts: posts)
  end

  # 掲示板に投稿する
  def create_post(conn, %{"id" => id, "post" => post_params}) do
    board = Bbs.get_board!(id)
    with {:ok, %Post{} = post} <- Bbs.create_post(board, post_params) do
      conn
      |> put_status(:created)
      |> put_view(SampleSpaWeb.PostView)
      |> render("show.json", post: post)
    end
  end

APIの確認

では実際にサーバを立ち上げてリクエストを送ってきちんと処理できるか確認しましょう.

$ mix phx.server

boardの登録と取得

curlで掲示板の情報をjsonを送ってみましょう.

$ curl -XPOST  -H "Content-type: application/json" -d '{"board": { "title": "テスト", "description": "テスト用の掲示板です." }}' http://localhost:4000/api/boards
{"data":{"description":"テスト用の掲示板です.","id":15,"title":"テスト"}}% 

次に全件取得してみましょう.

$ curl http://localhost:4000/api/boards
{"data":[{"description":"テスト用の掲示板です.","id":1,"title":"テスト"}]}%       

取得できました.これで登録できていることがわかります.

postの登録と取得

boardと同様にcurlで登録して,取得します.

$ curl -XPOST  -H "Content-type: application/json" -d '{"post": { "body": "テスト登録"}}' http://localhost:4000/api/boards/7
"data":{"body":"テスト登録","id":1}}%
$ curl http://localhost:4000/api/boards/1
{"data":[{"body":"テスト登録","id":1}]}%    

SPA(Vue)の作成

ここからが本番ですね.Vueを使ってSPAを作成します.
Phoenix 1.4からはassets配下はwebpackなのでそちらを利用します.
assetsディレクトリに移動して以下のライブラリをインストールします.

  • Vue ... jsフレームワーク
  • Vue-loader ... vueを使うためのやつ
  • vue-template-compiler ... vueを使うためのやつ
  • Vuex ... 状態管理
  • Vue-router ... SPAのルーティング制御
  • axios ... APIと通信するHTTPクライアント
  • Buefy ... cssフレームワーク/Vue専用

上3つはVueを使ってSPAを実現するための基本ライブラリだと思ってください.

$ cd assets
$ npm install vue vue-loader vue-template-compiler vuex vue-router axios buefy

vueファイルを読み込んでくれるようにwebpack.config.jsを修正します.
初めのconstがたくさんあるとこに以下を追加します.

assets/webpack.config.js
const path = require('path');
...省略...
// 追加
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = (env, options) => ({
  ...省略
  module: {
    rules: [
      ...省略
      //追加
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      ...省略
    ],
  },
  //追加
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      vue$: 'vue/dist/vue.esm.js',
    }
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: '../css/app.css' }),
    new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
    //追加
    new VueLoaderPlugin()
  ]
});

次にassets配下にVueのプロジェクトを構築するディレクトを配置します.

$ mkdir js/src
$ cd js/src
$ mkdir components router store
$ touch main.js App.vue
$ tree .
.
├── App.vue //Vue単一コンポーネントファイル
├── components //コンポーネントを配置するディレクトリ
├── main.js //vueの設定を行う
├── router //vue-router用のディレクトリ
└── store //vuex用のディレクトリ

まずはvueを読み込めているか確認したいので,App.vuemain.jsを編集します.
App.vueはただHello, Vue.js!と表示するだけです.

assets/js/src/App.vue
<template>
    <div>
      {{ text }}
    </div>
</template>

<script>
export default {
   name: 'app',
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>
asstes/js/src/main.js
import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App }
})

「なんとなくわかった.じゃあどうやってこのApp.vueを呼び出すの?」

そうですよね,Vue側を呼び出せるようにVueとPhoenixの連携の設定をしてきます.

VueとPhoenixの連携

VueとPhoenixをどうやって連携させるか.
答えはlib/sample_spa_web/controllers/page_controllertemplates/layout/app.html.eexを用います.

まずpage_controllerへのルーティングに少し修正を加えます.編集前はコメントアウトしておきます.

lib/sample_aps_web/route.ex
  scope "/", SampleSpaWeb do
    pipe_through :browser

    # get "/", PageController, :index
    get "/*path", PageController, :index
  end

これはroute.exの終わりに書きます.これによって任意のリクエストをpage_cntrollerで受け取ります.

page_controllerのコードを確認してみましょう.

lib/sample_spa_web/controllers/page_controller.ex
defmodule SampleSpaWeb.PageController do
  use SampleSpaWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html")
  end
end

page_controllerは,lib/sample_spa_web/templates/page/index.html.eexを呼び出しています.
そしてそのレイアウトを定義しているのが,lib/sample_spa_web/templates/layout/app.html.eexになります.

そこでapp.html.eexを編集して,Vue側を呼び出すように編集します.
<body>タグ内を全て書き換えます.

lib/sample_spa_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>SampleSpa · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <%# 変更箇所 %>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

さっきまで編集してたjs/src/main.jsを指定しないの?と言われそうですが,これには理由があります.
assets配下のwebpack.config.jsを見るとわかります.

asstes/webpack.config.js
module.exports = (env, options) => ({
  ...省略...
  entry: {
      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
  },

webpackのエントリポイントはデフォルトではjs/app.jsになっています.
なのでwebpack側にはja/app.jsを通してしかアクセスできません.
よって,js/app.jsを編集してjs/src/main.jsをインポートします.

asstes/js/app.js
// import "phoenix_html"
import "./src/main.js"

ここまでできたら,アプリを起動します.

$ mix phx.server

http://localhost:4000 にアクセスして以下のように見えていれば成功です.

image.png

これでPhoenixとvueの連携ができました👏

Componentsの作成

ここからは完全にVueのお話です.
Vueではコンポーネントベースに開発を進めて行きます.
事前にコンポーネントの分割ルールを作っておくことが重要となります.
今回は,Atomic Designを意識したコンポーネントの分割を考えます.

まずはディレクトリを作りましょう.

$ cd assets/js/src/components
$ mkdir atoms molecules organisms

cssフレームワークのBuefyを使うことも考慮して,以下のようなルールで今回はコンポーネントを作成していきます.

  • atoms ... Buefyで提供されるデザインを細かくコンポーネント化したものを配置する.
  • molecules ... atomsのコンポーネントを組み合わせて作成する.(ただし状態管理を担うvuexとやりとりしてはいけない)
  • organisms .... atoms, moleculesのコンポーネントを組み合わせて作成する.(ここでvuexとやりとりする)

ここら辺は実際に作ってみてわかる部分でもあるので進めてみましょう.

まずはBuefyのインポートの設定をmain.jsに追記します.

assets/js/src/main.js
import Buefy from 'buefy'
import 'buefy/dist/buefy.css'

Vue.use(Buefy)

ちなみにBuefyはBulmaをVue向けにしたフレームワークであり,Bulmaを包括しています.
なのでBulmaのデザインを全て使えるという利点があります.
(実はBulmaを使用したかったのですが,webpackでの設定が上手く行かなかったの代用しています...)

atoms

BulmaのHPで紹介されてるcardをコンポーネントをに分けて作成してみましょう.

スクリーンショット 2019-06-03 0.41.56.png

赤枠で囲っている部分をatomsのコンポーネントとして構築します.

全体をTheCard.vue,中身をTheCardContent.vue,下のボタンをTheCardFooterItem.vueとして作成します.

assets/js/src/components/atoms/TheCard.vue
<template>
    <div class="card">
        <header class="card-header">
             <p class="card-header-title">
                {{ title }}
             </p>
        </header>

        <div class="card-content">
            <slot name="content" />
        </div>

         <footer class="card-footer">
            <slot name="footer"/>
         </footer>
    </div>
</template>

<script>
export default {
    name: 'TheCard',
    props: {
        title: {
            type: String,
            default: 'タイトル'
        }
    }
}
</script>

TheCard.vueではcardコンポーネントの概形を定義する感じです.
<slot/>を使うことで他のコンポーネントを入れ子にすることができます.

assets/js/src/components/atoms/TheCardContent.vue
<template>
    <div class="content">
        {{ body }}
    </div>
</template>

<script>
export default {
    name: 'TheCardContent',
    props: {
        body: {
            type: String,
            default: 'コンテンツ'
        }
    }
}
</script>

TheCardContent.vueはテキストコンテンツを表示するだけです.

assets/js/src/components/atoms/TheCardFooterItem.vue
<template>
    <a @click="click" class="card-footer-item">
        {{ text }}
    </a>
</template>

<script>
export default {
    name: 'TheCardFooterItem',
    props: {
        text: {
            type: String,
            default: 'リンク'
        }
    },
    methods: {
        click() {
            this.$emit('click')
        }
    }
}
</script>

TheCardFooterItem.vue<a>タグでリンクの働きをします.
atomsではクリックされたときの挙動の制御は行いません.
this.$emit('click')でコンポーネント内の処理を,他のコンポーネントに引き継ぐことができます.

molecules

作成したatomsのコンポーネントを組み合わせて,掲示板の情報を表示するTheBoard.vueと名前のコンポーネントを作成します.

assets/js/src/components/moleculs/TheBoard.vue
<template>
    <the-card :title="board.title">
        <the-card-content
            slot="content"
            :body="board.discription"
        />
        <the-card-footer-item
            slot="footer"
            text="Opne"
            @click="click(board.id)"
         />
    </the-card>
</template>

<script>
import TheCard from '../atoms/TheCard'
import TheCardContent from '../atoms/TheCardContent'
import TheCardFooterItem from '../atoms/TheCardFooterItem'

export default {
    name: 'TheBoard',
    components: {
        TheCard,
        TheCardContent,
        TheCardFooterItem
    },
    props: {
        board: {
            type: Object,
            default() {
                return {
                    id: 1,
                    title: '掲示板',
                    discripition: 'サンプルです.'
                }
            }
        }
    },
    methods: {
        click(id) {
            this.$emit('click', id)
        }
    }
}
</script>

このようにコンポーネントベースでUIを組み立てて行きます.
organismsの話に移る前にVueでのデータの永続化を担当するvuexの設定を行います.

vuex

最初に作ったPhoenixのAPIを叩いて受け取ったレスポンスをvuexを使って保持します.
なおAPIを叩くにはaxiosというライブラリを使用します.

まずはvuexとaxiosを使う準備をしましょう.

storeディレクトリ配下にindex.jsmodulesディレクトリを作成しましょう.さらにmodulesディレクトリにはbbs.jsというファイルを配置しましょう.

$ cd assets/js/src/store
$ mkdir modules
$ touch index.js modules/bbs.js
assets/js/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// modules
import bbs from './modules/bbs'

Vue.use(Vuex)

const store = new Vuex.Store({
    modules: {
        bbs
    }
})

export default store

store/index.jsはvuexの設定を記述します.

assets/js/src/store/modules/bbs.js
import Vue from 'vue'

export default {
    namespaced: true,
    state: {
    },
    getters: {
    },
    mutations: {
    },
    actions: {
    }
}

moudles/bbs.jsはPhoenixのbbsコンテキストのモデルを保持するように設計します.
これから追記してきますが,bbs.jsにはstategettersmutationsactionsの4つの部分から成ります.

それぞれの役割は下図がわかりやすいと思います.(図はvuex公式より引用)

image.png

gettersは?となりそうですが,gettersはVueコンポーネントからstateの値を参照するのに用います.図のRenderに相当します.

では,modules/bbs.jsで掲示板のデータを取得できるように編集していきましょう.

assets/js/src/store/modules/bbs.js
import Vue from 'vue'

export default {
    namespaced: true,
    state: {
        boards: []
    },
    getters: {
        boards: state => state.boards
    },
    mutations: {
        setUser(state, { boards }) {
            state.boards = boards
        }
    },
    actions: {
        async getBoards({ commit }) {
            await Vue.axios.get('/api/boards')
            .then(res => {
                commit('setBoards', {
                boards: res.data
                })
            })
            .catch(err => {
                throw err.response.status
            })
        }
     }
}

重要となるのはactionsで部分でしょうか.axiosを使ってAPIを叩きます.
async awaitはは非同期処理です..thenからでAPIサーバからのレスポンスを受け取りmutationsを呼び出します.
catchは例外を処理します.つまりAPIサーバとの通信で何かしら失敗した場合の処理です.

「vuexの使い方のパターンはわかった.じゃあどうやって使うんだ?」

そうですよね,次はvuexとやりとりを実際に行うorganismsのコンポーネントを作成していきましょう.

organisms

organismsではatoms, moleculesのコンポーネントを組み合わせて作成します.
今回はvuexで取得したboardsを全て描画するTheBoardList.vueコンポーネントを作成します.

一応,作成の指針としては,コンポーネントを読み込んだタイミングでAPIにアクセスし,その結果をimportしたmolculetsのコンポーネントに流し込みます.

assets/js/src/components/organisms/TheBoardList.vue
<template>
    <div >
        <the-board
            v-for="board in boards"
            :key="board.id"
            :board="board"
        />
    </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

import TheBoard from '../molecules/TheBoard'

export default {
    name: 'TheBoardlist',
    components: {
        TheBoard
    },
    computed: {
        ...mapGetters({
            boards: 'bbs/boards'
        })
    },
    methods: {
        ...mapActions({
            getBoards: 'bbs/getBoards'
        })
    },
    created() {
        this.getBoards().catch(() => {
            console.error('Vuex Error')
        })
    }
}
</script>

vuexとのやりとりはヘルパー関数mapGettersmapActionsを用います.

  • mapGetters ... storeのgettersを呼び出す.computedに記述する.
  • mapActions ... storeのactionsを呼び出す.methodsに記述する.

このヘルパー関数を用いることでgettersやactionsに別名をつけられる上に,methodsはコンポーネント内でthisを使って呼び出すことができます.

created()はコンポーネントが呼び出されてはじめに呼び出されるメソッドです.ここでactionsを呼び出しvuex側でAPIと通信が行われます.

まあ論より証拠ってことで動かすのが早いですよね😅
このコンポーネントをApp.vueにimportして表示してみましょう.

App.vue
<template>
    <div>
      {{ text }}
      <the-board-list/>
    </div>
</template>

<script>
import TheBoardList from './components/organisms/TheBoardList'

export default {
   name: 'app',
   components: {
       TheBoardList
   },
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>

この状態で実行してみます.

$ mix phx.server

image.png

あら,真っ白でエラー吐かれちゃいました.

どうやらbabelでasync awaitを使用するには設定を加える必要があるようです.

babelでasync/awaitを使えるように修正

以下の記事を参考に修正を加えます.
babelでasyncを使おうとしたらregeneratorRuntime is not definedが出た

assets/.babelrc
{
    "presets": [
        [
          "@babel/preset-env", {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
}

スクリーンショット 2019-06-03 22.28.46.png

問題なく起動できました.データは適当に加えてある状態になってます.
ちなみにchromeにVue.js devtoolsを入れておくと開発者は幸せになれます.入れておきましょう.

Vue-routerの導入

レイアウトを整えつつ,Vue-routerを導入します.
Vue-routerは簡単に言ってしまえば,ルーティングによって描画するコンポーネントを書き換えるものです.

まずはVue-routerを使う準備をしましょう.assets/js/src/routerディレクトリ配下にindex.jsを置きます.

$ touch assets/js/src/router/index.js
assets/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

// components
import TheBoardList from '../components/organisms/TheBoardList'


Vue.use(VueRouter)

const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            component: TheBoardList,
        }
    ]
})

export default router

ここで設定しているのは,パスとそれに対応するコンポーネントです.
ここでは/TheBoardListを割り当てています.

次にassets/js/src/main.jsにこのrouterの設定をインポートします.

/assets/js/src/main.js
// importにrouterを追加
import router from './router'
...省略
new Vue({
  el: '#app',
  store,
  router, // 追加
  template: '<App/>',
  components: { App }
})

これでVue-routerを使う準備ができました.
ではasstes/js/src/App.vueにVue-routerを組み込んでみましょう.ついでにレイアウトも少し整えて置きます.

asstes/js/src/App.vue
<template>
    <div class="hero">
        <div class="hero-body">
            {{ text }}
            <router-view/>
        </div>
    </div>
</template>

<script>
export default {
   name: 'app',
   props: {
       text: {
           type: String,
           default: 'Hello, Vue.js!'
       }
   }
}
</script>

<router-view/>の部分で描画されるコンポーネントがパスに応じて変化します.

実際に起動してみましょう.

$ mix phx.server

image.png

上手くいってますね!
無事,Vue-routerが導入できました.

あとやらなければならないこととしては,

  • 投稿を全て描画するコンポーネントの作成
  • 掲示板を新規作成するFormを含むコンポーネントの作成
  • 掲示板に投稿するFormを含むコンポーネントの作成

が残ります.

これらは後日,追記していきます🙇‍♂️

まとめ

この記事の扱ったトピックのまとめです.

  • Phoenixでjsonを返すAPIの作成
    • リレーションシップを持ったモデル構造
    • phooenixのルーティング制御でVueのプロジェクトに繋ぐ
  • Vue.jsを使ったSPAの作成
    • コンポーネントベースのSPAの設計
    • axiosによるAPIアクセスとvuexによる状態管理
    • Vue-routerによるSPAのルーティング

おわりに

最初にも述べましたが,Erlang & Elixir Fest 2019に参加して熱量が上がったので,とにかく冷めないうちに記事を書こうと思って書きました.
全て完成してから,記事を出そうと思っていたのですが先に公開して更新していくことにしました.
未完成の部分はこれから更新していきます,申し訳ありません🙇‍♂️

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5