20191218のvue.jsに関する記事は30件です。

VueのエラーハンドラーとAxiosで快適な例外処理を行う

こんなことありませんか?

  • 例外処理忘れてて、コンソールに流れていた:scream_cat:
  • 例外発生時にアラートを表示するだけで4行も余分にコードを書かないといけなくて辛い:crying_cat_face:
XXX.vue
methods: {
  onClick() {
    try {
      this.something();
    } catch (error) {
      alert(error.message);
    }
  },
},

Vueのエラーハンドラーについて

Vue.config.errorHandlerを使えばVueで発生するエラーをほぼキャッチできます。非同期処理内で発生したエラーはキャッチできないため、残りはaddEventListenerでキャッチします。

main.ts
// ほとんどのエラーをキャッチ
Vue.config.errorHandler = function (err, vm, info) {
  // 2.2.0 以降で使用できます。
  alert(err);
}
// 残りのエラーをキャッチ
window.addEventListener("error", event => {
  alert(event.error);
});
window.addEventListener("unhandledrejection", event => {
  alert(event.reason);
});

以下の説明にあるようにバージョンごとに仕様が異なるので注意。

スクリーンショット 2019-12-16 23.11.26.png

Vue.config.errorHandlerはどこで発生したエラーをキャプチャできるのか - Qiitaが分かりやすくまとまってて参考になりました。

Axiosの共通処理について

Axiosの共通の処理はInterceptorsを使うと綺麗にかけます。以下の例ではrequestとresponseの時間を計測し、APIの通信に何ミリ秒かかったのかを毎回コンソールに出力します。Sentryを使うことで時間のかかってるAPIをフロント側から計測することも出来ますね。

Vueのエラー監視にSentryを使ってみた - Qiita

Repository.ts
import axios from 'axios';

const repository = axios.create({
  baseURL: 'http://localhost:8080',
});
repository.interceptors.request.use(request => {
  performance.mark('start');
  return request;
});
repository.interceptors.response.use(
  // 2XX範囲内のステータスコード
  (response) => {
    performance.mark('finish');
    performance.measure('request-to-response', 'start', 'finish');
    const message = `${performance.getEntriesByName('request-to-response')[0].duration}`;
    console.log(message);
    // Sentryでログを残すもよし
    // Sentry.captureMessage(message, Sentry.Severity.Debug);
    return response;
  },
  // 2XX範囲外のステータスコード
  (error) => {
      return Promise.reject(error);
  }
);
export default repository;

結果

  • 例外処理忘れても確実にアラートで表示してくれる:smirk_cat:
  • 共通の例外処理については機械的なtry,catchを書かなくてよくなった:smiley_cat:
XXX.vue
methods: {
  onClick() {
    this.something();
  },
},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueのエラーハンドラーとAxiosで快適なエラー処理を行う

突然ですが、こんなことありませんか?

  • エラー処理忘れてて、コンソールに流れていた:scream_cat:
  • エラー時にアラートを表示するだけで4行も余分にコードを書かないといけなくて辛い:crying_cat_face:
XXX.vue
methods: {
  onClick() {
    try {
      this.something();
    } catch (error) {
      alert(error.message);
    }
  },
},

Vueのエラーハンドラーについて

Vue.config.errorHandlerを使えばVueで発生するエラーをほぼキャッチできます。非同期処理内で発生したエラーはキャッチできないため、残りはaddEventListenerでキャッチします。

main.ts
// ほとんどのエラーをキャッチ
Vue.config.errorHandler = function (err, vm, info) {
  // 2.2.0 以降で使用できます。
  alert(err);
}
// 残りのエラーをキャッチ
window.addEventListener("error", event => {
  alert(event.error);
});
window.addEventListener("unhandledrejection", event => {
  alert(event.reason);
});

以下の説明にあるようにバージョンごとに仕様が異なるので注意。

スクリーンショット 2019-12-16 23.11.26.png

Vue.config.errorHandlerはどこで発生したエラーをキャプチャできるのか - Qiitaが分かりやすくまとまってて参考になりました。

Axiosの共通処理について

Axiosの共通の処理はInterceptorsを使うと綺麗にかけます。以下の例ではrequestとresponseの時間を計測し、APIの通信に何ミリ秒かかったのかを毎回コンソールに出力します。Sentryを使うことで時間のかかってるAPIをフロント側から計測することも出来ますね。

Vueのエラー監視にSentryを使ってみた - Qiita

Repository.ts
import axios from 'axios';

const repository = axios.create({
  baseURL: 'http://localhost:8080',
});
repository.interceptors.request.use(request => {
  performance.mark('start');
  return request;
});
repository.interceptors.response.use(
  // 2XX範囲内のステータスコード
  (response) => {
    performance.mark('finish');
    performance.measure('request-to-response', 'start', 'finish');
    const message = `${performance.getEntriesByName('request-to-response')[0].duration}`;
    console.log(message);
    // Sentryでログを残すもよし
    // Sentry.captureMessage(message, Sentry.Severity.Debug);
    return response;
  },
  // 2XX範囲外のステータスコード
  (error) => {
      return Promise.reject(error);
  }
);
export default repository;

結果

  • エラー処理忘れても確実にアラートで表示してくれる:smirk_cat:
  • 共通の例外処理については機械的なtry,catchを書かなくてよくなった:smiley_cat:
XXX.vue
methods: {
  onClick() {
    this.something();
  },
},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】タブ切り替えが出来る単一ファイルコンポーネントサンプル

はじめに

Vue.jsでタブ切り替えが出来る単一ファイルコンポーネントのサンプルを作成してみました。

v-for
v-bind
v-show
Vue.jsの基本機能である上記3点を活用しています。

スクリーンショット 2019-12-18 23.02.42.png

環境

OS: macOS Catalina 10.15.1
Vue: 2.6.10
vue-cli: 4.1.1

サンプル確認用リンク

こちらから動きの確認ができます。
JSfiddle(外部サイト)へのリンク

※動きの確認用のため、若干下記ソースコードと書き換えているところがあります。

ソースコード

Tab.vue
<template>
  <div class="tabs">
    <div class="btn-container">
      <button v-for="(tab, index) in tabs"
              :key="tab.id"
              :class="{ active: currentTab === index }"
              @click="currentTab = index">{{ tab.tabName }}</button>
    </div>
    <div class="tab-content">
      <div v-show="currentTab === 0">
        <h1>Tab1 content</h1>
        <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
      </div>
      <div v-show="currentTab === 1">
        <h1>Tab2 content</h1>
        <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
      </div>
      <div v-show="currentTab === 2">
        <h1>Tab3 content</h1>
        <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'tabs',
  data () {
    return {
      currentTab: 0,
      id: 0,
      tabName: "",
      tabs: [
        {id: 1, tabName: 'Tab1'},
        {id: 2, tabName: 'Tab2'},
        {id: 3, tabName: 'Tab3'}
      ],
    }
  }
}  
</script>

<style lang="scss" scoped>
.tabs {
  margin: 10px auto;
  padding: 10px;
  width: 80%;
  height: 80%;
  background: rgb(242, 242, 242);
  border-radius: 10px;
}
h1 p{
  font-family: 'Courier New', Courier, monospace;
}
h1 {
  font-size: 20px;
}
p {
  font-size: 8px;
}
.btn-container {
  display: flex;
  justify-content: center;
}
button {
  width: 20%;
  font-size: 20px;
  text-align: center;
  margin: 10px;
  padding: 3px 10px;
  background: rgb(186, 214, 238);
  border-radius: 10px;
}
button.active{
  background: lightcoral;
}
.tab-content div {
  padding: 30px;
  background: #ffffff;
  width: 80%;
  margin: 0 auto;
}
</style>

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

コンポーネント単位で作成出来ると、機能が同じならスタイルを変えて流用出来ていいですね。
やっとVue.jsの良さを実感出来てきました:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

リストレンダリング — Vue.js

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

Vue初級者がPropsとv-Slotをどう使い分けるかを考えた話

はじめに

Vue.jsをある程度やり、propsで子コンポーネントにデータの受け渡しをできるようになった…。
ところでSlotてあるけど、どういうシチュエーションで使っていくのだろうか、ってのを考えていきましょう。
ついでにPropsとどう使い分けていくのかも。

おさらい

まずはPropsSlotは何なのかのおさらいをしていきましょう。

Propsとは

Props - Vue.js

Vue.jsでコンポーネント間のデータの受け渡しをする上で最も基本的な機能ですね。
親側でv-bindや子側のProps名でデータを紐付けて、子側はProps内にプロパティで型とか状態を指定してって使い方。

parent.vue
<div>
  <child title="Qiita!"></child>
  <child :title="Vue"></child>
<div>
//*省略
data() {
  return {
    Vue: "Vue.js!"
  };
},
child.vue
props:{
  title:{
    type:String,
    required: true
  }
}

上のtitle="Qiita!"は文字列を受け渡し、下の:title="Vue"Vueというインスタンス変数などをv-bindで受け渡ししています。

Slotとは

Slot - Vue.js

コンポーネントの開始タグと終了タグの間に、何かしらの要素があった時に、それを子側で<slot></slot>と置換する機能ですね。
子側に<slot></slot>がないと、親側でタグの間に要素があってもそれは表示されないので、表示させたい場合は子側に<slot></slot>を指定してあげる必要があります。

parent.vue
<template>
  <child>
    Qiita!
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

<child>に囲まれたQiita!の文字列が、子の<slot></slot>と置換され、Qiita!が表示されます。
なお、これは文字列だけではなく他のタグ要素に囲まれた物も置換されます。

parent.vue
<template>
  <child>
    <h1>Vue.js!</h1>
    <span>YES!Vue!</span>
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

<h1>タグと<span>タグも、子の<slot></slot>と置換されます。
主に親コンポーネントの要素を子要素に差し込みたいってときに使うと思います。

使い分けを考える

値を渡すだけならProps

文字列なり数値なり、単にデータを受け渡すのならslotでやるよりもPropsでやるべきです。

状態で中の要素が変わるならslot

v-ifなどで中の要素の構成が動的に変わる事があり、それを子側で挟み込んでやりたい。
という場合にはslotを使ったほうが、子側にpropsを使い状態を渡してやるよりも依存関係が薄くなり、
子側のコンポーネントの再利用性が上がるので、このような場合はslotを使いましょう。

parent.vue
<template>
  <child>
    <template v-if="flag">
      <h1>Vue.js!</h1>
      <span>YES!Vue!</span>
    </template>
    <template v-else>
      <p>Error!!!</p>
    </template>
  </child>
</template>
child.vue
<template>
  <slot></slot>
</template>

flagの値がtrueの時は上の要素が、falseの時は下のErrorとかかれた要素が表示されます。
また、置換したい親側の要素をVueコンポーネントにしても、それを子側のslotで置換することができます。

AtomicDesignで使ってみよう

AtomicDesignを使ってコンポーネント設計を行っていた場合にでも、AtomMoleculesなど、コンポーネントの粒度が細かい箇所でよく使う機能になります。
特にAtomレベルのコンポーネントは、それ以上機能として分けれない、親との疎結合性を保つ必要がある、
などの必要があるのと、なによりもAtomコンポーネントの中に他の要素を入れる事はAtomicDesignの考えから離れてしまいます。
なので、AtomicDesignでも要素の子に別の要素を入れたい…!という時には、slotを使って置換させる、などの手法が特に有効と思います。

おわり

作成中のプロジェクトでslotを使う機会が出てきたので、今回は記事にしてみました。
propsslotもどちらも子に何かしらの値や要素を受け渡す便利な機能ですが、
どちらも使い方によってはアンチパターンとなるので、しっかりとコンポーネントの設計を考えていく必要がありますね。

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

初心者によるプログラミング学習ログ 187日目

100日チャレンジの187日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

187日目は

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

jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする

jsで配列の特定keyのだけ取り出してカンマ区切りの文字列にする

言葉だけだと何言ってるの??ってなるので

表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ

// これを
const array = [
  {id: 1, category: 'ねこ', name: 'ポチ'},
  {id: 1, category: 'ねこ', name: 'ねこねこ'},
  {id: 1, category: 'いぬ', name: 'にゃん'},
];

// ↓↓↓ こうしたい ↓↓↓
const string = 'ポチ, ねこねこ, にゃん';

結論

function(array) {
   // 配列からnameだけ取り出して「,」区切りの文字列にする
   return array.map( (row) => {return [row['name']]} ).join(',')
},

分解

function(array) {
   // 配列からkeyのnameだけ取り出した配列を作る
   const namesArray = array.map((row) => {
       return [row['name']]
   });

   // 配列を「,」区切りの文字列にする
   const namesString = nameArray.join(',');
},

Vue.jsで実際に使った時の用途

<template>
  <div>
    <span>
       <!-- 「ねこ, にゃん, ぽち」 のように表示する --!>
      {{ array | convertArrayToString }}
    </span>
  </div>
</template>

<script>
  data: {
    array: array
  },
  filters: {
    convertArrayToString(array) {
      // 配列からnameだけ取り出して「,」区切りの文字列にする
      return array.map( (row) => {return [row['name']]} ).join(',')
    },
  }
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする

jsで配列の特定keyだけ取り出してカンマ区切りの文字列にする

言葉だけだと何言ってるの??ってなるので

表示だけ考えて文字列でカンマ区切りで最後は必要ないってやつ

// これを
const array = [
  {id: 1, category: 'ねこ', name: 'ポチ'},
  {id: 1, category: 'ねこ', name: 'ねこねこ'},
  {id: 1, category: 'いぬ', name: 'にゃん'},
];

// ↓↓↓ こうしたい ↓↓↓
const string = 'ポチ, ねこねこ, にゃん';

結論

function(array) {
   // 配列からnameだけ取り出して「,」区切りの文字列にする
   return array.map( (row) => {return [row['name']]} ).join(',')
},

分解

function(array) {
   // 配列からkeyのnameだけ取り出した配列を作る
   const namesArray = array.map((row) => {
       return [row['name']]
   });

   // 配列を「,」区切りの文字列にする
   const namesString = nameArray.join(',');
},

Vue.jsで実際に使った時の用途

<template>
  <div>
    <span>
       <!-- 「ねこ, にゃん, ぽち」 のように表示する --!>
      {{ array | convertArrayToString }}
    </span>
  </div>
</template>

<script>
  data: {
    array: array
  },
  filters: {
    convertArrayToString(array) {
      // 配列からnameだけ取り出して「,」区切りの文字列にする
      return array.map( (row) => {return [row['name']]} ).join(',')
    },
  }
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】メソッド vs computedプロパティ(算出プロパティ) サンプル

メソッドとcomputedプロパティの違いに関する記述はすでにたくさんあるので、今回は個人的にわかりやすいと思った、これらの違いを説明したコードを紹介したいと思います。

簡単に違いと使い分けについて

Computedプロパティとmethodの違い

算出プロパティとメソッドの違いは、“算出プロパティが処理の結果をキャッシュする”ことです。
算出プロパティがキャッシュした結果を更新するのは、依存するデータが変更されたときです。
一方、メソッドは常に処理が実行されます。

Computedプロパティのメリット・メソッドとの使い分け

結果が同じなら、常にメソッドを使えば良いと思われるかもしれません。
算出プロパティのメリットは、結果をキャッシュしているため処理が高速であるという点です。
例えばリストをソートする処理を作成するときに、メソッドで実装するとソートするごとに毎回処理が実行されます。
ソートした結果を算出プロパティとして用意すれば、依存するデータが更新されるまでソート結果をキャッシュするため、処理が速くなります。

参照
【Vue.js】算出プロパティとメソッドの違い。どう使い分ければ良いの?

サンプル

参照
Vue JS 2 Tutorial #9 - Computed Properties

*プログラム
result.png

*コード
【メソッドの場合】

index.html
 <div id="vue-app">
    <h1>Computed Properties</h1>
    <button v-on:click="a++">Add to A</button>
    <button v-on:click="b++">Add to B</button>
    <p>A - {{ a }}</p>
    <p>B - {{ b }}</p>
    <p>Age + A = {{ addToA() }}</p>
    <p>Age + B = {{ addToB() }}</p>
  </div>
vue.js
 new Vue({
      el: "#vue-app",
      data: {
        age: 20,
        a: 0,
        b: 0
      },

      methods: {
        addToA: function() {
          console.log('addToA')
          return this.a + this.age;
        },
        addToB: function() {
          console.log('addToB')
          return this.b + this.age;
      }
    }
  });

【computedプロパティの場合】

index2.html
<div id="vue-app">
    <h1>Computed Properties</h1>
    <button v-on:click="a++">Add to A</button>
    <button v-on:click="b++">Add to B</button>
    <p>A - {{ a }}</p>
    <p>B - {{ b }}</p>
    <p>Age + A = {{ addToA }}</p>
    <p>Age + B = {{ addToB }}</p>
  </div>
vue2.js
 new Vue({
      el: "#vue-app",
      data: {
        age: 20,
        a: 0,
        b: 0
      },

    computed: {
      addToA: function() {
          console.log('addToA')
          return this.a + this.age;
        },
        addToB: function() {
          console.log('addToB')
          return this.b + this.age;
      }
    }

  });

コードだけ見ると、ほとんど変わらないことがわかるかと思います。
変わる箇所といえば、htmlのマスタッシュ内の書き方くらいです。

<p>Age + A = {{ addToA() }}</p>   <!-- メソッド -->
<p>Age + B = {{ addToA }}</p>   <!-- computedプロパティ -->

ディベロッパーツールで確認

しかし、ディベロッパーツールでそれぞれコンソールで確認してみてください。
(コード内にconsole.logを書いたのもそのためです)

「Add to A」、「Add to B」 いずれかのボタンをクリックすると、メソッドで書いたプログラムの場合(今回は「Add to A」をクリック)、AddToA、AddToB いずれも実行されているのがわかります。

method.png

一方computedプロパティで書いたプログラムの場合、「Add to A」ボタンをクリックすると、AddToA のみが実行されています。

computed.png

まとめ

上記のサンプルコードで、computedプロパティの特徴である「依存するデータが変更されたときのみ処理が行われる」の部分が確認できたかなと思います。

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

Vue + Expressのテンプレート作成メモ

はじめに

バックエンドをNode.js、フロントエンドをVue.jsでwebアプリを開発することが増えたので簡単にひな形を作る手順を残しておこうと思います。

手順

expressのプロジェクト作成

express プロジェクト名

vueのプロジェクトを作成

expressで作ったプロジェクトのルートディレクトリへ移動し
vue create public
publicというタイトルになっているので、気になる方はvueプロジェクトのindex.htmlのtittleタグを編集

vueプロジェクトでビルドする

publicディレクトリへ移動し
npm run build

expressのapp.jsに記述されているpublicのパスを変える

