20201010のvue.jsに関する記事は8件です。

【Vue.js + (laravel)】画像データをPOSTしたい

Vue.jsで画像データをPOSTしたかったのでformDataを使って実現しました

【画面側】
type="file" の入力欄と送信ボタンを用意
画像の選択(input)が行われたとき、送信ボタンが押されたときにそれぞれ処理を行う

 <template>
     <input @change="fileSelect" type="file" name="file">
     <button @click="upload" type="submit">送信</button>
 </template>

【js側】
この場合はTestViewという名前でコンポーネントを作っています。

<script>
    export default {
        name: "TestView",
        data: function () {
            return {
                selected_file: null
            }
        },
        methods: {
            //ファイル選択時の処理
            fileSelect: function(e) {
                //選択したファイルの情報を取得しプロパティにいれる
                this.selected_file = e.target.files[0];
            },
            //送信処理
            upload: function() {
                //formDataをnewする
                let formData = new FormData();
                //appendでデータを追加(第一引数は任意のキー)
                //他に送信したいデータがある場合にはその分appendする
                formData.append('file', this.selected_file);

                let config = {
                    headers: {
                        'content-type': 'multipart/form-data'
                    }
                };

                axios.post('/product', formData, config)
                    .then(function(response) {
                        console.log('成功')
                    })
                    .catch(function(error) {
                        console.log('失敗')
                    })
            }
        }
    }
</script>

【Laravel コントローラ側】
サーバ側でのデータの受け取り方

 public function test(Request $request)
    {
        //js側で設定したキーを指定
        $file = $request->file('file');
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初心者向け]Vue.js 重要単語集

Vue.js

  • 簡単に言うと、ユーザーインターフェイスを構築するためのフレームワークのこと
  • JavaScriptでDOMを操作するWebアプリケーションを構築するときに向いている
  • DOMよりも先にデータが存在していて、それに合わせてDOMが構築される
  • HTMLベースのテンプレート構文を使っているので、Vueインスタンスのデータと描画をDOMを宣言的に対応させることができる

Vueインスタンス

var app = new Vue({
  //下記などのオプションを記述する
})

el

アプリケーションを紐付ける要素のセレクタ

data

アプリケーションで使用するデータ、オブジェクトや配列も登録できる

computed

dataと似たように扱うことのできる、関数によって算出されたデータ、算出プロパティ

mothods

このアプリケーションで使用するメソッド、コードを管理しやすくするために処理を分けたり、イベントハンドラなど細かな実装を担当する

DOM

  • JavascriptからHTMLやCSSのデータを取得、操作するための仕組み

データバインディング

  • データと描画を同期させる仕組みのこと
  • JavaScriptとのデータとそれを使用する場所を紐付け、データに変化があれば自動的にDOMを更新する

ディレクティブ

  • 接頭辞「 v- 」が付いたVue.jsの特別な属性のことで、テンプレートとロジックを関連付ける (下記が代表的なもの)

v-for

配列やオブジェクトから要素を繰り返し描画する

v-on

DOMイベントのハンドリングに使う

v-model

データとフォームの入力項目のバインド(関連付け)をする

v-if

テンプレートベースで条件分岐をする

コンポーネント

  • 再使用可能なVueインスタンスを作ること
  • 機能ごとにJavaScriptとテンプレートを1つのセットにして、他の機能とは分離して開発できるようにする仕組み
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue + Wordpress + Heroku + S3でポートフォリオを構築する

1. はじめに

こんにちは。ツダと申します。私はカメラが趣味で、自分の写真のポートフォリオサイトを作成したいと思い、Vue.jsとWordpressを使って作成しました。

Tsuda Work

この記事では、私がポートフォリオを作るうえで行ったことについて紹介させていただければと思っています。

2. 技術スタック

  • Vue.js : @vue/cli 4.5.6
  • Wordpress : 5.5.1
  • PHP : 7.3.5
  • heroku : heroku/7.44.0 win32-x64 node-v12.16.2
  • S3

簡単なシーケンス図(トップ画面表示時)

Tsudawork.png

3. Wordpress環境を整える

3.1 概要

今回のポートフォリオでは、WordpressをAPIとして使用します。 ですので、フロントエンド側はVueを、バックエンド側はWordpressというような役割分担をするイメージです。

参考:WP REST API

3.2 Localbyflywheelをダウンロード

まずはWordpress環境を構築します。環境構築にはLocalbyFlywheelを用います
。環境構築方法については以下のサイトを参考にしていただければと思います。

3.3 DB設定

後述しますが、デプロイするサーバは「heroku」を使います。herokuで使用するDBとローカル環境で使用するDBが異なりますので、wp-config.phpに条件分岐を書きます。wp-config.phpは作成したWordpressプロジェクトのルートフォルダに配置してあります(Windowsの場合、C:\Users\<ユーザ名>\Local Sites\<作成したアプリ名>\app\public配下)

wp-config.php
...(省略)...

// ** MySQL settings - You can get this info from your web host ** //
if (
    @$_SERVER["SERVER_NAME"] === '<作成したWordpressのローカル環境のドメイン名>'
) {
    // ローカル環境の設定
    define( 'DB_NAME', 'local' );
    define( 'DB_USER', 'root' );
    define( 'DB_PASSWORD', 'root' );
    define( 'DB_HOST', 'localhost' );
} else {
    // heroku環境の設定
    $url = parse_url(getenv('CLEARDB_DATABASE_URL'));
    define('DB_NAME', trim($url['path'], '/'));
    define('DB_USER', $url['user']);
    define('DB_PASSWORD', $url['pass']);
    define('DB_HOST', $url['host']);
}
define( 'DB_CHARSET', 'utf8' );
define( 'DB_COLLATE', '' );