app.use(express.static(path.join(__dirname, 'public’)));

app.use(express.static(path.join(__dirname, 'public/dist’)));

一応この記述も消しておく

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade'); 

node.jsプロジェクトでサーバー起動

npm start

アクセスするとVueの初期画面が表示される

localhost:3000

おわりに

さくっと雛形を作ってすぐに開発に取り掛かりましょう!

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

【Nuxt.js】slot基礎編:コンポーネントに自由にデータを渡そう

前置き

Nuxt.jsと言えばコンポーネント。
componentの内容を、ページによって少しで良いから変えたい…
そんな時、slotに随分と助けられたので記録に残しました。
Vue.jsでも同様の使い方が可能です。
※サンプルテキストはHello Nuxt.js!ではなくHello Vue.js!になっていますが、ご了承ください。

間違い等がございましたら、優しくご指摘いただけると嬉しいです!

基礎編:超簡単な解説から実践編まで
応用編:全体のまとめ、propsとの違い
を書いていきます。

slotって?メリットは?

ウェブサイトを作ってて、
「同じコードをコピペしまくってめんどいなぁ〜
 あ!コンポーネント使えばいいじゃん!!
 あれ?でもページによって○○だけ変えたいんだよなぁ」
って時、ありますよね?

コンポーネントを使いまわしたいけど、
ここの文字だけ変えたい!って時に使います。
(文字以外も変更は利きますが、今回はわかりやすく文字で例えます)

コンポーネントが使えないから、
基本のコードコピペして中身だけ編集する…
なんて面倒なことをしなくていいんです!
コンポーネントにしちゃって、一部だけ変えればいいんです!
楽ちん!!

slotを使わない場合どうなるの?

じゃあコンポーネントを使って…親コンポーネントで中身を変えよう!
slotなくてもの中身に書けばよくない?
と思ったそこのアナタ!
こちらをご覧ください。

Component.vue // 子コンポーネント
<div class="ItemTitle">
 <h1>ここは親ページによってタイトルを変えたい</h1>
</div>
index.vue // 親ページ
<Component>
 Hello Vue.js!
</Component>
表示結果
<div class="ItemTitle">

</div>

そうなんです。。。
子のコンポーネントの中身は無視されちゃいます。
それも完全なシカトです。ツライ。。。

<Component>ここは無視される</Component>

slotの使い方(超簡単ver.)

そこでslot君の登場です!!
使い方は超〜〜〜簡単。
子コンポーネント内に変えたい部分をslotにするだけ。
上の説明に使ったコードでやってみます。

【表示結果】
picture_pc_96a0996a6f584fe201bfcad0b23a9059.png

<div class="itemTitle">
 <h1>Hello Vue.js!</h1>
</div>

【コード】

子コンポーネントだけslotを加えます。

Component.vue // 子コンポーネント
<div class="itemTitle">
 <h1>
  <slot /> // ここは親ページによってタイトルを変えたい
 </h1> 
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
</style>

親は一緒。

index.vue // 親ページ
<Component>
 Hello Vue.js!
</Component>

これだけで表示結果が変わる!!
slotくんに呼びかけて、ちゃんとデータを表示してくれます。
偉いぞslotくん!!!

slotの使い方(複数ver.)

便利なslotくん
「じゃあいっぱいslot使おう!
 h1とh2にも使おう〜」
って場合は
各slotに名前をつけてあげる必要があります。

「これ表示して?」って言われても、
名前で呼んでくれないと、
どのslotくんが表示すればいいのか分からなくなっちゃいますよね。

そういう先輩いません?
「これやっといて〜」って、
今誰に言ったの?私やるの???え、誰???
みたいな。

なので、
もし名前をつけないでslotを2つ用意すると
親に入れたものが2回表示されます。
「はいはい、私やりますね」って2人反応しちゃいます。

名前で呼ばなかった先輩はきっと
同じ仕事を無駄に2個やらせてしまったことを後悔するでしょう。
こんなにいらんわ。。。って。

【表示結果】
picture_pc_cd512416e11b9b6b113ed8fa8676b625.png

<div class="ItemTitle">
 <h1>
  Hellot Vue.js!
  about slot
 </h1>
 <h2>
  Hellot Vue.js!
  about slot
 </h2>
</div>

【コード】

Component.vue // 子コンポーネント
<div class="ItemTitle">
 <h1>
  <slot /> // ここは親ページによってタイトルを変えたい
 </h1>
 <h2>
  <slot /> // ここは親ページによってサブタイトルを変えたい
 </h2>
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
h2 {
 color: gray;
}
</style>
index.vue // 親ページ
<Component>
 Hellot Vue.js!
 about slot
</Component>
表示結果
<div class="ItemTitle">
 <h1>
  Hellot Vue.js!
  about slot
 </h1>
 <h2>
  Hellot Vue.js!
  about slot
 </h2>
</div>

slotの使い方(実践ver.)

ということで各slotくんに名前をつけてあげましょう。
「◯◯くん、これ表示しといてよ」って言ってくれたら、
はい、私ですね!表示しますね!ってなりますよね。

ここで1つ注意。
名前をつける場合は、
親でタグで囲う必要があります。
これさえできればもう完璧。

【表示結果】
picture_pc_912dbfccaa95a0ce68d99cf19a889283.png

<div class="itemTitle">
 <h1 class="title">Hello Vue.js!</h1>
 <p>slotを学ぼう</p>
 <div class="catchCopy"># slotって?メリットは?</div>
</div>

【コード】

ItemTitle.vue // 子コンポーネント
<div class="itemTitle">
 <h1>
  <slot name="title" /> // 置き換えたい部分のslot nameを決める
 </h1>
 <p>slotを学ぼう</p> 
 <h2>
  <slot name="catchCopy" /> // 置き換えたい部分のslot nameを決める
 </h2>
</div>

<style scoped>
.itemTitle {
 margin: 10px;
}
h1 {
 color: rgb(65, 193, 222);
}
p {
 color: gray;
h2 {
 color: rgb(76, 212, 227);
}
</style>
index.vue // 親ページ
<ItemTitle>
 <template v-slot:title>
  Hello Vue.js! // slot name="title"を置き換える
 </template>
 <template v-slot:catchCopy>
  # slotって?メリットは? // slot name="catchCopy"を置き換える
 </template>
</ItemTitle>

おめでとうございます!
これでVue.js、Nuxt.jsのslot、今日から使えますね!わーい!
次は更なる応用も記載していきます★

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

Nuxt.jsとmysqlを連携してデータを表示してみた

はじめに

前から気になっていたNuxt.jsを触ってみました。これ使って何かやってみようと考えた結果、
mysqlと連携させてみることにしました。Nuxt.jsに関して入門的な情報は既にいろいろと存在するので、
ここでは、そのつなぎの部分にフォーカスして記載したいと思います。

内容

下記の項目に沿って記載します。

  • Nuxt.jsについて
  • Nuxt.jsのインストール
  • データベース準備
  • 必要なモジュール
  • APIの実装
  • フロントエンドの実装
  • 結果
  • おわりに

Nuxt.jsについて

ご存知の方には恐縮ですが、Nuxt.jsはナクストと読むようであり、
Webアプリケーションを開発するのに必要なものが既にある程度整った状態で
スタートできるVue.jsベースのJavaScriptフレームワークです。
UIではVue.jsを使いますが、UI以外の機能についてもいろいろ組み込まれているようです。
フレームワークなので効率的にアプリケーションの開発ができるようですね。

Nuxt.jsのインストール

早速、自分のPCにインストールしてみます。
事前にnpm と Node.jsがインストールされていることが前提です。

npx create-nuxt-app プロジェクト名

この形式でインストールするようなので、

npx create-nuxt-app prj

プロジェクト名をprjとして実行してみました。
いくつか聞かれるので、

  • server framework を Express
  • Nuxt.js module をAxios
  • rendering mode をUniversal (SSR)

として設定しました。
何度も繰り返し試した結果、上記の設定に落ち着きました。
Nuxt v2.10.2 がインストールされたようです。

次に、アプリを起動する必要があるので、

npm run dev

を打ち込むと、サーバーが起動します。
nuxtserver.png

npm run スクリプト名

ここで、上記の形式で入力すると、package.json(下記)で記述されているscripts内のスクリプト名
(この場合は"dev")で定義されたコマンドが実行されます。

{
  "name": "prj",
  "version": "1.0.0",
  "description": "My swell Nuxt.js project",
  "author": "**** ****",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "cross-env": "^5.2.0",
    "express": "^4.16.4",
    "fs": "0.0.1-security",
    "mysql": "^2.17.1",
    "net": "^1.0.2",
    "nuxt": "^2.0.0",
    "tls": "0.0.1"
  },
  "devDependencies": {
    "nodemon": "^1.18.9"
  }
}

下記URLをブラウザで表示すると、

http://localhost:3000

nuxtinit.png

こんな感じに初期表示されました。
Nuxt.jsのデフォルトポートは3000のようです。
インストールされたあとのディレクトリ構造は下記のようになってました。

prj----assets
    |--components
    |--layouts
    |--middleware
    |--node_modules
    |--pages
    |--plugins
    |--server
    |--static
    |--store
    |nuxt.config.js
    |package.json

調べたところ、最初に表示されたページは、

prj--
    |--pages
         |index.vue

pagesディレクトリ内にあるindex.vueに記載された内容でした。

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        prj
      </h1>
      <h2 class="subtitle">
        My laudable Nuxt.js project
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

ごちゃごちゃ書かれているので、余計な記述をバッサリ削除してコードを見やすくします。

<template>
  <div class="container">
   あいうえお
  </div>
</template>

<script>
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>

表示は、下記のようになりました。

nuxtinit2.png

ここで気づいたのが、このindex.vueファイルを更新した場合、
ブラウザで再描画しなくても(F5押さなくても)自動でブラウザに反映されることです。
index.vueファイルを更新するたびに、自動でコンパイルが走るようで、
それが終わり次第ブラウザの表示が変更されるようです。

データベース準備

mysqlと連携させて何かしたいな?と考えたところ、
一番簡単なお試し内容として、DBに登録されているデータを取り出して、
それをブラウザに表示させてみるのがよさそうかなと思い、それを実装
してみることにしました。

なので、まずデータベースの準備をします。ローカル環境のmysqlにテスト用のデータベースを作成し
その中に都道府県情報のテーブルを作成しました。
データベース名:testdb
テーブル名:prefectures
prefdb.png
この情報を取り出してブラウザに表示させてみようと思います。

必要なモジュール

mysqlを使うには、どうやらmysql用のモジュールをインストールする必要が
あることがわかりましたので下記を実行します。

npm install --save mysql

APIの実装

index.vueの中のscritpt要素の中に、データベースアクセスの為の記述をしても
うまくいかずエラーを解消することができなかったので、
別途APIを作成し、そこにアクセスすることでDBの内容をとってくる、
という方法にしました。

prj--
    |--server
         |api.js

serverディレクトの直下にapi.jsというファイルを作成し、
dbに接続してデータを取ってくるコードを記述します。

const express = require('express')
const router = express.Router()
router.get('/prefectures', (req, res, next) => {
  const mysql = require('mysql');
  const connection = mysql.createConnection({
    host : 'localhost',
    user : 'testuser',
    database: 'testdb',
    password: 'testuser'
  });
  var ret=[];
  connection.connect();
  connection.query('SELECT * from prefectures;', function(error, row, fields){
    if (error) {
      console.log(error);
    } 
    var dat = [];
    for (var i = 0;i < row.length; i++) {
      dat.push({id: row[i].id, name: row[i].name});
    }
    ret = JSON.stringify(dat);
    res.header('Content-Type', 'application/json; charset=utf-8')
    res.send(ret)
  });
  connection.end();
})
module.exports = router

実はこれだけではだめなようで、同じディレクトリに存在するindex.jsというファイルを
一部編集しないといけないようです(★印の箇所)

const express = require('express')
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')
const app = express()

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

//↓↓↓★この行の追加
const apiRouter = require('./api')
async function start () {
//↓↓↓★この行の追加
app.use('/api', apiRouter)

  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  } else {
    await nuxt.ready()
  }

  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  app.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

下記でアクセスするとDBからのデータをJSON形式でとってこれるようになりました。

http://localhost:3000/api/prefectures

nuxt_api.png

参考サイト
https://dev.classmethod.jp/etc/node-js-module-mysq/
(他にもいくつか参考サイトがあったのですが忘れてしまった。。)

フロントエンドの実装

axiosというライブラリを使ってhttp通信を行います。
Node.jsで動作するhttpクライアントであり、通信するためのAPIが提供されているので
これを利用します(Ajaxのようなものですね。多分)

上記でばっさり削除したindex.vueの中を下記のように変更します。
先ほどのAPIにアクセスして取ってきたデータをitemsの中に格納すると、
Vue.jsによってフロントエンドに反映されます。

<template>
  <div class="container">
    <table border="1">
      <tbody>
      <tr>
        <th>ID</th>
        <th>都道府県名</th>
      </tr>
      <tr v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td>
        <td>{{ item.name }}</td>
      </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  mounted: function() {
    this.$axios
      .$get('/api/prefectures')
      .then(response => {
        this.items = response
      })
      .catch(error => {
        console.log(error)
      })
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>

結果

表示は、下記のようになりました。

nuxtresult.png

おわりに

今回は、mysqlとの連携に注目しましたが、Nuxt.jsに関する知識が浅いこともあり、
もっといろんなこともできると思われますので、今後も引き続きいろいろ試してみたいと思います。
私はPHPを使っての作業が多いので、JavaScriptだけで当たり前のようにWebアプリケーションを
作れるようなったことに関心を寄せています。
試した内容は大したものではないですが、それでも途中いくつもの壁にぶち当たりました。
が、なんとか乗り越えました。
今後はよりスムーズに実装できるよう一層進化していくことと思います。

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

【vue】Vue Styleguidistの使い方を説明① 〜Laravel + vue環境でVue Styleguidistを動かす〜

※元々ブログに書いていたのですが、qiitaに転載しました
https://www.moyashidaisuke.com/entry/vue-styleguidist-install

概要

vueのstyleguild「Vue Styleguidist」をLaravel + vue環境で使い始めてみました。
思ったより手間取ってしまったので、設定ファイルや、私の環境で発生したエラーの対応等を残しておきます。

↑のカメレオンはVue Styleguidistのロゴ。きもかわいい。

※Laravel環境じゃなくても多分参考になると思います。

Vue Styleguidistとは

とりあえず動作サンプル見た方がわかりやすいのでいきなりですがリンクを。

https://vue-styleguidist.github.io/basic/

こういうコンポーネントの仕様と、サンプルのドキュメントを生成してくれるツールです。 こういうの。

20191005220952.png

GitHubのStar数はこの記事を書いてる時点で1419なので、デファクトになってる感はまだ無いですが、競合のvueseよりはstar数多いのでこちらを採用しました。
https://github.com/vue-contrib/vuese

導入手順(理想形

特にはまらないですんなりいくパターン。

https://vue-styleguidist.github.io/docs/GettingStarted.html#_1-install

前提

  • Laravel + vue環境導入済み
  • npmじゃなくてyarn
  • Vue CLIは使ってない

公式手順だとnpmですが、私の環境ではyarnを使っているのでyarnの手順を紹介します。

インストール

$ yarn add -D vue-styleguidist

〜色々インストールされる。省略〜

Done in 296.61s.

style guildの設定

公式手順だとリンクが2つ貼ってあります。

https://vue-styleguidist.github.io/docs/Components.html#finding-components
https://vue-styleguidist.github.io/docs/Webpack.html

いきなりぶん投げられてわかんないですが、プロジェクトルートに

styleguide.config.jsというファイルを作成してください。

中身は

const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
  webpackConfig: {
    module: {
      rules: [
        // Vue loader
        {
          test: /\.vue$/,
          exclude: /node_modules/,
          loader: 'vue-loader'
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin()
    ]
  },
  // vueファイルへのpathを指定
  components: 'resources/js/components/**/[A-Z]*.vue',

}

で私の環境だといけました。

ポイントとしては、webpackConfiglaravel mixの設定を流用しても全く動かないです。(あれはmixで動的にconfigを生成したりしてるので)

また、vue-loaderの設定もちゃんと書いてあげないと動かないです。デフォルトで呼んでくれたりはしないようです。

あと、vueのファイルはLaravelだと普通resources/js以下で作成してる事が多いと思いますが、適時調整してください。

package.jsonにコマンド追加

これは公式そのままで大丈夫です。

{
  "scripts": {
+    "styleguide": "vue-styleguidist server",
+    "styleguide:build": "vue-styleguidist build"
  }
}

実行

hot reload版

yarn run styleguide

サーバが立ち上がってlocalhost:6060でつなげるようになります。vagrantやDocker等の仮想環境を使っている方はポートの設定をしてください。

私はdocker-composeを使っていたので

      ports:
        - 6060:6060 # styleguide

を追加しました。

htmlとjs吐き出す版

yarn run styleguide:build

styleguideというディレクトリにhtmlとjsが吐き出されますので、htmlを開けばOKです。

エラー色々

styleguide.config.jsの設定系

componentsへのpathがおかしい

画面を開くとこれが表示されるパターン。

Welcome to Vue Styleguidist!
We couldn’t find any components using these patterns:

src/{components,Components}/**/*.vue
Create styleguide.config.js file in your project root directory like this:

module.exports = {
  components: 'src/components/**/*.vue'
};
Read more in the locating components guide.

componentsの設定を自分の環境に合わせればOK。

Failed to compile

Failed to compile
./resources/js/components/XXXXX.vue 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

vue-loaderの設定をいれてあげればOK。

vueとvue-template-compilerのバージョン違い

Failed to compile
./resources/js/components/XXXXXX.vue (./node_modules/vue-styleguidist/loaders/vuedoc-loader.js!./resources/js/components/XXXXXX.vue)
Error: 

Vue packages version mismatch:

- vue@2.6.8 (/var/www/node_modules/vue/dist/vue.runtime.common.js)
- vue-template-compiler@2.6.10 (/var/www/node_modules/vue-docgen-api/node_modules/vue-template-compiler/package.json)

This may cause things to work incorrectly. Make sure to use the same version for both.
If you are using vue-loader@>=10.0, simply update vue-template-compiler.
If you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.

バージョン合わせないとダメらしい。
yarn.lockを確認すると

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

となっており、^2.0.0 2以上を使ってね、の指定で2.6.10をinstallしちゃってる。

というわけで、無理やり2.6.8を入れてみる。

$ yarn add vue-template-compiler@2.6.8
yarn add v1.13.0

info Direct dependencies
└─ vue-template-compiler@2.6.8
info All dependencies
└─ vue-template-compiler@2.6.8
Done in 223.61s.

これで2.6.8入ったと思いきや、2.6.10も入ったままなのでダメ。

yarn.lockはこんな状態

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.10"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
  integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

installしたりremoveしてもダメだったので、最終手段でyarn.lockを直接書き換える。

vue-template-compiler@2.6.8:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"

vue-template-compiler@^2.0.0:
  version "2.6.8"
  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.8.tgz#750802604595134775b9c53141b9850b35255e1c"
  integrity sha512-SwWKANE5ee+oJg+dEJmsdxsxWYICPsNwk68+1AFjOS8l0O/Yz2845afuJtFqf3UjS/vXG7ECsPeHHEAD65Cjng==
  dependencies:
    de-indent "^1.0.2"
    he "^1.1.0"
$ yarn install

これで動きました。

issueはちょいちょい見かけるのだけど、ちゃんとした対応方法は不明。誰か知ってたら教えて下さい。

https://github.com/vuejs/vue/issues/3941

実際のドキュメントを生成したり、パラメータの解説したりの予定。

第2段

https://www.moyashidaisuke.com/entry/vue-styleguild-sections

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

【Nuxt.js】Nuxtプロジェクトにpug/scss/coffeescriptを導入する

はじめに

Nuxt.jsは優れたWebアプリケーションフレームワークです。
しかし、デフォルトのhtml/css/jsで記述するのは労力がかかります。
せっかくなので、pug/scss/coffeeを使って時短を図りましょう。

導入方法

以下のコマンドをnuxtプロジェクトのディレクトリで実行するだけです。

$ npm i -D pug pug-plain-loader coffeescript coffee-loader node-sass sass-loader

これで、pugとscssは記述できるようになりましたが、coffeescriptを使うにはもう一つ作業が必要です。
アプリディレクトリ内のnuxt.config.jsの下の方にあるbuildを次のように変更してください。

nuxt.config.js
  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
      // -------- ここから ----------
      config.module.rules.push({
        test: /\.coffee$/,
        use: 'coffee-loader',
        exclude: /(node_modules)/
      })
      // -------- ここまで ----------
    }
  }

これで、pug/scss/coffeescriptが使えるようになったはずです。適当なvueファイルを作って正常に動作するか確認してみましょう。

sample.vue
<template lang='pug'>
div
  h1 {{ msg }}
</template>

<script lang='coffee'>
export default
  data: ->
    msg: 'Hello world!'
</script>

<style lang='scss'>
h1 {
  color: blue;
}
</style>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue Fes Japan 2019 公式サイトから学ぶ CSS コーディングの知見

この記事は CSS Advent Calendar 2019 の 20日目の記事です。

Vue Fes Japan 2019 が開催中止になってしまい、大変残念だったのですが... :cry:
(私は Vue.js 日本ユーザーグループのコアスタッフで、今回は Web サイト制作班として少しコミットしていたのでした)

先日公式サイトのソースコードが無事公開されました :tada:
そこで、Web サイト制作で得た CSS の知見がとてもためになったので、いくつかご紹介していきたいと思います。

Vue Fes Japan 2019 公式サイト

公式サイト: https://vuefes.jp/2019/
GitHub: https://github.com/vuejs-jp/vuefes-2019

2019 の公式サイトも、2018 の公式サイトと同様に、Nuxt.js の静的ファイルの生成機能を使って作成しています。

CSS の構成について

このように、「グローバルな CSS」と「閉じられた CSS(Scoped CSS)」で構成されています。

ちなみにこのプロジェクトでは UI フレームワークや CSS フレームワークは使っていません。

コーディングするときにとても嬉しかったポイント

Web サイトデザイン主担当の沖さん(@448jp)が、Web サイトの仕様をドキュメント化して共有してくれました。

スクリーンショット 2019-12-18 5.04.40.png

これがとても詳細まで定義されており、ブレークポイントごとの余白や、フォント・色などを CSS プロパティで指定してくださったおかげで、悩まずにすばやく実装することができました。

つよい... :muscle:

私は普段デザインカンプをもらってコーディングをするとき、ブレークポイント付近のデザインをエスパーしてコーディングしたり、仕様を自分で決めたりしていたのですが、そうするとデザイナーさんと齟齬が生まれたり手戻りが発生したりして、開発に余計な時間がかかってしまうことがあります。

沖さんのようなデザイナーがいる環境というのは、なかなか恵まれていると思うのですが、Web サイトをデザインする人は詳細な仕様を明文化することで、実装側がとても助かるということを知ってもらえると良いなと思いました :pray:

CSS コーディングの知見

グローバルな CSS の設計について

CSS の各レイヤーをディレクトリやファイルごとに分類することで、どんな意味を持った CSS をどこに配置するか、共同開発する際にメンバーと認識が合うようになります。

  • foundation
    • colors.scss ... 色の変数
    • reset.scss ... normalize.css によってブラウザごとの差異を整えた後に、さらにリセットしたいブラウザのデフォルトのスタイル
    • typography.scss ... タイポグラフィのスタイル
    • variables.scss ... 共通化して使える変数
  • main.scss ... 各 SCSS ファイルを import しているファイル
  • media_queries.scss ... メディアクエリによって要素を表示させるか否かを制御できるクラス
  • utilities.scss ... 各画面で共通して使えるクラス

また、foundation 配下の CSS を開発初期の段階で固めておくことで、CSS フレームワークを使わずとも統一感のとれた CSS を書いていくことができます。

ブレークポイントの名称は sp pc ではなく sm md lg を使う

私は今まで、ブレークポイントの切り替えで使う変数名の表記には sp pc を使っていたのですが、この 2パターンだと中間層のサイズのブレークポイントを書きたい際に困ることがありました。

ブレークポイントの名前をサイズの名前にすると、中間層のサイズも柔軟に表すことができるようになるのでオススメです。

https://github.com/vuejs-jp/vuefes-2019/blob/master/src/assets/stylesheets/foundation/variables.scss#L1-L4

$layout-breakpoint--is-small: 768px !default;
$layout-breakpoint--is-small-up: 769px !default;
$layout-breakpoint--is-medium: 980px !default;
$layout-breakpoint--is-medium-up: 981px !default;

sm (モバイル)サイズの要素の指定を相対指定にする

モバイル端末の違いによって、文字やコンテンツの幅が大きく変わってしまう問題については、サイズを相対指定(vw)にすることで解決しています。

スクリーンショット 2019-12-18 5.47.43.png

スクリーンショット 2019-12-18 5.48.07.png

文字やコンテンツの幅を相対指定にすることで、iPad と iPhone SE というような画面幅が違う端末でも、画崩れを起こさずに表現することができます。

ちなみにデスクトップサイズ以上の文字については、固定で指定しています(さすがにラップトップと大きいサイズのモニターでは幅が違いすぎるため)

計算が簡単なカードリストレイアウトの実装方法

SPONSORS セクションでは、スポンサープランごとに 1列に並ぶカードの幅を変える必要がありました。

  • ゴールドは 1列 4カード、シルバーは 1列 5カード... というように並ぶ
  • それぞれ 20px のガター(余白)がついている

スクリーンショット 2019-12-18 6.44.43.png

当初、これを実装するために考えていたこととしては、

  • スポンサープランごとのカードの幅を計算する
  • カードの margin-rightmargin-bottom に 20px を設定する
  • 右端に来る n 番目のカードは、 margin-right: 0; を設定する
    • n 番目の n の値は、スポンサープランごとに変わる

というようなことを書こうとしましたが、計算と実装が複雑になるため、別の方法をとることにしました。

計算が簡単になる方法として、ネガティブマージンと calc() を使うやり方があります。

template
    <ul
      v-for="sponsorPlan in sponsorPlansHavingSponsors"
      :key="sponsorPlan.plan"
    >
      <li class="sponsor-group" :class="sponsorPlan.plan">
        <h3 class="sponsor-plan">
          {{ sponsorPlan.name }}
        </h3>

        <ul>
          <li
            v-for="sponsor in sponsorsByPlan(sponsorPlan.plan)"
            :key="sponsor.sys.id"
            class="sponsor"
          >
            <!-- ... -->
          </li>
        </ul>
      </li>
    </ul>
scss
.sponsor-group {
  ul {
    display: flex; // flexbox で実装する
    flex-wrap: wrap; // アイテムがはみ出したときに折り返すようにする
    justify-content: center; // 中央揃えにする
    width: calc(100% + 20px); // ?幅を左右のガター分プラスする
    margin: -10px; // ?ネガティブマージンを使って余白を広めにとる
  }
  .sponsor {
    margin: 10px; // ?ガターの半分の幅を指定する(要素が隣り合うとガターの幅になる)
    width: calc((100% / 5) - 20px); // 普通サイズ(シルバー)の Sponsor バナーの横幅
  }
}
.sponsor-group.bronze {
  .sponsor {
    width: calc((100% / 6) - 20px); // ブロンズのバナーのときの横幅
  }
}
.sponsor-group.gold,
.sponsor-group.special {
  .sponsor {
    width: calc((100% / 4) - 20px); // ゴールド、スペシャルのバナーのときの横幅
  }
}

まず .sponsor-group にネガティブマージンを使って、ガターの幅分だけ要素の幅を広めに指定します。

次に、カードの上下左右にガター分の margin を指定します(このときガターの半分の幅を指定することで、カードが隣り合うとガターの幅になります)

そして、カードの幅を calc() で計算して、flexbox で要素を並べると、簡単にカードリストが出来上がります。

ちなみにカードの幅の calc((100% / 5) - 20px); という部分は、

(sponsor-group の幅 / 1列に入るカードの数) - ガターの幅

という計算式になります。

こうするとことで、 1列に入るカードの数 を変更すればカードの幅が変わるので、とても計算が簡単になります。

vuefes-2019/TheSponsorListSection.vue at master · vuejs-jp/vuefes-2019

↑のコードは TheSponsorListSection の簡略版ですが、コードだけではネガティブマージンをとっているあたりが分かりにくいと思うので、CodePen を用意しました。

See the Pen CSS coding for card list layout by rry (@ryamakuchi) on CodePen.

https://codepen.io/ryamakuchi/pen/LYExRpa

  • group
    • ネガティブマージンを width を使って、全体的にガターの幅分を広めにとる
  • card
    • 上下左右にガターをつけて、calc() で幅ぴったりにする

当初考えていた、左側と下にガターをつける方法と比べると、右端に来る n 番目のカードのスタイルの上書きをするということがなくなるため、計算が簡単になります。

補足:ちなみに calc() は、IE 10 / IE 11 では一部うまく機能しない場合もあるため、実装するときは気をつけてください。

まとめ

CSS の設計をきちんと行うことで、フレームワークを使うことなく、メンテナンスしやすい&共同開発しやすいサイト作成ができました。

もし CSS / UI フレームワークを使うことを検討しているのであれば、

create-nuxt-app で利用できる UI フレームワークを比較する - Qiita

も、あわせてご覧いただければ幸いです。

メリークリスマス! :christmas_tree:

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

v-date-pickerをカスタマイズしてbirthday-pickerを作る

TL;DR

  • VuejsのUIフレームワークであるVuetifyを使っていい感じの誕生日入力フォームを作成する
  • 以下を満たすものとする
    • 日本語対応
    • 日付の「日」は取る
    • 年から選択
    • 最初に年を選択するとき、1995年からになるようにする
    • 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
    • 日付を選択と同時にpickerを閉じる

動作環境

Vue.js v2.x
Vuetify v2.1.14

できたもの

https://codepen.io/kmr_0811/pen/oNgBzxR

解説

  • 日本語対応
    • v-date-pickerのlocaleプロパティを設定
  • 日付の「日」は取る
    • v-date-pickerのday-formatプロパティを設定
  • 日付範囲制限(未来や古すぎる日付を誕生日に設定できない)
    • max, minプロパティを設定
  • 最初に年を選択するとき、1995年からになるようにする
    • picker-dateプロパティを制御する必要がある
    • picker-dateではpickerの値をコントロールできる
    • そのため、v-modelでbindされたdateオブジェクトを触らずpickerについてのみ触ることができる
      <v-date-picker
        ref="picker"
        locale="jp-ja"
        v-model="date"
        :day-format="date => new Date(date).getDate()"
        :max="new Date().toISOString().substr(0, 10)"
        :picker-date="pickerDate"
        min="1950-01-01"
        @change="save"
      ></v-date-picker>
  • 年から選択
    • v-menuにバインドしたmenuをwatchし、v-date-pickerのactivePickerを制御する。
    • menuを選ばれたタイミングのみでactivePickerをYEAR指定にしないと、ずっと年選択になったりする
    • 選び始めたらpickerDateを初期化しないと、ユーザが入力してもpickerDateの値になってしまうので注意
  watch: {
    menu (val) {
      val && setTimeout(() => (
        // 年から選ぶようにする
        this.$refs.picker.activePicker = 'YEAR',
        // 選び始めたら初期化
        this.pickerDate = null
      ))
    },
  },
  • 日付を選択と同時にpickerを閉じる
    • 日付を選択したとき = v-date-pickerの@changeが発火するタイミング
    • その際、pickerDateと選択したdateを同期させないと、もう一度誕生日を選びなおした時に前回の日付が初期値として機能してくれないので注意
  methods: {
    save (date) {
      this.$refs.menu.save(date)
      // 再入力に備えて、入力が終わったら同期する
      this.pickerDate = date;
    },
  },

感想

v-date-pickerは抽象的な作りをしていて、開発者が用意されたプロパティやコンポーネントをよく調べる必要があります。
そのため、必然的にVueコンポーネントの学びを多く得られると思いました。

ぜひ、v-date-pickerもといVuetifyを触ってみてください!

付録


今回のコード

template

<div id="app">
  <v-app id="inspire">
    <v-menu
      ref="menu"
      v-model="menu"
      :close-on-content-click="false"
      transition="scale-transition"
      offset-y
      full-width
      min-width="290px"
    >
      <template v-slot:activator="{ on }">
        <v-text-field
          v-model="date"
          label="誕生日を入力"
          prepend-icon="event"
          readonly
          v-on="on"
        ></v-text-field>
      </template>
      <v-date-picker
        ref="picker"
        locale="jp-ja"
        v-model="date"
        :day-format="date => new Date(date).getDate()"
        :max="new Date().toISOString().substr(0, 10)"
        :picker-date="pickerDate"
        min="1950-01-01"
        @change="save"
      ></v-date-picker>
    </v-menu>
  </v-app>
</div>

script

new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data: () => ({
    date: null,
    menu: false,
    // pickerの初期値(95年から年が選べるようになる)
    pickerDate: '1995-1-1',
  }),
  watch: {
    menu (val) {
      val && setTimeout(() => (
        // 年から選ぶようにする
        this.$refs.picker.activePicker = 'YEAR',
        // 選び始めたら初期化
        this.pickerDate = null
      ))
    },
  },
  methods: {
    save (date) {
      this.$refs.menu.save(date)
      // 再入力に備えて、入力が終わったら同期する
      this.pickerDate = date;
    },
  },
})


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

Vue.jsで作るプロジェクトの背景にPlayCanvasを組み込むことでリッチにできそう。

PlayCanvasで作成したプロジェクトをVue.js背景として埋め込む

やったこと

  • PlayCanvasで作成したプロジェクトを背景として埋め込む

使った技術

  • Vue.js
  • PlayCanvas

作ったもの

このように背景に動くリッチなコンテンツを追加します。

作ったデモページ
https://trusting-heyrovsky-74ae1c.netlify.com/

PlayCanvasのプロジェクトを作る

作ったプロジェクトについて、作ったプロジェクトについてはこちらのページからEDITORボタンを押すとソースコードや配置などを確認することができます。

https://playcanvas.com/project/647534/

PlayCavnasについて詳しく知りたい方はこちらのリンクなどを見ていただけると幸いです

PlayCanvasのプロジェクトをPUBLISHする

今回はPlayCanvasのプロジェクトをこのような形で作成しました。

3D モデルについては Google PolyからFBXをダウンロードして使用しました。
https://poly.google.com/

設定を追加してから、PlayCanvasではウェブ上からPUBLISHします。

Transparent Canvasにチェックを入れる


PUBLISHする前にSETTINGSRENDERINGからTransparent Canvasにチェックを入れます。

PUBLISHをする

PUBLISHする際にはエディターの PUBLISH / DOWNLOADからPUBLISHすることができます。

PUBLISHをすると個別のURLが発行され誰でもURLにアクセスにすることでページを閲覧できるようになります

今回PUBLISHしたデモ

https://playcanv.as/p/8NZ92jAY/

HTMLに埋め込む

PlayCanvasで作成したゲームを背景として埋め込みます。
今回はVue.jsでinitしたプロジェクトを使用してPlayCanvasのプロジェクトを埋め込みます。

今回埋め込んだプロジェクトのリポジトリに公開しています。

PUBLISHしたURLをiframeで埋め込む

PlayCanvasをiframeで埋め込む際にはPUBLISHされたURLにoverlay=falseというクエリパラメータを付けると全画面で表示されるようになります。

https://playcanv.as/p/8NZ92jAY/?overlay=false

App.Vue

App.vue
<template>
<div>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
    <iframe src="https://playcanv.as/p/8NZ92jAY/?overlay=false" />
</div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
</script>

<style>

html{
  background:white;
}
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: white;
  margin-top: 60px;
  background: transparent;
  position:absolute;
  z-index: 1;
  width: 100vw;
  height: 100vh;
}
iframe{
  width: 100vw;
  height: 100vh;
  border: none;
  position:fixed;
  z-index: 0;
  top: 0;
  left: 0;
}
</style>

iframeを追加することで背景として埋め込む事ができます。ロードを考えるとあまり実用的ではなさそうだけれどロード画面とかを工夫できれば結構きれいなものが作れそう。

埋め込むとこのような形の表示になります。
https://trusting-heyrovsky-74ae1c.netlify.com/


PlayCanvas開発で参考になりそうな記事の一覧です。


入門


その他関連
- PlayCanvasタグの付いた記事一覧

PlayCanvasのユーザー会のSlackを作りました!

少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!

日本PlayCanvasユーザー会 - Slack

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

Vuex に型情報を付加する話と今後の野望(だったもの)

本記事は Vue Advent Calendar 2019 18 日目の記事です。

Vuex に型情報を付加する話と今後の野望だったものと題して、 Vue + Vuex with TypeScript の開発体験を向上するために試行錯誤したことについて話していきます。

はじめに

Vuex モジュールに型情報を付けたい、そう思ったのは Vue を TypeScript で書くみなさんなら誰しも1度は考えたことがあるのではないかと思います。

今年の春、ちょうど型パズルを使えるようになってきた時期だった私は去来したその考えをすぐ実行に移すこととしました。

1. 後付の型情報

最初は、モジュールの型情報を type キーワードや interface キーワードを使って定義したあと、mapState などのヘルパの返り値を as キーワードでキャストして型情報を付けていきました。

module.ts
export type State = {
  value: string;
}

export default {
  state: {
    value: 'Hello, Vuex!'
  }
}
Component.vue
<template>...<template>

<script lang="ts">
import { mapState } from 'vuex';
import { State } from '@/stores/modules/module.ts';

const mappedState = mapState('module', ['value']) as State;

export default Vue.extend({
  computed: {
    ...mappedState
  }
})
</script>

これで Vuex 由来のプロパティに型情報を乗せることができるようになりました。

しかし、この方法では以下の問題がありました。

  • 手動での型付けのため、モジュールの実態と型情報がリンクしていない
    • どこまで正確に型をつけるかは実装者の良心によるところが大きい
    • 実装と型定義の二重実装になり、手間がかかる
  • mapXXXX ヘルパは部分的にモジュールを引っ張ってこれる
    • 型パズルを駆使して適宜型情報を制限しなくてはならない
    • こちらも二度手間

これらの問題を解決するために考えついたのは、「mapXXXXヘルパに実装から抽出した型情報を持たせる」ことでした。

2.実装から型を抽出する

TypeScript で実装から型を抽出する方法、と聞いてみなさんは何を思い浮かべたでしょうか。
Compiler API などの裏技を除けば、関数の引数を使ってジェネリクス型に拾ってもらうのが一般的ではないかと思います。
実際、Vue.js がとった方法も Vue.extends() を用いた方法でした。

const just = <T>(value: T) => value;

just('hogehoge'); // T = string
just(1); // T = 1
just(true); // T = boolean

このコードスニペットのように、ジェネリクス型Tを引数として受け取る関数に、
任意の型を渡した際に T はその型として推論が行われます。

これを利用して、ジェネリクス型に State, Actions, Mutations, Getters の型をそれぞれ抽出してしまえば第1段階クリアです。

import { ActionTree, Module, MutationTree, GetterTree } from "vuex";
type State<S> = S | () => S;

interface FullyTypedModuleDefinition<
  S,
  R extends any,
  A extends ActionTree<S, R> = {},
  M extends MutationTree<S> = {},
  G extends GetterTree<S, R> = {}
> extends Module<S, R> {
  state?: State<S>;
  actions?: A;
  mutations?: M;
  getters?: G;
}

export interface FullyTypedModule<
  S,
  R extends any,
  A extends ActionTree<S, R> = {},
  M extends MutationTree<S> = {},
  G extends GetterTree<S, R> = {}
> extends Module<S, R> {
  state: State<S>;
  actions: A;
  mutations: M;
  getters: G;
}

/**
 * Defines fully-typed Vuex module.
 *
 * @param mod definition of Vuex module
 */
export const buildModule = <
  S,
  R extends any,
  A extends ActionTree<S, R> = ActionTree<S, R>,
  M extends MutationTree<S> = MutationTree<S>,
  G extends GetterTree<S, R> = GetterTree<S, R>
>(
  mod: FullyTypedModuleDefinition<S, R, A, M, G>
): FullyTypedModule<S, R, A, M, G> => ({
  state: {} as State<S>,
  actions: {} as A,
  mutations: {} as M,
  getters: {} as G,
  ...mod
});

モジュール定義から型情報をしっかり持ったモジュール定義を出力しているだけの関数です。
オプショナルな項目に空の初期値を与えたくらいの変化しかありません。

buildModule({
  state: {
    value: 'Hello, Vuex!'
  }
}); // S = { value: string }

これで実装に沿った型情報を抽出することができました。
(例が長くなるので載せていませんが、他の項目もちゃんと抽出できます)

3. 型パズルの時間

さて、あとは抽出した型情報をもとに、型パズルこねこねして mapXXXX の返り値の型を作るだけです。

3-1. 抽出した型情報から mapXXXX の返り値の型を組み立てる

import { Dictionary, Computed, mapState } from "vuex";

type MappedState<S> = Dictionary<Computed> & { [P in keyof S]: () => S[P] };

まずは返り値の型がなきゃ始まらないので、返り値の型を作ります。
Mapped Types を使えばかなり楽に定義できます。

Actions などは関数なのでちょっとトリッキーなことをする必要がありますが、
Conditional Types を使えばどうにかなります。見栄えは悪いですが

3-2. mapXXXX の型定義を参考に関数の型定義

export interface StateMapper<State> {
  <Key extends keyof State>(map: Key[]): MappedState<Pick<State, Key>>;
  <Key extends keyof State>(namespace: string, map: Key[]): MappedState<
    Pick<State, Key>
  >;
  (): MappedState<State>;
  (namespace: string): MappedState<State>;
}

interface の関数型のオーバーロードを使って、Vuex 内の mapXXXX の型定義を参考に新しい mapXXXX のインタフェースを定義していきます。

Vuex の mapXXXX にはオブジェクトでのマッピングによる定義もありますが、簡単のためオミットしました。
また、全てのキーを取得する際はキーの指定が必要ないような定義を追加しています。

3-3. 実装

定義したシグネチャに従って、mapXXXX をラップする形で実装を進めていきます。
関数の引数を介さないとジェネリクス型の評価を使用時に行えないので、
捨てパラメータとして _state: S を定義しています。

import { Dictionary, Computed, mapState } from "vuex";
import { keyOf } from "../utils/keyof";

type MappedState<S> = Dictionary<Computed> & { [P in keyof S]: () => S[P] };

const stateMapper = <S, K extends keyof S>(_state: S, map: K[]) =>
  mapState(map as string[]) as MappedState<S>;
const stateMapperWithNamespace = <S, K extends keyof S>(
  _state: S,
  namespace: string,
  map: K[]
) => mapState(namespace, map as string[]) as MappedState<S>;

export interface StateMapper<State> {
  <Key extends keyof State>(map: Key[]): MappedState<Pick<State, Key>>;
  <Key extends keyof State>(namespace: string, map: Key[]): MappedState<
    Pick<State, Key>
  >;
  (): MappedState<State>;
  (namespace: string): MappedState<State>;
}

export const mapStateWithType = <S>(state: S): StateMapper<S> => <
  K extends keyof S
>(
  ...args: [K[]] | [string, K[]] | [string] | []
) => {
  if (!args.length) {
    return stateMapper(state, keyOf(state));
  }

  const isNamespaceOnly = (
    val: [K[]] | [string, K[]] | [string] | []
  ): val is [string] => val.length === 1 && typeof val[0] === "string";
  if (isNamespaceOnly(args)) {
    return stateMapperWithNamespace(state, args[0], keyOf(state));
  }

  const isWithNamespace = (val: [K[]] | [string, K[]]): val is [string, K[]] =>
    typeof val[0] === "string";
  if (isWithNamespace(args)) {
    const [namespace, map] = args;
    return stateMapperWithNamespace(state, namespace, map);
  }

  const [map] = args;
  return stateMapper(state, map);
};

あとは 2. で抽出した型情報のついたモジュールの項目を mapStateWithType に食わせれば型情報のついた mapXXXX ヘルパの出来上がりです。

作成したライブラリはこちらになります。

4. 今後の野望だったもの

このライブラリを作り終え、使っていくうちにある野望が浮かんできました。

モジュール定義のときも型情報を付けたい!!

今回のライブラリで提供したのは既に定義済みのモジュールから
マッピングヘルパを介してコンポーネントに型情報を提供する方法であり、
モジュール定義自体は Vuex 自身の甘い型定義に頼るほかありませんでした。

なので、モジュール定義自体も Vue.extends() が対応したようにしっかりと型定義できれば、
より良い開発体験が得られるに違いない!と考えたわけです。

その際に重視したのは、現行の Vuex API を踏襲して
そのまま Vuex で使用可能な定義モジュールを出力することでした。

そうすれば、公式に沿ったピュアなオブジェクトを介して、定義とマッピングを自作のライブラリで型情報をつけられる。
使って貰える人は両方セットではなくてどっちかほしい方だけでも使えるようになる、と考えてのことでした。

結果的に言えば、この試みは失敗でした。

4-1. 自己言及的な API の存在

その原因は自己言及的な Vuex の構造にあります。
例えば、ある Action は他の Actions を呼び出せます。
それは、Action の第1引数の中に Actions が含まれているからです。
つまり、Actions の定義の際には Actions 全体の情報が必要になる、「鶏が先か卵が先か」のジレンマが存在したのです。

Vue 自体の構造にも同じことが言えます。
算出プロパティは他の算出プロパティを呼び出せる、
すなわち算出プロパティの定義には他の算出プロパティが必要になるはずでした。

しかし、Vue 自体の API はそれらは全て this オブジェクトから利用するため、
Vue.extends() は定義から抽出した型情報を関数内の this オブジェクトの型として反映させる形で、鶏と卵のジレンマを回避することができたのです。

ですが、Vuex 定義はそうは行きません。
ここで、開発を諦めるか、API 構造を変更するかの選択を迫られました。

悩みましたが、API 構造を変更することを選びました。

  • Actions が Actions を利用するのではなく、 Orchestrator という項目を新規定義して、これが Actions の呼び出しをコントロールするように API 構造を修正
  • Actions, Getters の自己言及的な API を型定義から削除

この2点を実施することで、項目同士の依存関係は以下のようになりました。

vuex-typed-definer.png

これなら、再帰的な定義を必要とせずに型定義を作成できます。

しかし、なおも壁が立ちはだかります。

4-2. 型定義を抽出できない

前の依存グラフを見ると、例えば State は他の全ての項目から依存されています。
これは、ジェネリクス関数の引数定義に型変数が複数回登場するということを意味します。

ジェネリクス関数の引数定義で型情報を抽出するには、引数から型が一意に定まることが重要です。
しかし、引数定義に型変数が複数回登場すると、型が一意に定まらず、型変数の表す型が曖昧になってしまいます。

これを解決するためには、一度に全ての項目を定義することを諦めねばなりませんでした。
メソッドチェーンなどを利用して次の項目の定義を促すような API を作成する必要があったのです。

現状、この問題を解決するためのスマートな API インタフェースを考案している段階です。

おわりに

現状の Vuex API は TypeScript で利用するためにはあまりにも型情報が曖昧です。

それを解決するために色々と模索してきた上で思うのは、
「確かにこれを現状のまま解決するのにはコストが掛かりすぎる」ということです。

今、私が夢想しているのは、Vue Composition API によって Vuex の必要なくなった未来です。

Vuex がもたらしたのは、データハンドリングフローの一貫性と Vue.js Dev Tools によるアプリケーションの状態の透明化、タイムトラベル・デバッグでした。
Vue Composition API にこれらは不可能なことでしょうか?
私は決して不可能なことではないんじゃないかな、とかなり楽観的に考えています。。。

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

docker-composeでexit status 1

はじめに

macOS Mojave 10.14.6
Docker 2.1.0.5

とりあえず

$ docker-compose up
...
front_nuxt  | npm ERR! Exit status 1
front_nuxt  | npm ERR! 
front_nuxt  | npm ERR! Failed at the Musclers@1.0.0 dev script.
front_nuxt  | npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
front_nuxt  | 
front_nuxt  | npm ERR! A complete log of this run can be found in:
front_nuxt  | npm ERR!     /root/.npm/_logs/2019-12-18T01_41_30_431Z-debug.log
front_nuxt  | front_nuxt exited with code 1

といってフロントくん(nuxt)が起動しない

$ docker-compose ps
   Name                 Command               State           Ports         
----------------------------------------------------------------------------
api_rails    bash -c rm -f 'tmp/pids/se ...   Up      0.0.0.0:8080->8080/tcp
db_psql      docker-entrypoint.sh postgres    Up      5432/tcp              
front_nuxt   docker-entrypoint.sh npm r ...   Exit 1      0.0.0.0:3000->3000/tcp

まぁ、だよね

原因

これがイマイチわからない。

やったこと

pruneでimageを一括削除した。

$ docker-compose down
$ docker system prune --volumes
# docker system pruneではvolumeまで削除してくれない
$ docker-compose up --build
# build忘れないようにね

で、buildが終わった後、無事起動しました。

最後に

最後にって書く必要あるのかわからないけどrailsとか使ってるなら、
db:createmigratedb:seed忘れないようにね。
DB空っぽだから何もできずに発狂した人もいるらしい←

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

Vue.jsでaxiosをモック化してテストする!

axiosのモック化にかなり手間取ったため、作業メモとして残しておこうと思いました。

必要なライブラリ

  • @vue/cli-plugin-unit-jest
  • @vue/test-utils
  • flush-promises

jestを使ってモック化するため、cli-plugin-unit-jestを入れました。

最後のflush-promisesはVue Test Utilsの公式サイトで非同期動作のテクニックとして紹介されており、非同期処理を強制的に(?)実行させるためにインストールしました。
非同期のテスト

テストコード

axiosのインポート

import axios from 'axios'

まずはaxiosモジュールをインポートします。

システムによっては、axiosの設定をindex.jsに書いていると思いますが、テストコードではこのindex.jsではなく、生(と言っていいかは謎ですが)のaxiosをimportします。
理由としては、index.jsをimportすると、そのファイルに書かれている、例えばaxios.create等のメソッドもモック化しないといけないからです。
もちろん、別でindex.jsのテストは必要なのですが、今回のテスト対象は、axios.postやgetを使っているコンポーネントであるため、index.jsではなく、axiosをimportします。

axiosのモック化

jest.mock('axios')

axiosモジュールをモック化します。
これで、プロダクトコードでaxiosを呼び出した場合、これから登録するモック関数が呼ばれるようになります。
このjest.mock('axios')は、describeの前に実行してください。

mountまたはshallowMountの実行時オプションにsync: falseを付ける

const wrapper = mount(sampleComponent, { sync: false })

axios.postにモック関数を登録する

例えばこのようなプロダクトコードがあるとします。

methods: {
  aFunc() {
    axios.post('/sample')
      .then(response => {
        console.log('成功')
      })
      .catch(error => {
        console.log('失敗')
      })
  }
}

このaxios.postから返すレスポンスをモックにします。

const response = {
  message: '成功'
}
axios.post.mockImplementationOnce((url) => {
  return Promise.resolve(response)
})

mockImplementationOnceは、postが呼び出されたときに「一度だけ」関数の中が実行されます。

もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定したPromise.resolveが返りますが、2回目のpostはエラーになります。
この場合の解決策としては2つあります。それについては後述します。
ちなみにプロダクトコードでthis.$httpのように使われている場合は、テストコードのimport文直後くらいにprototype.$http = axiosを入れてください。
私はcreateLocalVueでlocalVueを作成し、それに対して定義してます。

const localVue = createLocalVue()

localVue.prototype.$http = axios

モック関数の他例

もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定したPromise.resolveが返りますが、2回目のpostはエラーになります。

例えばこのようなコードがあった場合は、mockImplementationOnceを一つ指定しただけだとエラーになります。

methods: {
  aFunc() {
    axios.post('/sample')               // post呼び出し1回目
      .then(response => {
        console.log('/sample成功')
        axios.post('/sample2')          // post呼び出し2回目
          .then(response => {
            console.log('/sample2成功')
          })
          .catch(error => {
            console.log('/sample2失敗')
          })
      })
      .catch(error => {
        console.log('/sample失敗')
      })
  }
}

ではどうすればよいか、ですが、求めるレスポンスによって2通りのやり方があります。

レスポンスが同じでも良い場合

jest.fn().mockImplementationを使います。

const response = {
  message: '成功'
}
axios.post.mockImplementation((url) => {
  return Promise.resolve(response)
})

こうするとaxios.postが呼ばれたときには 常にresponseが返されます。

レスポンスが異なる場合

jest.fn().mockImplementationOnceを呼び出されるAPIの数分定義します。

const response1 = {
  message: '成功'
}
const response2 = {
  message: '失敗'
}
axios.post.mockImplementationOnce((url) => {
  return Promise.resolve(response1)
}).mockImplementationOnce((url) => {
  return Promise.resolve(response2)
})

こうすることで、1回目のAPI呼び出しのときはresponse1が返り、2回目のAPI呼び出しの時はresponse2が返ります。

モックの検証

きちんと想定通りのモックが呼ばれているかを検証します。
検証したいことによって、いろいろとメソッドが用意されていますが、ここではtoHaveBeenCalledWithを使います。
その他のメソッドについてはJestのドキュメントをご覧ください。

レスポンスが異なる場合の検証をしてみましょう。

expect(axios.post).toHaveBeenCalledWith('/sample')
expect(axios.post).toHaveBeenCalledWith('/sample2')

expectの引数には、実行したモックを指定します。
toHaveBeenCalledWithの引数には、APIが実行されたときの引数を指定します。
今回、API実行時にはurlのみを引数としてaxios.postに渡しているため、それを指定します。
params等の引数を第2引数に渡している場合は、expect(axios.post).toHaveBeenCalledWith('/sample', { param1: 'aaa' })のように渡します。

axiosではなく作ったモジュールの一部をモック化する!

  1. モジュールを読み込む
import sampleModule from '@/mixin/sampleModule.js'
  1. jest.mockでモック化する
jest.mock('@/mixin/sampleModule')
  1. モック化したいメソッドにjest.fnを入れ込む
sampleModule.methods.sampleMethod = jest.fn((arg1, arg2) => {
  return data
})

これでモック化できます。

テスト対象コンポーネントのメソッドをモック化する!

  1. jest.mockでモック化する
const sampleMethod = jest.fn()
  1. mountオプションにmethodsを指定する
const wrapper = mount(sampleComponent, {
  localVue,
  methods: { sampleMethod }
})

ちゃんと調べたわけではないのですが、どうやらconstの名前のメソッドがモック化されるようです。

以上です!

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の4本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編
↑↑今ここ↑↑

前回まで

Vue.jsでフロントエンド実装と、
LaravelのAPI実装が完了しました。

APIにつないでない状態の
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページと、
・タスク一覧取得API
・タスク詳細取得API
・タスク登録API
・タスク更新API
・タスク削除API
が完成している状態です。

今回はこの静的ページと
APIを繋ぎ込んでいきます。

この全体図の赤色部分になります。

Untitled Diagram.png

axios

今回、フロントページから
AjaxでAPIにリクエストを送信して
データの取得や更新を行います。

Ajax通信を簡単に実装するため、
今回はaxiosというパッケージを利用します。
https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9

特に難しいところはありませんが、
axiosの使い方を簡単に把握しておきましょう。

laravel/uiでベースを構築したので、
自分でインストールや設定作業などしなくても
最初からaxiosが利用できる状態です。

タスク一覧取得API繋ぎ込み

早速、タスク一覧ページとタスク一覧取得APIを繋ぎ込んでみましょう。

まずは<script>に必要なデータ、メソッドを定義します。

resources/js/components/TaskListComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                tasks: []
+            }
+        },
+        methods: {
+            getTasks() {
+                axios.get('/api/tasks')
+                    .then((res) => {
+                        this.tasks = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTasks();
+        }
+    }

</script>

まず data には空配列の tasks を用意します。

そして、 methods にある getTasks() メソッドで、
タスク一覧取得APIにリクエストして
そのレスポンスを先ほどの tasks の中に入れています。
(このメソッドで先ほど話したaxiosを利用してリクエストしています)

そして、画面描画時にこの getTasks() メソッドが実行されるように、
mounted() でメソッドを呼び出しています。

これで<script>側は完了です。

次に<templete>側も修正します。

resources/js/components/TaskListComponent.vue
- <tr>
-     <th scope="row">1</th>
-     <td>Title1</td>
-     <td>Content1</td>
-     <td>Ichiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">2</th>
-     <td>Title2</td>
-     <td>Content2</td>
-     <td>Jiro</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>
- <tr>
-     <th scope="row">3</th>
-     <td>Title3</td>
-     <td>Content3</td>
-     <td>Saburo</td>
-     <td>
-         <button class="btn btn-primary">Show</button>
-     </td>
-     <td>
-         <button class="btn btn-success">Edit</button>
-     </td>
-     <td>
-         <button class="btn btn-danger">Delete</button>
-     </td>
- </tr>


+ <tr v-for="task in tasks">
+     <th scope="row">{{ task.id }}</th>
+     <td>{{ task.title }}</td>
+     <td>{{ task.content }}</td>
+     <td>{{ task.person_in_charge }}</td>
+     <td>
+         <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">
+             <button class="btn btn-primary">Show</button>
+         </router-link>
+     </td>
+     <td>
+         <router-link v-bind:to="{name: 'task.edit', params: {taskId: task.id }}">
+             <button class="btn btn-success">Edit</button>
+         </router-link>
+     </td>
+     <td>
+         <button class="btn btn-danger">Delete</button>
+     </td>
+ </tr>

まずはべた書きで表示していた
3行のデータを削除します。

そして、先ほど定義したtasksデータをv-forで表示します。
<tr v-for="task in tasks">

ID、Title、Content、Person In Chargeの
各カラムは {{ task.title }} のようにデータを動的に表示させます。

- <td>Title1</td>
+ <td>{{ task.title }}</td>

また、「Show」「Edit」ボタンの
リンクURLのパラメータもべた書きしていたので、
ちゃんと動的にidを設定します。

- <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
+ <router-link v-bind:to="{name: 'task.show', params: {taskId: task.id }}">

これで、
APIからデータを取得し
それをv-forで画面に一覧表示できるようになりました。

commit:タスク一覧ページAPI繋ぎ込み

タスク一覧ページ完成です。

タスク詳細取得API繋ぎ込み

次に、タスク詳細ページとタスク詳細取得APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskShowComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

一覧ページと同じように、
data に空のtaskを用意。
methodsgetTask() でAPIからタスクデータを取得。
mounted() で画面描画時にメソッド呼び出し。
としています。

次に<templete>側。

resources/js/components/TaskShowComponent.vue
<div class="form-group row border-bottom">
    <label for="id" class="col-sm-3 col-form-label">ID</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
-           v-bind:value="taskId">
+           v-model="task.id">

</div>

<div class="form-group row border-bottom">
    <label for="title" class="col-sm-3 col-form-label">Title</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
-           value="title title">
+           v-model="task.title">

</div>

<div class="form-group row border-bottom">
    <label for="content" class="col-sm-3 col-form-label">Content</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
-           value="content content">
+           v-model="task.content">

</div>

<div class="form-group row border-bottom">
    <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
    <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
-           value="Ichiro">
+           v-model="task.person_in_charge">

</div>

各データをv-modelで表示するようにしました。

これでAPI取得したデータをタスク詳細ページに表示できました。

commit:タスク詳細ページAPI繋ぎ込み

タスク詳細ページ完成です。

タスク登録API繋ぎ込み

次に、タスク登録ページとタスク登録APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskCreateComponent.vue
<script>
-    export default {}
+    export default {
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            submit() {
+                axios.post('/api/tasks', this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'});
+                    });
+            }
+        }
+    }

</script>

空のtaskデータを用意するところは先ほどと同じです。

methodssubmit() メソッドで、
taskデータをタスク登録APIにPOST送信する処理を書いています。

また、APIによるデータ登録完了後、
this.$router.push({name: 'task.list'}); でタスク一覧ページにリダイレクトしています。

 
 
次に<templete>側。

resources/js/components/TaskCreateComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
    <div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
    </div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

各フォームはv-modeltaskデータとバインディングすることで、
フォームにデータが入力されたら
<scripts>側のtaskデータも更新されるようになっています。

そして、
<form v-on:submit.prevent="submit">
で、フォーム送信時に先ほど定義したsubmitメソッドを呼び出すようにしています。

これで、入力内容が反映されたtaskデータを
submitメソッドでAPI送信できる状態になっています。

commit:タスク登録ページAPI繋ぎ込み

これでタスク登録ページ完成です。

タスク更新API繋ぎ込み

次に、タスク編集ページとタスク更新APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskEditComponent.vue
<script>
    export default {
        props: {
            taskId: String
        },
+        data: function () {
+            return {
+                task: {}
+            }
+        },
+        methods: {
+            getTask() {
+                axios.get('/api/tasks/' + this.taskId)
+                    .then((res) => {
+                        this.task = res.data;
+                    });
+            },
+            submit() {
+                axios.put('/api/tasks/' + this.taskId, this.task)
+                    .then((res) => {
+                        this.$router.push({name: 'task.list'})
+                    });
+            }
+        },
+        mounted() {
+            this.getTask();
+        }
    }

</script>

タスク詳細ページとタスク登録ページでやったことを
両方やっているだけです。

空のtaskデータを用意し、
getTask() メソッドでAPIから取得したデータをセットする。

submit メソッドでは、
タスク更新APIにputリクエストを送信しています。

 
 
次に<template>

resources/js/components/TaskEditComponent.vue
- <form>
+ <form v-on:submit.prevent="submit">
    <div class="form-group row">
        <label for="id" class="col-sm-3 col-form-label">ID</label>
-        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+        <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-model="task.id">
    </div>
    <div class="form-group row">
        <label for="title" class="col-sm-3 col-form-label">Title</label>
-        <input type="text" class="col-sm-9 form-control" id="title">
+        <input type="text" class="col-sm-9 form-control" id="title" v-model="task.title">
    </div>
    <div class="form-group row">
        <label for="content" class="col-sm-3 col-form-label">Content</label>
-        <input type="text" class="col-sm-9 form-control" id="content">
+        <input type="text" class="col-sm-9 form-control" id="content" v-model="task.content">
    </div>
    <div class="form-group row">
        <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
-        <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+        <input type="text" class="col-sm-9 form-control" id="person-in-charge" v-model="task.person_in_charge">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>

</form>

これはタスク登録ページと同じです。

各フォームはv-modeltaskデータとバインディングして、
formの v-on:submit.prevent="submit"sumitメソッドを呼んでいます。

commit:タスク編集ページAPI繋ぎ込み

これでタスク編集ページは完成。

タスク削除API繋ぎ込み

最後に、タスク一覧ページのDeleteボタンとタスク削除APIを繋ぎ込んでいきます。

まずは<script>

resources/js/components/TaskListComponent.vue
methods: {
    getTasks() {
        axios.get('/api/tasks')
            .then((res) => {
                this.tasks = res.data;
            });
    },
+    deleteTask(id) {
+        axios.delete('/api/tasks/' + id)
+            .then((res) => {
+                this.getTasks();
+            });
+    }
},

deleteTask() メソッドを追加しました。
タスクIDを引数で受け取り、
タスク削除APIにリクエストを送信しています。

削除完了したら、
getTasks() メソッドを呼んで
タスク一覧を再読み込みしています。

次に<template>

resources/js/components/TaskListComponent.vue
<td>
-    <button class="btn btn-danger">Delete</button>
+    <button class="btn btn-danger" v-on:click="deleteTask(task.id)">Delete</button>

</td>

もともと設置していたDeleteボタンに
v-on:click="deleteTask(task.id)" を追加しました。

これで、このボタンをクリックしたら deleteTask() メソッドが呼ばれます。

commit:タスク一覧ページ削除API繋ぎ込み

これでタスク一覧ページの削除処理もできたので、
全ページ、全機能が完成しました。

おわりに

シンプルなCRUD機能のアプリを
Vue.jsのSPAとLaravelのAPIで構築しました。

Vue側もLaravel側もほとんど難しいところもなく、
かなり簡単に書けたと思います。

今回はできるだけ簡単に一通りの機能を作るチュートリアルとしたかったため、
本来実装すべき処理を省いた箇所が多いです。

Vue側では
Ajaxのエラーハンドリングや
API送信前のバリデーションなど
本来は実装すべきです。

Laravel側もバリデーションや
APIの認証処理などがあるといいです。

今回のチュートリアルで
ざっくりと全体イメージをまずはつかんで、
今後上記のような詳細な処理を少しずつ追加していくといいかと思います。

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の3本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、Vue.jsでフロントエンドのみ実装し、
静的なSPAができました。

べた書きのサンプルデータが表示されている状態で、
・タスク一覧
・タスク詳細
・タスク登録
・タスク編集
のページが完成しています。

API実装の進め方

この全体図の緑色部分にある
5つのAPIを実装していきます。

Untitled Diagram.png

今回は一番シンプルな形で進めるので、
各APIの処理は全てコントローラ内で数行で完結します。

また、API自体の実装の前に
DBのセットアップや最低限のテストデータも準備します。

SQLiteのセットアップ

今回は作業簡略化のため
MySQLやPostgreSQLを用意せず
SQLiteを使います。

まずはSQLiteのストレージとなるファイルを用意します。

database/database.sqlite に空のファイルを作成すればOKです。

次に、.envのDB接続情報を修正します。

.env
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=laravel
- DB_USERNAME=root
- DB_PASSWORD=


+ DB_CONNECTION=sqlite

これでSQLiteを利用するための設定は完了です。

ただし、PHPのSQLiteドライバーが有効になっている必要がありますので
もしなっていなければ有効にしてください。
https://awesome-linus.com/2019/05/24/php-sqlite-driver-install/

migration作成

migrationでタスクテーブルを作成します。

まずは下記コマンドでmigrationファイルを生成。

php artisan make:migration create_tasks_table

生成されたmigrationのupメソッドの中をこのように書き換えます。

create_tasks_table.php
Schema::create('tasks', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title', 100);
    $table->string('content', 100);
    $table->string('person_in_charge', 100);
    $table->timestamps();
});

commit:migration作成

モデル作成

次に、タスクテーブルに対応する
タスクモデルを作ります。

php artisan make:model Task

生成されたモデルファイルに、
$fillable のみ追記しておきます。

app/Task.php
  class Task extends Model
  {
+    protected $fillable = [
+        'title',
+        'content',
+        'person_in_charge',
+    ];
  }

commit:タスクモデル作成

seeder作成

次に、テストデータを自動生成するための
seederを作成します。

まずは下記コマンドでseederファイルを生成。

php artisan make:seeder TasksTableSeeder

生成されたseederファイルのrunメソッドを
このように修正します。

database/seeds/TasksTableSeeder.php
 public function run()
 {
+    for ($i = 1; $i <= 10; $i++) {
+        Task::create([
+                'title' => 'title' . $i,
+                'content' => 'content' . $i,
+                'person_in_charge' => 'person_in_charge' . $i,
+            ]
+        );
+    }
 }

また、このseederを実行するためにDatabaseSeederファイルも修正します。

database/seeds/DatabaseSeeder.php
 public function run()
 {
-    // $this->call(UsersTableSeeder::class);
+    $this->call(TasksTableSeeder::class);
 }

commit:タスクseeder作成

テーブル、テストデータ生成

テーブルとテストデータを生成する準備は整いましたので、
実際に生成しましょう。

php artisan migrate --seed

これで先ほど作成したmigrationとseederが実行され、
テーブルとテストデータが10件できてるはずです。

データがちゃんと入っているか確認した場合は
tinkerを使ってみてください。

$ php artisan tinker


>>> Task::all();

これでタスクテーブルのデータが一覧で表示されます。

タスク一覧取得API実装

それでは早速API実装を始めます。
まずはタスク一覧取得APIから。

ルーティングを追加。

routes/api.php
+ Route::get('/tasks', 'TaskController@index');

次に、タスクコントローラを作成し、
そこにindexメソッドを追加します。

まずはartisanコマンドでコントローラファイル自体を生成。

php artisan make:controller TaskController

そして、indexメソッド追加。

app/Http/Controllers/TaskController.php
+ <?php
+
+ namespace App\Http\Controllers;
+
+ use App\Task;
+
+ class TaskController extends Controller
+ {
+     public function index()
+     {
+         return Task::all();
+     }
+ }

ただTaskモデルから全件取得してreturnするだけです。

POSTMANなどで
http://localhost:8000/api/tasks
にリクエストすると
タスク一覧が取得できると思います。
routes/api.phpにルーティング定義すると、自動でパスの頭に/apiがつきます。

レスポンスはこのようなjson形式になります。

レスポンス形式
[
    {
        "id": 1,
        "title": "title1",
        "content": "content1",
        "person_in_charge": "person_in_charge1",
        "created_at": "2019-12-17 00:43:38",
        "updated_at": "2019-12-17 00:43:38"
    },
    {
        "id": 2,
        "title": "title2",
        "content": "content2",
        "person_in_charge": "person_in_charge2",
        "created_at": "2019-12-17 00:43:38",
        "updated_at": "2019-12-17 00:43:38"
    },
]

commit:タスク一覧取得API実装

タスク詳細取得API実装

次にタスク詳細取得APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::get('/tasks/{task}', 'TaskController@show');

コントローラにshowメソッドを追加。

app/Http/Controllers/TaskController.php
+ public function show(Task $task)
+ {
+     return $task;
+ }

URLパラメータで受け取ったタスクモデルを
そのままreturnするだけです。
※これでLaravelが勝手にjson形式のレスポンスを返却します

commit:タスク詳細取得API実装

タスク登録API実装

次に、タスク登録APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
+ Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');

※ルーティングの定義順を間違えると正しく動かないので、この通りに記述してください

コントローラにstoreメソッド追加。

app/Http/Controllers/TaskController.php
  use App\Task;
+ use Illuminate\Http\Request;


+ public function store(Request $request)
+ {
+     return Task::create($request->all());
+ }

リクエストで受け取ったデータをそのまま
モデルのcreateでデータ登録しているだけです。

このようなjson形式のデータを受け取ることを想定しています。

リクエスト形式
{
    "title": "new title",
    "content": "new content",
    "person_in_charge": "new person_in_charge1"
}

commit:タスク登録API実装

タスク更新API実装

次に、タスク更新APIです。

ルーティング追加。

routes/api.php
  Route::get('/tasks', 'TaskController@index');
  Route::post('/tasks', 'TaskController@store');
  Route::get('/tasks/{task}', 'TaskController@show');
+ Route::put('/tasks/{task}', 'TaskController@update');

コントローラにupdateメソッド追加。

app/Http/Controllers/TaskController.php
+ public function update(Request $request, Task $task)
+ {
+     $task->update($request->all());
+
+     return $task;
+ }

受け取るリクエストの形は、
登録APIと同じjson形式です。

URLパラメータで受け取ったTaskモデルのupdateメソッドで
そのままデータを更新するだけです。

commit:タスク更新API実装

タスク削除API実装

次はタスク削除API。

ルーティング追加。

routes/api.php
  Route::get('/tasks/{task}', 'TaskController@show');
  Route::put('/tasks/{task}', 'TaskController@update');
+ Route::delete('/tasks/{task}', 'TaskController@destroy');

コントローラにdestroyメソッド追加。

app/Http/Controllers/TaskController.php
+ public function destroy(Task $task)
+ {
+     $task->delete();
+ 
+     return $task;
+ }

URLパラメータでTaskを受け取り、
それをそのままdeleteします。

commit:タスク削除API実装

おわりに

これで今回必要なAPIはすべて実装完了です。

POSTMANなどを利用して、
各APIの動作を確認するといいと思います。

本来は、このAPIでは
バリデーションを入れたり、
検索処理を入れたりすることになるかと思います。

次回は、
フロントのVueからAjaxで
このAPIに対してリクエスト送信し、
実際にデータの表示、更新、登録、削除ができるようにします。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の2本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

前回まで

前回は、環境構築と必要なパッケージのインストールを行いました。

http://localhost:8000
でLaravelのウェルカムページが表示される状態で
次に進んでください。

コンポーネントの構成

本記事では、この全体図の青色部分、
Vue.jsによるフロントエンド実装のみを行います。

Untitled Diagram.png

作るページ(コンポーネント)は全部で4つです。

  • タスク一覧
  • タスク詳細
  • タスク登録
  • タスク編集

最初に各ページの完成状態の画像を確認します。

  • タスク一覧
    list.PNG

  • タスク詳細
    show.PNG

  • タスク登録
    create.PNG

  • タスク編集
    edit.PNG

前にインストールしたlaravel/ui vueに
デフォルトで組み込まれているbootstrapを使って
最低限のシンプルなUIにしています。
※今回はbootstrapの使い方には言及しません

各ページ上部にある黒い背景色の部分はヘッダーナビで、
全ページ固定で表示されるコンポーネントです。

ヘッダーナビより下の
一覧テーブルや入力フォーム部分が
URLごとに切り替わるメインのコンポーネントになります。

それでは、各ページのメインコンポーネントに加えて
ヘッダーーコンポーネントの
計5つを実装していきます。

ベースbladeとベースルーティングを追加

このアプリでは、
初回アクセス時のみLaravel側でリクエストを受けて
ページを表示し、
それ以降はフロント側のVue Routerによってルーティングが行われます。

その最初のリクエストを受け取る
Laravel側のルーティングとbladeファイルを追加します。

routes/web.php
- Route::get('/', function () {
-     return view('welcome');
- });
+ Route::get('/{any}', function() {
+     return view('app');
+ })->where('any', '.*');
resouces/views/app.blade.php
+ <!doctype html>
+ <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+     <meta charset="utf-8">
+     <meta name="viewport" content="width=device-width, initial-scale=1">
+ 
+     <!-- CSRF Token -->
+     <meta name="csrf-token" content="{{ csrf_token() }}">
+ 
+     <title>{{ config('app.name', 'Vue Laravel SPA') }}</title>
+ 
+     <!-- Styles -->
+     <link href="{{ mix('/css/app.css') }}" rel="stylesheet">
+ </head>
+ <body>
+ <div id="app">
+ 
+ </div>
+ <!-- Scripts -->
+ <script src="{{ mix('/js/app.js') }}" defer></script>
+ </body>
+ </html>

commit:ベースのbladeとルーティング追加

これで、どのURLでアクセスしても
このapp.blade.phpが表示されるようになりました。

また、前回の記事でインストールした
Vue.jsやbootstrapも
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
このjs、cssファイルで読み込まれているため
利用できる状態です。

試しにデフォルトで用意されている
ExampleComponentを表示してみてください。

resouces/views/app.blade.php
 <div id="app">
+ <example-component></example-component>
 </div>

これで
http://localhost:8000
にアクセスすると、
このようにExampleComponentが表示されると思います。

example.PNG

これが正しく表示されていれば、
Vue.js、bootstrapがちゃんと使えている状態です。
(このExampleComponentはbootstrapが使われています)

ヘッダーコンポーネント実装

ベースのbladeが配置できたので、
次に全ページ共通で固定表示する
ヘッダーコンポーネントを実装します。

HeaderComponentの追加

resources/js/components/HeaderComponent.vue
+ <template>
+     <div class="container-fluid bg-dark mb-3">
+         <div class="container">
+             <nav class="navbar navbar-dark">
+                 <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
+                 <div>
+                     <button class="btn btn-success">List</button>
+                     <button class="btn btn-success">ADD</button>
+                 </div>
+             </nav>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

classがいろいろとたくさん設定されていますが、
全部bootstrapのclassで見た目を整えているだけなので、
あまり気にしなくてOKです。
 

そのコンポーネントをVueインスタンスに登録

resources/js/app.js
+ import HeaderComponent from "./components/HeaderComponent";
//↑ファイル先頭

  Vue.component('example-component', require('./components/ExampleComponent.vue').default);
+ Vue.component('header-component', HeaderComponent);

 
 

登録したコンポーネントをベースbladeに追加

resources/views/app.blade.php
 <div id="app">
+     <header-component></header-component>
 </div>

commit:ヘッダーコンポーネント実装

この状態でページを表示してみます。
npm run dev または npm run watch でソースをビルドするのを忘れないようにしましょう

ページ上部に黒いヘッダーナビが表示されていると思います。

まだボタンのリンク先は設定されていませんが、
この後ページを追加した際にこのボタンのリンクも設定します。

タスク一覧コンポーネント実装

まずタスク一覧コンポーネントを追加します。

resources/js/components/TaskListComponent.vue
+ <template>
+     <div class="container">
+         <table class="table table-hover">
+             <thead class="thead-light">
+             <tr>
+                 <th scope="col">#</th>
+                 <th scope="col">Title</th>
+                 <th scope="col">Content</th>
+                 <th scope="col">Person In Charge</th>
+                 <th scope="col">Show</th>
+                 <th scope="col">Edit</th>
+                 <th scope="col">Delete</th>
+             </tr>
+             </thead>
+             <tbody>
+             <tr>
+                 <th scope="row">1</th>
+                 <td>Title1</td>
+                 <td>Content1</td>
+                 <td>Ichiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">2</th>
+                 <td>Title2</td>
+                 <td>Content2</td>
+                 <td>Jiro</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             <tr>
+                 <th scope="row">3</th>
+                 <td>Title3</td>
+                 <td>Content3</td>
+                 <td>Saburo</td>
+                 <td>
+                     <button class="btn btn-primary">Show</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-success">Edit</button>
+                 </td>
+                 <td>
+                     <button class="btn btn-danger">Delete</button>
+                 </td>
+             </tr>
+             </tbody>
+         </table>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ID、Title、Content(内容)、Person In Charge(担当者)、各種操作ボタン
をカラムに持つテーブルです。

現時点では、サンプルとして3行ほどべた書きで
タスクを表示しています。

後々の作業でここは
LaravelAPIからデータを受け取り表示するようになります。

また、
Show、Edit、Deleteのボタンを設置していますが
いまはリンク先が設定されていません。

後々各コンポーネントを実装したらリンク先を設定していきます。
 
 
追加したタスク一覧コンポーネントを
Vue Routerに登録します。

resources/js/app.js
+ import VueRouter from 'vue-router';
  import HeaderComponent from "./components/HeaderComponent";
+ import TaskListComponent from "./components/TaskListComponent";


  window.Vue = require('vue');


+ Vue.use(VueRouter);
+ 
+ const router = new VueRouter({
+     mode: 'history',
+     routes: [
+         {
+             path: '/tasks',
+             name: 'task.list',
+             component: TaskListComponent
+         },
+     ]
+ });


  const app = new Vue({
      el: '#app',
+     router
  });

VueRouter自体の詳しい解説は省略しますが、
ポイントはここです。

routes: [
    {
        path: '/tasks',
        name: 'task.list',
        component: TaskListComponent
    },
]

ここで、
「/tasks」のURLでアクセスしたら
「TaskListComponent」を表示する。
このルーティングの名前は「task.list」である。
と設定しています。

別ページ(コンポーネント)を追加した際は、
同じようにこの routes に設定を加えていくことになります。
 
 

そして、ルーティングで紐づけられたコンポーネントを表示するために、
ベースのbladeに <router-view> を配置する必要があります。

resources/views/app.blade.php
  <div id="app">
     <header-component></header-component>


+    <router-view></router-view>
  </div>

先ほどVue Routerで設定したとおり、
URLに紐づくコンポーネントがこの
<router-view> の部分に表示されることになります。

この状態で
http://localhost:8000/tasks
にアクセスしてみましょう。
※ビルドを忘れずに

お手本で見た通りの
一覧テーブルが表示されていると思います。

ついでに、
ヘッダーコンポーネントにある
「List」ボタンのリンク先を設定しておきましょう。

resources/js/components/HeaderComponent.vue
<nav class="navbar navbar-dark">
    <span class="navbar-brand mb-0 h1">Vue Laravel SPA</span>
    <div>
        <button class="btn btn-success">List</button>
+        <router-link v-bind:to="{name: 'task.list'}">
            <button class="btn btn-success">List</button>
+        </router-link>
        <button class="btn btn-success">ADD</button>
    </div>

</nav>

このように <route-link>v-bind:to
リンク先のルーティング名を設定することで
SPAのリンクとして動作させることができます。

commit:タスク一覧コンポーネント実装

タスク詳細コンポーネント実装

次に、タスク詳細コンポーネントを追加します。

まずコンポーネントファイル作成。

resources/js/components/TaskShowComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row border-bottom">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id"
+                                v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="title"
+                                value="title title">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="content"
+                                value="content content">
+                     </div>
+                     <div class="form-group row border-bottom">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="person-in-charge"
+                                value="Ichiro">
+                     </div>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

taskIdをURLパラメータとして受け取って、
そのIDのみ
<input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
v-bind:value="taskId" 部分で動的に表示しています。

それ以外のcontent、person-in-chargeは
まだべた書きにしているだけです。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskShowComponent from "./components/TaskShowComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},

+ {
+     path: '/tasks/:taskId',
+     name: 'task.show',
+     component: TaskShowComponent,
+     props: true
+ },

これで、/tasks/:taskId のURLでアクセスすると、
TaskShowComponentが表示されます。

:taskId の部分は、任意のタスクIDが入ります。

このURLパラメータが、
先ほどのタスク詳細コンポーネントの中で使われていた
taskId となります。

http://localhost:8000/tasks/3
のように :taskId の部分に好きな数字を入れてアクセスすると
タスク詳細コンポーネントが表示されます。

ついでにタスク一覧コンポーネントに置いていた
「Show」ボタンのリンク先を設定しておきましょう。

resources/js/components/TaskListComponent.vue

+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 1}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 2}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.show', params: {taskId: 3}}">
        <button class="btn btn-primary">Show</button>
+    </router-link>

これで、一覧ページの「Show」ボタンをクリックすると
タスク詳細ページに遷移するようになりました。

commit:タスク詳細コンポーネント実装

タスク登録コンポーネント実装

次にタスク登録コンポーネントを実装します。

まずコンポーネントファイル作成。

resources/js/components/TaskCreateComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {}
+ </script>

ただ空のフォームを表示しているだけです。
現時点では送信処理は書いていません。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import VueRouter from 'vue-router';
import HeaderComponent from "./components/HeaderComponent";
import TaskListComponent from "./components/TaskListComponent";
+ import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},

+ {
+     path: '/tasks/create',
+     name: 'task.create',
+     component: TaskCreateComponent
+ },
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},

これで、
http://localhost:8000/tasks/create
でアクセスすればタスク登録ページが表示されます。

ついでにヘッダーコンポーネントに置いていた
「Add」ボタンのリンク先を設定しておきます。

resources/js/components/HeaderComponent.vue
<div>
    <router-link v-bind:to="{name: 'task.list'}">
        <button class="btn btn-success">List</button>
    </router-link>
+    <router-link v-bind:to="{name: 'task.create'}">
        <button class="btn btn-success">ADD</button>
+    </router-link>

</div>

commit:タスク登録コンポーネント実装