...(省略)...

参考:PaaS入門 〜Heroku + wordpress〜

3.4 セキュリティ面の設定

WordPressはあくまでAPIとしての役割で使用するため、WordPressサイトにはアクセスされたくありません。そこで、直接アクセスされるのを防ぐための各種設定を行います。

ログイン画面のパスを変更する

WordPressはログイン画面へのパスがデフォルトで "/wp-admin" として設定されています。しかし、これではログイン画面へのアクセスが容易にできてしまうので、これを変更するためのプラグイン「Login Rebuilder」が用意されています。

参考: Login Rebuilderの使い方|WordPressのログインURLの変更方法

サイトへのアクセスを制御する

次にサイトへのアクセスを制御します。何も設定していない場合、サイトへアクセスした際に記事が表示されてしまいますので、表示されないように設定します。

設定するためには、設定しているテーマのfunctions.phpに下記のコードを追加します。

functions.php
function access_restriction() {
    // 許可するアクセス
    // 管理画面
     if (is_admin()) {
         return;
    }
    // APIのアクセス
    if (strpos($_SERVER['REQUEST_URI'],'wp-json')) {
       return;
    }
    // ログイン画面
    if (strpos($_SERVER['REQUEST_URI'],'<Login Rebuilderで設定したログイン画面へのパス>')) {
       return;
    }
     wp_die('アクセスできません', 'アクセス拒否', array('responce' => 403));
 }
 add_action('init', 'access_restriction');

管理画面、APIへのアクセス、およびログイン画面へのアクセスは必要なので許可し、それ以外を拒否するように設定しています。実際にアクセスしてみると下のような拒否画面が現れます。

WS000021.JPG

参考:WordPress へのアクセス制限を functions.php で設定する

3.5 S3を使うための設定をする

次の記事に沿ってWordpressでS3を使用するための準備を行います。

Heroku + WordpressでS3を使用する

3.6 Herokuへデプロイする

まずは下記の記事を参考に、Herokuが使える状態にします。

参考:Heroku初心者がHello, Herokuをしてみる

そして下記の記事を参考に、作成したWordpressプロジェクトをHerokuへデプロイします。

参考:PaaS入門 〜Heroku + wordpress〜

主に次のことをやる必要があります。

  • Wordpressのルートディレクトリで「git init」+「git commit -m "commit"」
  • Wordpress用のプロジェクトアプリ作成
  • composer.jsonおよびcomposer.lockの作成
  • DB作成
  • デプロイ

3.7 パーマリンクの設定

WP REST APIにリクエストを投げると、デフォルトの状態では404エラーが返ってくるそうです。そのためにパーマリンクの設定をしてあげます。

※私の環境でこの設定をしなかったところ、 管理画面からの記事の編集・投稿ができませんでしたので設定することがおすすめです。

設定方法

設定方法はいたって簡単です。Wordpressの管理画面左にあるタブから「設定→パーマリンク設定」と進み、共通設定の「投稿名」にチェックをいれるだけとなっております。

WS000030.jpg

これで404エラーの原因となるパーマリンク設定が解消されるはずです。

参考:WP REST APIで404が返ってくる。これはパーマリンク設定のせいだ!

4. Vue.jsでプロジェクトを作成していく

4.1 環境構築

まずVueの環境構築を行います。Vueを使用する方法は主に二つあります

  • CDN
  • Vue CLI

ここではVue CLIを用いた方法を選択します。「参考」に記載させていただいた記事をもとに、「Welcome to Your Vue.js App」が表示されればOKです。

参考:Vue.js を vue-cli を使ってシンプルにはじめてみる

4.2 必要なモジュールをインストール

  • axios
$ npm install axios
  • bootstrap-vue
$ npm install vue bootstrap-vue bootstrap
  • vue-router
$ npm install vue-router

参考:
axios - npm
Getting Started | BootstrapVue
Installation | Vue Router

4.3 アプリの全体像

アプリの全体像は次のようになっています。

main.js
import Vue from 'vue'
import Articles from './Articles.vue'
import Article from './Article.vue'
import Router from 'vue-router'
import Top from './Top.vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// Router
Vue.use(Router)

// Install BootstrapVue
Vue.use(BootstrapVue)

// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)


const router = new Router({
  mode:'history',
  routes: [
    {
      path: '/',
      component: Articles
    },    
    {
      path: '/category/:value',
      name: 'category',
      component: Articles,
    },
    {
      path: '/post/:value',
      name: 'post',
      component: Article
    }
  ]
})

Vue.config.productionTip = false