タスク編集コンポーネント実装

次に、タスク編集コンポーネントを実装します。

まずコンポーネントファイルを作成。

resources/js/components/TaskEditComponent.vue
+ <template>
+     <div class="container">
+         <div class="row justify-content-center">
+             <div class="col-sm-6">
+                 <form>
+                     <div class="form-group row">
+                         <label for="id" class="col-sm-3 col-form-label">ID</label>
+                         <input type="text" class="col-sm-9 form-control-plaintext" readonly id="id" v-bind:value="taskId">
+                     </div>
+                     <div class="form-group row">
+                         <label for="title" class="col-sm-3 col-form-label">Title</label>
+                         <input type="text" class="col-sm-9 form-control" id="title">
+                     </div>
+                     <div class="form-group row">
+                         <label for="content" class="col-sm-3 col-form-label">Content</label>
+                         <input type="text" class="col-sm-9 form-control" id="content">
+                     </div>
+                     <div class="form-group row">
+                         <label for="person-in-charge" class="col-sm-3 col-form-label">Person In Charge</label>
+                         <input type="text" class="col-sm-9 form-control" id="person-in-charge">
+                     </div>
+                     <button type="submit" class="btn btn-primary">Submit</button>
+                 </form>
+             </div>
+         </div>
+     </div>
+ </template>
+ 
+ <script>
+     export default {
+         props: {
+             taskId: String
+         }
+     }
+ </script>

詳細ページと同様に、
taskId をURLパラメータで受け取り、
IDの欄にデータを表示しています。

このコンポーネントをVue Routerに登録します。

resources/js/app.js
import TaskCreateComponent from "./components/TaskCreateComponent";
import TaskShowComponent from "./components/TaskShowComponent";
+ import TaskEditComponent from "./components/TaskEditComponent";


{
    path: '/tasks',
    name: 'task.list',
    component: TaskListComponent
},
{
    path: '/tasks/create',
    name: 'task.create',
    component: TaskCreateComponent
},
{
    path: '/tasks/:taskId',
    name: 'task.show',
    component: TaskShowComponent,
    props: true
},

+ {
+     path: '/tasks/:taskId/edit',
+     name: 'task.edit',
+     component: TaskEditComponent,
+     props: true
+ },

これで、
http://localhost:8000/tasks/:taskId/edit
にアクセスするとタスク編集ページが表示されます。

:taskId の部分は任意のタスクIDになります。

ついでにタスク一覧コンポーネントに置いていた
「Edit」ボタンのリンク先も設定しておきます。

resources/js/components/TaskListComponent.vue

+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 1}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 2}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>


+    <router-link v-bind:to="{name: 'task.edit', params: {taskId: 3}}">
        <button class="btn btn-success">Edit</button>