new Vue({
  render: h => h(Top),
  router
}).$mount('#app')
Top.vue
<template>
  <div id="app">
    <b-navbar class="navbar bg-white" fixed="top" toggleable="lg">
      <b-navbar-brand href="/">Tsuda Work</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
              <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
              <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
                <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
              </b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
        <b-navbar-nav class="ml-auto">
          <b-nav-item class="nav-item" href="https://twitter.com/tsuda215">
            <div>Twitter</div>
          </b-nav-item>   
        </b-navbar-nav>
      </b-collapse>
    </b-navbar> 
    <router-view class="article-router" :key="$route.path"></router-view>
    <footer class="page-footer font-small blue pt-4">
      <div class="footer-copyright text-center py-3">© 2019-2020 Tsuda Work.</div>
    </footer>
  </div>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Top',
  data() {
      return {
        categories: [],
      }
  },
  mounted() {
    const url = states.hostname + states.categoriesUrl;
    (async () => {
      try {
        // カテゴリー取得
        const res = await axios.get(url);
        var categoriesTmp = [].concat(res.data);
        for (var categoryTmp of categoriesTmp) {
          // サブカテゴリ―の場合は処理をスキップ
          if (categoryTmp.parent > 0) {
            continue;
          }

          // カテゴリー作成
          var category = {};
          category['id'] = categoryTmp.id;
          category['name'] = categoryTmp.name;
          category['subCategories'] = [];

          // サブカテゴリー取得
          const resForSubCategories = await axios.get(url + "?parent=" + categoryTmp.id);
          var subCategoriesTmp = [].concat(resForSubCategories.data);

          for (var subCategoryTmp of subCategoriesTmp) {
            // サブカテゴリー作成
            var subCategory = {};
            subCategory['id'] = subCategoryTmp.id;
            subCategory['name'] = subCategoryTmp.name;
            // サブカテゴリ―配列に追加
            category['subCategories'].push(subCategory);
          }

          // カテゴリー配列に追加
          this.categories.push(category);
        }
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
#app {
  display: flex;
  flex-direction: column;
  min-height: 82vh;
  font-family: "Yu Gothic","Yu Gothic UI","Meiryo UI", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "MS PGothic", sans-serif; 
}
.navbar {
  background-color:rgba(0, 0, 0, 0);
}
.page-footer {
  margin-bottom:0;  
}
@media screen and (orientation:portrait) {
  .article-router {
    margin-top:10vh;
  }
}
@media screen and (orientation:landscape) {
  .article-router {
    margin-top:7vw;
  }
  @media screen and (max-width: 800px){
    .article-router{
      margin-top:9vw;
    }
  }
}
</style>
Articles.vue
<template>
  <div id="app">
    <transition name="fade">
      <!-- フェードイン実装のためv-ifは必要 -->
      <div v-if="ok" class="album py-5">
        <div class="container">
          <div class="row">
            <div class="col-md-4 mb-4" v-for="post in posts" :key="post.title.rendered">
              <div class="card h-100 shadow-sm">
                <router-link class="nav-link" :to="{name:'post', params:{value:post.id}}">
                    <img class="card-img-top" :src="post._embedded['wp:featuredmedia'][0].source_url" alt="">
                </router-link>
              </div>
            </div>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Articles',
  data() {
      return {
        posts: [],
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var categoryId = this.$route.params.value;
    var url = states.hostname + states.postsUrl;
    if (categoryId != undefined) {
      url = states.hostname + states.categoryUrl + categoryId + '&_embed';
    }
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
.card {
  display: flex;
  justify-content: center; /*左右中央揃え*/
  align-items: center; /*上下中央揃え*/
}

.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>
Article.vue
<template>
  <transition name="fade">
    <div v-if="ok" class="container">
      <div class="row">
        <div class="articleContainer">
          <div class="article">
            <h3 class="title">{{post.title.rendered}}</h3> 
            <div class="content" v-html="post.content.rendered">
            </div>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Article',
  data() {
      return {
        post: null,
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var postId = this.$route.params.value;
    const url = states.hostname + states.postUrl + postId + '?_embed';
    (async () => {
      try {
        const res = await axios.get(url);
        this.post = res.data;
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

<style>
.articleContainer {
  display: flex;
  justify-content: center; /*左右中央揃え*/
  align-items: center; /*上下中央揃え*/
  width: 100%;
}

.article > .title {
  text-align: center;
}

.article img {
  width: 100%;
}

@media screen and (min-width: 800px){
    .article{
        width:60%;
    }
}

@media screen and (max-width: 799px){
    .article{
        width:85%;
    }
}

/* フェードインの設定 */
.fade-enter-active
/* , .fade-leave-active */
{
  transition: opacity 2s;
}
.fade-enter
/* , .fade-leave-to */
{
  opacity: 0;
}
.fade-leave-active {
  display:none;
}

</style>
property.json
{
    "hostname":"https://<Wordpressプロジェクトのホスト名>/",
    "postUrl":"wp-json/wp/v2/posts/",
    "postsUrl":"wp-json/wp/v2/posts?_embed",
    "categoryUrl":"wp-json/wp/v2/posts?categories=",
    "categoriesUrl":"wp-json/wp/v2/categories"
}

※注意

  • ホスト名を間違えると"No 'Access-Control-Allow-Origin' header is present on the requested resource."という旨のエラーが出ることを確認しました。
  • httpsをhttpにして設定すると、Heroku上のVueから画像が取得できないことを確認しました。

4.4 アプリの解説

Navbar

Navbarを使用する前に、vueでbootstrapを使用できるようにする必要があります。

参考:Getting Started | BootstrapVue

インストールコマンドは次の通りです。

# With npm
npm install vue bootstrap-vue bootstrap

# With yarn
yarn add vue bootstrap-vue bootstrap

インストールした後はmain.jsにてbootstrapを使えるように設定しています。

main.js
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

...(省略)...

// Install BootstrapVue
Vue.use(BootstrapVue)

// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)

次にメニュー部分の実装に入ります。
参考:Navbar

Top.vue
<template>
  <div id="app">
    <b-navbar class="navbar bg-white" fixed="top" toggleable="lg">
      <b-navbar-brand href="/">Tsuda Work</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
              <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
              <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
                <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
              </b-dropdown-item>
          </b-nav-item-dropdown>
        </b-navbar-nav>
        <b-navbar-nav class="ml-auto">
          <b-nav-item class="nav-item" href="https://twitter.com/tsuda215">
            <div>Twitter</div>
          </b-nav-item>   
        </b-navbar-nav>
      </b-collapse>
    </b-navbar> 
    <router-view class="article-router" :key="$route.path"></router-view>
    <footer class="page-footer font-small blue pt-4">
      <div class="footer-copyright text-center py-3">© 2019-2020 Tsuda Work.</div>
    </footer>
  </div>
</template>

ポイントは次の部分です。

Top.vue
<b-nav-item-dropdown :text="category.name" class="nav-item" v-for="category in categories" :key="category.id">
    <b-dropdown-item><router-link class="nav-link" :to="{name:'category', params:{value:category.id}}">All</router-link></b-dropdown-item>
    <b-dropdown-item :text="subCategory.name" class="nav-item" v-for="subCategory in category.subCategories" :key="subCategory.id">
      <router-link class="nav-link" :to="{name:'category', params:{value:subCategory.id}}">{{subCategory.name}}</router-link>
    </b-dropdown-item>
</b-nav-item-dropdown>

親カテゴリはAllとして必ず作成するようにしています。そしてサブカテゴリの数だけループを回し、追加のドロップダウンアイテムを作成しています。カテゴリ作成ロジックは次のようにしています。

Top.vue
mounted() {
  const url = states.hostname + states.categoriesUrl;
  (async () => {
    try {
      // カテゴリー取得
      const res = await axios.get(url);
      var categoriesTmp = [].concat(res.data);
      for (var categoryTmp of categoriesTmp) {
        // サブカテゴリ―の場合は処理をスキップ
        if (categoryTmp.parent > 0) {
          continue;
        }

        // カテゴリー作成
        var category = {};
        category['id'] = categoryTmp.id;
        category['name'] = categoryTmp.name;
        category['subCategories'] = [];

        // サブカテゴリー取得
        const resForSubCategories = await axios.get(url + "?parent=" + categoryTmp.id);
        var subCategoriesTmp = [].concat(resForSubCategories.data);

        for (var subCategoryTmp of subCategoriesTmp) {
          // サブカテゴリー作成
          var subCategory = {};
          subCategory['id'] = subCategoryTmp.id;
          subCategory['name'] = subCategoryTmp.name;
          // サブカテゴリ―配列に追加
          category['subCategories'].push(subCategory);
        }

        // カテゴリー配列に追加
        this.categories.push(category);
      }
    } catch (error) {
      console.log(error);
    }
  })();
},

記事表示部分

記事の表示は次の部分です。

Articles.vue
<div class="container">
  <div class="row">
    <div class="col-md-4 mb-4" v-for="post in posts" :key="post.title.rendered">
      <div class="card h-100 shadow-sm">
        <router-link class="nav-link" :to="{name:'post', params:{value:post.id}}">
            <img class="card-img-top" :src="post._embedded['wp:featuredmedia'][0].source_url" alt="">
        </router-link>
      </div>
    </div>
  </div>
</div>

こちらもbootstrapの使用が必要になります。

フェードインの実装

記事の一覧を表示する際にフェードインを使用しています。
参考:Enter/Leave & List Transitions

フェードインの実装にはvueのtransitionという機能を使用しています。transitionを使用するためにはフェードさせたい要素をで囲む必要があります。

Articles.vue
<transition name="fade">
  <!-- フェードイン実装のためv-ifは必要 -->
  <div v-if="ok" class="album py-3">
...(省略)...
  </div>
</transition>

v-ifに指定の"ok"はデフォルトでfalseとなっています。

Articles.vue
data() {
    return {
      posts: [],
      // フェードイン実装のために必要
      ok: false
    }
},

そしてマウント時にtrueとすることで、要素を表示させます。

Articles.vue
mounted() {
...(省略)...
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },

表示の際は下記のcssを使用しているので、フェードインでの表示となります。

Articles.vue
<style>
...(省略)...
.fade-enter-active, .fade-leave-active {
  transition: opacity 1s;
}

.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>

Wordpress REST APIを用いて記事取得

記事を取得する際はWordpress REST APIにリクエストを投げますが、リクエストを投げる際はaxiosを使用しています。

Articles.vue
<script>
import axios from 'axios';
import states from "./assets/property.json";
export default {
  name: 'Articles',
  data() {
      return {
        posts: [],
        // フェードイン実装のために必要
        ok: false
      }
  },
  mounted() {
    var categoryId = this.$route.params.value;
    var url = states.hostname + states.postsUrl;
    if (categoryId != undefined) {
      url = states.hostname + states.categoryUrl + categoryId + '&_embed';
    }
    (async () => {  
      try {
        const res = await axios.get(url);
        this.posts = this.posts.concat(res.data);
        // マウント時にok=trueを実施
        this.ok = true;
      } catch (error) {
        console.log(error);
      }
    })();
  },
}
</script>

実際にリクエストを投げているのは次の部分です。

const res = await axios.get(url);

記事のIDを取得したときなど、単数を指定するものはObject、IDを指定せず記事を複数取得した場合はObjectの配列がres.dataには格納されています。後は取得したものを加工し、vueで描画すればOKです。

参考:《WordPress》2017年末にWP REST API で取得してVue.jsで描画するまでのまとめ。

4.5 アプリをHerokuにデプロイする。

「参考」に記載の記事をもとに、大まかには下記の作業を行う必要があります。

  • expressのインストール
  • server.jsの作成
  • デプロイ

これらの作業を完了しアプリを表示すると、Wordpressで作成したアプリに対してリクエストが飛んでいるはずです。無事記事の一覧が表示されていればOKです。

参考:Vue.jsで作ったアプリをHerokuにデプロイ

4.6 リロードの設定

Vueアプリはデフォルトでrouterのパスに「ハッシュ(#)」が含まれています。これはrouterを「historyモード」に変更することで削除することができますが、historyモードを使用することでページをリロードした際に、404エラーが返却されてしまいます(historyモードでない場合にはこの問題は発生しません)。

しかしながら一点問題があります。シングルページのクライアントサイドアプリケーションなので、適切なサーバーの設定をしないと、ユーザーがブラウザで直接 http://oursite.com/user/id にアクセスした場合に 404 エラーが発生します。- HTML5 History モード

しかし、適切な設定をすれば「ハッシュを削除しつつ、リロードにも対応する」ことが可能となります。

connect-history-api-fallbackをインストールする

npm install --save connect-history-api-fallback

参考:connect-history-api-fallback

server.jsにコードを追加する

server.js

var express = require('express');
var path = require('path');


+ const history = require('connect-history-api-fallback');


var serveStatic = require('serve-static');
app = express();


+ app.use(history());


app.use(serveStatic(__dirname + "/dist"));
var port = process.env.PORT || 5000;
app.listen(port);
console.log('server started '+ port);

こちらローカル環境では動作しておりませんが、Herokuでは動作することを確認しております。

参考:vue-routerのルーティングURLからハッシュを除去しつつ、URL直接指定でも表示させる(Node, Express)

5. その他

5.1 スリープ防止のため、スケジューラ作成

Herokuのサイトは一定時間が経過すると、スリープ状態に入ってしまうため、次にアクセスする際にサイトが表示されるまで時間がかかるという問題が発生してしまいます。ここではそれを防止する方法について紹介します。

参考:herokuを24時間稼働させる設定

herokuにスケジューラを追加

まずはアプリケーションフォルダ配下で下記コマンドを実行し、スケジューラのアドオンを追加していきます。

$ heroku addons:create scheduler:standard

下記のように表示されれば成功です。

Creating scheduler:standard on ⬢ <アプリ名>... free
 To manage scheduled jobs run:
 heroku addons:open scheduler

Created <スケジューラの名前>
Use heroku addons:docs scheduler to view documentation

ジョブを作成する

続いて下記コマンドを実行して、ジョブの作成画面を表示します。

$ heroku addons:open scheduler

すると下記のような画面が表示されるはずです。

WS000013.jpg

「Create Job」を押下します。

WS000018.jpg

すると下記のようなウィンドウが表示されますので、

Schedule : Every 10 minutes
Run Command : curl <サイトのURL>

を入力し、「Save Job」を押下します。
これでジョブの作成は完了です。後は時間をおいてもう一度サイトにアクセスしてみると、表示が遅い問題が解消されているはずです。

5.2 Wordpressでアップロードできる画像の大きさを上げる

HerokuにWordpressをデプロイした直後は2MBまでの画像しかアップロードすることができません。この最大値を上げるための設定を行います。

.user.iniの追加

Herokuにはphp.iniの設定を書き換える方法として.user.iniの作成を挙げています。
参考:Customizing Web Server and Runtime Settings for PHP

具体的には、まず下記のようなファイルを作成します。

.user.ini
post_max_size = 100M
upload_max_filesize = 100M

そしてこのファイルをWordpressのルートディレクトリに配置します。後はアプリをデプロイするとHerokuが自動で設定値を認識し、画像のアップロードサイズが増加しているはずです。

6. まとめ

以上がVue + Wordpressを用いてポートフォリオを作成した手順となります。初めてVueを用いてWebサイトを作成しましたが、APIの併用もしたことで大変勉強になりました。簡単にはなってしまいますが、以上で記事を締めさせていただこうと思います。最後に、参考にさせていただいた記事の執筆者の方々、本当にありがとうございます。そして勝手に引用させていただき恐縮です。

備考

今回紹介させていただいたポートフォリオは無料で作成することが可能となっております。しかしSSLを常用化したり、独自ドメインを設定したい場合は有料となってしまいます。

参考させていただいたリンクまとめ

Wordpress

Vue.js

その他

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

?【Nuxt.js】Nuxt.jsプロジェクトをnpm run devしたらエラーが出て動かないときの解決法

環境

Windows 10
npm 6.14.6
Nuxt.js v2.14.6

やりたいこと

Nuxt.jsプロジェクトをpullしてきてVSCodeのターミナルでnpm run devしたら
いきなり大量のエラーが吐かれて実行できない、、、のでこれを解消する

PS C:\hoge\{プロジェクト名}> npm run dev

> {プロジェクト名}@1.0.0 dev C:\hoge\{プロジェクト名}
> nuxt

操作可能なプログラムまたはバッチ ファイルとして認識されていません。
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! {プロジェクト名}@1.0.0 dev: `nuxt`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the {your_project}@1.0.0 dev script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\{Windowsアカウント名}AppData\Roaming\npm-cache\_logs\2020-10-10T08_18_23_229Z-debug.log

やったこと

いきなりエラーが大量に出てきて怖いですが大丈夫です、ただ必要な物がインストールされていないだけです
慌てず、急がず、とにかくnpm installを打ちます

PS C:\hoge\{プロジェクト名}> npm install

色々インストールされる
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                                     :-:
                                   .==-+:
                                  .==. :+- .-=-
                                 .==.   :==++-+=.
                                :==.     -**: :+=.
                               :+-      :*+++. .++.
                              :+-      -*= .++: .=+.
                             -+:      =*-   .+*: .=+:        
                            -+:     .=*-     .=*-  =+:       
                          .==:     .+*:        -*-  -+-      
                         .=+:.....:+*-.........:=*=..=*-     
                         .-=------=++============++====:     

                          Thanks for installing nuxtjs 
                 Please consider donating to our open collective
                        to help us maintain this package.

                           Number of contributors: 229
                              Number of backers: 377
                            Annual budget: US$ 67,637
                           Current balance: US$ 38,286

これが出てきたらNuxtインストール完了
あとは心置きなくnpm run devしましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsとVueの一連の流れについてのメモ書き

RailsとVueに関して今まで流れがわからなかったのですが、
色々いじっていたらようやく流れが理解できてきたので見返せるようにメモ書きをします。
初学者なので理解が間違っている可能性が非常に高いため参考程度に!
一般的な正解ではなく、あくまで私の理解と納得解です。

環境

MacOS Mojave
Ruby 2.6.4
Rails 6.0.3.3
Vue 2.6.12
Webpack 4.44.2
yarn 1.22.5
Docker 2.3.0.5
VScode
Vuetify
Vue-router
Vue-Store

ブラウザからroutes.rbまで

この記事では、以下のURLを使います。
http://localhost:3000/
http://localhost:3000/user

はじめに、WebブラウザからURLを入力します。
http://localhost:3000/

routes.rb
Rails.application.routes.draw do

  root to: 'home#index'
  get  "/user", to: 'home#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  end

ブラウザからRailsサーバへURLが送られます。
http://localhost:3000/のURLを指定しているため
routes.rbの"root to: 'home#index'"の読み込み先が
参照されます。

ルートからコントローラー

homeコントローラーのindexの関数が呼び出されます。

home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

ちなみに正しいか分かりませんがroutes.rbも参照されるコントローラーも
毎回インスタンスが作成されている認識です。
だからルート間やコントローラ間で関連性がないのです。

本題に戻りますが、indexの関数が呼び出されます。
今回何も無いのでindexにマッチするVeiwファイルが参照されますが、
index関数内に@home = Home.allのようなモデルのインスタンスを作成する
プログラムがあった場合、モデルインスタンスが作成されて、
必要あればモデルがデータベースから情報を取得して@homeインスタンスに格納する
ステップがあるはずですが、今回は使用しません。流れとして覚えておきます。

コントローラーからVeiwファイル(erbファイル)

コントローラーのindex関数が対応するファイルを参照します。
veiw/home/index.html.erbを参照します。

ここでは以下の設定が投入されていますが、
これは、app/javascript/pack内のhello.vueファイルを参照することを意味します。

index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

Veiwファイル(erbファイル)からhello.vue(jsファイル)

Vueアプリケーションを使用するための根幹のファイルですね。
拡張子がVueの各ファイルはオブジェクトでまとまってこのファイルに
結合されるイメージです。

render: h => h(App)

で分かりますがapp.vueのテンプレートを
呼ぶことで app.vueの内容が表示されます。

このファイルはVueの各機能を使えるようにしてくれるものでして
このファイル自体にプロントの内容が記入されているわけではないです。
裏側で管理してくれる感じのファイルです。

    vuetify,
    router,
    store,

を定義してVueで使えるようにしています。

親コンポーネントファイルはAppとしています。
SPAを実現しているのはrouterで
データを格納するのがvuexで
UIのフレームワークがvuetifyです。

hello_vue.js
import Vue from 'vue';
import App from '../src/components/app/app';
import Vuetify from 'vuetify';
import router from '../src/router';
import store from "../src/vuex/index"
import 'vuetify/dist/vuetify.min.css';


Vue.use(Vuetify);
const vuetify = new Vuetify();

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    vuetify,
    router,
    store,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})

次に見ていくのはAppファイルです。

hello.vue(jsファイル)からapp.vue(vueコンポーネント)

すごくシンプルにしていますが、
このファイルが親コンポーネントでこのファイルがブラウザに表示されます。
しかし

<router-view></router-view>

がいることで
hello_vueでroute機能を持たせているためrouteが待ったを掛けます。

app.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>

app.vue(vueコンポーネント)からrouter.js(URLによってコンポーネントを決める)

route機能を持つとURL情報に従って指定のコンポーネントを返すようになります。

router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from "../src/components/home/Home"
import User from "../src/components/user/User"



Vue.use(VueRouter);

export default new VueRouter({
    mode: 'history',
    routes: [{
        path: '/',
        component: Home
    }, {
        path: '/user',
        component: User
    }]
});

ブラウザでhttp://localhost:3000/
を打ちましたので

{
        path: '/',
        component: Home
    }

によりHomeのコンポーネントを返すように決まりました。
はじめのほうのroutes.rbにて

  root to: 'home#index'

のルート情報と連動しているのでURL情報をvueでも取得できています。

router.js(URLによってコンポーネントを決める)からHome.vue(子コンポーネント)

VueのルートにてURL情報から表示するコンポーネントを決めたので
ここでの対象コンポーネントであるHome.vueを見ていきます。

Home.vue
<template>
    <div>
        <p>Home</p>
        <p>{{ store}}</p>

    </div>
</template>


<script>
export default {
    computed: {
        store(){
            return this.$store.state.store
        }
    }
    }
</script>

templateは実際にブラウザに表示される部分でscriptはjsの処理の部分ですね。
template内の

<p>{{ store }}</p>

を表示したいのでscript処理内を見ていきます。

computed: {
    store(){
        return this.$store.state.store
    }
}

と書いてありますが、これは、Vuexを定義しているファイルの
state.storeのデータを持ってきてと言っていますのでvuexを定義しているファイルを見ていきます。

Home.vue(子コンポーネント)からindex.js(vuexを定義したファイル)

index.js
import Vuex from 'vuex';
import Vue from 'vue';

Vue.use(Vuex);


export default new Vuex.Store({
    state: {
        store: "Store"
    }
});

ファイル名が分かりにくくすいませんがvuexを定義したファイルです。

store:"Store"

"Store"をstoreで定義しているので、どのコンポーネントからでもstoreと書けば"Store"を呼び出せます。

index.js(vuexを定義したファイル)からHome.vue(子コンポーネント)

再びHome.vueへ戻ります。

<p>{{ store }}</p>

を表示したいのでscript処理内を見ていきます。

computed: {
    store(){
        return this.$store.state.store
    }
}

scriptでvuexで"Store"を持ってきて、

{{ store }}

に入れて表示するという流れです。

Home.vue(子コンポーネント)からapp.vue(vueコンポーネント)

RouteでURLのコンポーネント(Home)が決まったと思います。
それをapp.vueにあった、に代入します。

app.vue
<template>
  <div>
    <router-view></router-view>
  </div>
</template>

下の

<router-view></router-view>

Home.vue
<template>
    <div>
        <p>Home</p>
        <p>{{ store}}</p>

    </div>
</template>


<script>
export default {
    computed: {
        store(){
            return this.$store.state.store
        }
    }
    }
</script>

に代入されます。

app.vue(vueコンポーネント)からhello_vue.js

はじめの管理ファイルに戻ってきます。
app.vueファイルが全てオブジェクトになってhello_vue.jsに渡ってきます。

hello_vue.js
import Vue from 'vue';
import App from '../src/components/app/app';
import Vuetify from 'vuetify';
import router from '../src/router';
import store from "../src/vuex/index"
import 'vuetify/dist/vuetify.min.css';


Vue.use(Vuetify);
const vuetify = new Vuetify();

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    vuetify,
    router,
    store,
    render: h => h(App)
  }).$mount()
  document.body.appendChild(app.$el)

  console.log(app)
})

以下のレンダー関数により

render: h => h(App)

全てのVueファイルがオブジェクトになって返されます。

hello_vue.jsからVeiwファイル(erbファイル)

やっとRails側に戻ってきます。
全てのVueファイルがindex.html.erbに結合されます。

index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>

Veiwファイル(erbファイル)からコントローラー

ちょっと曖昧なのですが、最後にVeiwファイルをまとめてコントローラーがブラウザに返します。

スクリーンショット 2020-10-10 15.57.04.png

ブラウザにhttp://localhost:3000/を打ち込んだら
Home.vueに記載されているHomeとStoreが返されました。

同じように別のURLを打ったら?

別のURLで試してみます。
http://localhost:3000/user
上記のURLをブラウザで打ち込みます。

get  "/user", to: 'home#index'

が使用されます。
コントローラーは同じで、erbも同じですので割愛します。

routes.rb
Rails.application.routes.draw do

  root to: 'home#index'
  get  "/user", to: 'home#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  end
router.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from "../src/components/home/Home"
import User from "../src/components/user/User"



Vue.use(VueRouter);

export default new VueRouter({
    mode: 'history',
    routes: [{
        path: '/',
        component: Home
    }, {
        path: '/user',
        component: User
    }]
});

vue側のrouter.jsで以下のURLとコンポーネントが使用されます。
今回はUserコンポーネントです。

{
        path: '/user',
        component: User
    }

Userコンポーネントは以下のようにシンプルです。
あとは同じようにrouteがapp.vueに結合してくれてhello_vueで表示してくれます。
後は同じ手順です。

User.vue
<template>
    <div>
        <p>User</p>
    </div>
</template>

スクリーンショット 2020-10-10 16.04.02.png

Userが表示されました。

因みに、routerを使っているのになぜSPAにならないのか
(/と/user)で2回サーバにアクセスしに行っているのかですが、
ブラウザに直接URLを打ち込んでいるからです。

Home.vueやUser.vueで各コンポーネントに接続できるようにプログラムすれば
SPAを実現できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue の style 内で Stylus 利用時に 絶対パスで import する

1.絶対パスで import する ... けどうまくいかん

Nuxt.js とかだとデフォルトで ~ でうまくいくんだけど、
Vue.js だと @ でうまく指定できずずっと悩んでた。
とりあえずシンプルに解決したのでメモ。

2.結論

@ の前に、~ をつける。

3.例えば ...

グローバルな .stylimport したい。

失敗例

hogehoge.vue
<style lang="stylus" scoped>
@import "@/assets/css/_mixins.styl"
</style>

TypeScript でプロジェクトを作成していると、プロジェクト直下の tsconfig.json に以下の文がある。

tsconfig.json
{
  "compilerOptions":
・・・中略
  "paths": {
    "@/*": [
      "src/*"
    ]
  },

なので、@ つければいけるかと思いきや、失敗。
なぜかは今はわからないけど、~ をさらにつけることで解決。

成功例

hogehoge.vue
<style lang="stylus" scoped>
- @import "@/assets/css/_mixins.styl"
+ @import "~@/assets/css/_mixins.styl"
</style>

なんでや。

追記 (2020/10/11 16:21時点)

上記の ~@ という記述方法はどうやら、 Stylus で適用されるらしい。
script 内では結局 @ だけでいいみたい。

hogehoge.vue
・・・中略
<script lang="typescript">
import { defineComponent, computed } from "vue"
export default defineComponent({
  const data = reactive({
    style: computed(() => {
      return {
        backgroundImage: `url(${require(`@/assets/graphics/unit/unit1_1.png`)})`
      }
    })
  })
})
</script>

~ これはなんだ・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js(TypeScript)でvue-click-outsideライブラリを使う

Vue.js(TypeScript)で@types/vue-click-outsideが存在せず、インポートに苦戦したのでメモ書き程度に残しておきます。

結論

require('vue-click-outside')で読み込んで、directivesに記載

Compornent.vue
<template>
  <div>
    <button v-click-outside="click">クリック</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue} from 'nuxt-property-decorator'
const ClickOutside = require('vue-click-outside')

@Component({
  directives: {ClickOutside}
})

export default class Compornent extends Vue {
  click() {
    alert('click');
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】動的に数が変わる複数フォームの入力バインディング

概要

フォームが複数ある場合に、入力バインディングする方法をまとめました。
基本的な内容ですが、備忘録も兼ねて初心者の方向けに書きたいと思います。

課題

例えば、フォームの数が固定されている場合は、各フォームに対応する分だけdataオブジェクトにプロパティを用意することで実装できます。
しかし、フォーム数が動的に変わる場合は、フォーム数に応じてdataオブジェクトにプロパティを追加する必要があるので面倒です。

また、プロパティ数が多いのは見栄えが悪いです。

フォーム数が固定されている場合
<template>
  <div>
    <!-- ループ数固定でv-forを使った場合 -->
    <div v-for="n in 3" :key="n">
      <input type="checkbox" :value="n" v-model="$data[`hoge${n}`]" />
      チェックボックス{{ n }}
    </div>

  <!-- べた書きした場合 -->
    <div>
      <input type="checkbox" value="4" v-model="hoge4" />
      チェックボックス4
    </div>
    <div>
      <input type="checkbox" value="5" v-model="hoge5" />
      チェックボックス5
    </div>
    <div>
      <input type="checkbox" value="6" v-model="hoge6" />
      チェックボックス6
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hoge1: undefined,
      hoge2: undefined,
      hoge3: undefined,
      hoge4: undefined,
      hoge5: undefined,
      hoge6: undefined
    }
  }
}
</script>

解決方法

解決法としては、dataオブジェクトに一つのプロパティを用意し、配列の要素として入力バインディングしていきます。

チェックボックスの場合

dataオブジェクトのプロパティとしてhogeを用意し、初期値を配列にしておきます。
v-modelにはhogeを指定します。

<template>
  <div>
    <!-- loopの値によってループ数が変動 -->
    <div v-for="n in loop" :key="n">
      <input type="checkbox" v-model="hoge" />
      チェックボックス{{ n }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hoge: [], // チェックを付けるごとに順に要素が追加される。(例)[1,3]
      loop: 3
    }
  }
}
</script>

ラジオボタンの場合

dataオブジェクトのプロパティとしてhogeを用意し、v-modelにはhoge[n - 1]を指定します。
配列の要素ごとにフォームを対応付けます。

<template>
  <div>
    <!-- loopの値によってループ数が変動 -->
    <div v-for="n in loop" :key="n">
      ラジオボタン{{ n }}
      <input type="radio" :name="'name' + n" value="A" v-model="hoge[n - 1]" />A
      <input type="radio" :name="'name' + n" value="B" v-model="hoge[n - 1]" />B
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hoge: [], //(例)上から順にフォームに「A」「B」「A」を選択すると、["A","B","A"]
      loop: 3
    }
  }
}
</script>