+    </router-link>

commit:タスク編集コンポーネント実装

おわりに

これで、
・タスク一覧ページ
・タスク詳細ページ
・タスク登録ページ
・タスク編集ページ
が実装できました。

現時点ではAPIでデータを取得する処理はできていませんが、
この状態でもVue.jsによる 静的な SPAにはなっています。

もしデータベースを利用しないような
ウェブサイトなどをVue.jsでSPAとして構築する場合は
今回解説した内容を基本として
ページの追加をしていくだけです。

それでは、次にLaravelのAPI実装に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編

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

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編

はじめに

Vue.jsとLaravelによるSPA実装のチュートリアル記事です。

本記事は、4本の連載記事の1本目です。

Vue.js + LaravelでシンプルなSPA構築チュートリアル:概要編
↑↑今ここ↑↑
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:LaravelAPI編
Vue.js + LaravelでシンプルなSPA構築チュートリアル:VueとAPI結合編

Vue.js 2.5
Laravel 6.7
を利用していますが、
別のバージョンでも大枠は同じだと思うので、
チュートリアルとしては参考にしていただけると思います。

アプリ構成

タスクを
・一覧表示
・詳細表示
・登録
・更新
・削除
する機能がある
シンプルなアプリケーションです。

一番シンプルな状態でCRUDの実装を
一通り実践することができます。

Untitled Diagram.png

Vue.jsでフロントエンドを実装し、
LaravelでAPIを実装します。

各コンポーネントでは、
ajaxでLaravelのAPIにリクエストし、
データを取得、更新します。

SPAになっているので、
フロントの各コンポーネントは
ページリロードせずにVue.jsによって表示切替されます。

構築の流れ

まずこの記事で
環境構築と必要なパッケージのインストール、セットアップまで行います。

そして、
1、Vue.jsで静的なSPA実装
2、LaravelでAPI実装
3、フロントエンドとAPIの結合
という順番で実装を進めます。

上の構成図で言うと、
まず青色のVue.jsフロントエンド部分のみ実装し、
そのあと緑色のLaravelAPIを実装し、
最後に赤色のフロントエンドのAjax通信部分を実装してAPIと結合する
という流れです。

少し長くなるので、
上記の3ステップはそれぞれ別のQiita記事とします。

完成品のソースコードはGitHubに公開しています。
https://github.com/MinatoNaka/VueLaravelSpa

また、構築手順の通り1ステップごとにコミットしていますので、
コミット一覧を順に追っていくと
実装の流れが理解しやすいと思います。
https://github.com/MinatoNaka/VueLaravelSpa/commits/master

環境構築

それでは、この記事では
環境構築と必要なパッケージのインストール、セットアップを済ませます。

PHP、Composer、NPMが利用可能な環境での構築を前提としています。
(筆者はWindowsのPCにて構築しています)

Laravelプロジェクト作成

まずは、
新品のLaravelプロジェクトを作成します。
任意のディレクトリで、下記コマンドを実行。

composer create-project --prefer-dist laravel/laravel vue-laravel-spa

commit:Laravelプロジェクト作成

新品プロジェクトの状態で
一度表示確認してみます。

まずはサーバ起動

cd vue-laravel-spa

php artisan serve

このURLでアクセスします。
http://localhost:8000/

Laravelのウェルカムページが表示されれば
正常に動作しています。

キャプチャ.PNG

laravel/uiインストール

次に、laravel/uiというパッケージをcomposerでインストールします。

これは、
Laravelでフロントエンド開発をするための
ベースを簡単に提供してくれるツールです。
下記コマンドを実行。

composer require laravel/ui

commit:laravel/uiインストール

laravel/ui vueインストール

先ほどインストールしたlaravel/uiを使うと、
bootstrapやvue、reactなどさまざまな
フロントエンドのベースコードを生成できます。
Laravel 6.x JavaScriptとCSSスカフォールド

今回はvueのベースを作ります。

php artisan ui vue

このコマンドを実行すると、
package.jsonに様々なフロントエンドパッケージが追加されたり、
ベースとなるjsファイルやサンプルのVueコンポーネント、
Laravel Mixの設定ファイルなどが自動で配置されます。
commit:laravel/ui vueインストール

フロントエンドパッケージインストール

laravel/uiのvueベースをインストールした際に、
必要なフロントエンドパッケージがpackage.jsonに追記されました。
bootstrap、jquery、vueなどが追記されています。

これらのパッケージをインストールします。

npm install

このコマンドを実行したら、
/node_modules/ ディレクトリが作成され、
その配下に様々なパッケージのディレクトリ、ファイルが追加されます。

commit:フロントエンドパッケージインストール
/node_modules/ ディレクトリはgitignoreされているためコミットに含まれません

Vue Routerインストール

今回はVue.jsでSPAを作るので、
Vue Routerというパッケージを追加でインストールしておきます。

Vue Routerとは、
Vue.jsでSPAを構築するためのルーティング処理を行う
Vue公式のツールです。

npm install --save vue-router

commit:Vue Routerインストール

フロントエンドビルド実行

必要なパッケージは全てインストール完了したので、
最後にフロントエンドソースコードをビルドしてみます。

npm run dev

このコマンドを実行することで、
Laravel Mixのビルド処理が実行され、
コンパイルされたjs、cssが
/public/js public/cssに出力されます。
※Laravel Mixについては詳しく言及しません。わからない方は、こちらの記事を参照ください
Laravel Mixとは?webpackをより便利に、簡単に。Laravel以外でも使えるよ。

この後実装するHTMLファイルでは、
このコンパイルされたjs、cssを読み込むことになります。

コンパイル済みファイルはgit管理する必要がないので
gitignoreに追記しておきます。

.gitignore
+ /public/js
+ /public/css

フロントエンドビルド実行

今後jsファイルやcssファイル、vueコンポーネントを更新した際は、
毎回 npm run dev でソースをビルドしないと画面に反映されないので注意してください。

毎回ビルドを実行するのが面倒な場合は
npm run watch を実行するとウォッチモードになり
ビルド対象ファイルを更新、保存すると自動でビルドが実行されるようになるので便利です。

おわりに

これで、環境構築と必要なパッケージ類のインストールは完了です。
次は「Vue.jsで静的なSPA実装」に進みましょう。
Vue.js + LaravelでシンプルなSPA構築チュートリアル:Vueフロントエンド編

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

人生を要約するサービス "Digest-of-Life" -5秒でエモい画像を作れるようにするProject-

お詫び

アドベントカレンダーの記事と同時にリリースする予定で作成していましたが、
バックエンドで使用するAPIのproduction Licenceが間に合わず、リリースできませんでした...
来月くらいにリリースする予定ですので、もしご興味を持っていただけたならば、ストックをお願いします。
(記事の更新で通知させていただきます。:bow_tone1:

作ったもの(というか作っているもの)

d-o-life6.gif

"Digest-of-Life"

人生を要約するサービスを作ってます。
提供するものは、大切にしたい言葉を、きれいな画像とともに保存するサービスです。
こっそり記録していってもいいし、その場で画像に合成して共有してもいい

文字を画像にしたいとき、きれいな画像で言葉をドーンと伝えたいとき等にご利用ください。

動機(なぜ作ろうと思ったか)

digest-of-life.003.jpeg.jpg

伊坂幸太郎著「モダンタイムス」の作中のセリフですが、昔読んでからずーっと頭の片隅に残ってまして、、、
でも、個人的には哲学は人生の要約だと信じているので、それを形にできたらなと思い、
今回、個人の哲学を記録するサービスを作るに至りました。

アプリケーション構成

digest-of-life.008.jpeg.jpg

ほぼすべてFirebaseで作成しました。
APIデータベースログイン機能javascriptちょこっと書くだけで実装できるFirebase先生素晴らしい!

ホスティングサービス: Firebase Hosting

  • これがないとWebサービスが動かない。
  • ローカルのエミュレータで動作確認したあと、コマンド1つでデプロイできる。

API: Firebase Functions

  • 面倒なサーバーサイドのAPIをjavascriptで数行書いてデプロイするだけで動く。
  • 今回は、Unsplush apiapi keyを秘匿するために使用。

データベース: Firebase Realtime Database

  • メソッド1つ書くだけで、clientからDBにCRUDできる。すごい。。。
  • 今回は、ユーザーが保存した言葉や写真のデータを永続化するために使用。

ユーザー管理: Google アカウント

  • Firebase側にGoogle ログインをアプリに統合する機能が用意されていたので。
  • ユーザーを管理せずに、ユーザーを識別できるって素晴らしい。

画像サービス: Unsplush api

  • 今回の目玉、めちゃきれいな画像が大量にあるサイトのAPI
  • APIを利用すると、細かい検索機能が使える。
  • 普通のdevelopアカウントだと50requests/hour、ガイドラインに沿ってることを示せれば、Production版api keyがもらえる

Unsplash_CaseStudy.png

要素技術

使用した技術要素と検証に使ったサンプルコードを書きます。

vuetify なるべくCSS書かないマン

20180730160701.jpg
- https://vuetifyjs.com/ja/
- <v-XXXX>のような独自のコンポーネントを使用して、独自のクラスでflex margin padding等を適用します。
- 今回の見た目はほぼvuetifyのコンポーネントの見た目そのままになってます。
- モーダルページングトースト等がすごく楽になります。

html2canvas Webページの一部を画像にする

image.png
- https://html2canvas.hertzen.com/
- 「WebページをレンダリングしてCanvas上に描画する」処理を行います。

  • 2種類のやり方のうち後者を採用しています。

①読み込んだ要素をtoDataURLにして<img>srcに埋め込むタイプ
(こちらの記事より: [JavaScript] JSだけでスクリーンショットを撮ってダウンロードもする方法(Qiita))

html
<div id="target">
<!-- ここがキャプチャされる -->
</div>
<!-- 下に生成した画像が出る -->
<img src="" id="result" />
javascript
html2canvas(document.getElementById("target"),{
  onrendered: function(canvas){
  //imgタグのsrcの中に、html2canvasがレンダリングした画像を指定する。
  var imgData = canvas.toDataURL();
  document.getElementById("result").src = imgData;
 }
});

②読み込んだ要素を指定した場所の子要素にcanvasを挿入する

html
<div id="target">
<!-- ここがキャプチャされる -->
</div>

<div id="result">
<!-- ここに生成した画像が出る -->
</div>
javascript
html2canvas(document.querySelector("#target"))
  .then(function(canvas) {
    var result = document.querySelector("#result");
    result.innerHTML = "";
    result.appendChild(canvas);
}

ハマったところ

vuetify<v-text-field>vue.jsv-model.lazy が使えない

イベントリスナーを使用して手動で行う必要があります
- https://github.com/vuetifyjs/vuetify/issues/1810

  • 使えないようです。「入力が完了したら自動で検索」を実装したかったのですが、できませんでした。
  • エンターキーで検索に切り替えました。
tmp.vue
<v-text-field v-model="serchWords" @keydown.enter="execSearch(serchWords)"/>

html2canvasで、外部から読み込んだ画像が表示されない

デフォルトだとCORSで引っかかって、描画できないみたいです。

attribute: allowTaint
type: boolean
default: false
description: Whether to allow cross-origin images to taint the canvas

javascript
tml2canvas(document.querySelector("#target"), {
+        letterRendering: 0,
+        useCORS: true,
+        allowTaint: true
      }).then(function(canvas) {
        var result = document.querySelector("#result");
        result.innerHTML = "";
        result.appendChild(canvas);
      });

Firebase Funtionsで外部通信ができない

Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions

firebase emulatorsのAPIの向き先がクラウドになる。

$ firebase emulators:start

上記コマンドで HostingFunctionsを起動した状態で、Hosting上のvue.jsからAPI(Functions)を叩くと、クラウドのFunctionsを見に行ってしまい、CROSエラーになる。(404とかではなくて)

  • emulatorではHostingだけを上げて、Functionsはデプロイしてテストするようにしました。(functions.https.onCall()の方を使ってます。)
  • 見逃してるだけできっと設定があると思います。

今後の展望

  • UnsplushからProduction api keyがいただけたらサービスとしてリリースしようと思います。(1月予定)
  • 欲しい機能とかあれば気軽にコメントください!!喜びます:blush:
v1.0予定機能
- googleでログインでき、「個人の言葉」を美しい画像ともに保存できる
- 画像と言葉の組み合わせで、「5秒でエモい画像が作れる」
- 画像のランダム生成で大喜利
- Twitterでの共有

最後に

ここまで、お読みいいただきありがとうございます!
最後に、開発途中で特に意味もなく作った画像を添えておきます。
image.png

おまけ:宣伝用Key Note

digest-of-life.001.jpeg.jpg
digest-of-life.002.jpeg.jpg
digest-of-life.003.jpeg.jpg
digest-of-life.004.jpeg.jpg
digest-of-life.005.jpeg.jpg
digest-of-life.006.jpeg.jpg
digest-of-life.007.jpeg.jpg

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

Vuetify�の開発に貢献する(ドキュメント翻訳とOSSコミット)

はじめに

VuetifyはVue.jsのUIフレームワークで、OSSで開発されています。もしVuetifyの開発に貢献していきたい場合、大きく分けて次の2つの方法があります。

  • ドキュメントを翻訳する
  • バグ修正をしてPRを投げる

これらについて自分自身の経験を元に、具体的な方法を解説していきます。

ドキュメントを翻訳する

Vuetifyのドキュメントには、日本語版のページがあります。ただページを見てもらうと分かる通り、日本語訳がまだまだ不十分です。
バージョン1系のころまでは翻訳もGitHubでのPRベースでも受付けていたようですが、現行のバージョン2系ではCrowdinという、翻訳支援のサービス上で翻訳するようになっています。

Crowdinでの翻訳作業について

CrowdinはGitHubのアカウントで登録することができます。登録後にVuetifyのプロジェクトページから、翻訳を開始することができます。プロジェクトページには各言語の翻訳の進捗状況が載っているのですが、日本語は現状「8%」とかなり低いです?

スクリーンショット 2019-12-17 16.08.18.png

Crowdinは翻訳支援のサービスということもあり、翻訳に関する機能が充実しています。以下は実際のCrowdin上の編集画面です。左から翻訳したいワード選び入力欄に翻訳を入力して、「SAVE」ボタンで保存します。
ワードによっては入力欄に翻訳提案を出してくれることがあります。今回の場合は他の箇所で「Hide controls」が「コントロールを隠す」とすでに翻訳されていたため、「Hide on scroll」が類似ワードとして判定されました1。これを「コントロールを隠す」から「スクロールを隠す」に変更するだけで翻訳が済むので、作業のスピードアップや表現の統一に繋げることができます。

スクリーンショット 2019-12-17 16.26.55.png

日本語版のページに反映されるまでの流れ

実際の流れは不明ですが、観察する限りでは次のような流れで反映しているようです。私たちが行うのは1〜3までの部分で、これを繰り返していきます。

  1. Vuetifyのプロジェクトページから、「japanese」を選択
  2. 翻訳したいファイルを選択
  3. 編集画面から翻訳して保存
  4. 翻訳したファイルがコアメンバーによって、GitHubにマージされる(例:https://github.com/vuetifyjs/vuetify/pull/8632)
  5. リリース

バグ修正をしてPRを投げる

Vuetifyのソースコードはvuetifyjs/vuetifyのリポジトリで管理されています。バグ修正は翻訳に比べると難易度が高く技術的なハードルも上がります。ただスター数が20000以上という大規模リポジトリで、README.mdにも自分のGitHubアイコンが表示されるようになるので、結構嬉しいです!
ちなみに私自身が初めてOSSにPRを出してマージされたのも実はVuetifyでした2。ルールに沿って意図した修正ならそこまで厳しい指摘は飛ばずにマージされる印象なので、大規模リポジトリと恐れずに初めての方でもおすすめです。

バグ探し

自分でバグを見つけてPRを出すことももちろんできますが、そう簡単に発見できるものでもないのでIssueから探してみるのがおすすめです。いっぱいあるのでどれに取り組むのか悩ましいですが、自分の場合は次のような基準で探しています。

  • [Bug Report] というタイトルが付いていること
    • T: bugというラベルが付いているとなお良い(コアメンバーが少なくとも一度は確認して付けたラベルのため、バグである信ぴょう性が高そう)
  • 誰もアサインされていないこと
  • codepenなどへのリンクがあり、そのページでバグが再現できること

また初めての方は、good first issueのラベルがついたIssueもあるので、これを手がかりにするのも一つの手です。

環境構築

取り組むIssueを決めたら、環境構築を行っていきます。ここの内容についてはドキュメントのContributingにも書かれているので合わせてご参照ください。

commitizenのインストール

Vuetifyのリポジトリのコミットはcommitizenというツールで、対話的に生成されたコミットになっています(たまに普通のコミットもあったりしますが・・・)。そのためcommitizenをインストールしておいて、PRを出すときのコミットもcommitizenを使って行うようにします。

$ npm install -g commitizen

開発環境のセットアップ

次にvuetifyjs/vuetifyリポジトリをforkしてcloneをします。その後、Vuetifyのソースコードのフォルダに移動して、パッケージのインストールとビルドを実行します。

$ git clone git@github.com:<github name>/vuetify.git
$ cd vuetify/packages/vuetify/
$ yarn
$ yarn build

プレイグラウンドファイルのコピー

リポジトリの「packages/vuetify/dev」フォルダに、Playground.template.vueというファイルがあります。
開発環境はこのフォルダ内のPlayground.vueのvueファイルの内容で起動するようになっているため、そのためのサンプルファイルです。デフォルトだとPlayground.vueはバージョン管理に入っていないために存在しないため、いったん同じ内容でコピーをしておきます。

$ pwd
<project root>/vuetify/packages/vuetify
$ cp dev/Playground.template.vue dev/Playground.vue

開発環境で修正したコンポーネントなどの動作を確認したいときは、このPlayground.vueファイルを修正して確認していく流れになります。後にまた書きますが、PRを出すときもこのPlayground.vueファイルの内容をペーストする必要があります。

開発環境の起動

開発環境を起動するには次のコマンドを実行します。特に設定を変えてなければコンパイル後にhttp://localhost:8080 で、「Welcome to Vuetify」というタイトルのページが開くはずです。

$ yarn dev

開発の流れ

開発環境が構築できたら、実際の修正に移っていきます。といっても例がないと説明がしずらいので今回は、[Bug Report] v-btn: Loading indicator too large on small buttonsというIssueに、仮に私自身が取り組む場合の流れを紹介していきます。

修正するコードに当てをつける

Issueタイトルと付いているラベルから、Buttonsに関するバグのようです。Vuetifyのコンポーネントのソースコードは「packages/vuetify/src/components」フォルダ配下に、コンポーネントごとにフォルダが切られています。Buttonsの場合は「packages/vuetify/src/components/VBtn」のような具合です。
コンポーネントのフォルダの中身は、コンポート定義・ユニットテスト・sassのスタイルなどが入っていて、他のコンポーネントもだいだいはこの構成になっています。このフォルダの中身を直せばバグも直るだろうという当てを付けます。
スクリーンショット 2019-12-18 1.45.00.png

プレイグラウンドファイルを修正

コードを直したあと、先ほど作成したPlayground.vueにButtonsのコンポーネントを置いてみて、開発環境で確認します。codepenがあるIssueならそれをコピペして、貼り付けるのが楽です。

<template>
  <v-container>
    <!--  -->
    <div class="text-center">
      <v-btn outlined x-small loading>btn</v-btn>
    </div>
  </v-container>
</template>

<script>
export default {
  data: () => ({
    //
  })
}
</script>

commitizenでコミットをする

開発環境でうまく動作しそうだったら、コミットします。前述した通り、commitizenでコミットを行います。git czというコマンドを打つと次のような対話式のターミナルが開き、これに順番に答えていきます。詳しい説明はコミットのガイドラインがドキュメントにあるので、こちらをご確認ください。

$ git cz
cz-cli@4.0.3, cz-conventional-changelog@3.0.1

? Select the type of change that you're committing: (Use arrow keys)
❯ feat:     A new feature 
  fix:      A bug fix 
  docs:     Documentation only changes 
  style:    Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 
  refactor: A code change that neither fixes a bug nor adds a feature 
  perf:     A code change that improves performance 
  test:     Adding missing tests or correcting existing tests 

PRを出す

修正をして大丈夫そうだったらPRを出してみましょう!まず修正内容によってPRを出すブランチが決まっているので、ドキュメントのプルリクエストの部分の説明をよく読んでおきます。バグ修正の場合はmasterブランチへのPRになります。
またPRはテンプレートがあるので、これに沿った形でPRの説明を埋めていきます。説明通りに内容を埋めたりチェックを付けていけばそこまで難しくないですが、気をつけるべきところは次の点です。

  • Motivation and Context
    Issueに対しての修正の場合は、#9870 のようにIssue番号を書く
  • Markup
    // Paste your FULL Playground.vue here となっている箇所に、自分の開発環境で動かしていた修正内容が確認できるような、Playground.vueの中身をペーストします。

PRをサブミットするとCIが動作します。もしバツマークが付いた場合はコードカバレッジのチェックに引っ掛かってしまっている場合が多いです。その場合はテストコードを追加する必要があります。
テストコードについても説明すると長くなってしまう(自分自身の理解が足りないのもある)ので、コンポーネントのフォルダの__tests__ 配下の既存テストを参考にして、追加するのが一番良いと思います。Unit testingのページも合わせてご参照ください。

無事にCIが通ったら、あとはマージされることを祈りましょう!

まとめ

少し長くなってしまいましたが、改めて今回はVuetifyの開発に貢献する方法として、次の2点を解説しました。

  • ドキュメントを翻訳する
  • バグ修正をしてPRを投げる

今後もVuetifyが開発され続けていくためにも引き続き貢献は続けていきたいと思っています。今回の記事で少しでもみなさんがVuetifyに貢献するきっかけになれば嬉しく思っています!


  1. Translation Memoryという機能によって、行われています。 

  2. https://github.com/vuetifyjs/vuetify/pull/6373 

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

Vue-multiselectをnuxt.jsで使用する

概要

Vue.jsにはVue-multiselectという、多機能なセレクトボックスのライブラリがありますが、これをnuxt.jsで使いたい場合の方法を記載します。

Vue-multiselectについて

どのような機能があるかは、Vue-multiselectにて紹介されています。また、「Vue-multiselect」を使って万能なセレクトボックスを実装するの記事も参考になります。
サーチ機能や、タグを追加する機能もあり、かなり便利な感じはします。

nuxt.jsでの使用

nuxt-vue-multiselectというライブラリを使用します。タグの使い方は、上記のVue-multiselectと同様です。
導入方法は以下の通りです。

①install

npm i nuxt-vue-multiselectを実行。

②nuxt.config.jsへの設定追加

modulesのセクションへ、'nuxt-vue-multiselect'を追加。

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

Vue.js + Vue2-DropzoneでS3の署名付きURLへファイルアップロード

この記事はうるる Advent Calendar 2019 18日目の記事です。

はじめに

株式会社うるるにお世話になっている、フリーランスエンジニアの福田と申します。
昨日の記事で、S3の署名付きURLを用いてファイルアップロードを行う方法が紹介されていました。
そこで、「じゃあフロントエンドから送るにはどうすりゃいいの?」という記事を書いてみたいと思います。

要約

  • Vue.jsでS3の署名付きURLを用いてファイルアップロードを行う方法の紹介です
  • Vue2-Dropzoneを使用します
  • CodeSandboxを使って動かしながら読み進められるように書いてます

スタート!

CodeSandbox上でVue2-DropzoneをInstallする

  • 上記のリンクからCodeSandboxへアクセス
  • DependenciesAdd Dependencyボタンをクリック
  • Vue2-dropzoneを検索して追加

これでVue2-Dropzoneを使用できるようになりました。

とりあえず動くようにする

Vue2-dropzoneのサイトのガイドに沿って修正してみます。
App.vueを、こういう風に修正しましょう。

<template>
  <div id="app">
    <vue-dropzone ref="myVueDropzone" id="dropzone" :options="dropzoneOptions"></vue-dropzone>
  </div>
</template>

<script>
import vue2Dropzone from 'vue2-dropzone'
import 'vue2-dropzone/dist/vue2Dropzone.min.css'

export default {
  name: "App",
  components: {
    vueDropzone: vue2Dropzone
  },
  data: function () {
    return {
      dropzoneOptions: {
          url: 'https://httpbin.org/post',
          thumbnailWidth: 150,
          maxFilesize: 0.5,
          headers: { "My-Awesome-Header": "header value" }
      }
    }
  }
};
</script>

Drop files here to uploadと書かれた四角い領域が表示されましたか?
そこをクリックするか、ファイルをドラッグアンドドロップすればサムネイルが表示されます。
これでUIは完成です。

Fileオブジェクトを送信できるようにする

次はFileオブジェクトを送信できるようにしましょう。
Vue2-DropzoneはFormデータとして画像データを送信しますが、署名付きURLでS3へ送る場合はFileオブジェクトを送信する必要があります。
その方法を説明します。

Vue2-Dropzoneには様々なイベントが存在します。
ファイル送信時に発火するsendingイベントにメソッドイベントハンドラを定義すると、引数としてFileオブジェクトとXHRオブジェクトを受け取ることができます。
そのXHRオブジェクトのBodyにFileオブジェクトをセットすればOKです。
また、署名付きURLへオブジェクトを送信する場合はPUTメソッドで送信する必要があるので、その設定も変えます。

こちらのissuesを参考にしました。
https://github.com/enyo/dropzone/issues/33#issuecomment-150659202

修正箇所を抜粋したものがこちら。

<!-- method に PUT を明示 -->
<!-- @vdropzone-sending を追加 -->
<vue-dropzone
  ref="myVueDropzone"
  id="dropzone"
  method="PUT"
  :options="dropzoneOptions"
  @vdropzone-sending="sending"
></vue-dropzone>
export default {
  
  methods: {
    sending(file, xhr) {
      const _send = xhr.send;
      xhr.send = function() {
        // Fileオブジェクトを送信するようにする
        _send.call(xhr, file);
      };
    }
  }
}

ファイルの送信先を動的に変える

通常、ファイルのアップロード先はoptionsurlで指定したURIになるのですが、これを署名付きURLに変えます。
DropzoneのQueueにあるファイルが処理されるタイミングでprocessingイベントが発火するので、その時にoptionsurlを変えてやります。
以下、修正箇所の抜粋です。

<!-- @vdropzone-processing を追加 -->
<vue-dropzone
  ref="myVueDropzone"
  id="dropzone"
  method="PUT"
  :options="dropzoneOptions"
  @vdropzone-sending="sending"
  @vdropzone-processing="processing"
></vue-dropzone>
export default {
  
  methods: {
    
    processing(file) {
      // 署名付きURLを取得
      // httpbin はHTTPリクエストをテストできるサービス
      const uploadUrl = "https://httpbin.org/put"
      // 実際はこんな感じになると思います
      // const uploadUrl = await axios.get('/api/signed_url')

      // Dropzoneの送信先を変える
      this.$refs.myVueDropzone.dropzone.options.url = uploadUrl
    }
  }
}

全体的にはこのようになっていると思います。
<template>
  <div id="app">
    <vue-dropzone
      ref="myVueDropzone"
      id="dropzone"
      method="PUT"
      :options="dropzoneOptions"
      @vdropzone-sending="sending"
      @vdropzone-processing="processing"
    ></vue-dropzone>
  </div>
</template>

<script>
import vue2Dropzone from "vue2-dropzone";
import "vue2-dropzone/dist/vue2Dropzone.min.css";

export default {
  name: "App",
  components: {
    vueDropzone: vue2Dropzone
  },
  data: function() {
    return {
      dropzoneOptions: {
        url: "https://httpbin.org/post",
        method: "PUT",
        thumbnailWidth: 150,
        maxFilesize: 0.5,
        headers: { "My-Awesome-Header": "header value" }
      }
    };
  },
  methods: {
    sending(file, xhr) {
      const _send = xhr.send;
      xhr.send = function() {
        // Fileオブジェクトを送信するようにする
        _send.call(xhr, file);
      };
    },
    processing(file) {
      // 署名付きURLを取得
      // httpbin はHTTPリクエストをテストできるサービス
      const uploadUrl = "https://httpbin.org/put";
      // 実際はこんな感じになると思います
      // const uploadUrl = await axios.get('/api/signed_url')

      // Dropzoneの送信先を変える
      this.$refs.myVueDropzone.dropzone.options.url = uploadUrl;
    }
  }
};
</script>

アップロードしてみた結果がこちら。
processingメソッド内でセットしたhttps://httpbin.org/putにPUTメソッドで送信されていればOKです!
mojikyo45_640-2.gif

事前に署名付きURLを取得する

ファイル送信前に署名付きURLを取得しておけば、API呼び出しのターンアラウンドタイムを節約できます。
Dropzoneはファイルの追加のタイミングでイベントを発火するので、イベントハンドラを定義して署名付きURLを取得します。
取得したURLはFileオブジェクトに退避しておいて、ファイル送信時に使用します。

<!-- @vdropzone-file-added を追加 -->
<vue-dropzone
  ref="myVueDropzone"
  id="dropzone"
  method="PUT"
  :options="dropzoneOptions"
  @vdropzone-sending="sending"
  @vdropzone-processing="processing"
  @vdropzone-file-added="fileAdded"
></vue-dropzone>
export default {
  
  methods: {
    
    fileAdded(file) {
      // ↓↓↓ここはprocessingから移植↓↓↓
      // Fileオブジェクトに署名付きURLのプロパティを追加
      file.uploadUrl = "https://httpbin.org/put"
      // 実際はこんな感じになると思います
      // file.uploadUrl = await axios.get('/api/signed_url')
      // ↑↑↑ここはprocessingから移植↑↑↑
    },
    processing(file) {
      // Fileオブジェクトに退避しておいた署名付きURLでファイル送信先を上書き
      this.$refs.myVueDropzone.dropzone.options.url = file.uploadUrl
    }
  }
}

全体的にはこのようになっていると思います。
<template>
  <div id="app">
    <vue-dropzone
      ref="myVueDropzone"
      id="dropzone"
      method="PUT"
      :options="dropzoneOptions"
      @vdropzone-sending="sending"
      @vdropzone-processing="processing"
      @vdropzone-file-added="fileAdded"
    ></vue-dropzone>
  </div>
</template>

<script>
import vue2Dropzone from "vue2-dropzone";
import "vue2-dropzone/dist/vue2Dropzone.min.css";

export default {
  name: "App",
  components: {
    vueDropzone: vue2Dropzone
  },
  data: function() {
    return {
      dropzoneOptions: {
        url: "https://httpbin.org/post",
        method: "PUT",
        thumbnailWidth: 150,
        maxFilesize: 0.5,
        headers: { "My-Awesome-Header": "header value" }
      }
    };
  },
  methods: {
    sending(file, xhr) {
      const _send = xhr.send;
      xhr.send = function() {
        // Fileオブジェクトを送信するようにする
        _send.call(xhr, file);
      };
    },
    fileAdded(file) {
      // ↓↓↓ここはprocessingから移植↓↓↓
      // Fileオブジェクトに署名付きURLのプロパティを追加
      file.uploadUrl = "https://httpbin.org/put";
      // 実際はこんな感じになると思います
      // file.uploadUrl = await axios.get('/api/signed_url')
      // ↑↑↑ここはprocessingから移植↑↑↑
    },
    processing(file) {
      // Fileオブジェクトに退避しておいた署名付きURLでファイル送信先を上書き
      this.$refs.myVueDropzone.dropzone.options.url = file.uploadUrl;
    }
  }
};
</script>

先程と同様の結果になっていればOKです!

余談ですが、こんなことも

サムネイルをVue.jsのコンポーネントにする

サムネイル生成時に発火するvdropzone-thumbnailイベントを利用すれば、dataUrlを含んだFileオブジェクトを取得できます。
それをコンポーネントのpropsに渡してサムネイルを描画してやれば実現可能です。
コンポーネントなのでスタイルは自由に決められますし、様々なイベントを設定することもできます。

サムネイルのドラッグアンドドロップ

サムネイルをコンポーネント化することができれば、DnDも実現可能です。
Vue.jsのDnDのライブラリは色々あると思いますが、それらと組み合わせることでリッチな画像アップローダーを簡単に作れます。

この辺の話は別の機会に記事に纏められればと思います。

終わり

Advent Calendar 18日目でした。
明日19日目は tatsukoni さんによる記事を乞うご期待!
https://adventar.org/calendars/4548

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

遭難者を救うために学内Mapを作った話

自己紹介

こんにちは!2019年度の筑波大学園祭実行委員でWeb担当をしていました @SIY1121 です。
今回は学園祭の来場者向けWebマップアプリを開発しました!
すべて紹介するとかなり量が多くなるので。地図の表示部分に絞って少し紹介したいと思います。

ざっくりいうと

  • 全国2位の広さを誇る筑波大学 筑波キャンパス で学園祭が行われた
  • 広いだけに、来場者が迷ってしまう
  • Vue.js + SVG + 自作webpackloader でGoogleMapsライクなWebアプリを開発した
  • コア機能はOSS化予定!

so.png

スマホに最適化されています↓
https://www.sohosai.com/map

遭難者が相次ぐ大学

筑波大学 筑波キャンパスは全国で2番目に広いキャンパスです。
その大きさは258ha(ディズニーランド5個分)にもなります。

雙峰祭

雙峰祭(そうほうさい)は、毎年3万人以上が訪れる筑波大学の学園祭です。
一般の方がたくさんやってきますが、広いので迷う方も多いです。

Webマップを作った

迷ってしまってもスマホで現在位置を調べたり、企画を調べたりできるWebアプリを作りました。
期間中10万ページビューを達成し、GPSを使用したユーザーは1.5万人にも登りました。
so.png

https://www.sohosai.com/map

使ったもの

Vue.js

言わずと知れたフロントエンドのフレームワーク。

SVG

地図はイラレで作成し、SVGで出力しています。
SVGを使用することにより、拡縮しても画質が劣化しない上、建物や文字、アイコン等の要素を個別に制御することができるので

  • 建物がタップできる
  • 地図をピンチイン・アウトしても文字やアイコンのサイズや角度を固定したりできる

といったマップに欠かせない機能が実装しやすくなります。

図2.png

eazy-pz-as

マップを拡縮したり回転できる機能は、easy-pz-asを使用しました。
指定したsvgを拡縮したり移動したりできるようになります。
ただ、これはVue.jsを想定した作りになっていないので、使うには多少工夫が必要でした。

自作webpack loader

以下の3つの理由からwebpack loader を自作しました。

  • Vue.js でSVGを制御するには .vue ファイルにSVGをインラインで記述する必要があり、イラレで生成された数万行のSVGを vueファイルに入れるのは扱いにくい
  • 文字やアイコンのサイズや回転を固定するには、一つ一つの要素に変形の中心座標を設定する必要があるため、すべての要素の座標を調べ、SVG内にベタ書きするのは人間がすることではない
  • デザインの変更などにより、SVGがバージョンアップされるため、上記のようにSVGに変更を加えていると、もう一度変更し直す必要がある

そもそもwebpack loader とは?

webフロントエンド向けアセットバンドラ「webpack」の内部でファイルを変換したり、依存関係を構築するする工程で使用されます。
身近な例では、Vue.js単一コンポーネントや、Reactのjsxなど、ブラウザがそのままでは理解できないファイルをjsに変換するのに使われています。

自作webpack loader で何をしてるか

so.png

今回作成したwebpack loaderは SVGにVueで制御するために必要な属性を自動で付与し、.vue にインライン展開します。
具体的には、指定した要素(アイコンや文字)のサイズや回転を固定するためにスタイルをバインディングします。

一番簡単なwebpack loader

作り方はとっても簡単で、ソースコードをstringで受け取り、何かしらの処理を施したソースコードを返す関数をexportするだけです。

map-loader.js
// webpackはnode.js上で動くため、CommonJS
module.exports = function(source) {
  // 処理
  return source
}

どのファイルにどのwebpack loaderを使うかはwebpackのconfigで定義できます。
VueやReactで専用のツールチェインを使う場合でも、それぞれの方法でwebpackのconfigを設定できます。

今回はこの処理の中で SVG の処理&挿入を行っています。

SVGに属性を付加する

Vueファイルには <img src="/path/to/svg" svg-map > のように書いておき、正規表現で挿入するsvgファイル名を抽出します。(jsdomを使ってもいいかも)
次に、抽出したパス名からSVGファイルを読み込みます。読み込んだsvgはただの文字列なのでjsdomを使ってDOMを生成し、扱いやすくします。

map-loader.js
let svgString = fs.readFileSync(filePath, 'UTF-8')

const document = new JSDOM(svgString).window.document
const elements = document.querySelectorAll('#アイコン > g')
elements.forEach((el, index) => {
  const refID = `icon-${index}`
  el.setAttribute('ref', refID)
  el.setAttribute(':style',`getElementStyle('${refID}')`)
})

svgString = root.outerHTML // 属性が付与されたsvg文字列

上の例は、svgを読み込み #アイコン直下の要素(イラレ上でアイコンレイヤー直下のオブジェクト)に対して ref:style属性を付与しています。
最後に、先程の imgタグを処理後のsvgStringで置換して、sourceとして返却します。

挿入先のVueファイル

挿入先のVueファイルに以下のようなメソッドを追加しておきます。
webpack loader で :styleにバインドしたgetElementStyleがこれに相当します。

Map.vue
getElementStyle(refID) {
  const target = this.$refs[el]
  if (!target) return ''

  /* 地図の変形に応じて要素も変形
    rotateZで要素を逆回転させると回転が固定されているように見える
    scaleで地図の拡大率の逆数を掛けることでサイズが固定されているように見える
  */
  const transform = `
        rotateZ(${this.mapTransform.rotate.deg * -1}deg)
         scale(${1 / this.mapTransform.scale})
        `

  // 要素を囲う最小の長方形を取得
  const box = target.getBBox()
  // 要素が変形する中心を指定
  transformOrigin = `${box.x + box.width / 2}px ${box.y + box.height / 2}px`

  // 要素に適用するstyleを返す
  return {
    transform,
    transformOrigin
  }
}

これで アイコンが地図の拡縮、回転を行っても固定されるようになります。
コンパイル時にvueファイルとSVGファイルが内部で統合されるので、巨大なvueファイルができることもなく、気軽にSVGを差し替えることもできます。

おわりに

初めてwebpack loaderを自作しました。
今回の知見を生かして、コア機能はOSS化を予定しています。
誰でも簡単にWebマップを作れるようになって、遭難者を救っていただければ幸いです。

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

Nuxt.jsでvue-carouselで商品画像一覧をカスタマイズして表示する

概要

商品画像を画像と商品名の組み合わせのコンポーネントを作成し、カルーセルスライダーで表示を行う。
商品は4つずつ並べて横にスライドさせていけるイメージです。

画面イメージ

screencapture-localhost-client-products-category-1-2019-12-18-00_29_38.png

参考

nuxt.jsにvue-carouselを導入してスライダーを作成

実装手順

vue-carouselのインストール

npm install -S vue-carousel

Nuxtのプラグインでvue-carousel.jsを作成し、下記を実装する

vue-carousel.js
import Vue from 'vue'
import VueCarousel from 'vue-carousel'

Vue.use(VueCarousel)
nuxt.config.js
plugins: [
    { src: '~/plugins/vue-carousel', ssr: false }
  ],

コンポーネント設計

今回は、4枚ごとの商品画像を表示するようのコンポーネントとして実装していおきます。

スクリーンショット 2019-12-18 0.37.18.png

実装

実際のコンポーネントの実装

<carousel><slide>の中に商品画像と価格と商品名をv-forで描画するようにする

:per-page="4"で4枚単位で表示するように指定しました。

中身のCSSの設定は.VueCarousel-slideの中に記載

ProductCardCarousel.vue
<template>
  <carousel :per-page="4" :pagination-enabled="false">
    <slide v-for="(prodduct_item, key) in productList" :key="key">
      <div class="product-card">
        <div v-if="prodduct_item.imgURL != ''">
          <img class="product-card-img" :src="prodduct_item.imgURL" />
        </div>
        <div v-else>
          <img
            class="product-card-img"
            :src="require('@/assets/img/NoImage.png')"
          />
        </div>
        <div class="product-card-content">
          <div class="product-card-price">
            {{ prodduct_item.price }}ポイント
          </div>
          <div class="product-card-text">{{ prodduct_item.name }}</div>
        </div>
      </div>
    </slide>
  </carousel>
</template>

<script>
import Carousel from 'vue-carousel/src/Carousel.vue'
import Slide from 'vue-carousel/src/Slide.vue'
export default {
  components: {
    Carousel,
    Slide
  },
  layout: 'client/simple',
  props: {
    productList: {
      type: Array,
      required: true,
      default: () => []
    }
  }
}
</script>

<style lang="scss" scoped>
@import '~/assets/scss/base.scss';

.VueCarousel-slide {
  padding: $space_m $space_m $space_m $space_m;
  .product-card {
    .product-card-img {
      border-radius: 50%;
      height: 80px;
    }
    .product-card-content {
      text-align: center;
      .product-card-price {
        font-size: $font-size_xs;
        font-weight: bold;
        padding: $space-s 0 0 0;
      }

      .product-card-text {
        font-size: $font-size_xs;
        padding: $space-s 0 0 0;
      }
    }
  }
}
</style>

ProductCardCarousel.vue を使用する際には下記のように実装

<template>
  <div>
    <div v-for="(category_item, key) in categoryList" :key="key">
      <div class="category-title">{{ category_item.categoryTitle }}</div>
      <product-card-carousel
        :product-list="category_item.productList"
      ></product-card-carousel>
      <product-link
        :url="category_item.categoryLink"
        :link-name="category_item.categoryName"
      ></product-link>
    </div>
  </div>
</template>

<script>
import ProductCardCarousel from '~/components/client/ProductCardCarousel'
import ProductLink from '~/components/client/ProductLink'

export default {
  components: {
    ProductCardCarousel,
    ProductLink
  },
  layout: 'client/simple',
  data() {
    return {
      categoryList: []
    }
  },
  // レンダリングの前に商品情報を取得する
  async asyncData({ app, params, store, $axios }) {
    const { data } = await $axios
      .$get(
        `/api/user/product/category/${params.id}?event_id=${
          store.getters['event_info/eventSelected'].id
        }`
      )
      .catch(errors => {})

    return {
      categoryList: data
    }
  }
}
</script>

まとめ

vue-carouselがあればデザインのカスタマイズもしつつ簡単に実装可能でした
あくまでメモ用なのでソースが汚かったりおかしかったりするかもしれませんが
良かったら参考にしてください。

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

[Vue + php] phpの配列データをVueで検索/ソートさせてみた

Vueでリスト検索とソート機能を作ってみたので
さらに応用して、phpの配列データからVueに流し込んで処理した機能を作ってみました。

ロジック

php配列データ

JSONデータに変換

JSONデータをJSの配列に格納

JSの配列をvueのdataに格納
これでvueで取り扱えるデータになりました。

computed(算出プロパティ)
matched:
フォーム入力の数値 <= budgetの数値のリストのリストを表示
sorted:
ボタンのオンオフで昇順・降順ソート

limited:
limit数分表示できる

<?php
    $list = [
        ['id' => '1', 'name' => '商品A', 'price' => '500'],
        ['id' => '2', 'name' => '商品B', 'price' => '300'],
        ['id' => '3', 'name' => '商品C', 'price' => '2000'],
        ['id' => '4', 'name' => '商品D', 'price' => '5000'],
        ['id' => '5', 'name' => '商品E', 'price' => '1500'],
        ['id' => '6', 'name' => '商品F', 'price' => '250'],
        ['id' => '7', 'name' => '商品G', 'price' => '100'],
        ['id' => '8', 'name' => '商品H', 'price' => '750'],
    ];
    $list_json = json_encode($list);
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vue App</title>
</head>
<body>
    <div id="app">
        <input v-model.number="budget">円以下
        <p>{{ matched.length }}件表示中</p>
        <button v-on:click="order=!order">価格 ▼</button>
        <div v-for="item in limited" v-bind:key="item.id">
            {{ item.name }}: {{ item.price }}円
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>

    <script>
        let list = JSON.parse('<?php echo $list_json; ?>');
        const app = new Vue({
            el: '#app',
            data: {
                // 検索初期値
                budget: '',
                // 検索数
                limit: 10000000000000,
                // 検索リスト
                list: list,
                // ソート初期値
                order: false,
            },
            computed: {
                matched: function() {
                    return this.list.filter(function(el) {
                        return el.price <= this.budget
                    }, this)
                },
                sorted: function() {
                    return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc')
                },
                limited: function() {
                    return this.sorted.slice(0, this.limit)
                }
            }
        });
    </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む