テキストフィールドの場合

dataオブジェクトのプロパティとしてhogeを用意し、v-modelにはhoge[n - 1]を指定します。
配列の要素ごとにフォームを対応付けます。

<template>
  <div>
    <!-- loopの値によってループ数が変動 -->
    <div v-for="n in loop" :key="n">
      <input type="checkbox" v-model="hoge[n - 1]" />
      チェックボックス{{ n }}
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hoge: [], // (例)上から順にフォームに「A」「B」「C」と入力すると、["A","B","C"]
      loop: 3
    }
  }
}
</script>

セレクトボックスの場合

dataオブジェクトのプロパティとしてhogeを用意し、v-modelにはhoge[n - 1]を指定します。
配列の要素ごとにフォームを対応付けます。

<template>
  <div>
    <!-- loopの値によってループ数が変動 -->
    <div v-for="n in loop" :key="n">
      セレクトボックス{{ n }}
      <select v-model="hoge[n - 1]">
        <option value="A">A</option>
        <option value="B">B</option>
        <option value="C">C</option>
      </select>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hoge: [], //(例)上から順に「A」「B」「C」を選択すると、["A","B","C"]
      loop: 3
    }
  }
}
</script>

まとめ

プロパティを一つ一つ用意するのか、まとめられるところは配列としてまとめるのか、使い分けが大事だと思います。
今回は配列にまとめる方法を書きました。
誰かの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む